1.日志管理設計說明
1.1 業務設計說明
本子產品主要是實作對使用者行為日志(例如誰在什麼時間點執行了什麼操作,通路了哪些方法,傳遞的什麼參數,執行時長等)進行記錄、查詢、删除等操作。其表設計語句如下:
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='系統日志';
1.2 原型設計說明
基于使用者需求,實作靜态頁面(html/css/js),通過靜态頁面為使用者呈現基本需求實作,如圖所示:

說明:假如客戶對此原型進行了确認,後續則可以基于此原型進行研發
1.3 API設計說明
日志業務背景 API 分層架構及調用關系如圖所示:
2.日志管理清單頁面呈現
2.1 業務時序分析
當點選首頁左側的"日志管理"菜單時,其總體時序分析如圖所示:
2.2 服務端實作
2.2.1 Controller實作
-
業務描述與設計實作
基于日志管理的請求業務,在 PageController 中添加 doLogUI 方法,doPageUI 方法分别用于傳回日志清單頁面,日志分頁頁面
-
關鍵代碼設計與實作
第一步:在 PageController 中定義傳回日志清單的方法。代碼如下:
@RequestMapping("log/log_list")
public String doLogUI() {
return "sys/log_list";
}
第二步:在 PageController 中定義用于傳回分頁頁面的方法。代碼如下:
@RequestMapping("doPageUI")
public String doPageUI() {
return "common/page";
}
2.3 用戶端實作
2.3.1 日志菜單事件處理
-
業務描述與設計
首先準備日志清單頁面 (/templates/pages/sys/log_list.html) ,然後在starter.html 頁面中點選日志管理菜單時異步加載日志清單頁面
-
關鍵代碼設計與實作
找到項目中的 starter.html 頁面,頁面加載完成以後,注冊日志管理菜單項的點選事件,當點選日志管理時,執行事件處理函數。關鍵代碼如下:
$(function() {
doLoadUI("load-log-id","log/log_list")
})
function doLoadUI(id,url) {
$("#"+id).click(function(){
$("#mainContentId").load(url);
});
}
其中,load 函數為 jquery 中的 ajax 異步請求函數
2.3.2 日志清單頁面事件處理
-
業務描述與設計實作
當日志清單頁面加載完成以後異步加載分頁頁面(page.html)
-
關鍵代碼設計與實作
在 log_list.html 頁面中異步加載 page 頁面,這樣可以實作分頁頁面重用,哪裡需要分頁頁面,哪裡就進行頁面加載即可。關鍵代碼如下:
$(function(){
$("#pageId").load("doPageUI");
});
說明:資料加載通常是一個相對比較耗時操作,為了改善使用者體驗,可以先為使用者呈現一個頁面,資料加載時,顯示資料正在加載中,資料加載完成以後再呈現資料。這樣也可滿足現階段不同類型用戶端需求(例如手機端,電腦端,電視端,手表端。)
3.日志管理清單資料呈現
3.1 資料架構分析
日志查詢服務端資料基本架構,如圖所示:
3.2 伺服器API架構及業務時序圖分析
服務端日志分頁查詢代碼基本架構,如圖所示:
服務端日志清單資料查詢時序圖,如圖所示:
3.3 服務端業務及代碼實作
3.3.1 pojo類實作
-
業務描述及設計實作
建構實體對象(POJO)封裝從資料庫查詢到的記錄,一行記錄映射為記憶體中一個的這樣的對象。對象屬性定義時盡量與表中字段有一定的映射關系,并添加對應的set/get/toString 等方法,便于對資料進行更好的操作
- 關鍵代碼分析及實作
package com.cy.pj.sys.pojo;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
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;
說明:通過此對象除了可以封裝從資料庫查詢的資料,還可以封裝用戶端請求資料,實作層與層之間資料的傳遞。
思考:這個對象的 set 方法,get 方法可能會在什麼場景用到?
3.3.2 Dao接口實作
-
業務描述及設計實作
通過資料層對象,基于業務層參數資料查詢日志記錄總數以及目前頁要呈現的使用者行為日志資訊
-
關鍵代碼分析及實作
第一步:定義資料層接口對象,通過将此對象保證給業務層以提供日志資料通路。代碼如下:
@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);
說明:
- 當DAO 中方法參數多餘一個時盡量使用@Param 注解進行修飾并指定名字,然後在Mapper 檔案中便可以通過類似#{username}方式進行擷取,否則隻能通過#{arg0},#{arg1}或者#{param1},#{param2}等方式進行擷取。
- 當 DAO 方法中的參數應用在動态 SQL 中時無論多少個參數,盡量使用@Param 注解進行修飾并定義
3.3.3 Mapper檔案實作
-
業務描述及設計實作
基于 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.pojo.SysLog">
select *
<include refid="queryWhereId"/>
order by createdTime desc
limit #{startIndex},#{pageSize}
</select>
思考:
- 動态 sql:基于使用者需求動态拼接 SQL
- Sql 标簽元素的作用是什麼?對sql 語句中的共性進行提取,以遍實作更好的複用.
- Include 标簽的作用是什麼?引入使用 sql 标簽定義的元素
3.3.4 Service接口及實作類
-
業務描述與設計實作
業務層主要是實作子產品中業務邏輯的處理。在日志分頁查詢中,業務層對象首先要通過業務方法中的參數接收控制層資料(例如 username,pageCurrent)并校驗。然後基于使用者名進行總記錄數的查詢并校驗,再基于起始位置及頁面大小進行目前頁記錄的查詢,最後對查詢結果進行封裝并傳回
-
關鍵代碼設計及實作
業務值對象定義,基于此對象封裝資料層傳回的資料以及計算的分頁資訊,具體代碼參考如下:
package com.cy.pj.common.pojo;
@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;
}
}
定義日志業務接口及方法,暴露外界對日志業務資料的通路,其代碼參考如下:
package com.cy.pj.sys.service;
public interface SysLogService {
/**
* @param name 基于條件查詢時的參數名
* @param pageCurrent 目前的頁碼值
* @return 目前頁記錄+分頁資訊
*/
PageObject<SysLog> findPageObjects(String username,Integer pageCurrent);
}
日志業務接口及實作類,用于具體執行日志業務資料的分頁查詢操作,其代碼如下:
package com.cy.pj.sys.service.impl;
@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 = 7793296502722655579L;
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 等
3.3.5 Controller類實作
-
業務描述與設計實作
控制層對象主要負責請求和響應資料的處理,例如,本子產品首先要通過控制層對象處理請求參數,然後通過業務層對象執行業務邏輯,再通過 VO 對象封裝響應結果(主要對業務層資料添加狀态資訊),最後将響應結果轉換為 JSON 格式的字元串響應到用戶端
-
關鍵代碼設計與實作
定義控制層值對象(VO),目的是基于此對象封裝控制層響應結果(在此對象中主要是為業務層執行結果添加狀态資訊)。Spring MVC 架構在響應時可以調用相關 API(例如jackson)将其對象轉換為 JSON 格式字元串
package com.cy.pj.common.pojo;
@Data
public class JsonResult implements Serializable {
private static final long serialVersionUID = -856924038217431339L;//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;
@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 java.util.logging.Logger;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.cy.pj.common.vo.JsonResult;
@ControllerAdvice
public class GlobalExceptionHandler {
//JDK 中的自帶的日志 API
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public JsonResult doHandleRuntimeException(RuntimeException e){
e.printStackTrace();//也可以寫日志異常資訊
return new JsonResult(e);//封裝
}
}
控制層響應資料處理分析,如圖所示:
3.4 用戶端關鍵業務及代碼實作
3.4.1 用戶端頁面事件分析
當使用者點選首頁日志管理時,其頁面流轉分析如圖所示:
3.4.2 日志清單資訊呈現
-
業務描述與設計實作
日志分頁頁面加載完成以後,向服務端發起異步請求加載日志資訊,當日志資訊加載完成需要将日志資訊、分頁資訊呈現到清單頁面上
-
關鍵代碼設計與實作
第一步:分頁頁面加載完成,向服務端發起異步請求,代碼參考如下:
$(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;
}
3.4.3 分頁資料資訊呈現
-
業務描述與設計實作
日志資訊清單初始化完成以後初始化分頁資料(調用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 函數 }
3.4.4 清單頁面資訊查詢實作
-
業務描述及設計
當使用者點選日志清單的查詢按鈕時,基于使用者輸入的使用者名進行有條件的分頁查詢,并将查詢結果呈現在頁面
-
關鍵代碼設計與實作
第一步:日志清單頁面加載完成,在查詢按鈕上進行事件注冊。代碼如下:
第二步:定義查詢按鈕對應的點選事件處理函數。代碼如下:
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);
}
);
}
4.日志管理删除操作實作
4.1 資料架構分析
當使用者執行日志删除操作時,用戶端與服務端互動時的基本資料架構,如圖所示:
4.2 删除業務時序分析
用戶端送出删除請求,服務端對象的工作時序分析,如圖所示:
4.3 服務端關鍵業務及代碼實作
4.3.1 Dao接口實作
-
業務描述及設計實作
資料層基于業務層送出的日志記錄 id,進行日志删除操作
-
關鍵代碼設計及實作
在 SysLogDao 中添加基于 id 執行日志删除的方法。代碼參考如下:
4.3.2 Mapper檔案實作
-
業務描述及設計實作
在 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 的文法
4.3.3 Service接口及實作類
-
業務描述與設計實作
在日志業務層定義用于執行删除業務的方法,首先通過方法參數接收控制層傳遞的多個記錄的 id,并對參數 id 進行校驗。然後基于日志記錄 id 執行删除業務實作。最後傳回業務執行結果
-
關鍵代碼設計與實作
第一步:在 SysLogService 接口中,添加基于多個 id 進行日志删除的方法。關鍵代碼如下:
第二步:在 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;
}
4.3.4 Controller類實作
-
業務描述與設計實作
在日志控制層對象中,添加用于處理日志删除請求的方法。首先在此方法中通過形參接收用戶端送出的資料,然後調用業務層對象執行删除操作,最後封裝執行結果,并在運作時将響應對象轉換為 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
4.4 用戶端關鍵業務及代碼實作
4.4.1 日志清單頁面事件處理
-
業務描述及設計實作
使用者在頁面上首先選擇要删除的元素,然後點選删除按鈕,将使用者選擇的記錄 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();
}
說明:最後将如上方法添加在删除操作成功以後的代碼塊中
5.日志管理資料添加實作
5.1 服務端關鍵業務及代碼實作
這塊業務學了 AOP 以後再實作
5.1.1 Dao 接口實作
-
業務描述與設計實作
資料層基于業務層的持久化請求,将業務層送出的使用者行為日志資訊寫入到資料庫
-
關鍵代碼設計與實作
在 SysLogDao 接口中添加用于實作日志資訊持久化的方法。關鍵代碼如下:
5.1.2 Mapper 映射檔案
-
業務描述與設計實作
基于 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>
5.1.3 Service 接口及實作類
-
業務描述與設計實作
将日志切面中抓取到的使用者行為日志資訊,通過業務層對象方法持久化到資料庫
-
關鍵代碼實作
第一步:在 SysLogService 接口中,添加儲存日志資訊的方法。關鍵代碼如下:
第二步:在 SysLogServiceImpl 類中添加,儲存日志的方法實作。關鍵代碼如下:
@Override
public void saveObject(SysLog entity) {
sysLogDao.insertObject(entity);
}
5.1.4 日志切面Aspect實作
-
業務描述與設計實作
在日志切面中,抓取使用者行為資訊,并将其封裝到日志對象然後傳遞到業務,通過業務層對象對日志日志資訊做進一步處理。此部分内容後續結合 AOP 進行實作(暫時先了解,不做具體實作)
-
關鍵代碼設計與實作
springboot 工程中應用 AOP時,首先要添加如下依賴(假如有則無需添加):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定義日志切面類對象,通過環繞通知處理日志記錄操作。關鍵代碼如下:
@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);
}
}
方法中用到的 ip 位址擷取需要提供一個如下的工具類:(不用自己實作,直接用)
public class IPUtils {
private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
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;
}
}
原理分析,如圖所示:
6.總結
6.1 重難點分析
- 日志管理整體業務分析與實作
- 分層架構(應用層 MVC:基于 spring 的 mvc 子產品)。
- API 架構(SysLogDao,SysLogService,SysLogController)。
- 業務架構(查詢,删除,添加使用者行為日志)。
- 資料架構(SysLog,PageObject,JsonResult,…)。
- 日志管理持久層映射檔案中 SQL元素的定義及編寫
- 定義在映射檔案”mapper/sys/SysLogMapper.xml”(必須在加載範圍内)。
- 每個 SQL 元素必須提供一個唯一 ID,對于 select 必須指定結果映射(resultType)。
- 系統底層運作時會将每個 SQL 元素的對象封裝一個值對象(MappedStatement)。
- 日志管理子產品資料查詢操作中的資料封裝
- 資料層(資料邏輯)的 SysLog 對象應用(一行記錄一個 log 對象)。
- 業務層(業務邏輯)PageObject 對象應用(封裝每頁記錄以及對應的分頁資訊)。
- 控制層(控制邏輯)的 JsonResult 對象應用(對業務資料添加狀态資訊)。
- 日志管理控制層請求資料映射,響應資料的封裝及轉換(轉換為 json 串)
- 請求路徑映射,請求方式映射(GET,POST),請求參數映射(直接量,POJO)。
- 響應資料兩種(頁面,JSON 串)。
- 日志管理子產品異常處理如何實作的
- 請求處理層(控制層)定義統一(全局)異常處理類。
- 使用注解@RestControllerAdvice 描述類,使用@ExceptionHandler 描述方法.
- 異常處理規則:能處理則處理,不能處理則抛出。
6.2 FAQ 分析
▪ 使用者行為日志表中都有哪些字段?(面試時有時會問)
▪ 使用者行為日志是如何實作分頁查詢的?(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),斷點,搜尋引擎)
6.3 BUG分析
- 無法找到對應的 Bean對象(NoSuchBeanDefinitionException),如圖所示: 問題分析:
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結
- 檢測 key 的名字寫的是否正确。
- 檢測 spring 對此 Bean 對象的掃描,對于 dao 而言。
- 使用有@Mapper 注解描述或者在@MapperScan 掃描範圍之内。
- 以上都正确,要檢測是否編譯了。
- 綁定異常(BindingException),如圖所示: 問題分析:
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結
- 接口的類全名與對應的映射檔案命名空間不同。
- 接口的方法名與對應的映射檔案元素名不存在。
- 檢測映射檔案的路徑與 application.properties 或者 application.yml 中的配置是否一緻。
- 以上都沒有問題時,檢測你的類和映射檔案是否正常編譯。
- 反射異常(ReflectionException),如圖所示: 問題分析:
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結
- 映射檔案中動态 sql 中使用的參數在接口方法中沒有使用@Param 注解修飾
-
假如使用了注解修飾還要檢測名字是否一緻。
說明:當動态 sql 的參數在接口中沒有使用@Param 注解修飾,還可以借助_parameter 這個變量擷取參數的值(mybatis 中的一種規範)。
- 結果映射異常,如圖所示: 問題分析:getRowCount 元素可能沒有寫 resultType 或 resultMap。
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結 - 綁定異常,如圖所示: 問題分析:綁定異常,檢測 findPageObjects 方法參數與映射檔案參數名字是否比對或者假如版本不是最新版本需要使用@Param 注解描述。
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結 - Bean建立異常,如圖所示: 問題分析:應該是查詢時的結果映射對的類全名寫錯了。
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結 - 請求方式不比對,如圖所示: 問題分析:請求方式與控制層處理方式不比對。
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結 - 響應結果異常,如圖所示: 問題分析:服務端響應資料不正确,例如服務端沒有注冊将對象轉換為 JSON 串的Bean
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結 - 請求參數異常,如圖所示: 問題分析:用戶端請求參數中不包含服務端控制層方法參數或格式不比對。
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結 - JS編寫錯誤,如圖所示: 問題分析:點選右側 VM176:64 位置進行檢視。
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結 - JS編寫錯誤,如圖所示: 問題分析:找到對應位置,檢測 data 的值以及資料來源。
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結 - JS編寫錯誤,如圖所示: 問題分析:找到對應位置,假如無法确定位置,可排除法或打樁,debug 分析。
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結 - JS寫錯誤,如圖所示: 問題分析:調用 length 方法的對象有問題,可先檢測下對象的值。
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結 - JS編寫錯誤,如圖所示: 問題分析:檢測 record 定義或指派的地方。
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結 - 資源沒找到,如圖所示: 問題分析:服務端資源沒找到,檢查 url 和 controller 映射,不要點選圖中的 jquery。
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結 - 視圖解析異常,如圖所示: 問題分析:檢查服務端要通路的方法上是否有@ResponseBody 注解。
動吧旅遊生态系統--日志1.日志管理設計說明2.日志管理清單頁面呈現3.日志管理清單資料呈現4.日志管理删除操作實作5.日志管理資料添加實作6.總結