1. 程式人生 > >一文帶你學會基於SpringAop實現操作日誌的記錄

一文帶你學會基於SpringAop實現操作日誌的記錄

### 前言 大家好,這裡是經典雞翅,今天給大家帶來一篇基於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> maps = jdbcTemplate.queryForList(sql, selectPrimaryKey); if(maps!=null){ operateLogInfo.setPreModifiedData(new Gson().toJson(maps)); } }catch (Exception e){ e.printStackTrace(); throw new RuntimeException("查詢操作前資料出錯!"); } }else { throw new RuntimeException("表名、主鍵名或主鍵值 存在空值情況,請核實!"); } }else{ operateLogInfo.setPreModifiedData(""); } ``` ### 切面的完整實現程式碼 ``` @Aspect @Component public class OperateLogAspect { @Autowired private JdbcTemplate jdbcTemplate; @Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)") private void operateLogPointCut(){ } @Around("operateLogPointCut()") public Object around(ProceedingJoinPoint pjp) throws Throwable { Object responseObj = null; OperateLogInfo operateLogInfo = new OperateLogInfo(); String flag = "success"; try{ HttpServletRequest request = SpringContextUtil.getHttpServletRequest(); DomainUserDetails currentUser = SecurityUtils.getCurrentUser(); if(currentUser!=null){ operateLogInfo.setUserId(currentUser.getId()); operateLogInfo.setUserName(currentUser.getUsername()); } 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)); } 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> maps = jdbcTemplate.queryForList(sql, selectPrimaryKey); if(maps!=null){ operateLogInfo.setPreModifiedData(new Gson().toJson(maps)); } }catch (Exception e){ e.printStackTrace(); throw new RuntimeException("查詢操作前資料出錯!"); } }else { throw new RuntimeException("表名、主鍵名或主鍵值 存在空值情況,請核實!"); } }else{ operateLogInfo.setPreModifiedData(""); } //操作時間 Date beforeDate = new Date(); Long startTime = beforeDate.getTime(); operateLogInfo.setExecuteTime(beforeDate); responseObj = pjp.proceed(); Date afterDate = new Date(); Long endTime = afterDate.getTime(); Long duration = endTime - startTime; operateLogInfo.setDuration(duration); operateLogInfo.setIp(getIp(request)); operateLogInfo.setResult(flag); }catch (RuntimeException e){ throw new RuntimeException(e); }catch (Exception e){ flag = "fail"; operateLogInfo.setResult(flag); operateLogInfo.setErrorMessage(e.getMessage()); operateLogInfo.setErrorStackTrace(e.getStackTrace().toString()); e.printStackTrace(); }finally { insertIntoLogTable(operateLogInfo); } return responseObj; } private void insertIntoLogTable(OperateLogInfo operateLogInfo){ operateLogInfo.setId(UUID.randomUUID().toString().replace("-","")); String sql="insert into energy_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()); } 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; } } ``` ### 示例的使用方式 針對於示例來說我們要在controller上面打上操作日誌的註解。 ``` @PostMapping("/updateInfo") @OperateLog(operation = "修改資訊",operateType = GlobalStaticParas.OPERATE_MOD) public void updateInfo(@RequestBody Info info) { service.updateInfo(info); } ``` 針對於Info的實體類,我們則要對其中的欄位和表名進行標識。 ``` @Data @SelectTable(tableName = "info",idName = "id") public class Info { @SelectPrimaryKey private String id; private String name; } ``` ### 總結 文章寫到這,也就結束了,文中難免有不足,歡迎大家批評指正,另外可以關注我的公眾號,進群交流哦。