項目名稱:教育網—在線調查系統java
項目整體流程圖:web
設計調查:調查-->包裹--->問題(增刪改查)spring
1.調整包裹順序sql
2.移動複製包裹數據庫
3.深度刪除express
主要生成survey_id、survey_name、completed(是否完成)、logoPath(涉及到圖片上傳)json
springMVC文件上傳:數組
①form標籤的enctype屬性:multipart/form-data②form標籤的method屬性:post
③生成文件上傳框:input type="file"
文件的保存
①調用multiPartFile.transfer()方法
②文件的路徑不能使用絕對的物理路徑
<img src="E:\good.jpg"/>
這樣的路徑瀏覽器沒法顯示圖片
③有效的路徑形式瀏覽器
<img src="surveyLogos/logo.gif"/>
這個路徑有效是由於它是一個虛擬路徑。
④虛擬路徑VS真實物理路徑
[1]真實物理路徑:Web應用中的文件和目錄在硬盤上保存的真實路徑(注意:這裏指的是部署目錄)。
D:\WorkSpaceShenZhen170228\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\Survey_1_UI\surveyLogos\logo.gif
瀏覽器不能直接訪問這個路徑,因此須要由服務器將它轉換爲瀏覽器能夠訪問的虛擬路徑
Web應用在不一樣的操做系統下、在不一樣的服務器上部署時真實物理路徑是有可能變化的。
[2]虛擬路徑:服務器虛擬出來供瀏覽器訪問的路徑,以主機地址爲基準的
http://localhost:8080/Survey_1_UI/surveyLogos/logo.gif
無論Web應用部署在什麼操做系統的什麼服務器上,虛擬路徑都是相同的。
⑤在handler方法中保存文件時如何將文件保存到img標籤能夠訪問的路徑下
[1]保存文件的目標路徑必定在部署目錄下
[2]部署目錄會隨着部署的服務器、操做系統不一樣而發生變化
[3]因此要經過不變的虛擬路徑動態生成有可能變化的真實物理路徑
String 真實物理路徑 = servletContext.getRealPath(虛擬路徑);
⑥壓縮圖片
[1]直接複製一個工具方法resizeImages()
[2]兩個須要手動導入的API
import com.sun.image.codec.jpeg.JPEGImageEncoder;
import com.sun.image.codec.jpeg.JPEGCodec;
[3]傳入的參數
inputStream:上傳文件的輸入流
realPath:/surveyLogos目錄的真實路徑,後面沒有斜槓,並且不帶具體文件名
[4]返回值:能夠直接用於設置Survey對象的logoPath屬性
①驗證的內容
[1]文件的大小[2]文件的類型
[1]檢測用戶是否上傳了文件[2]獲取相關數據:文件大小、文件內容類型[3]若是檢測到大小或類型不符合要求,則拋出對應的異常
①分頁支持:MyBatis插件PageHelper②要查詢的數據:Survey對象
[1]限制條件1:當前用戶[2]限制條件2:未完成③SurveyMapper.selectAllSurvey(userId,completed);
考慮到未來也會查詢全部已完成的調查,因此userId和completed都須要傳入
[1]用戶沒有上傳文件時保持舊的logo_path字段值不變[2]用戶若是上傳了文件那麼就將logo_path字段值修改成新值[3]用戶若是上傳了不符合要求的圖片要回到更新調查的表單頁面並顯示錯誤消息[4]回到更新調查的表單頁面顯示錯誤消息時要保證表單上模型數據回顯正常[5]更新完成後回到分頁頁面,且回到的是以前所在的頁碼
[6]文件上傳驗證失敗後,再正常更新仍是可以回到以前所在的分頁頁面
包裹的序號默認採用包裹的id
原理:經過mybatis的xml映射文件獲取自增主鍵獲取包裹的序號,若是採用插入後查詢id最大值賦值給Order會由於線程問題出錯。
③獲取自增主鍵值的方式以及相關UPDATE語句
useGeneratedKeys="true" keyProperty="bagId"
update guest_bag set bag_order=#{bagId} where bag_id=#{bagId}
難點:將選項轉化爲json進行處理。
DataprocessUtils.processOptionToJson(Question question);
判斷題型,簡答題不處理將option字符串根據「\r\n」拆分爲數組藉助於工具將數組轉換爲JSON字符串DataprocessUtils.processOptionFromJson(Question question);判斷題型,簡答題不處理藉助於工具將JSON格式的option字符串還原爲List將List組合成以「\r\n」分開的字符串
藉助於工具將JSON格式的option字符串還原爲List
將重複操做提取出來
答案回顯:type1,2,3
包裹和問題數據的來源
答案數據存儲的數據結構:
Session
allBagMap
根據bagId→paramMap
根據表單標籤的name屬性值→values數組
根據values數組進行標籤的回顯
checkboxradiotext
<input type="submit" name="submit_prev" value="返回上一個包裹"/>
<input type="submit" name="submit_next" value="進入下一個包裹"/>
<input type="submit" name="submit_quit" value="放棄"/>
<input type="submit" name="submit_done" value="完成"/>
boolean contains = parameterMap.containsKey("submit_prev");if(contains){//說明用戶點擊的是"返回上一個包裹"
}
③四個按鈕的顯示條件
size-1實際上就是最後一個包裹的索引
一、使用異常映射機制統一管理項目中錯誤消息
why?
常規的是當不符合業務狀況時產生異常信息返回,但容易由於我的書寫代碼的行爲習慣致使,編程混亂,
會增長交流的成本,下降開發效率。因此須要採用異常映射機制統一管理錯誤消息。
if(錯誤條件){
map.put("message","對不起,這個用戶名已經被佔用了,請從新註冊!");return "頁面";
}
how?
異常映射機制統一管理項目錯誤信息:
拿註冊用戶名字存在爲例:
//已存在則拋出異常
if(adminCount > 0) { throw new AdminNameExistsException(GlobalMessage.ADMIN_NAME_EXISTS); }
AdminNameExistsException是自定義的Exception
public class AdminNameExistsException extends RuntimeException { private static final long serialVersionUID = 1L; public AdminNameExistsException(String message) { super(message); } }
異常映射機制在spring.xml中進行異常映射:映射到相應頁面
<!--簡單異常映射解析器,對於用戶 名存在throw的異常進行映射跳轉到指定視圖 --> <bean id="SimpleMappingExceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="exceptionMappings"> <!-- key屬性是異常類型 --> <!-- 標籤體配置目標視圖 --> <props> <prop key="com.lamsey.survey.e.UserNameAlreadyExistException">guest/user_regist</prop> <prop key="com.lamsey.survey.e.UserLoginFailedException">guest/user_login</prop> <prop key="com.lamsey.survey.e.UserAccessForbiddenException">guest/user_login</prop> <prop key="com.lamsey.survey.e.FileTypeInvalidForSaveException">guest/survey_addUI</prop> <prop key="com.lamsey.survey.e.FileTooLargeForSaveException">guest/survey_addUI</prop> <prop key="com.lamsey.survey.e.FileTypeInvalidForEditException">guest/survey_editUi</prop> <prop key="com.lamsey.survey.e.FileTooLargeForEditException">guest/survey_editUi</prop> <prop key="com.lamsey.survey.e.RemoveSurveyException">error</prop> <prop key="com.lamsey.survey.e.RemoveBagException">error</prop> <prop key="com.lamsey.survey.e.SurveyWithoutAnyBagException">error</prop> <prop key="com.lamsey.survey.e.SurveyHasEmptyBagException">error</prop> <prop key="com.lamsey.survey.e.BagOrderDuplicateException">guest/bag_AdjustUI</prop> <prop key="com.lamsey.survey.e.AdminLoginFailedException">manager/admin_login</prop> <prop key="com.lamsey.survey.e.HasNoAuthorityException">error</prop> <prop key="com.lamsey.survey.e.AdminAccessForbiddenException">error</prop> </props> </property> </bean>
頁面對異常進行捕獲顯示:
<c:if test="${requestScope.exception != null }"> <%-- request.setAttribute("exception",exception) --%> <%-- request.getAttribute("exception") --%> <%-- exception.getMessage() --%> <div class="form-group"> ${requestScope.exception.message}</div> </c:if>
jsp四大域對象常常用來保存數據信息。
pageContext 能夠保存數據在同一個jsp頁面中使用
request 能夠保存數據在同一個request對象中使用。常常用於在轉發的時候傳遞數據
session 能夠保存在一個會話中使用
application(ServletContext) 就是ServletContext對象
jsp 中九大內置對象分別是:
request 對象 請求對象,能夠獲取請求信息
response 對象 響應對象。能夠設置響應信息
pageContext 對象 當前頁面上下文對象。能夠在當前上下文保存屬性信息
session 對象 會話對象。能夠獲取會話信息。
exception 對象 異常對象只有在jsp頁面的page 指令中設置 isErrorPage="true" 的時候纔會存在
application 對象 ServletContext對象實例,能夠獲取整個工程的一些信息。
config 對象 ServletConfig對象實例,能夠獲取Servlet的配置信息
out 對象 輸出流。
page 對象 表示當前Servlet對象實例(無用,用它不如使用this對象)。
九大內置對象,都是咱們能夠在【代碼腳本】中或【表達式腳本】中直接使用的對象。
二、經過序列化和反序列化技術實現對象的深度複製
爲何用深度複製?
若咱們系統中存在大量的對象是經過拷貝生成的,若是咱們每個類都寫一個clone()方法,並將還須要進行深拷貝,新建大量的對象,這個工程是很是大的,
這裏咱們能夠利用序列化來實現對象的拷貝。
複製包裹,
執行深度複製
Bag targetBag = (Bag)DataprocessUtils.deeplyCopy(sourceBag);
如何利用序列化來完成對象的拷貝呢?
在內存中經過字節流的拷貝是比較容易實現的。把母對象寫入到一個字節流中,再從字節流中將其讀出來,
這樣就能夠建立一個新的對象了,而且該新對象與母對象之間並不存在引用共享的問題,真正實現對象的深拷貝。
1.首先將數據進行序列化
deeplyCopy(Serializable source)
2.將序列化的數據
/** * 經過序列化和反序列的方式對對象進行深度複製 */ //深克隆: 具備相同的值,可是兩個全新的對象實例,相互之間不會受影響 // 被複制對象的全部變量都含有與原來的對象相同的值,除去那些引用其餘對象的變量。 // 那些引用其餘對象的變量將指向被複制過的新對象,而再也不是原有的那些被引用的對象。 public static Object deeplyCopy(Serializable source){ if(source == null) { return null; } //1.聲明一個變量用來保存複製獲得的目標對象 Object targetObject = null; //2.聲明四個變量用來保存四個流 ObjectInputStream ois =null; ObjectOutputStream oos = null; ByteArrayInputStream bais = null; ByteArrayOutputStream baos = null; //3.try...catch...finally結構 try{ //4.建立字節數組輸出流 baos = new ByteArrayOutputStream(); //5.根據字節數組輸出流建立對象輸出流 oos = new ObjectOutputStream(baos); //6.執行對象的序列化操做(本質:將對象序列化後獲得的數據寫入字節數組) oos.writeObject(source); //7.獲取保存了序列化數據的字節數組 byte[] byteArray = baos.toByteArray(); //8.建立字節數組輸入流 bais = new ByteArrayInputStream(byteArray); //9.根據字節數組輸入流建立對象輸入流 ois = new ObjectInputStream(bais); //10.執行反序列化操做 targetObject = ois.readObject(); }catch(Exception e){ e.printStackTrace(); } finally{ //11.釋放資源 if(oos != null){ try{ oos.close(); }catch(Exception e){ e.printStackTrace(); } } if(ois != null){ try{ ois.close(); }catch(Exception e){ e.printStackTrace(); } } } return targetObject; }
三、使用pageHelper對商品結果進行分頁瀏覽功能
why?
普通的sql語句分頁:
limit x,y;
#x:起始數據行,y:要查詢的數據行
SELECT last_name,salary FROM employees ORDER BY salary DESC #分頁 (寫在order by的後面) #limit 0,10; LIMIT 20,10;#21-30段數據:第3頁 #公式:limit (pageNo - 1) * pageSize , pageSize;
使用普通分頁太麻煩了,利用mybatis的pageHelper插件會更容易操做。
how?
public PageInfo<Survey> getSurveyPage(Integer userId, boolean completed, Integer pageNum) { //設置每頁顯示數量 int pageSize = 5; PageHelper.startPage(pageNum, pageSize); //執行分頁查詢 List<Survey> list = surveyMapper.selectAllSurvey(userId, completed); //用PageInfo對結果進行包裝 int navigatePages = 6; PageInfo<Survey> page = new PageInfo<>(list, navigatePages); return page; }
4.JFreeChart將選擇題的答案數據導出爲餅圖
why?
JFreeChart是JAVA平臺上的一個開放的圖表繪製類庫。它徹底使用JAVA語言編寫,是爲applications, applets, servlets 以及JSP等使用所設計。
JFreeChart可生成餅圖(pie charts)、柱狀圖(bar charts)、散點圖(scatter plots)、時序圖(time series)、甘特圖(Gantt charts)等等
多種圖表,而且能夠產生PNG和JPEG格式的輸出,還能夠與PDF和EXCEL關聯。
由於要對每道題統計數據,因此採用餅狀圖進行顯示每道選擇題的結果。簡答題
how?
@RequestMapping(value="manager/statistics/showAnswerChart/{questionId}",method=RequestMethod.GET) public void showAnswerChart(@PathVariable(value="questionId") Integer questionId, HttpServletResponse response) throws IOException{ //1.調用Service方法生成JFreeChart對象 JFreeChart chart = statisticsService.getChart(questionId); //2.將JFreeChart對象生成的圖表圖片返回給瀏覽器 //經過response對象獲取一個可以給瀏覽器返回數據的輸出流 ServletOutputStream outputStream = response.getOutputStream(); //藉助ChartUtilities工具類的方法將圖表數據寫入到上面獲取的輸出流 ChartUtilities.writeChartAsJPEG(outputStream, chart, 1200, 600); //③當前Handler方法經過上面的輸出流已經可以給瀏覽器明確的響應數據,因此再也不前往任何一個視圖 //因此沒有任何返回值 }
JFreeChart對象的建立
public JFreeChart getChart(Integer questionId) { //獲取題目數據 Question question = questionMapper.selectByPrimaryKey(questionId); int count = answerMapper.selectQuestionEngagedCount(questionId); //獲取圖例區數據 List<String> optionList = question.getOptionList(); //獲取標籤區數據 Map<String, Object> map = new HashMap<>(); for(int index= 0;index < optionList.size();index++){ //(1)option做爲標籤名 String option = optionList.get(index); //(2)index結合questionId查詢optionEngagedCount String optionValue = "%," + index + ",%"; int optionCount = answerMapper.SelectOptionEngagedCount(questionId,optionValue); map.put(option, optionCount); } String title = question.getQuestionName()+count+"次參與"; Object chart = DataprocessUtils.generateChart(title, map); return (JFreeChart) chart; }
//經過response對象獲取一個可以給瀏覽器返回數據的輸出流
ServletOutputStream outputStream = response.getOutputStream();
//藉助ChartUtilities工具類的方法將圖表數據寫入到上面獲取的輸出流
ChartUtilities.writeChartAsJPEG(outputStream, chart, 1200, 600);
統計答案中的數據,
SELECTCOUNT(*)FROMguest_answerWHERE question_id = 19AND CONCAT(",", answer_content, ",") LIKE '%,1,%'
answer_context
總結:首先建立JFreeChart對象(title,各個選項的count存進map裏面),而後藉助ChartUtilities工具類的方法將圖表數據寫入文件到指定目的地
建立response的outPutStream進行輸出到瀏覽器
5.使用POI彙總數據,並將整個調查參與的結果導出爲Excel表格
why?
爲了將全部調查問卷的數據進行收集
how?
[1]數據→Excel[2]Excel→數據
②項目中將數據導出爲Excel的數據來源
[1]從URL地址中匹配surveyId[2]根據surveyId深度加載Survey對象[3]根據Survey對象中的包裹、問題數據建立List<Question>[4]根據surveyId查詢全部答案數據:List<Answer>[5]根據surveyId查詢surveyEngagedCount④生成Excel文件所須要的數據的要求
⑤符合要求的數據結構
/** * 導出excel表 * @throws IOException */ @RequestMapping(value="manager/survey/exportExcel/{surveyId}",method=RequestMethod.GET) public void exportExcel(@PathVariable(value="surveyId") Integer surveyId, HttpServletResponse response) throws IOException{ //1.生成excel對象 HSSFWorkbook workbook = statisticsService.getWorkBook(surveyId); //2.將Excel文件如下載形式返回給瀏覽器 //i.設置響應數據的內容類型 response.setContentType("application/vnd.ms-excel"); //ii.生成文件名 String filename = System.nanoTime()+".xls"; //iii.在響應消息頭中設置文件名 response.setHeader("Content-Disposition", "attachment;filename="+filename); //iv.獲取一個可以給瀏覽器返回二進制數據的輸出流 ServletOutputStream outputStream = response.getOutputStream(); //v.將workbook對象寫入這個輸出流 workbook.write(outputStream); }
//1.生成excel對象
public HSSFWorkbook getWorkBook(Integer surveyId) throws FileNotFoundException { //1.獲取數據 //2.建表 HSSFWorkbook workbook = new HSSFWorkbook(); //獲取表名 //獲取題目數據,構建excel表名 Survey survey = surveyMapper.getSurveyDeeply(surveyId); String surveyName = survey.getSurveyName(); int count = answerMapper.getSurveyEngagedCount(surveyId); String sheetName = surveyName+"共有"+count+"調查"; HSSFSheet sheet = workbook.createSheet(sheetName); //iv.若是surveyEngagedCount被參與的次數爲零,則中止函數執行 if(count == 0) { return workbook; } //建立首行,包括行標題 //1.遍歷全部題目填進第一行 LinkedHashSet<Bag> bagSet = survey.getBagSet(); List<Question> questionList = new ArrayList<>(); for(Bag bag:bagSet){ LinkedHashSet<Question> questionSet = bag.getQuestionSet(); //把set轉化爲List方便索引一一取出 questionList.addAll(questionSet); } //填寫首行 HSSFRow firstRow = sheet.createRow(0); for(int i=0;i<questionList.size();i++){ Question question = questionList.get(i); String questionName = question.getQuestionName(); HSSFCell cell = firstRow.createCell(i); cell.setCellValue(questionName); } //填充全部行答案數據 //查出全部批次的answerContext //answerContext必需要與questionId一一對應 //uuid questionId answerContext //[4]根據surveyId查詢全部答案數據:List<Answer> List<Answer> answerList = answerMapper.selectAnswerListBySurveyId(surveyId); //2.轉換數據格式 Map<String, Map<Integer, String>> bigMap = getBigMap(answerList); //填充答案行 //按照questionList中一一查出的id對smallMap進行取值,從而一一對應 //v.從bigMap中獲取values部分 Collection<Map<Integer,String>> values = bigMap.values(); //vi.將values轉換爲List集合 List<Map<Integer,String>> smallMapList = new ArrayList(values); //遍歷smallMapList //Map<uuid, Map<questionId, answerContext>> bigMap //uuid-->對應一行的questionId,因此uuid的數目爲行(即smallMapList.size()),以questionId遍歷question單元格 for(int i=0;i<smallMapList.size();i++){ //獲取第一個 Map<Integer, String> smallMap = smallMapList.get(i); //viii.這裏注意:i控制行索引 int rowIndex = i + 1; //ix.根據rowIndex建立行 HSSFRow row = sheet.createRow(rowIndex); //x.建立具體單元格 for(int j=0;j<questionList.size();j++){ HSSFCell cell = row.createCell(j); //xi.以j爲索引從questionList中獲取Question對象 Question question = questionList.get(j); //xii.從Question對象中獲取questionId Integer questionId = question.getQuestionId(); //xiii.以questionId爲鍵從smallMap中獲取對應的答案內容 String context = smallMap.get(questionId); //xiv.用content設置當前單元格內容 cell.setCellValue(context); } } return workbook; }
把全部答案內容進行處理:
//根據answerList將數據轉換爲適合生成Excel表的形式 //一個uuid對應一套的questionId,因此smallMap中的questionId只要相同就要賦值給同樣的smallMap元素 //不停建立map,獲得不一樣的地址,相同的uuid的smallMap指向同一個地址 private Map<String, Map<Integer, String>> getBigMap(List<Answer> answerList) { //1.建立空的bigMap Map<String,Map<Integer,String>> bigMap = new HashMap<>(); //2.遍歷answerList,在遍歷過程當中解析Answer對象的數據存入bigMap for(int i=0;i<answerList.size();i++){ Answer answer = answerList.get(i); String uuid = answer.getUuid(); Integer questionId = answer.getQuestionId(); String context = answer.getAnswerContext(); //3.先嚐試從bigMap中獲取smallMap,由於answer中有不少重複的uuid //避免重複建立 Map<Integer, String> smallMap = bigMap.get(uuid); if(smallMap==null){ //4.smallMap若是爲null,說明這是此前沒有建立過對應的smallMap smallMap = new HashMap<>(); //5.將建立好的smallMap存入bigMap,下次再經過一樣的uuid獲取就不會是null了 bigMap.put(uuid, smallMap); } //6.將數據存入smallMap smallMap.put(questionId, context); } return bigMap; }
關鍵點:
建立bigMap-->smallMap獲得
String context = smallMap.get(questionId);
按照questionList中一一查出的id對smallMap進行取值,從而一一對應
總結:建立
HSSFWorkbook 建表
1).對每一行進行填充,第一行填充題目:
構建questionList,list有索引,後面進行答案填充時能夠利用索引找到對應的答案
for(Bag bag:bagSet){
LinkedHashSet<Question> questionSet = bag.getQuestionSet(); //把set轉化爲List方便索引一一取出 questionList.addAll(questionSet); } //填寫首行 HSSFRow firstRow = sheet.createRow(0); for(int i=0;i<questionList.size();i++){ Question question = questionList.get(i); String questionName = question.getQuestionName(); HSSFCell cell = firstRow.createCell(i); cell.setCellValue(questionName); }
2).填充答案
for(int i=0;i<smallMapList.size();i++){
//獲取第一個
Map<Integer, String> smallMap = smallMapList.get(i); //viii.這裏注意:i控制行索引 int rowIndex = i + 1; //ix.根據rowIndex建立行 HSSFRow row = sheet.createRow(rowIndex); //x.建立具體單元格 for(int j=0;j<questionList.size();j++){ HSSFCell cell = row.createCell(j); //xi.以j爲索引從questionList中獲取Question對象 Question question = questionList.get(j); //xii.從Question對象中獲取questionId Integer questionId = question.getQuestionId(); //xiii.以questionId爲鍵從smallMap中獲取對應的答案內容 String context = smallMap.get(questionId); //xiv.用content設置當前單元格內容 cell.setCellValue(context); } }
六、使用Spring提供的緩存抽象機制整合EHCache爲項目提供二級緩存
why?
爲了減輕數據庫的負擔,每次加載調查問卷時能夠進行緩存。
適合做爲緩存的條件:
1.常常查詢
2.能夠容忍偶爾的併發問題
3.不會被其餘應用修改
Survey項目中適合存入二級緩存的數據
EngageService.PageInfo<Survey> getSurveyPage(Integer userId, boolean completed, Integer pageNum);EngageService.Survey getSurveyDeeply(Integer surveyId);
try{ //1.查詢緩存 value = getCache(key); //2.若是緩存不存在 if(value==null){ //3.查詢數據庫 value=dao.select(); //4.設置緩存 setCache(key,value); } //5.返回查詢值 return value; } catch(Exception e){ }
配置EhCacheCacheManager
切面及切面表達式配置(帥選出須要緩存的方法)
<!-- spring 整合ehcache --> <!-- 自定義key生成器 --> <bean id="userKeyGenerator" class="com.lamsey.survey.Ehcache.UserKeyGenerator"/> <!-- 配置 EhCacheManagerFactoryBean工廠--> <bean id="ehCacheManagerFactoryBean" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" > <property name="configLocation" value="classpath:ehcache.xml"></property> </bean> <!-- 配置EhCacheCacheManager --> <bean id="ehCacheCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" > <property name="cacheManager" ref="ehCacheManagerFactoryBean"></property> </bean> <!--切面及切面表達式配置 --> <aop:config> <!-- 利用切面表達式找到切面切入點,進行切面編程 --> <aop:pointcut expression="execution(* *..ResService.getResByServletPath(String)) or execution(* *..AnswerService.getSurveyPage(Integer, boolean, Integer)) or execution(* *..AnswerService.getSurveyDeeply(Integer)) or execution(* *..SurveyService.completedSurvey(Integer))" id="cachePointCut" /> <!-- 承上啓下,獲得切入點,同時鏈接處理的方法。對切入點進行處理(cache) --> <!-- 緩存切面優先級高於數據庫事務切面優先級 --> <aop:advisor advice-ref="cacheAdvice" pointcut-ref="cachePointCut" order="1"/> </aop:config> <!-- 對切入點進行處理,這裏表現爲緩存 --> <!-- 這裏的自定義key【className.method.param1..paramn】 --> <cache:advice id="cacheAdvice" cache-manager="ehCacheCacheManager" key-generator="userKeyGenerator"> <!-- 在cache屬性中指定緩存區域的名稱 --> <!-- 指定要使用緩存的具體方法,要求必須是緩存切入點覆蓋範圍內的方法 --> <cache:caching cache="surveyCache"> <cache:cacheable method=" getResByServletPath" /> <cache:cacheable method="getSurveyDeeply"/> </cache:caching> <!-- 使用另一個有可能被清空數據的緩存區域 --> <cache:caching cache="surveyCacheEvicable"> <cache:cacheable method="getSurveyPage" /> <!-- 執行updateSurveyCompleted方法時清空當前緩存區域 --> <!-- 由於調查有可能更新,當更新後就須要進行從新獲取參與調查 ,因此清空該緩存--> <cache:cache-evict method="completedSurvey" all-entries="true" /> </cache:caching> </cache:advice>
爲了減小沒必要要的事務操做讓緩存切面的優先級高於事務切面的優先級。
由於是記錄到數據庫,考慮到殺雞不用牛刀。因此採用aop進行日誌記錄
利用切面來記錄日誌:
環繞通知,記錄用戶操做信息等(利用ThreadLocal產生request)
環繞通知:一個完整的try...catch...finally結構
/** * 日誌記錄儀 * @author Administrator * */ @Component @Aspect public class LogRecord { @Autowired LogService logService; @Around("execution(* *..*Service.update*(..)) || execution(* *..*Service.remove*(..))||execution(* *..*Service.regist(..))||execution(* *..*Service.save*(..)) && !execution(* com.lamsey.survey.component.service.m.LogServiceImpl.*(..))" ) public Object recordLog(ProceedingJoinPoint joinPoint){ String logOperator=null; String logOperateTime=null, methodName=null, methodType=null, inputData=null, outputData=null, exceptionType=null, exceptionMessage=null; Object returnValue =null; //獲取調用目標方法時的實參數組 //調用目標方法 try { //獲取目標方法簽名 Signature signature = joinPoint.getSignature(); //簽名中獲取方法類型屬於的類,接口 methodType = signature.getDeclaringTypeName(); //獲取方法的名字 methodName = signature.getName(); //輸入的參數 Object[] args = joinPoint.getArgs(); if(args.length>0 && args!=null){ List<Object> list = Arrays.asList(args); inputData = list.toString(); } else{ inputData="沒有輸入的參數"; } // returnValue = joinPoint.proceed(args); // } catch (Throwable e) { //將捕獲到的目標方法異常繼續向上拋出 e.printStackTrace(); //異常的類型及信息 Throwable cause = e.getCause(); if(cause!=null){ //獲取異常緣由的類型 exceptionType = cause.getClass().getName(); cause = cause.getCause(); } exceptionMessage = e.getMessage(); } finally{ //時間 logOperateTime = new SimpleDateFormat("yyyy年MM月dd日hh:mm:ss").format(new Date()); //outputValue if(returnValue!=null){ outputData = returnValue.toString(); } else{ outputData ="無有效的輸出數據"; } } //收集當前登陸的用戶信息 //建立TreadLocal,從該變量中當前線程上獲取request對象:獲取session HttpServletRequest request = SysContent.getRequest(); HttpSession session = request.getSession(); Admin admin=(Admin) session.getAttribute(GlobalNames.LOGIN_ADMIN); User user = (User) session.getAttribute(GlobalNames.LOGIN_USER); String adminPart = (admin==null)?"admin沒有登錄":admin.getAdminName(); String userPart = (user==null)?"user沒有登錄":user.getUserName(); //logOperator logOperator = adminPart + "/" + userPart; //將產生的信息存進日誌數據庫 logService.saveLog(new Log(null, logOperator, logOperateTime, methodName, methodType,inputData, outputData, exceptionType, exceptionMessage)); //將目標方法返回的數據繼續返回給上層調用的方法 return returnValue; } }
①配置切面類對應的bean②配置日誌切面的切入點表達式
③總體配置方式
④無限死循環的問題
保存日誌的方法自己也要記錄日誌,從而致使無限死循環
①在固定時間執行固定操做。②石英調度(Quartz)是實現定時任務的其中一種方式。
③石英調度和Spring整合思路
1.)工做bean配置
工做bean:建立Quartz任務類:繼承org.springframework.scheduling.quartz.QuartzJobBean
2.)配置石英任務觸發器(克龍表達式)
<property name="cronExpression" value="0 0 0 15 * ? *"></property>
3.)配置任務調度工廠Bean
<!-- 註冊監聽器 ,保證一啓動就建立三張表--> <bean id="createTableListener" class="com.lamsey.survey.log.listener.CreateTableListener"></bean>
<!--========= Quartz石英時鐘====== --> <!-- 工做的bean --> <bean id="jobDetailBean" class="org.springframework.scheduling.quartz.JobDetailBean"> <!--CreateTable的bean由 JobDetailBean建立,不是ioc容器建立,因此logServiceImpl須要注意 --> <property name="jobClass" value="com.lamsey.survey.log.quartz.CreateTable" ></property> <property name="jobDataMap"> <map> <!-- 特殊配置:裝配logService --> <entry key="logService" value-ref="logServiceImpl"></entry> </map> </property> </bean>
<!-- 配置石英任務觸發器 --> <bean id="cronTriggerFactoryBean" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> <property name="jobDetail" ref="jobDetailBean"></property> <property name="cronExpression" value="0 0 0 15 * ? *"></property> </bean> <!-- 設置日程表 --> <!-- 配置任務調度工廠Bean --> <bean id="startQuertz" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref bean="cronTriggerFactoryBean"/> </list> </property> </bean>
7.使用路由器數據源實現數據庫操做在主數據庫和日誌數據庫之間的切換
多數據源使用路由器數據源管理而後再裝配
路由器數據源:抽象類AbstractRoutingDataSource
因此要想實現數據庫操做的切換,須要實現抽象路由數據源
③在Spring配置文件中配置自定義路由器數據源從當前線程上獲取key信息將key信息從線程上移除將key信息做爲返回值返回
以鍵值對形式指定全部目標數據源指定默認數據源——在determineCurrentLookupKey()返回null時使用
Spring監聽器建表石英任務建表保存日誌信息分頁查詢日誌數據※注意:由於每次用完後key信息須要從線程上移除,因此哪怕是同一個線程每個具體操做前也須要重複設置※注意:自動建表時的SQL須要參照主數據庫的manager_log或將manager_log複製到日誌數據庫
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import com.lamsey.survey.log.thread.NMRoutingToken; /** * 路由器數據源切換實現 * @author Administrator * */ public class NMRoutingDataSource extends AbstractRoutingDataSource{ @Override protected Object determineCurrentLookupKey() { //獲取當前線程的令牌 NMRoutingToken token = NMRoutingToken.getCurrentToken(); if (token != null) { String dataSourceName = token.getDataSourceName(); //將key從當前線程上移除 NMRoutingToken.unbindToken(); return dataSourceName; } return null; } }
<!--2.配置數據源 --> <!-- 垂直分庫,log庫另外存儲,因此須要採用路由數據源 --> <context:property-placeholder location="classpath:dbconfig.properties"/> <bean id="comboPooledDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${prop.user}"></property> <property name="password" value="${prop.password}"></property> <property name="jdbcUrl" value="${prop.jdbcUrl}"></property> <property name="driverClass" value="${prop.driverClass}"></property> </bean> <bean id="logDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${log.user}"></property> <property name="password" value="${log.password}"></property> <property name="jdbcUrl" value="${log.jdbcUrl}"></property> <property name="driverClass" value="${log.driverClass}"></property> </bean> <!-- 實現了抽現類AbstractRoutingDataSource的類 --> <bean id="nMRoutingDataSource" class="com.lamsey.survey.log.router.NMRoutingDataSource"> <property name="targetDataSources"> <map> <!-- 當輸入log時,調用 logDataSource數據庫--> <entry key="LOG_DATA_SOURCE_KEY" value-ref="logDataSource"></entry> </map> </property> <property name="defaultTargetDataSource" ref="comboPooledDataSource"/> </bean>