本模塊主要是實現對用戶行爲日誌(例如誰在什麼時間點執行了什麼操做,訪問了哪些方法,傳遞的什麼參數,執行時長等)進行記錄、查詢、刪除等操做。其表設計語句以下:css
CREATE TABLE `sys_logs` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) DEFAULT NULL COMMENT '登錄用戶名', `operation` varchar(50) DEFAULT NULL COMMENT '用戶操做', `method` varchar(200) DEFAULT NULL COMMENT '請求方法', `params` varchar(5000) DEFAULT NULL COMMENT '請求參數', `time` bigint(20) NOT NULL COMMENT '執行時長(毫秒)', `ip` varchar(64) DEFAULT NULL COMMENT 'IP地址', `createdTime` datetime DEFAULT NULL COMMENT '日誌記錄時間', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='系統日誌';
基於用戶需求,實現靜態頁面(html/css/js),經過靜態頁面爲用戶呈現基本需求實現,如圖所示。
說明:假如客戶對此原型進行了確認,後續則能夠基於此原型進行研發。html
日誌業務後臺API分層架構及調用關係如圖所示:
說明:分層目的主要將複雜問題簡單化,實現各司其職,各盡所能。java
當點擊首頁左側的"日誌管理"菜單時,其整體時序分析如圖所示:
jquery
▪ 業務描述與設計實現
基於日誌管理的請求業務,在PageController中添加doLogUI方法,doPageUI方法分別用於返回日誌列表頁面,日誌分頁頁面。web
▪ 關鍵代碼設計與實現
第一步:在PageController中定義返回日誌列表的方法。代碼以下:面試
@RequestMapping("{module}/{moduleUI}") public String doModuleUI(@PathVariable String moduleUI){ return "sys/"+moduleUI; }
第二步:在PageController中定義用於返回分頁頁面的方法。代碼以下:ajax
@RequestMapping("doPageUI") public String doPageUI() { return "common/page"; }
▪ 業務描述與設計
首先準備日誌列表頁面(/templates/pages/sys/log_list.html),而後在starter.html頁面中點擊日誌管理菜單時異步加載日誌列表頁面。spring
▪ 關鍵代碼設計與實現
找到項目中的starter.html 頁面,頁面加載完成之後,註冊日誌管理菜單項的點擊事件,當點擊日誌管理時,執行事件處理函數。關鍵代碼以下:sql
$(function(){ doLoadUI("load-log-id","log/log_list") }) function doLoadUI(id,url){ $("#"+id).click(function(){ $("#mainContentId").load(url); }); }
其中,load函數爲jquery中的ajax異步請求函數。數據庫
▪ 業務描述與設計實現
當日志列表頁面加載完成之後異步加載分頁頁面(page.html)。
▪ 關鍵代碼設計與實現:
在log_list.html頁面中異步加載page頁面,這樣能夠實現分頁頁面重用,哪裏須要分頁頁面,哪裏就進行頁面加載便可。關鍵代碼以下:
$(function(){ $("#pageId").load("doPageUI"); });
說明:數據加載一般是一個相對比較耗時操做,爲了改善用戶體驗,能夠先爲用戶呈現一個頁面,數據加載時,顯示數據正在加載中,數據加載完成之後再呈現數據。這樣也可知足現階段不一樣類型客戶端需求(例如手機端,電腦端,電視端,手錶端。)
日誌查詢服務端數據基本架構,如圖所示。
服務端日誌分頁查詢代碼基本架構,如圖所示:
服務端日誌列表數據查詢時序圖,如圖所示:
▪ 業務描述及設計實現
構建實體對象(POJO)封裝從數據庫查詢到的記錄,一行記錄映射爲內存中一個的這樣的對象。對象屬性定義時儘可能與表中字段有必定的映射關係,並添加對應的set/get/toString等方法,便於對數據進行更好的操做。
▪ 關鍵代碼分析及實現
package com.cy.pj.sys.pojo; import java.io.Serializable; import java.util.Date; public class SysLog implements Serializable { private static final long serialVersionUID = 1L; private Integer id; //用戶名 private String username; //用戶操做 private String operation; //請求方法 private String method; //請求參數 private String params; //執行時長(毫秒) private Long time; //IP地址 private String ip; //建立時間 private Date createdTime; /**設置:*/ public void setId(Integer id) { this.id = id; } /**獲取:*/ public Integer getId() { return id; } /**設置:用戶名*/ public void setUsername(String username) { this.username = username; } /** 獲取:用戶名*/ public String getUsername() { return username; } /**設置:用戶操做*/ public void setOperation(String operation) { this.operation = operation; } /**獲取:用戶操做*/ public String getOperation() { return operation; } /**設置:請求方法*/ public void setMethod(String method) { this.method = method; } /**獲取:請求方法*/ public String getMethod() { return method; } /** 設置:請求參數*/ public void setParams(String params) { this.params = params; } /** 獲取:請求參數 */ public String getParams() { return params; } /**設置:IP地址 */ public void setIp(String ip) { this.ip = ip; } /** 獲取:IP地址*/ public String getIp() { return ip; } /** 設置:建立時間*/ public void setCreateDate(Date createdTime) { this.createdTime = createdTime; } /** 獲取:建立時間*/ public Date getCreatedTime() { return createdTime; } public Long getTime() { return time; } public void setTime(Long time) { this.time = time; } }
說明:經過此對象除了能夠封裝從數據庫查詢的數據,還能夠封裝客戶端請求數據,實現層與層之間數據的傳遞。
▪ 業務描述及設計實現
經過數據層對象,基於業務層參數數據查詢日誌記錄總數以及當前頁要呈現的用戶行爲日誌信息。
▪ 關鍵代碼分析及實現:
第一步:定義數據層接口對象,經過將此對象保證給業務層以提供日誌數據訪問。代碼以下:
package com.cy.pj.sys.dao; import org.apache.ibatis.annotations.Mapper; @Mapper public interface SysLogDao { }
第二步:在SysLogDao接口中添加getRowCount方法用於按條件統計記錄總數。代碼以下:
/** * @param username 查詢條件(例如查詢哪一個用戶的日誌信息) * @return 總記錄數(基於這個結果能夠計算總頁數) */ int getRowCount(@Param("username") String username);
第三步:在SysLogDao接口中添加findPageObjects方法,基於此方法實現當前頁記錄的數據查詢操做。代碼以下:
/** * @param username 查詢條件(例如查詢哪一個用戶的日誌信息) * @param startIndex 當前頁的起始位置 * @param pageSize 當前頁的頁面大小 * @return 當前頁的日誌記錄信息 * 數據庫中每條日誌信息封裝到一個SysLog對象中 */ List<SysLog> findPageObjects( @Param("username")String username, @Param("startIndex")Integer startIndex, @Param("pageSize")Integer pageSize);
說明:
1) 當DAO中方法參數多餘一個時儘可能使用@Param註解進行修飾並指定名字,而後在Mapper文件中即可以經過相似#{username}方式進行獲取,不然只能經過#{arg0},#{arg1}或者#{param1},#{param2}等方式進行獲取。
2) 當DAO方法中的參數應用在動態SQL中時不管多少個參數,儘可能使用@Param註解進行修飾並定義。
▪ 業務描述及設計實現
基於Dao接口建立映射文件,在此文件中經過相關元素(例如select)描述要執行的數據操做。
▪ 關鍵代碼設計及實現
第一步:在映射文件的設計目錄(mapper/sys)中添加SysLogMapper.xml映射文件,代碼以下:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.cy.pj.sys.dao.SysLogDao"> </mapper>
第二步:在映射文件中添加sql元素實現,SQL中的共性操做,代碼以下:
<sql id="queryWhereId"> from sys_Logs <where> <if test="username!=null and username!=''"> username like concat("%",#{username},"%") </if> </where> </sql>
第三步:在映射文件中添加id爲getRowCount元素,按條件統計記錄總數,代碼以下:
<select id="getRowCount" resultType="int"> select count(*) <include refid="queryWhereId"/> </select>
第四步:在映射文件中添加id爲findPageObjects元素,實現分頁查詢。代碼以下:
<select id="findPageObjects" resultType="com.cy.pj.sys.entity.SysLog"> select * <include refid="queryWhereId"/> order by createdTime desc limit #{startIndex},#{pageSize} </select>
1) 動態sql:基於用戶需求動態拼接SQL
2) Sql標籤元素的做用是什麼?對sql語句中的共性進行提取,以遍實現更好的複用.
3) Include標籤的做用是什麼?引入使用sql標籤訂義的元素
第五步:單元測試類SysLogDaoTests,對數據層方法進行測試。
package com.cy.pj.sys.dao; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import com.cy.pj.sys.entity.SysLog; @SpringBootTest public class SysLogDaoTests { @Autowired private SysLogDao sysLogDao; @Test public void testGetRowCount() { int rows=sysLogDao.getRowCount("admin"); System.out.println("rows="+rows); } @Test public void testFindPageObjects() { List<SysLog> list= sysLogDao.findPageObjects("admin", 0, 3); for(SysLog log:list) { System.out.println(log); } } }
▪ 業務描述與設計實現
業務層主要是實現模塊中業務邏輯的處理。在日誌分頁查詢中,業務層對象首先要經過業務方法中的參數接收控制層數據(例如username,pageCurrent)並校驗。而後基於用戶名進行總記錄數的查詢並校驗,再基於起始位置及頁面大小進行當前頁記錄的查詢,最後對查詢結果進行封裝並返回。
▪ 關鍵代碼設計及實現
業務值對象定義,基於此對象封裝數據層返回的數據以及計算的分頁信息,具體代碼參考以下:
package com.cy.pj.common.pojo.bo; import lombok.Data; import java.io.Serializable; import java.util.List; @Data public class PageObject<T> implements Serializable { private static final long serialVersionUID = 6780580291247550747L;//類泛型 /**當前頁的頁碼值*/ private Integer pageCurrent=1; /**頁面大小*/ private Integer pageSize=3; /**總行數(經過查詢得到)*/ private Integer rowCount=0; /**總頁數(經過計算得到)*/ private Integer pageCount=0; /**當前頁記錄*/ private List<T> records; public PageObject(){} public PageObject(Integer pageCurrent, Integer pageSize, Integer rowCount, List<T> records) { super(); this.pageCurrent = pageCurrent; this.pageSize = pageSize; this.rowCount = rowCount; this.records = records; // this.pageCount=rowCount/pageSize; // if(rowCount%pageSize!=0) { // pageCount++; // } this.pageCount=(rowCount-1)/pageSize+1; } public Integer getPageCurrent() { return pageCurrent; } public void setPageCurrent(Integer pageCurrent) { this.pageCurrent = pageCurrent; } public Integer getPageSize() { return pageSize; } public void setPageSize(Integer pageSize) { this.pageSize = pageSize; } public Integer getRowCount() { return rowCount; } public void setRowCount(Integer rowCount) { this.rowCount = rowCount; } public Integer getPageCount() { return pageCount; } public void setPageCount(Integer pageCount) { this.pageCount = pageCount; } public List<T> getRecords() { return records; } public void setRecords(List<T> records) { this.records = records; } }
定義日誌業務接口及方法,暴露外界對日誌業務數據的訪問,其代碼參考以下:
package com.cy.pj.sys.service.impl; import com.cy.pj.common.exception.ServiceException; import com.cy.pj.common.pojo.bo.PageObject; import com.cy.pj.sys.dao.SysLogDao; import com.cy.pj.sys.pojo.SysLog; import com.cy.pj.sys.service.SysLogService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class SysLogServiceImpl implements SysLogService { @Autowired private SysLogDao sysLogDao; @Override public PageObject<SysLog> findPageObjects( String name, Integer pageCurrent) { //1.驗證參數合法性 //1.1驗證pageCurrent的合法性, //不合法拋出IllegalArgumentException異常 if(pageCurrent==null||pageCurrent<1) throw new IllegalArgumentException("當前頁碼不正確"); //2.基於條件查詢總記錄數 //2.1) 執行查詢 int rowCount=sysLogDao.getRowCount(name); //2.2) 驗證查詢結果,假如結果爲0再也不執行以下操做 if(rowCount==0) throw new ServiceException("系統沒有查到對應記錄"); //3.基於條件查詢當前頁記錄(pageSize定義爲2) //3.1)定義pageSize int pageSize=2; //3.2)計算startIndex int startIndex=(pageCurrent-1)*pageSize; //3.3)執行當前數據的查詢操做 List<SysLog> records= sysLogDao.findPageObjects(name, startIndex, pageSize); //4.對分頁信息以及當前頁記錄進行封裝 //4.1)構建PageObject對象 PageObject<SysLog> pageObject=new PageObject<>(); //4.2)封裝數據 pageObject.setPageCurrent(pageCurrent); pageObject.setPageSize(pageSize); pageObject.setRowCount(rowCount); pageObject.setRecords(records); pageObject.setPageCount((rowCount-1)/pageSize+1); //5.返回封裝結果。 return pageObject; } }
在當前方法中須要的ServiceException是一個本身定義的異常, 經過自定義異常可更好的實現對業務問題的描述,同時能夠更好的提升用戶體驗。參考代碼以下:
package com.cy.pj.common.exception; public class ServiceException extends RuntimeException{ private static final long serialVersionUID = 5843835376260549700L; public ServiceException() { super(); } public ServiceException(String message) { super(message); // TODO Auto-generated constructor stub } public ServiceException(Throwable cause) { super(cause); // TODO Auto-generated constructor stub } }
說明:幾乎在全部的框架中都提供了自定義異常,例如MyBatis中的BindingException等。
定義Service對象的單元測試類,代碼以下:
package com.cy.pj.sys.service; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import com.cy.pj.common.vo.PageObject; import com.cy.pj.sys.entity.SysLog; @SpringBootTest public class SysLogServiceTests { @Autowired private SysLogService sysLogService; @Test public void testFindPageObjects() { PageObject<SysLog> pageObject= sysLogService.findPageObjects("admin", 1); System.out.println(pageObject); } }
▪ 業務描述與設計實現
控制層對象主要負責請求和響應數據的處理,例如,本模塊首先要經過控制層對象處理請求參數,而後經過業務層對象執行業務邏輯,再經過VO對象封裝響應結果(主要對業務層數據添加狀態信息),最後將響應結果轉換爲JSON格式的字符串響應到客戶端。
▪ 關鍵代碼設計與實現
定義控制層值對象(VO),目的是基於此對象封裝控制層響應結果(在此對象中主要是爲業務層執行結果添加狀態信息)。Spring MVC框架在響應時能夠調用相關API(例如jackson)將其對象轉換爲JSON格式字符串。
package com.cy.pj.common.pojo.vo; import lombok.Data; import java.io.Serializable; @Data public class JsonResult implements Serializable { private static final long serialVersionUID = -6205785765802397766L;//SysResult/Result/R /**狀態碼*/ private int state=1;//1表示SUCCESS,0表示ERROR /**狀態信息*/ private String message="ok"; /**正確數據*/ private Object data; public JsonResult(){} /*返回的狀態信息*/ public JsonResult(String message) { this.message = message; } /*通常查詢時調用,封裝查詢結果*/ public JsonResult(Object data) { this.data = data; } public JsonResult(Throwable t){ this.state=0;//報錯後更改狀態碼 this.message=t.getMessage();//返回錯誤信息 } }
定義Controller類,並將此類對象使用Spring框架中的@Controller註解進行標識,表示此類對象要交給Spring管理。而後基於@RequestMapping註解爲此類定義根路徑映射。代碼參考以下:
package com.cy.pj.sys.controller; import com.cy.pj.sys.service.SysLogService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/log/") public class SysLogController { @Autowired private SysLogService sysLogService; }
在Controller類中添加分頁請求處理方法,代碼參考以下:
@RequestMapping("doFindPageObjects") @ResponseBody public JsonResult doFindPageObjects(String username, Integer pageCurrent){ PageObject<SysLog> pageObject= sysLogService.findPageObjects(username,pageCurrent); return new JsonResult(pageObject); }
定義全局異常處理類,對控制層可能出現的異常,進行統一異常處理,代碼以下:
package com.cy.pj.common.web; import com.cy.pj.common.pojo.vo.JsonResult; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; @ControllerAdvice public class GlobalExceptionHandler { //JDK中的自帶的日誌API @ExceptionHandler(RuntimeException.class) @ResponseBody public JsonResult doHandleRuntimeException( RuntimeException e){ e.printStackTrace();//也能夠寫日誌異常信息 return new JsonResult(e);//封裝 } }
控制層響應數據處理分析,如圖所示:
當用戶點擊首頁日誌管理時,其頁面流轉分析如圖-8所示:
▪ 業務描述與設計實現
日誌分頁頁面加載完成之後,向服務端發起異步請求加載日誌信息,當日志信息加載完成須要將日誌信息、分頁信息呈現到列表頁面上。
▪ 關鍵代碼設計與實現
第一步:分頁頁面加載完成,向服務端發起異步請求,代碼參考以下:
$(function(){ //爲何要將doGetObjects函數寫到load函數對應的回調內部。 $("#pageId").load("doPageUI",function(){ doGetObjects(); }); });
第二步:定義異步請求處理函數,代碼參考以下:
function doGetObjects(){ //debugger;//斷點調試 //1.定義url和參數 var url="log/doFindPageObjects" var params={"pageCurrent":1};//pageCurrent=2 //2.發起異步請求 //請問以下ajax請求的回調函數參數名能夠是任意嗎?//能夠,必須符合標識符的規範 $.getJSON(url,params,function(result){ //請問result是一個字符串仍是json格式的js對象?對象 doHandleQueryResponseResult(result); } );//特殊的ajax函數 }
result 結果對象分析,如圖所示:
第三步:定義回調函數,處理服務端的響應結果。代碼以下:
function doHandleQueryResponseResult (result){ //JsonResult if(result.state==1){//ok //更新table中tbody內部的數據 doSetTableBodyRows(result.data.records);//將數據呈如今頁面上 //更新頁面page.html分頁數據 //doSetPagination(result.data); //此方法寫到page.html中 }else{ alert(result.message); } }
第四步:將異步響應結果呈如今table的tbody位置。代碼參考以下:
function doSetTableBodyRows(records){ //1.獲取tbody對象,並清空對象 var tBody=$("#tbodyId"); tBody.empty(); //2.迭代records記錄,並將其內容追加到tbody for(var i in records){ //2.1 構建tr對象 var tr=$("<tr></tr>"); //2.2 構建tds對象 var tds=doCreateTds(records[i]); //2.3 將tds追加到tr中 tr.append(tds); //2.4 將tr追加到tbody中 tBody.append(tr); } }
第五步:建立每行中的td元素,並填充具體業務數據。代碼參考以下:
function doCreateTds(data){ var tds="<td><input type='checkbox' class='cBox' name='cItem' value='"+data.id+"'></td>"+ "<td>"+data.username+"</td>"+ "<td>"+data.operation+"</td>"+ "<td>"+data.method+"</td>"+ "<td>"+data.params+"</td>"+ "<td>"+data.ip+"</td>"+ "<td>"+data.time+"</td>"; return tds; }
▪ 業務描述與設計實現
日誌信息列表初始化完成之後初始化分頁數據(調用setPagination函數),而後再點擊上一頁,下一頁等操做時,更新頁碼值,執行基於當前頁碼值的查詢。
▪ 關鍵代碼設計與實現:
第一步:在page.html頁面中定義doSetPagination方法(實現分頁數據初始化),代碼以下:
function doSetPagination(page){ //1.始化數據 $(".rowCount").html("總記錄數("+page.rowCount+")"); $(".pageCount").html("總頁數("+page.pageCount+")"); $(".pageCurrent").html("當前頁("+page.pageCurrent+")"); //2.綁定數據(爲後續對此數據的使用提供服務) $("#pageId").data("pageCurrent",page.pageCurrent); $("#pageId").data("pageCount",page.pageCount); }
第二步:分頁頁面page.html中註冊點擊事件。代碼以下:
$(function(){ //事件註冊 $("#pageId").on("click",".first,.pre,.next,.last",doJumpToPage); })
第三步:定義doJumpToPage方法(經過此方法實現當前數據查詢)
function doJumpToPage(){ //1.獲取點擊對象的class值 var cls=$(this).prop("class");//Property //2.基於點擊的對象執行pageCurrent值的修改 //2.1獲取pageCurrent,pageCount的當前值 var pageCurrent=$("#pageId").data("pageCurrent"); var pageCount=$("#pageId").data("pageCount"); //2.2修改pageCurrent的值 if(cls=="first"){//首頁 pageCurrent=1; }else if(cls=="pre"&&pageCurrent>1){//上一頁 pageCurrent--; }else if(cls=="next"&&pageCurrent<pageCount){//下一頁 pageCurrent++; }else if(cls=="last"){//最後一頁 pageCurrent=pageCount; }else{ return; } //3.對pageCurrent值進行從新綁定 $("#pageId").data("pageCurrent",pageCurrent); //4.基於新的pageCurrent的值進行當前頁數據查詢 doGetObjects(); }
修改分頁查詢方法:(看黃色底色部分)
function doGetObjects(){ //debugger;//斷點調試 //1.定義url和參數 var url="log/doFindPageObjects" //? 請問data函數的含義是什麼?(從指定元素上獲取綁定的數據) //此數據會在什麼時候進行綁定?(setPagination,doQueryObjects) var pageCurrent=$("#pageId").data("pageCurrent"); //爲何要執行以下語句的斷定,而後初始化pageCurrent的值爲1 //pageCurrent參數在沒有賦值的狀況下,默認初始值應該爲1. if(!pageCurrent) pageCurrent=1; var params={"pageCurrent":pageCurrent};//pageCurrent=2 //2.發起異步請求 //請問以下ajax請求的回調函數參數名能夠是任意嗎?能夠,必須符合標識符的規範 $.getJSON(url,params,function(result){ //請問result是一個字符串仍是json格式的js對象?對象 doHandleQueryResponseResult(result); } );//特殊的ajax函數 }
▪ 業務描述及設計
當用戶點擊日誌列表的查詢按鈕時,基於用戶輸入的用戶名進行有條件的分頁查詢,並將查詢結果呈如今頁面。
▪ 關鍵代碼設計與實現:
第一步:日誌列表頁面加載完成,在查詢按鈕上進行事件註冊。代碼以下:
$(".input-group-btn").on("click",".btn-search",doQueryObjects)
第二步:定義查詢按鈕對應的點擊事件處理函數。代碼以下:
function doQueryObjects(){ //爲何要在此位置初始化pageCurrent的值爲1? //數據查詢時頁碼的初始位置也應該是第一頁 $("#pageId").data("pageCurrent",1); //爲何要調用doGetObjects函數? //重用js代碼,簡化jS代碼編寫。 doGetObjects(); }
第三步:在分頁查詢函數中追加name參數定義(看黃色底色部分),代碼以下:
function doGetObjects(){ //debugger;//斷點調試 //1.定義url和參數 var url="log/doFindPageObjects" //? 請問data函數的含義是什麼?(從指定元素上獲取綁定的數據) //此數據會在什麼時候進行綁定?(setPagination,doQueryObjects) var pageCurrent=$("#pageId").data("pageCurrent"); //爲何要執行以下語句的斷定,而後初始化pageCurrent的值爲1 //pageCurrent參數在沒有賦值的狀況下,默認初始值應該爲1. if(!pageCurrent) pageCurrent=1; var params={"pageCurrent":pageCurrent}; //爲何此位置要獲取查詢參數的值? //一種冗餘的應用方法,目的時讓此函數在查詢時能夠重用。 var username=$("#searchNameId").val(); //以下語句的含義是什麼?動態在json格式的js對象中添加key/value, if(username) params.username=username;//查詢時須要 //2.發起異步請求 //請問以下ajax請求的回調函數參數名能夠是任意嗎?能夠,必須符合標識符的規範 $.getJSON(url,params,function(result){ //請問result是一個字符串仍是json格式的js對象?對象 doHandleQueryResponseResult(result); } ); }
當用戶執行日誌刪除操做時,客戶端與服務端交互時的基本數據架構,如圖所示。
客戶端提交刪除請求,服務端對象的工做時序分析,如圖所示。
▪ 業務描述及設計實現
數據層基於業務層提交的日誌記錄id,進行日誌刪除操做。
▪ 關鍵代碼設計及實現:
在SysLogDao中添加基於id執行日誌刪除的方法。代碼參考以下:
int deleteObjects(@Param("ids")Integer... ids);
▪ 業務描述及設計實現
在SysLogDao接口對應的映射文件中添加用於執行刪除業務的delete元素,此元素內部定義具體的SQL實現。
▪ 關鍵代碼設計與實現
在SysLogMapper.xml文件添加delete元素,關鍵代碼以下:
<delete id="deleteObjects"> delete from sys_Logs where id in <foreach collection="ids" open="(" close=")" separator="," item="id"> #{id} </foreach> </delete>
FAQ分析:如上SQL實現可能會存在什麼問題?(可靠性問題,性能問題)
從可靠性的角度分析,假如ids的值爲null或長度爲0時,SQL構建可能會出現語法問題,可參考以下代碼進行改進(先對ids的值進行斷定):
<delete id="deleteObjects"> delete from sys_logs <if test="ids!=null and ids.length>0"> where id in <foreach collection="ids" open="(" close=")" separator="," item="id"> #{id} </foreach> </if> <if test="ids==null or ids.length==0"> where 1=2 </if> </delete>
從SQL執行性能角度分析,通常在SQL語句中不建議使用in表達式,能夠參考以下代碼進行實現(重點是forearch中or運算符的應用):
<delete id="deleteObjects"> delete from sys_logs <choose> <when test="ids!=null and ids.length>0"> <where> <foreach collection="ids" item="id" separator="or"> id=#{id} </foreach> </where> </when> <otherwise> where 1=2 </otherwise> </choose> </delete>
說明:這裏的choose元素也爲一種選擇結構,when元素至關於if,otherwise至關於else的語法。
▪ 業務描述與設計實現
在日誌業務層定義用於執行刪除業務的方法,首先經過方法參數接收控制層傳遞的多個記錄的id,並對參數id進行校驗。而後基於日誌記錄id執行刪除業務實現。最後返回業務執行結果。
▪ 關鍵代碼設計與實現
第一步:在SysLogService接口中,添加基於多個id進行日誌刪除的方法。關鍵代碼以下:
int deleteObjects(@Param("ids")Integer... ids);
第二步:在SysLogServiceImpl實現類中添加刪除業務的具體實現。關鍵代碼以下:
@Override public int deleteObjects(Integer... ids) { //1.斷定參數合法性 if(ids==null||ids.length==0) throw new IllegalArgumentException("請選擇一個"); //2.執行刪除操做 int rows; try{ rows=sysLogDao.deleteObjects(ids); }catch(Throwable e){ e.printStackTrace(); //發出報警信息(例如給運維人員發短信) throw new ServiceException("系統故障,正在恢復中..."); } //4.對結果進行驗證 if(rows==0) throw new ServiceException("記錄可能已經不存在"); //5.返回結果 return rows; }
▪ 業務描述與設計實現
在日誌控制層對象中,添加用於處理日誌刪除請求的方法。首先在此方法中經過形參接收客戶端提交的數據,而後調用業務層對象執行刪除操做,最後封裝執行結果,並在運行時將響應對象轉換爲JSON格式的字符串,響應到客戶端。
▪ 關鍵代碼設計與實現
第一步:在SysLogController中添加用於執行刪除業務的方法。代碼以下:
@RequestMapping("doDeleteObjects") @ResponseBody public JsonResult doDeleteObjects(Integer... ids){ sysLogService.deleteObjects(ids); return new JsonResult("delete ok"); }
第二步:啓動tomcat進行訪問測試,打開瀏覽器輸入以下網址:
http://localhost/log/doDeleteObjects?ids=1,2,3
▪ 業務描述及設計實現
用戶在頁面上首先選擇要刪除的元素,而後點擊刪除按鈕,將用戶選擇的記錄id異步提交到服務端,最後在服務端執行日誌的刪除動做。
▪ 關鍵代碼設計與實現
第一步:頁面加載完成之後,在刪除按鈕上進行點擊事件註冊。關鍵代碼以下:
... $(".input-group-btn") .on("click",".btn-delete",doDeleteObjects) ...
第二步:定義刪除操做對應的事件處理函數。關鍵代碼以下:
function doDeleteObjects(){ //1.獲取選中的id值 var ids=doGetCheckedIds(); if(ids.length==0){ alert("至少選擇一個"); return; } //2.發異步請求執行刪除操做 var url="log/doDeleteObjects"; var params={"ids":ids.toString()}; console.log(params); $.post(url,params,function(result){ if(result.state==1){ alert(result.message); doGetObjects(); }else{ alert(result.message); } }); }
第三步:定義獲取用戶選中的記錄id的函數。關鍵代碼以下:
function doGetCheckedIds(){ //定義一個數組,用於存儲選中的checkbox的id值 var array=[];//new Array(); //獲取tbody中全部類型爲checkbox的input元素 $("#tbodyId input[type=checkbox]"). //迭代這些元素,每發現一個元素都會執行以下回調函數 each(function(){ //假如此元素的checked屬性的值爲true if($(this).prop("checked")){ //調用數組對象的push方法將選中對象的值存儲到數組 array.push($(this).val()); } }); return array; }
第四步:Thead中全選元素的狀態影響tbody中checkbox對象狀態。代碼以下:
function doChangeTBodyCheckBoxState(){ //1.獲取當前點擊對象的checked屬性的值 var flag=$(this).prop("checked");//true or false //2.將tbody中全部checkbox元素的值都修改成flag對應的值。 //第一種方案 /* $("#tbodyId input[name='cItem']") .each(function(){ $(this).prop("checked",flag); }); */ //第二種方案 $("#tbodyId input[type='checkbox']") .prop("checked",flag); }
第五步:Tbody中checkbox的狀態影響thead中全選元素的狀態。代碼以下:
function doChangeTHeadCheckBoxState(){ //1.設定默認狀態值 var flag=true; //2.迭代全部tbody中的checkbox值並進行與操做 $("#tbodyId input[type='checkbox']") .each(function(){ flag=flag&$(this).prop("checked") }); //3.修改全選元素checkbox的值爲flag $("#checkAll").prop("checked",flag); }
第六步:完善業務刷新方法,當在最後一頁執行刪除操做時,基於全選按鈕狀態及當前頁碼值,刷新頁面。關鍵代碼以下:
function doRefreshAfterDeleteOK(){ var pageCount=$("#pageId").data("pageCount"); var pageCurrent=$("#pageId").data("pageCurrent"); var checked=$("#checkAll").prop("checked"); if(pageCurrent==pageCount&&checked&&pageCurrent>1){ pageCurrent--; $("#pageId").data("pageCurrent",pageCurrent); } doGetObjects(); }
這塊業務學了AOP之後再實現.
▪ 業務描述與設計實現
數據層基於業務層的持久化請求,將業務層提交的用戶行爲日誌信息寫入到數據庫。
▪ 關鍵代碼設計與實現
在SysLogDao接口中添加用於實現日誌信息持久化的方法。關鍵代碼以下:
int insertObject(SysLog sysLog);
▪ 業務描述與設計實現
基於SysLogDao中方法的定義,編寫用於數據持久化的SQL元素。
▪ 關鍵代碼設計與實現
在SysLogMapper.xml中添加insertObject元素,用於向日志表寫入用戶行爲日誌。關鍵代碼以下:
<insert id="insertObject"> insert into sys_logs (username,operation,method,params,time,ip,createdTime) values (#{username},#{operation},#{method},#{params},#{time},#{ip},#{createdTime}) </insert>
▪ 業務描述與設計實現
將日誌切面中抓取到的用戶行爲日誌信息,經過業務層對象方法持久化到數據庫。
▪ 關鍵代碼實現
第一步:在SysLogService接口中,添加保存日誌信息的方法。關鍵代碼以下:
void saveObject(SysLog sysLog);
第二步:在SysLogServiceImpl類中添加,保存日誌的方法實現。關鍵代碼以下:
@Override public void saveObject(SysLog sysLog) { sysLogDao.insertObject(sysLog); }
▪ 業務描述與設計實現
在日誌切面中,抓取用戶行爲信息,並將其封裝到日誌對象而後傳遞到業務,經過業務層對象對日誌日誌信息作進一步處理。此部份內容後續結合AOP進行實現(暫時先了解,不作具體實現)。
▪ 關鍵代碼設計與實現
springboot工程中應用AOP時,首先要添加以下依賴(假若有則無需添加):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
定義切面使用的註解:
package com.cy.pj.common.annotion; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD)//註解做用域是方法 @Retention(RetentionPolicy.RUNTIME)//註解做用生命週期是運行時 public @interface RequiredLog { String value() default "" ; }
封裝兩個工具類:
package com.cy.pj.common.utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; public class IPUtils { public static String getIpAddr() { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String ip = null; try { ip = request.getHeader("x-forwarded-for"); if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } } catch (Exception e) { logger.error("IPUtils ERROR ", e); } return ip; } private static Logger logger = LoggerFactory.getLogger(IPUtils.class); }
package com.cy.pj.common.utils; import com.cy.pj.sys.pojo.SysUser; import org.apache.shiro.SecurityUtils; public class ShiroUtils { public static String getUsername(){ return getUser().getUsername(); } public static SysUser getUser() { return (SysUser) SecurityUtils.getSubject().getPrincipal(); } }
定義日誌切面類對象,經過環繞通知處理日誌記錄操做。關鍵代碼以下:
package com.cy.pj.common.aspect; import com.cy.pj.common.annotion.RequiredLog; import com.cy.pj.common.utils.IPUtils; import com.cy.pj.common.utils.ShiroUtils; import com.cy.pj.sys.pojo.SysLog; import com.cy.pj.sys.service.SysLogService; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.Date; @Aspect @Component public class SysLogAspect { private Logger log= LoggerFactory.getLogger(SysLogAspect.class); @Autowired private SysLogService sysLogService; @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)") public void logPointCut(){} @Around("logPointCut()") public Object around(ProceedingJoinPoint jointPoint) throws Throwable{//鏈接點 long startTime=System.currentTimeMillis(); //執行目標方法(result爲目標方法的執行結果) Object result=jointPoint.proceed(); long endTime=System.currentTimeMillis(); long totalTime=endTime-startTime; log.info("方法執行的總時長爲:"+totalTime); saveSysLog(jointPoint,totalTime); return result; } private void saveSysLog(ProceedingJoinPoint point, long totleTime) throws NoSuchMethodException,SecurityException, JsonProcessingException { //1.獲取日誌信息 MethodSignature ms= (MethodSignature)point.getSignature(); Class<?> targetClass=point.getTarget().getClass(); String className=targetClass.getName(); //獲取接口聲明的方法 String methodName=ms.getMethod().getName(); Class<?>[] parameterTypes=ms.getMethod().getParameterTypes(); //獲取目標對象方法(AOP版本不一樣,可能獲取方法對象方式也不一樣) Method targetMethod=targetClass.getDeclaredMethod(methodName,parameterTypes); //獲取用戶名,學完shiro再進行自定義實現,沒有就先給固定值 String username= ShiroUtils.getPrincipal().getUsername(); //獲取方法參數 Object[] paramsObj=point.getArgs(); System.out.println("paramsObj="+paramsObj); //將參數轉換爲字符串 String params=new ObjectMapper().writeValueAsString(paramsObj); //2.封裝日誌信息 SysLog log=new SysLog(); log.setUsername(username);//登錄的用戶 //假如目標方法對象上有註解,咱們獲取註解定義的操做值 RequiredLog requestLog=targetMethod.getDeclaredAnnotation(RequiredLog.class); if(requestLog!=null){ log.setOperation(requestLog.value()); } log.setMethod(className+"."+methodName);//className.methodName() log.setParams(params);//method params log.setIp(IPUtils.getIpAddr());//ip 地址 log.setTime(totleTime);// log.setCreateDate(new Date()); //3.保存日誌信息 sysLogService.saveObject(log); } }
原理分析,如圖所示:
▪ 日誌管理總體業務分析與實現。
1) 分層架構(應用層MVC:基於spring的mvc模塊)。
2) API架構(SysLogDao,SysLogService,SysLogController)。
3) 業務架構(查詢,刪除,添加用戶行爲日誌)。
4) 數據架構(SysLog,PageObject,JsonResult,..)。
▪ 日誌管理持久層映射文件中SQL元素的定義及編寫。
1) 定義在映射文件」mapper/sys/SysLogMapper.xml」(必須在加載範圍內)。
2) 每一個SQL元素必須提供一個惟一ID,對於select必須指定結果映射(resultType)。
3) 系統底層運行時會將每一個SQL元素的對象封裝一個值對象(MappedStatement)。
▪ 日誌管理模塊數據查詢操做中的數據封裝。
1) 數據層(數據邏輯)的SysLog對象應用(一行記錄一個log對象)。
2) 業務層(業務邏輯)PageObject對象應用(封裝每頁記錄以及對應的分頁信息)。
3) 控制層(控制邏輯)的JsonResult對象應用(對業務數據添加狀態信息)。
▪ 日誌管理控制層請求數據映射,響應數據的封裝及轉換(轉換爲json 串)。
1) 請求路徑映射,請求方式映射(GET,POST),請求參數映射(直接量,POJO)。
2) 響應數據兩種(頁面,JSON串)。
▪ 日誌管理模塊異常處理如何實現的。
1) 請求處理層(控制層)定義統一(全局)異常處理類。
2) 使用註解@RestControllerAdvice描述類,使用@ExceptionHandler描述方法.
3) 異常處理規則:能處理則處理,不能處理則拋出。
▪ 用戶行爲日誌表中都有哪些字段?(面試時有時會問)
▪ 用戶行爲日誌是如何實現分頁查詢的?(limit)
▪ 用戶行爲數據的封裝過程?(數據層,業務層,控制層)
▪ 項目中的異常是如何處理的?
▪ 頁面中數據亂碼,如何解決?(數據來源,請求數據,響應數據)
▪ 說說的日誌刪除業務是如何實現?
▪ Spring MVC 響應數據處理?(view,json)
▪ 項目你經常使用的JS函數說幾個?(data,prop,ajax,each,..)
▪ MyBatis中的@Params註解的做用?(爲參數變量指定其其別名)
▪ Jquery中data函數用於作什麼?能夠藉助data函數將數據綁定到指定對象,語法爲data(key[,value]),key和value爲本身業務中的任意數據,假如只有key表示取值。
▪ Jquery中的prop函數用於獲取html標籤對象中」標準屬性」的值或爲屬性賦值,其語法爲prop(propertyName[,propertyValue]),假如只有屬性名則爲獲取屬性值。
▪ Jquery中attr函數爲用戶獲取html標籤中任意屬性值或爲屬性賦值的一個方法,其語法爲attr(propertyName[,propertyValue]),假如只有屬性名則爲獲取屬性值。
▪ 日誌寫操做事務的傳播特性如何配置?(每次開啓新事務,沒學就暫時擱置)?▪ 日誌寫操做爲何應該是異步的?(用戶體驗會更好,不會阻塞用戶正常業務)▪ Spring 中的異步操做如何實現?,(本身直接建立線程或者藉助池中線程)▪ Spring 中的@Async如何應用?(沒學就暫時擱置)▪ 項目中的BUG分析及解決套路?(排除法,打樁(log),斷點,搜索引擎)