SpringAop實(shí)現(xiàn)操作日志記錄
前言
大家好,這里是經(jīng)典雞翅,今天給大家?guī)?lái)一篇基于SpringAop實(shí)現(xiàn)的操作日志記錄的解決的方案。大家可能會(huì)說(shuō),切,操作日志記錄這么簡(jiǎn)單的東西,老生常談了。不!
網(wǎng)上的操作日志一般就是記錄操作人,操作的描述,ip等。好一點(diǎn)的增加了修改的數(shù)據(jù)和執(zhí)行時(shí)間。那么!我這篇有什么不同呢!今天這種不僅可以記錄上方所說(shuō)的一切,還增加記錄了操作前的數(shù)據(jù),錯(cuò)誤的信息,堆棧信息等。正文開(kāi)始~~~~~
思路介紹
記錄操作日志的操作前數(shù)據(jù)是需要思考的重點(diǎn)。我們以修改場(chǎng)景來(lái)作為探討。當(dāng)我們要完全記錄數(shù)據(jù)的流向的時(shí)候,我們必然要記錄修改前的數(shù)據(jù),而前臺(tái)進(jìn)行提交的時(shí)候,只有修改的數(shù)據(jù),那么如何找到修改前的數(shù)據(jù)呢。有三個(gè)大的要素,我們需要知道修改前數(shù)據(jù)的表名,表的字段主鍵,表主鍵的值。這樣通過(guò)這三個(gè)屬性,我們可以很容易的拼出 select * from 表名 where 主鍵字段 = 主鍵值。我們就獲得了修改前的數(shù)據(jù),轉(zhuǎn)換為json之后就可以存入到數(shù)據(jù)庫(kù)中了。如何獲取三個(gè)屬性就是重中之重了。我們采取的方案是通過(guò)提交的映射實(shí)體,在實(shí)體上打上注解,根據(jù) Java 的反射取到值。再進(jìn)一步拼裝獲得對(duì)象數(shù)據(jù)。那么AOP是在哪里用的呢,我們需要在記錄操作日志的方法上,打上注解,再通過(guò)切面獲取到切點(diǎn),一切的數(shù)據(jù)都通過(guò)反射來(lái)進(jìn)行獲得。
定義操作日志注解
既然是基于spinrg的aop實(shí)現(xiàn)切面。那么必然是需要一個(gè)自定義注解的。用來(lái)作為切點(diǎn)。我們定義的注解,可以帶一些必要的屬性,例如操作的描述,操作的類(lèi)型。操作的類(lèi)型需要說(shuō)一下,我們分為新增、修改、刪除、查詢(xún)。那么只有修改和刪除的時(shí)候,我們需要查詢(xún)一下修改前的數(shù)據(jù)。其他兩種是不需要的,這個(gè)也可以用來(lái)作為判斷。
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface OperateLog { String operation() default ""; String operateType() default ""; }
定義用于找到表和表主鍵的注解
表和表主鍵的注解打在實(shí)體上,內(nèi)部有兩個(gè)屬性 tableName 和 idName。這兩個(gè)屬性的值獲得后,可以進(jìn)行拼接 select * from 表名 where 主鍵字段。
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SelectTable { String tableName() default ""; String idName() default ""; }
定義獲取主鍵值的注解
根據(jù)上面所說(shuō)的三個(gè)元素,我們還缺最后一個(gè)元素主鍵值的獲取,用于告訴我們,我們應(yīng)該從提交的請(qǐng)求的那個(gè)字段,拿到其中的值。
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SelectPrimaryKey { }
注解的總結(jié)
有了上面的三個(gè)注解,注解的準(zhǔn)備工作已經(jīng)進(jìn)行完畢。我們通過(guò)反射取到數(shù)據(jù),可以獲得一切。接下來(lái)開(kāi)始實(shí)現(xiàn)切面,對(duì)于注解的值進(jìn)行拼接處理,最終存入到我們的數(shù)據(jù)庫(kù)操作日志表中。
切面的實(shí)現(xiàn)
對(duì)于切面來(lái)說(shuō),我們需要實(shí)現(xiàn)切點(diǎn)、數(shù)據(jù)庫(kù)的插入、反射的數(shù)據(jù)獲取。我們先分開(kāi)進(jìn)行解釋?zhuān)詈蠼o出全面的實(shí)現(xiàn)代碼。方便大家的理解和學(xué)習(xí)。
切面的定義
基于spring的aspect進(jìn)行聲明這是一個(gè)切面。
@Aspect @Component public class OperateLogAspect { }
切點(diǎn)的定義
切點(diǎn)就是對(duì)所有的打上OperateLog的注解的請(qǐng)求進(jìn)行攔截和加強(qiáng)。我們使用annotation進(jìn)行攔截。
@Pointcut("@annotation(com.jichi.aop.operateLog.OperateLog)") private void operateLogPointCut(){ }
獲取請(qǐng)求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; }
數(shù)據(jù)庫(kù)的日志插入操作
我們將插入數(shù)據(jù)庫(kù)的日志操作進(jìn)行單獨(dú)的抽取。
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()); }
環(huán)繞通知的實(shí)現(xiàn)
日志的實(shí)體類(lèi)實(shí)現(xiàn)
@TableName("operate_log") @Data public class OperateLogInfo { //主鍵id @TableId private String id; //操作人id private String userId; //操作人名稱(chēng) private String userName; //操作內(nèi)容 private String operation; //操作方法名稱(chēng) private String method; //操作后的數(shù)據(jù) private String modifiedData; //操作前數(shù)據(jù) private String preModifiedData; //操作是否成功 private String result; //報(bào)錯(cuò)信息 private String errorMessage; //報(bào)錯(cuò)堆棧信息 private String errorStackTrace; //開(kāi)始執(zhí)行時(shí)間 private Date executeTime; //執(zhí)行持續(xù)時(shí)間 private Long duration; //ip private String ip; //操作類(lèi)型 private String operateType; }
準(zhǔn)備工作全部完成。接下來(lái)的重點(diǎn)是對(duì)環(huán)繞通知的實(shí)現(xiàn)。思路分為數(shù)據(jù)處理、異常捕獲、finally執(zhí)行數(shù)據(jù)庫(kù)插入操作。環(huán)繞通知的重點(diǎn)類(lèi)就是ProceedingJoinPoint ,我們通過(guò)它的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()); //獲取執(zhí)行的方法 String method = signature.getDeclaringType().getName() + "." + signature.getName(); operateLogInfo.setMethod(method); String operateType = declaredAnnotation.operateType();
獲取請(qǐng)求的數(shù)據(jù),也是通過(guò)這個(gè)類(lèi)來(lái)實(shí)現(xiàn),這里有一點(diǎn)是需要注意的,就是我們要約定參數(shù)的傳遞必須是第一個(gè)參數(shù)。這樣才能保證我們?nèi)〉降臄?shù)據(jù)是提交的數(shù)據(jù)。
if(pjp.getArgs().length>0){ Object args = pjp.getArgs()[0]; operateLogInfo.setModifiedData(new Gson().toJson(args)); }
接下來(lái)的一步就是對(duì)修改前的數(shù)據(jù)進(jìn)行拼接。之前我們提到過(guò)如果是修改和刪除,我們才會(huì)進(jìn)行數(shù)據(jù)的拼接獲取,主要是通過(guò)類(lèi)來(lái)判斷書(shū)否存在注解,如果存在注解,那么就要判斷注解上的值是否是控制或者,非空才能正確的進(jìn)行拼接。取field的值的時(shí)候,要注意私有的變量需要通過(guò)setAccessible(true)才可以進(jìn)行訪問(wèn)。
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]; //獲取操作前的數(shù)據(jù) 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("操作日志類(lèi)型為修改或刪除,實(shí)體類(lèi)必須指定表面和主鍵注解!"); } 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("實(shí)體類(lèi)必須指定主鍵屬性!"); } } 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<Map<String, Object>> maps = jdbcTemplate.queryForList(sql, selectPrimaryKey); if(maps!=null){ operateLogInfo.setPreModifiedData(new Gson().toJson(maps)); } }catch (Exception e){ e.printStackTrace(); throw new RuntimeException("查詢(xún)操作前數(shù)據(jù)出錯(cuò)!"); } }else { throw new RuntimeException("表名、主鍵名或主鍵值 存在空值情況,請(qǐng)核實(shí)!"); } }else{ operateLogInfo.setPreModifiedData(""); }
切面的完整實(shí)現(xiàn)代碼
@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()); //獲取執(zhí)行的方法 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]; //獲取操作前的數(shù)據(jù) 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("操作日志類(lèi)型為修改或刪除,實(shí)體類(lèi)必須指定表面和主鍵注解!"); } 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("實(shí)體類(lèi)必須指定主鍵屬性!"); } } 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<Map<String, Object>> maps = jdbcTemplate.queryForList(sql, selectPrimaryKey); if(maps!=null){ operateLogInfo.setPreModifiedData(new Gson().toJson(maps)); } }catch (Exception e){ e.printStackTrace(); throw new RuntimeException("查詢(xún)操作前數(shù)據(jù)出錯(cuò)!"); } }else { throw new RuntimeException("表名、主鍵名或主鍵值 存在空值情況,請(qǐng)核實(shí)!"); } }else{ operateLogInfo.setPreModifiedData(""); } //操作時(shí)間 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; } }
示例的使用方式
針對(duì)于示例來(lái)說(shuō)我們要在controller上面打上操作日志的注解。
@PostMapping("/updateInfo") @OperateLog(operation = "修改信息",operateType = GlobalStaticParas.OPERATE_MOD) public void updateInfo(@RequestBody Info info) { service.updateInfo(info); }
針對(duì)于Info的實(shí)體類(lèi),我們則要對(duì)其中的字段和表名進(jìn)行標(biāo)識(shí)。
@Data @SelectTable(tableName = "info",idName = "id") public class Info { @SelectPrimaryKey private String id; private String name; }
總結(jié)
文章寫(xiě)到這,也就結(jié)束了,文中難免有不足,歡迎大家批評(píng)指正
以上就是SpringAop實(shí)現(xiàn)操作日志記錄的詳細(xì)內(nèi)容,更多關(guān)于SpringAop 操作日志記錄的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- SpringBoot使用AOP實(shí)現(xiàn)統(tǒng)一角色權(quán)限校驗(yàn)
- Spring AOP實(shí)現(xiàn)功能權(quán)限校驗(yàn)功能的示例代碼
- SpringBoot中使用AOP實(shí)現(xiàn)日志記錄功能
- SpringBoot使用AOP實(shí)現(xiàn)日志記錄功能詳解
- Spring AOP如何自定義注解實(shí)現(xiàn)審計(jì)或日志記錄(完整代碼)
- 在springboot中使用AOP進(jìn)行全局日志記錄
- Spring AOP實(shí)現(xiàn)復(fù)雜的日志記錄操作(自定義注解)
- springMVC自定義注解,用AOP來(lái)實(shí)現(xiàn)日志記錄的方法
- 使用Spring AOP做接口權(quán)限校驗(yàn)和日志記錄
相關(guān)文章
SpringBoot實(shí)現(xiàn)文件下載的四種方式
本文主要介紹了SpringBoot實(shí)現(xiàn)文件下載的四種方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-11-11關(guān)于Spring?Validation數(shù)據(jù)校檢的使用流程分析
在實(shí)際項(xiàng)目中,對(duì)客戶(hù)端傳遞到服務(wù)端的參數(shù)進(jìn)行校驗(yàn)至關(guān)重要,SpringValidation提供了一種便捷的方式來(lái)實(shí)現(xiàn)這一需求,通過(guò)在POJO類(lèi)的屬性上添加檢查注解,本文給大家介紹Spring?Validation數(shù)據(jù)校檢的使用流程,感興趣的朋友一起看看吧2024-11-11mybatis-plus自帶QueryWrapper自定義sql實(shí)現(xiàn)復(fù)雜查詢(xún)實(shí)例詳解
MyBatis-Plus是一個(gè)MyBatis(opens new window)的增強(qiáng)工具,在 MyBatis的基礎(chǔ)上只做增強(qiáng)不做改變,MyBatis可以無(wú)損升級(jí)為MyBatis-Plus,這篇文章主要給大家介紹了關(guān)于mybatis-plus自帶QueryWrapper自定義sql實(shí)現(xiàn)復(fù)雜查詢(xún)的相關(guān)資料,需要的朋友可以參考下2022-10-10SpringBoot多數(shù)據(jù)源配置并通過(guò)注解實(shí)現(xiàn)動(dòng)態(tài)切換數(shù)據(jù)源
本文主要介紹了SpringBoot多數(shù)據(jù)源配置并通過(guò)注解實(shí)現(xiàn)動(dòng)態(tài)切換數(shù)據(jù)源,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08通過(guò)實(shí)例解析Java不可變對(duì)象原理
這篇文章主要介紹了通過(guò)實(shí)例解析Java不可變對(duì)象原理,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10Java中你絕對(duì)沒(méi)用過(guò)的一個(gè)關(guān)鍵字Record的使用
這篇文章主要給大家介紹一個(gè)?Java?中的一個(gè)關(guān)鍵字?Record,那?Record?關(guān)鍵字跟不可變類(lèi)有什么關(guān)系呢?看完今天的文章你就知道了,快跟隨小編一起學(xué)習(xí)一下吧2022-11-11hibernate 配置數(shù)據(jù)庫(kù)方言的實(shí)現(xiàn)方法
這篇文章主要介紹了hibernate 配置數(shù)據(jù)庫(kù)方言的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05Java中l(wèi)ambda表達(dá)式的基本運(yùn)用
大家好,本篇文章主要講的是Java中l(wèi)ambda表達(dá)式的基本運(yùn)用,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話(huà)記得收藏一下2022-01-01