• spring boot 使用AOP+自定义注解+反射实现操作日志记录修改前数据和修改后对比数据,并保存至日志表


    一、添加aop starter依赖

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-aop</artifactId>
    4. </dependency>

    二:自定义字段翻译注解。(修改功能时,需要显示如某字段修改前为张三,修改后为李四,name字段对应的中文注释)

    1. package com.test.common.annotation;
    2. import java.lang.annotation.*;
    3. /**
    4. * 写入日志表时,字段对应的中文注释
    5. */
    6. @Retention(RetentionPolicy.RUNTIME) // 注解会在class字节码文件中存在,在运行时可以通过反射获取到
    7. @Target({ElementType.FIELD,ElementType.METHOD})//定义注解的作用目标**作用范围字段、枚举的常量/方法
    8. @Documented //说明该注解将被包含在javadoc中
    9. public @interface FieldMeta {
    10. /**
    11. * 汉字全称
    12. * @return
    13. */
    14. String value();
    15. }

    使用FieldMeta自定义注解,看个人业务自行觉得是否需要重新定义实体

    1. package com.test.customer.domain;
    2. import java.math.BigDecimal;
    3. import java.util.Date;
    4. import java.util.List;
    5. import java.util.Map;
    6. import com.test.common.annotation.FieldMeta;
    7. import org.apache.commons.lang3.builder.ToStringBuilder;
    8. import org.apache.commons.lang3.builder.ToStringStyle;
    9. import com.test.common.core.domain.BaseEntity;
    10. public class TestDangAnDetail extends BaseEntity {
    11. private static final long serialVersionUID = 1L;
    12. /**
    13. * 次数
    14. */
    15. @FieldMeta("总次数")
    16. private Long CountNum;
    17. }

    parseClass调用查询方法的类(查询数据工具类)

    1. //抽象类
    2. public interface ContentParser {
    3. /**
    4. * 获取信息返回查询出的对象
    5. *
    6. * @param joinPoint 查询条件的参数
    7. * @param dbAccessLogger 注解
    8. * @return 获得的结果
    9. */
    10. Object getOldResult(JoinPoint joinPoint, DBAccessLogger dbAccessLogger,String sqlSessionFactoryName);
    11. /**
    12. * 获取信息返回查询出的对象
    13. *
    14. * @param joinPoint 查询条件的参数
    15. * @param dbAccessLogger 注解
    16. * @return 获得的结果
    17. */
    18. Object getNewResult(JoinPoint joinPoint, DBAccessLogger dbAccessLogger,String sqlSessionFactoryName);
    19. }

    实现类 :通过该实现类获取更新前后的数据。
    该实现类的实现原理为:获取入参出入的id值,获取sqlSessionFactory,通过sqlSessionFactory获取selectByPrimaryKey()该方法,执行该方法可获取id对应数据更新操作前后的数据。

    1. @Component
    2. public class DefaultContentParse implements ContentParser {
    3. /**
    4. * 获取更新方法的第一个参数解析其id
    5. * @param joinPoint 查询条件的参数
    6. * @param enableModifyLog 注解类容
    7. * @return
    8. */
    9. @Override
    10. public Object getOldResult(JoinPoint joinPoint, DBAccessLogger enableModifyLog,String sqlSessionFactoryName) {
    11. Object info = joinPoint.getArgs()[0];
    12. Object id = ReflectionUtils.getFieldValue(info, "id");
    13. Assert.notNull(id,"未解析到id值,请检查入参是否正确");
    14. Class<?> aClass = enableModifyLog.serviceClass();
    15. Object result = null;
    16. try {
    17. SqlSessionFactory sqlSessionFactory = SpringUtil.getBean(sqlSessionFactoryName);
    18. Object instance = Proxy.newProxyInstance(
    19. aClass.getClassLoader(),
    20. new Class[]{aClass},
    21. new MyInvocationHandler(sqlSessionFactory.openSession().getMapper(aClass))
    22. );
    23. Method selectByPrimaryKey = aClass.getDeclaredMethod("selectByPrimaryKey", Long.class);
    24. //调用查询方法
    25. result = selectByPrimaryKey.invoke(instance, id);
    26. } catch (Exception e) {
    27. e.printStackTrace();
    28. }
    29. return result;
    30. }
    31. @Override
    32. public Object getNewResult(JoinPoint joinPoint, DBAccessLogger enableModifyLog,String sqlSessionFactoryName) {
    33. return getOldResult(joinPoint,enableModifyLog,sqlSessionFactoryName);
    34. }
    35. }

    三:定义AOP切面

    注意:如不返回操作是否成功状态可能会导致前端出现警告,JSON为空不能被解析

    1. import com.test.common.annotation.FieldMeta;
    2. import com.test.common.core.domain.entity.SysUser;
    3. import com.test.common.enums.BusinessStatus;
    4. import com.test.common.enums.OperatorType;
    5. import com.test.common.utils.*;
    6. import com.test.customer.domain.UserDataOperationLogs;
    7. import com.test.customer.service.IUserDataOperationLogsService;
    8. import com.test.framework.service.OperateLog;
    9. import com.test.common.enums.BusinessType;
    10. import com.test.framework.service.ContentParser;
    11. import com.test.system.domain.SysOperLog;
    12. import com.test.system.service.ISysOperLogService;
    13. import lombok.extern.slf4j.Slf4j;
    14. import com.alibaba.fastjson.JSON;
    15. import com.fasterxml.jackson.databind.DeserializationFeature;
    16. import com.fasterxml.jackson.databind.ObjectMapper;
    17. import com.fasterxml.jackson.databind.SerializationFeature;
    18. import org.aspectj.lang.ProceedingJoinPoint;
    19. import org.aspectj.lang.annotation.Around;
    20. import org.aspectj.lang.annotation.Aspect;
    21. import org.aspectj.lang.reflect.MethodSignature;
    22. import org.springframework.beans.factory.annotation.Autowired;
    23. import org.springframework.context.ApplicationContext;
    24. import org.springframework.stereotype.Component;
    25. import org.springframework.web.bind.annotation.RequestMethod;
    26. import org.springframework.web.context.request.RequestContextHolder;
    27. import org.springframework.web.context.request.ServletRequestAttributes;
    28. import javax.servlet.http.HttpServletRequest;
    29. import java.lang.reflect.Field;
    30. import java.lang.reflect.Method;
    31. import java.util.Date;
    32. import java.util.HashMap;
    33. import java.util.Map;
    34. /**
    35. * 拦截@EnableGameleyLog注解的方法
    36. * 将具体修改存储到数据库中
    37. */
    38. @Aspect
    39. @Component
    40. @Slf4j
    41. public class OperateAspect {
    42. @Autowired
    43. private IUserDataOperationLogsService iUserDataOperationLogsService;
    44. @Autowired
    45. private DefaultContentParse defaultContentParse;
    46. @Autowired
    47. private ApplicationContext applicationContext;
    48. // 环绕通知
    49. @Around("@annotation(operateLog)")
    50. public Object around(ProceedingJoinPoint joinPoint, OperateLog operateLog) throws Throwable{
    51. Map<String, Object> oldMap=new HashMap<>();
    52. UserDataOperationLogs lg = new UserDataOperationLogs();
    53. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    54. HttpServletRequest request = attributes.getRequest();
    55. // 当不传默认operateType时 根据Method类型自动匹配
    56. setAnnotationType(request, operateLog);
    57. // fixme 1.0.9开始不再提供自动存入username功能,请在存储实现类中自行存储
    58. // 从切面织入点处通过反射机制获取织入点处的方法
    59. MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    60. Object[] args=joinPoint.getArgs(); // 请求参数
    61. // 获取当前的用户
    62. SysUser currentUser = ShiroUtils.getSysUser();
    63. lg.setCreateBy(currentUser.getLoginName());
    64. lg.setStatus(0l);
    65. lg.setUpdateTime(new Date());
    66. if (OperateType.UPDATE.equals(operateLog.operateType())) {
    67. try {
    68. ContentParser contentParser=(ContentParser) applicationContext.getBean(operateLog.parseclass());
    69. Object oldObject = contentParser.getResult(joinPoint, operateLog);
    70. if (operateLog.needDefaultCompare()) {
    71. oldMap = (Map<String, Object>) objectToMap(oldObject); // 存储修改前的对象
    72. lg.setStoreId((Long) oldMap.get("storeId"));
    73. lg.setItemId((Long) oldMap.get("itemId"));
    74. }
    75. } catch (Exception e) {
    76. e.printStackTrace();
    77. log.error("service加载失败:", e);
    78. }
    79. }
    80. // joinPoint.proceed()执行前是前置通知,执行后是后置通知
    81. Object object=joinPoint.proceed();
    82. if (OperateType.UPDATE.equals(operateLog.operateType())) {
    83. ContentParser contentParser;
    84. try {
    85. String responseParams=JSON.toJSONString(object);
    86. contentParser=(ContentParser) applicationContext.getBean(operateLog.parseclass());
    87. object = contentParser.getResult(joinPoint, operateLog);
    88. } catch (Exception e) {
    89. log.error("service加载失败:", e);
    90. }
    91. // 默认不进行比较,可以自己在logService中自定义实现,降低对性能的影响
    92. if (operateLog.needDefaultCompare()) {
    93. lg.setContent(defaultDealUpdate(object, oldMap));
    94. }
    95. // 如果使用默认缓存 则需要更新到最新的数据
    96. if(operateLog.defaultCache()
    97. && operateLog.parseclass().equals(DefaultContentParse.class)){
    98. defaultContentParse.updateCache(joinPoint, operateLog,object);
    99. }
    100. } else{
    101. String responseParams=JSON.toJSONString(object);
    102. }
    103. //保存当前日志到数据库中
    104. int logs = iUserDataOperationLogsService.insertUserDataOperationLogs(lg);
    105. log.info("新增用户操作数据日志成功");
    106. //返回用户的操作是否成功
    107. AjaxResult ajaxResult = logs > 0 ? success() : error();
    108. return ajaxResult;
    109. }
    110. private String defaultDealUpdate(Object newObject, Map<String, Object> oldMap){
    111. try {
    112. Map<String, Object> newMap = (Map<String, Object>) objectToMap(newObject);
    113. StringBuilder str = new StringBuilder();
    114. Object finalNewObject = newObject;
    115. oldMap.forEach((k, v) -> {
    116. Object newResult = newMap.get(k);
    117. if (null!=v && !v.equals(newResult)) {
    118. Field field = ReflectionUtils.getAccessibleField(finalNewObject, k);
    119. FieldMeta dataName = field.getAnnotation(FieldMeta.class);
    120. SysUser currentUser = ShiroUtils.getSysUser();
    121. if (null!=dataName) {
    122. str.append("【"+currentUser.getLoginName()+"】").append("修改了【").append(dataName.value() +"】").append("将【").append(v).append("】").append(" 改为:【").append(newResult).append("】。\n");
    123. }
    124. }
    125. });
    126. return str.toString();
    127. } catch (Exception e) {
    128. log.error("比较异常", e);
    129. throw new RuntimeException("比较异常",e);
    130. }
    131. }
    132. private Map<?, ?> objectToMap(Object obj) {
    133. if (obj == null) {
    134. return null;
    135. }
    136. ObjectMapper mapper = new ObjectMapper();
    137. mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
    138. mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    139. // 如果使用JPA请自己打开这条配置
    140. // mapper.addMixIn(Object.class, IgnoreHibernatePropertiesInJackson.class);
    141. Map<?, ?> mappedObject = mapper.convertValue(obj, Map.class);
    142. return mappedObject;
    143. }
    144. private void setAnnotationType(HttpServletRequest request,OperateLog modifyLog){
    145. if(!modifyLog.operateType().equals(OperateType.NONE)){
    146. return;
    147. }
    148. String method=request.getMethod();
    149. if(RequestMethod.GET.name().equalsIgnoreCase(method)){
    150. ReflectAnnotationUtil.updateValue(modifyLog, "operateType", OperateType.SELECT);
    151. } else if(RequestMethod.POST.name().equalsIgnoreCase(method)){
    152. ReflectAnnotationUtil.updateValue(modifyLog, "operateType", OperateType.ADD);
    153. } else if(RequestMethod.PUT.name().equalsIgnoreCase(method)){
    154. ReflectAnnotationUtil.updateValue(modifyLog, "operateType", OperateType.UPDATE);
    155. } else if(RequestMethod.DELETE.name().equalsIgnoreCase(method)){
    156. ReflectAnnotationUtil.updateValue(modifyLog, "operateType", OperateType.DELETE);
    157. }
    158. }
    159. }

    或者写成

    1. //环绕通知切面,切点:DBAccessLogger 更新拦截数据
    2. @Aspect
    3. @Component
    4. @Order(1)
    5. public class DBAccessLoggerAspect {
    6. // 注入Service用于把日志保存数据库
    7. @Autowired
    8. private LogService logService;
    9. @Around("@annotation(com.****.log.DBAccessLogger)") // 环绕通知
    10. public Object execute(ProceedingJoinPoint pjp) throws Exception {
    11. // 获得当前访问的class
    12. Class<?> className = pjp.getTarget().getClass();
    13. // 获得访问的方法名
    14. String methodName = pjp.getSignature().getName();
    15. @SuppressWarnings("rawtypes")
    16. Class[] argClass = ((MethodSignature) pjp.getSignature()).getParameterTypes();
    17. // 操作结果,默认为成功
    18. Long operResult = DictLogConstant.LOGS_OPER_SUCCESS;
    19. //返回值
    20. Object rvt = null;
    21. Method method = className.getMethod(methodName, argClass);
    22. DBAccessLogger dbAcessLoggerannotation = method.getAnnotation(DBAccessLogger.class);
    23. String accessTable = dbAcessLoggerannotation.accessTable();
    24. DBOperationType accessType = dbAcessLoggerannotation.accessType();
    25. DatabaseEnum databaseEnum = dbAcessLoggerannotation.accessDatasource();
    26. String accessDatasource = databaseEnum.constName;
    27. //crd操作直接执行方法
    28. if (accessType == DBOperationType.DELETE || accessType == DBOperationType.SELECT || accessType == DBOperationType.CREATE) {
    29. try {
    30. rvt = pjp.proceed();
    31. } catch (Throwable e) {
    32. e.printStackTrace();
    33. throw new RuntimeException(e.getMessage());
    34. }
    35. // 如果没有返回结果,则不将该日志操作写入数据库。
    36. if (rvt == null) return rvt;
    37. }
    38. if ((accessType == DBOperationType.DELETE)
    39. && ((CUDResult) rvt).getReturnVal() == 0) {
    40. operResult = DictLogConstant.LOGS_OPER_FAILURE;
    41. }
    42. if (accessTable != null) {
    43. if (accessType == DBOperationType.SELECT) {
    44. Log sysLog = new Log();
    45. /**
    46. 设置要存放的日志信息
    47. **/
    48. logService.createLog(sysLog);
    49. }
    50. else if (accessType == DBOperationType.DELETE || accessType == DBOperationType.CREATE) {
    51. for (Long recordId : ((CUDResult) rvt).getRecordIds()) {
    52. Log sysLog = new Log();
    53. /**
    54. 设置要存放的日志信息
    55. **/
    56. logService.createLog(sysLog);
    57. }
    58. }
    59. else {
    60. //更新操作
    61. Log sysLog = new Log();
    62. /**
    63. 设置日志信息
    64. **/
    65. //获取更行前的数据
    66. Map<String, Object> oldMap = null;
    67. Object oldObject;
    68. try {
    69. ContentParser contentParser = SpringUtil.getBean(dbAcessLoggerannotation.parseClass());
    70. //获取更行前的数据
    71. oldObject = contentParser.getOldResult(pjp, dbAcessLoggerannotation, databaseEnum.sqlsessionName);
    72. oldMap = (Map<String, Object>) objectToMap(oldObject);
    73. } catch (Exception e) {
    74. e.printStackTrace();
    75. }
    76. //执行service
    77. Object serviceReturn;
    78. try {
    79. serviceReturn = pjp.proceed();
    80. } catch (Throwable throwable) {
    81. throwable.printStackTrace();
    82. throw new RuntimeException(throwable.getMessage());
    83. }
    84. CUDResult crudResult = (CUDResult) serviceReturn;
    85. Object afterResult = null;
    86. try {
    87. ContentParser contentParser = SpringUtil.getBean(dbAcessLoggerannotation.parseClass());
    88. //更新后的数据
    89. afterResult = contentParser.getNewResult(pjp, dbAcessLoggerannotation, databaseEnum.sqlsessionName);
    90. } catch (Exception e) {
    91. e.printStackTrace();
    92. }
    93. //修改前后的变化
    94. sysLog.setOperation(accessType.getValue());
    95. try {
    96. String updatedefrent = defaultDealUpdate(afterResult, oldMap);
    97. sysLog.setParams(updatedefrent);
    98. } catch (Exception e) {
    99. e.printStackTrace();
    100. }
    101. /**
    102. 设置日志信息
    103. **/
    104. logService.createLog(sysLog);
    105. return serviceReturn;
    106. }
    107. }
    108. if (operResult.longValue() == DictLogConstant.LOGS_OPER_FAILURE) {
    109. // 当数据库的UPDATE 和 DELETE操作没有对应的数据记录存在时,抛出异常
    110. throw new DBAccessException(accessType.getValue() + "的数据记录不存在!");
    111. }
    112. return rvt;
    113. }
    114. private Map<?, ?> objectToMap(Object obj) {
    115. if (obj == null) {
    116. return null;
    117. }
    118. ObjectMapper mapper = new ObjectMapper();
    119. mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
    120. mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    121. //如果使用JPA请自己打开这条配置
    122. //mapper.addMixIn(Object.class, IgnoreHibernatePropertiesInJackson.class);
    123. Map<?, ?> mappedObject = mapper.convertValue(obj, Map.class);
    124. return mappedObject;
    125. }
    126. /**
    127. *
    128. * @param newObject 更新过后的结果
    129. * @param oldMap 更新前的结果
    130. * @return
    131. */
    132. private String defaultDealUpdate(Object newObject, Map<String, Object> oldMap) {
    133. try {
    134. Map<String, Object> newMap = (Map<String, Object>) objectToMap(newObject);
    135. StringBuilder str = new StringBuilder();
    136. Object finalNewObject = newObject;
    137. oldMap.forEach((k, v) -> {
    138. Object newResult = newMap.get(k);
    139. if (v != null && !v.equals(newResult)) {
    140. Field field = ReflectionUtils.getAccessibleField(finalNewObject, k);
    141. //获取类上的注解
    142. DataName dataName = field.getAnnotation(DataName.class);
    143. if (field.getType().getName().equals("java.util.Date")) {
    144. SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    145. if (dataName != null) {
    146. str.append("【").append(dataName.name()).append("】从【")
    147. .append(format.format(v)).append("】改为了【").append(format.format(newResult)).append("】;\n");
    148. } else {
    149. str.append("【").append(field.getName()).append("】从【")
    150. .append(format.format(v)).append("】改为了【").append(format.format(newResult)).append("】;\n");
    151. }
    152. } else {
    153. if (dataName != null) {
    154. str.append("【").append(dataName.name()).append("】从【")
    155. .append(v).append("】改为了【").append(newResult).append("】;\n");
    156. } else {
    157. str.append("【").append(field.getName()).append("】从【")
    158. .append(v).append("】改为了【").append(newResult).append("】;\n");
    159. }
    160. }
    161. }
    162. });
    163. return str.toString();
    164. } catch (Exception e) {
    165. throw new RuntimeException("比较异常", e);
    166. }
    167. }
    168. }

    四:自定义日志注解

    1. package com.test.framework.service;
    2. import com.test.common.utils.IService;
    3. import com.test.common.utils.OperateType;
    4. import com.test.framework.service.impl.DefaultContentParse;
    5. import java.lang.annotation.Documented;
    6. import java.lang.annotation.ElementType;
    7. import java.lang.annotation.Retention;
    8. import java.lang.annotation.RetentionPolicy;
    9. import java.lang.annotation.Target;
    10. /**
    11. * 记录编辑详细信息的标注
    12. * @author
    13. */
    14. @Documented
    15. @Retention(RetentionPolicy.RUNTIME)
    16. @Target({ElementType.METHOD})
    17. public @interface OperateLog {
    18. // 模块名称
    19. String moduleCode() default "";
    20. // 操作的类型 可以直接调用OperateType 不传时根据METHOD自动确定
    21. OperateType operateType() default OperateType.NONE;
    22. // 获取编辑信息的解析类,目前为使用id获取,复杂的解析需要自己实现,默认不填写则使用默认解析类
    23. Class parseclass() default DefaultContentParse.class;
    24. // 查询数据库所调用的class文件
    25. Class serviceclass() default IService.class;
    26. // 具体业务操作名称
    27. String handleName() default "";
    28. // 是否需要默认的改动比较
    29. boolean needDefaultCompare() default false;
    30. // id的类型
    31. Class idType() default Long.class;
    32. // 是否使用默认本地缓存
    33. boolean defaultCache() default false;
    34. }

    五、@OperateLog注解使用

    1. /**
    2. * 修改记录详情
    3. * @OperateLog:AOP自定义日志注解
    4. */
    5. @PostMapping("/editSave")
    6. @OperateLog(moduleCode="editSave", operateType= OperateType.UPDATE, handleName="修改档案信息", needDefaultCompare=true)
    7. @ResponseBody
    8. public AjaxResult editSave(TCustomerItemRecDetail tCustomerItemRecDetail){
    9. return toAjax(testDangAnDetailService.updateTestDangAnDetail(tCustomerItemRecDetail));
    10. }

    修改功能时,需要实现我们自定义的IService接口,并重写 selectById 方法,在修改前我们需要根据主键id去数据库查询对应的信息,然后在和修改后的值进行比较。

    在这里插入图片描述

    六:IService接口

    1. /**
    2. * 修改时需要记录修改前后的值时,需要根据主键id去查询修改前的值时需要
    3. * @author zhang
    4. */
    5. public interface IService<T, S> {
    6. T selectById(S id);
    7. }

  • 相关阅读:
    Visual Studio 中的键盘快捷方式大全
    封装了一个居左的iOS轮播视图
    Java程序员找工作需要掌握哪些技能
    Android使用Banner框架实现轮播图
    CaiT:Facebook提出高性能深度ViT结构 | ICCV 2021
    sqlserver2012 完全卸载
    shell脚本自动化执行jar包
    2022-08-01 C++并发编程(四)
    Python 列表推导式:计算不同运动组合的热量列表及最大最小热量消耗情况
    【Reinforcement Learning】强化学习基础内容有哪些?
  • 原文地址:https://blog.csdn.net/yuechuzhixing/article/details/132761054