項目名稱:教育網—在線調查系統
項目總體流程圖:
設計調查:調查-->包裹--->問題(增刪改查)
1.調整包裹順序
2.移動復制包裹
3.深度刪除
主要生成survey_id、survey_name、completed(是否完成)、logoPath(涉及到圖片上傳)
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]文件上傳驗證失敗后,再正常更新還是能夠回到之前所在的分頁頁面
包裹的CRUD
包裹的序號默認采用包裹的id
原理:通過mybatis的xml映射文件獲取自增主鍵獲取包裹的序號,如果采用插入后查詢id最大值賦值給Order會因為線程問題出錯。
- 保存bag對象
- 查詢guest_bag表中bag_id的最大值
- 使用這個最大值設置bag_order
- T1:保存bag對象(bag_id的最大值是6)
- T2:保存bag對象(bag_id的最大值是7)
- T1:查詢最大值,得到的結果:7
- T1:設置bag_order為7就錯了
- T2……
- T1:保存bag對象,立即獲取剛剛自增產生的bag_id——6
- T2:保存bag對象,立即獲取剛剛自增產生的bag_id——7
- T1:使用已經獲取到的自增主鍵值設置bag_order為6
- T2:使用已經獲取到的自增主鍵值設置bag_order為7
③獲取自增主鍵值的方式以及相關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實際上就是最后一個包裹的索引
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對象)。
九大內置對象,都是我們可以在【代碼腳本】中或【表達式腳本】中直接使用的對象。
2、通過序列化和反序列化技術實現對象的深度復制
為什么用深度復制?
若我們系統中存在大量的對象是通過拷貝生成的,如果我們每一個類都寫一個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; }
3、使用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); } }
6、使用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進行日志記錄
- log_id
- log_operator
- log_operate_time
- method_name
- method_type
- input_data
- output_data
- exception_type
- exception_message
利用切面來記錄日志:
環繞通知,記錄用戶操作信息等(利用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>