一文帶你學會基於SpringAop實現操作日誌的記錄
阿新 • • 發佈:2020-05-27
### 前言
大家好,這裡是經典雞翅,今天給大家帶來一篇基於SpringAop實現的操作日誌記錄的解決的方案。大家可能會說,切,操作日誌記錄這麼簡單的東西,老生常談了。不!
![](https://img2020.cnblogs.com/blog/1534147/202005/1534147-20200526231912651-2032276683.png)
網上的操作日誌一般就是記錄操作人,操作的描述,ip等。好一點的增加了修改的資料和執行時間。那麼!我這篇有什麼不同呢!今天這種不僅可以記錄上方所說的一切,還增加記錄了操作前的資料,錯誤的資訊,堆疊資訊等。正文開始~~~~~
### 思路介紹
記錄操作日誌的操作前資料是需要思考的重點。我們以修改場景來作為探討。當我們要完全記錄資料的流向的時候,我們必然要記錄修改前的資料,而前臺進行提交的時候,只有修改的資料,那麼如何找到修改前的資料呢。有三個大的要素,我們需要知道修改前資料的表名,表的欄位主鍵,表主鍵的值。這樣通過這三個屬性,我們可以很容易的拼出 select * from 表名 where 主鍵欄位 = 主鍵值。我們就獲得了修改前的資料,轉換為json之後就可以存入到資料庫中了。如何獲取三個屬性就是重中之重了。我們採取的方案是通過提交的對映實體,在實體上打上註解,根據 Java 的反射取到值。再進一步拼裝獲得物件資料。那麼AOP是在哪裡用的呢,我們需要在記錄操作日誌的方法上,打上註解,再通過切面獲取到切點,一切的資料都通過反射來進行獲得。
### 定義操作日誌註解
既然是基於spinrg的aop實現切面。那麼必然是需要一個自定義註解的。用來作為切點。我們定義的註解,可以帶一些必要的屬性,例如操作的描述,操作的型別。操作的型別需要說一下,我們分為新增、修改、刪除、查詢。那麼只有修改和刪除的時候,我們需要查詢一下修改前的資料。其他兩種是不需要的,這個也可以用來作為判斷。
```
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {
String operation() default "";
String operateType() default "";
}
```
### 定義用於找到表和表主鍵的註解
表和表主鍵的註解打在實體上,內部有兩個屬性 tableName 和 idName。這兩個屬性的值獲得後,可以進行拼接 select * from 表名 where 主鍵欄位。
```
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelectTable {
String tableName() default "";
String idName() default "";
}
```
### 定義獲取主鍵值的註解
根據上面所說的三個元素,我們還缺最後一個元素主鍵值的獲取,用於告訴我們,我們應該從提交的請求的那個欄位,拿到其中的值。
```
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SelectPrimaryKey {
}
```
### 註解的總結
有了上面的三個註解,註解的準備工作已經進行完畢。我們通過反射取到資料,可以獲得一切。接下來開始實現切面,對於註解的值進行拼接處理,最終存入到我們的資料庫操作日誌表中。
### 切面的實現
對於切面來說,我們需要實現切點、資料庫的插入、反射的資料獲取。我們先分開進行解釋,最後給出全面的實現程式碼。方便大家的理解和學習。
#### 切面的定義
基於spring的aspect進行宣告這是一個切面。
```
@Aspect
@Component
public class OperateLogAspect {
}
```
#### 切點的定義
切點就是對所有的打上OperateLog的註解的請求進行攔截和加強。我們使用annotation進行攔截。
```
@Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)")
private void operateLogPointCut(){
}
```
#### 獲取請求ip的共用方法
```
private String getIp(HttpServletRequest request){
String ip = request.getHeader("X-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
```
#### 資料庫的日誌插入操作
我們將插入資料庫的日誌操作進行單獨的抽取。
```
private void insertIntoLogTable(OperateLogInfo operateLogInfo){
operateLogInfo.setId(UUID.randomUUID().toString().replace("-",""));
String sql="insert into log values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
jdbcTemplate.update(sql,operateLogInfo.getId(),operateLogInfo.getUserId(),
operateLogInfo.getUserName(),operateLogInfo.getOperation(),operateLogInfo.getMethod(),
operateLogInfo.getModifiedData(),operateLogInfo.getPreModifiedData(),
operateLogInfo.getResult(),operateLogInfo.getErrorMessage(),operateLogInfo.getErrorStackTrace(),
operateLogInfo.getExecuteTime(),operateLogInfo.getDuration(),operateLogInfo.getIp(),
operateLogInfo.getModule(),operateLogInfo.getOperateType());
}
```
#### 環繞通知的實現
日誌的實體類實現
```
@TableName("operate_log")
@Data
public class OperateLogInfo {
//主鍵id
@TableId
private String id;
//操作人id
private String userId;
//操作人名稱
private String userName;
//操作內容
private String operation;
//操作方法名稱
private String method;
//操作後的資料
private String modifiedData;
//操作前資料
private String preModifiedData;
//操作是否成功
private String result;
//報錯資訊
private String errorMessage;
//報錯堆疊資訊
private String errorStackTrace;
//開始執行時間
private Date executeTime;
//執行持續時間
private Long duration;
//ip
private String ip;
//操作型別
private String operateType;
}
```
準備工作全部完成。接下來的重點是對環繞通知的實現。思路分為資料處理、異常捕獲、finally執行資料庫插入操作。環繞通知的重點類就是ProceedingJoinPoint ,我們通過它的getSignature方法可以獲取到打在方法上註解的值。例如下方。
```
MethodSignature signature = (MethodSignature) pjp.getSignature();
OperateLog declaredAnnotation = signature.getMethod().getDeclaredAnnotation(OperateLog.class);
operateLogInfo.setOperation(declaredAnnotation.operation());
operateLogInfo.setModule(declaredAnnotation.module());
operateLogInfo.setOperateType(declaredAnnotation.operateType());
//獲取執行的方法
String method = signature.getDeclaringType().getName() + "." + signature.getName();
operateLogInfo.setMethod(method);
String operateType = declaredAnnotation.operateType();
```
獲取請求的資料,也是通過這個類來實現,這裡有一點是需要注意的,就是我們要約定引數的傳遞必須是第一個引數。這樣才能保證我們取到的資料是提交的資料。
```
if(pjp.getArgs().length>0){
Object args = pjp.getArgs()[0];
operateLogInfo.setModifiedData(new Gson().toJson(args));
}
```
接下來的一步就是對修改前的資料進行拼接。之前我們提到過如果是修改和刪除,我們才會進行資料的拼接獲取,主要是通過類來判斷書否存在註解,如果存在註解,那麼就要判斷註解上的值是否是控制或者,非空才能正確的進行拼接。取field的值的時候,要注意私有的變數需要通過setAccessible(true)才可以進行訪問。
```
if(GlobalStaticParas.OPERATE_MOD.equals(operateType) ||
GlobalStaticParas.OPERATE_DELETE.equals(operateType)){
String tableName = "";
String idName = "";
String selectPrimaryKey = "";
if(pjp.getArgs().length>0){
Object args = pjp.getArgs()[0];
//獲取操作前的資料
boolean selectTableFlag = args.getClass().isAnnotationPresent(SelectTable.class);
if(selectTableFlag){
tableName = args.getClass().getAnnotation(SelectTable.class).tableName();
idName = args.getClass().getAnnotation(SelectTable.class).idName();
}else {
throw new RuntimeException("操作日誌型別為修改或刪除,實體類必須指定表面和主鍵註解!");
}
Field[] fields = args.getClass().getDeclaredFields();
Field[] fieldsCopy = fields;
boolean isFindField = false;
int fieldLength = fields.length;
for(int i = 0; i < fieldLength; ++i) {
Field field = fieldsCopy[i];
boolean hasPrimaryField = field.isAnnotationPresent(SelectPrimaryKey.class);
if (hasPrimaryField) {
isFindField = true;
field.setAccessible(true);
selectPrimaryKey = (String)field.get(args);
}
}
if(!isFindField){
throw new RuntimeException("實體類必須指定主鍵屬性!");
}
}
if(StringUtils.isNotEmpty(tableName) &&
StringUtils.isNotEmpty(idName)&&
StringUtils.isNotEmpty(selectPrimaryKey)){
StringBuffer sb = new StringBuffer();
sb.append(" select * from ");
sb.append(tableName);
sb.append(" where ");
sb.append(idName);
sb.append(" = ? ");
String sql = sb.toString();
try{
List