springboot自定义注解aop实现数据变更记录

在日常开发过程中,有时候需要记录用户修改了某条数据的哪些字段,需要知道修改前和修改后的数据,如以下示例:修改用户名和昵称。
这是我个人做的一个通用后台管理系统练习项目;
后端架构:
> springboot
> mybatis-plus
> druid
> saToken
> mysql
> redis
> quartz
> swagger+knife4
> 持续迭代中...
前端架构:

> vue3
> vite
> elementPlus
> axios
> pinia
> vform3
> vue-router
> vuex
目前已完成基本权限管理,支持开箱即用,持续迭代中...

用户名原始值:test1

昵称原始值:测试用户

用户名修改为:test666

昵称修改为:测试用户666

查看变更记录列表如图:

1.创建注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ChangeRecordLog {

    // 获取编辑信息的解析类,目前为使用id获取
    Class parseclass() default DefaultContentParse.class;

    // 查询数据库所调用的class文件 selectById方法所在的Service类
    Class serviceclass() default IService.class;

    // 是否需要比较新旧数据
    boolean needDefaultCompare() default false;

    // id的类型
    Class idType() default Integer.class;

    // 操作对象的id字段名称
    String tableId() default "id";

    //操作类型 add update delete
    String type() default "update";


}

2.创建切面类

@Aspect
@Component
@Slf4j
@SuppressWarnings("all")
public class ChangeRecordAspect {

    @Autowired
    private ChangeRecordMapper changeRecordMapper;

    @Autowired
    private ApplicationContext applicationContext;

    //保存修改之前的数据
    Map<String, Object> oldMap = new HashMap<>();

    /**
     * 处理请求前执行
     */
    @Before(value = "@annotation(operateLog)")
    public void boBefore(JoinPoint joinPoint, ChangeRecordLog operateLog)
    {
        ContentParser contentParser = (ContentParser) applicationContext.getBean(operateLog.parseclass());
        //旧值
        Object oldObject = contentParser.getResult(joinPoint, operateLog, operateLog.tableId());
        if (operateLog.needDefaultCompare()) {
            oldMap = (Map<String, Object>) objectToMap(oldObject); // 存储修改前的对象
        }
    }


    /**
     * 处理请求后执行
     * @param joinPoint
     * @param operateLog
     * @throws Throwable
     */
    @After("@annotation(operateLog)")
    public void around(JoinPoint joinPoint, ChangeRecordLog operateLog) throws Throwable {
        try {
            ChangeRecord changeRecord = new ChangeRecord();
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            // 从切面织入点处通过反射机制获取织入点处的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod(); // 获取切入点所在的方法
            String meName = method.getName(); // 获取请求的方法名
            String className = joinPoint.getTarget().getClass().getName(); // 获取请求的类名
            String methodName = className + "." + meName;
            String uri = request.getRequestURL().toString(); // 请求uri
            Object[] args = joinPoint.getArgs();
            // 请求参数 预留
            //String requestParams = JSON.toJSONString(args);
            //创建人信息
            changeRecord.setChangeTime(new Date());
            changeRecord.setCreateName(LoginUtils.getUsername());
            Object object = null;
            ContentParser contentParser;
            try {
                contentParser = (ContentParser) applicationContext.getBean(operateLog.parseclass());
                object = contentParser.getResult(joinPoint, operateLog, operateLog.tableId());
            } catch (Exception e) {
                e.printStackTrace();
                log.error("service加载失败:", e);
            }
            if ("add".equals(operateLog.type())) {
                Map<String, Object> dataMap = (Map<String, Object>) objectToMap(object);
                log.info("新增的数据:{}" + dataMap.toString());
            }
            /*if ("delete".equals(operateLog.type())) {
                log.info("删除的数据:{}" + oldMap);
            }*/
            if (operateLog.needDefaultCompare()) {
                //比较新数据与数据库原数据
                List<Map<String, Object>> list = defaultDealUpdate(object, oldMap, operateLog.tableId());
                for (Map<String, Object> dataMap : list) {
                    changeRecord.setChangeField(String.valueOf(dataMap.get("filedName")));
                    changeRecord.setBeforeChange(String.valueOf(dataMap.get("oldValue")));
                    changeRecord.setAfterChange(String.valueOf(dataMap.get("newValue")));
                    changeRecord.setTypeId(Long.parseLong(String.valueOf(dataMap.get(operateLog.tableId()))));
                    String remark = "修改" + changeRecord.getChangeField() + "为" + changeRecord.getAfterChange() +
                            ",原" + changeRecord.getChangeField() + "为" +
                            (StringUtils.isBlank(changeRecord.getBeforeChange()) ? "空" : changeRecord.getBeforeChange());
                    changeRecord.setRemark(remark);
                    changeRecordMapper.insert(changeRecord);
                }
            }
        } catch (Exception e) {
            log.info("自定义变更记录注解出现异常");
            e.printStackTrace();
        }

    }

    private List<Map<String, Object>> defaultDealUpdate(Object newObject, Map<String, Object> oldMap, String tableId) {
        try {
            List<Map<String, Object>> list = new ArrayList<>();
            Map<String, Object> newMap = (Map<String, Object>) objectToMap(newObject);
            Object finalNewObject = newObject;
            oldMap.forEach((k, v) -> {
                Object newResult = newMap.get(k);
                if (null != v && !v.equals(newResult)) {
                    Field field = ReflectionUtils.getAccessibleField(finalNewObject, k);
                    DataName dataName = field.getAnnotation(DataName.class);
                    if (null != dataName && StringUtils.isNotEmpty(dataName.name())) {
                        //翻译表达式 0=男,1=女
                        String readConverterExp = dataName.readConverterExp();
                        Map result = new HashMap();
                        result.put("filedName", dataName.name());
                        result.put(tableId, newMap.get(tableId));
                        if (StringUtils.isNotEmpty(dataName.readConverterExp())) {
                            String oldValue = convertByExp(
                                    String.valueOf(v), dataName.readConverterExp(), ",");
                            String newValue = convertByExp(
                                    String.valueOf(newResult), dataName.readConverterExp(), ",");
                            result.put("oldValue", oldValue);
                            result.put("newValue", newValue);
                        } else {
                            result.put("oldValue", v);
                            result.put("newValue", newResult);
                        }
                        list.add(result);
                    }
                }
            });
            log.info("比较的数据哈:{}" + list.toString());
            return list;
        } catch (Exception e) {
            log.error("比较异常", e);
            e.printStackTrace();
            throw new RuntimeException("比较异常", e);
        }
    }

    private Map<?, ?> objectToMap(Object obj) {
        if (obj == null) {
            return null;
        }
        ObjectMapper mapper = new ObjectMapper();
        mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 如果使用JPA请自己打开这条配置
        // mapper.addMixIn(Object.class, IgnoreHibernatePropertiesInJackson.class);
        Map<?, ?> mappedObject = mapper.convertValue(obj, Map.class);
        return mappedObject;
    }

    /**
     * 翻译
     *
     * @param propertyValue 参数值如:0
     * @param converterExp  翻译注解的值如:0=男,1=女,2=未知
     * @param separator     分隔符
     * @return 解析后值
     */
    public static String convertByExp(String propertyValue, String converterExp, String separator) {
        StringBuilder propertyString = new StringBuilder();
        String[] convertSource = converterExp.split(",");
        for (String item : convertSource) {
            String[] itemArray = item.split("=");
            if (StringUtils.containsAny(propertyValue, separator)) {
                for (String value : propertyValue.split(separator)) {
                    if (itemArray[0].equals(value)) {
                        propertyString.append(itemArray[1] + separator);
                        break;
                    }
                }
            } else {
                if (itemArray[0].equals(propertyValue)) {
                    return itemArray[1];
                }
            }
        }
        return StringUtils.stripEnd(propertyString.toString(), separator);
    }

}

3.字段别名注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Documented
public @interface DataName {
    String name() default ""; // 字段名称

    /**
     * 读取枚举内容转义表达式 (如: 0=男,1=女,2=未知)
     */
    String readConverterExp() default "";
}


//在实体类上使用
     /**
     * 用户账号
     */
    @DataName(name = "用户名")
    private String userName;

    /**
     * 用户昵称
     */
    @DataName(name = "昵称")
    private String nickName;

4.工具类-解析接口

public interface ContentParser {

    /**
     * 获取信息返回查询出的对象
     *
     * @param joinPoint  查询条件的参数
     * @param operateLog 注解
     * @return 获得的结果
     */
    public Object getResult(JoinPoint joinPoint, ChangeRecordLog operateLog, String tableId);


}

5.工具类-解析接口实现类

@Component
public class DefaultContentParse implements ContentParser {

    @Autowired
    private ApplicationContext applicationContext;

    @Override
    public Object getResult(JoinPoint joinPoint, ChangeRecordLog operateLog, String tableId) {
        Object info = joinPoint.getArgs()[0];
        Object id = ReflectionUtils.getFieldValue(info, tableId);
        Assert.notNull(id, "id不能为空");
        Class idType = operateLog.idType();
        if (idType.isInstance(id)) {
            Class cls = operateLog.serviceclass();
            IService service = (IService) applicationContext.getBean(cls);
            Object result = service.getById((Serializable) id);
            return result;
        } else {
            throw new RuntimeException("请核实id type");
        }
    }
}

6.反射工具类

@Slf4j
public class ReflectionUtils {

	/**
	 * 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数.
	 * @param obj 读取的对象
	 * @param fieldName 读取的列
	 * @return 属性值
	 */
	public static Object getFieldValue(final Object obj, final String fieldName) {
		Field field = getAccessibleField(obj, fieldName);
		if (field == null) {
			throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + obj + "]");
		}

		Object result = null;
		try {
			result = field.get(obj);
		} catch (IllegalAccessException e) {
			log.error("不可能抛出的异常{}", e.getMessage());
			e.printStackTrace();
		}
		return result;
	}

	/**
	 * 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问.如向上转型到Object仍无法找到, 返回null.
	 * @param obj  查找的对象
	 * @param fieldName  列名
	 * @return 列
	 */
	public static Field getAccessibleField(final Object obj, final String fieldName) {
		for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {
			try {
				Field field = superClass.getDeclaredField(fieldName);
				makeAccessible(field);
				return field;
			} catch (NoSuchFieldException e) { // NOSONAR
				// Field不在当前类定义,继续向上转型
				e.printStackTrace();
				continue; // new add
			}
		}
		return null;
	}

	/**
	 * 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
	 * @param
	 */
	public static void makeAccessible(Field field) {
		if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) || Modifier
				.isFinal(field.getModifiers())) && !field.isAccessible()) {
			field.setAccessible(true);
		}
	}

	/**
	 * 获取两个对象同名属性内容不相同的列表
	 * @param class1 old对象
	 * @param class2 new对象
	 * @return  区别列表
	 * @throws ClassNotFoundException 异常
	 * @throws IllegalAccessException 异常
	 */
	public static List<Map<String ,Object>> compareTwoClass(Object class1, Object class2) throws ClassNotFoundException, IllegalAccessException {
		List<Map<String,Object>> list=new ArrayList<Map<String, Object>>();
		// 获取对象的class
		Class<?> clazz1 = class1.getClass();
		Class<?> clazz2 = class2.getClass();
		// 获取对象的属性列表
		Field[] field1 = clazz1.getDeclaredFields();
		Field[] field2 = clazz2.getDeclaredFields();
		StringBuilder sb=new StringBuilder();
		// 遍历属性列表field1
		for(int i=0;i<field1.length;i++) {
			// 遍历属性列表field2
			for (int j = 0; j < field2.length; j++) {
				// 如果field1[i]属性名与field2[j]属性名内容相同
				if (field1[i].getName().equals(field2[j].getName())) {
					if (field1[i].getName().equals(field2[j].getName())) {
						field1[i].setAccessible(true);
						field2[j].setAccessible(true);
						// 如果field1[i]属性值与field2[j]属性值内容不相同
						if (!compareTwo(field1[i].get(class1), field2[j].get(class2))) {
							Map<String, Object> map2 = new HashMap<String, Object>();
							DataName name=field1[i].getAnnotation(DataName.class);
							String fieldName="";
							if(name!=null){
								fieldName=name.name();
							} else {
								fieldName=field1[i].getName();
							}
							map2.put("name", fieldName);
							map2.put("old", field1[i].get(class1));
							map2.put("new", field2[j].get(class2));
							list.add(map2);
						}
						break;
					}
				}
			}
		}
		return list;
	}

	/**
	 * 对比两个数据是否内容相同
	 * @param  object1  比较对象1
	 * @param  object2  比较对象2
	 * @return boolean类型
	 */
	public static boolean compareTwo(Object object1,Object object2){
		if(object1==null&&object2==null){
			return true;
		}
		if(object1==null&&object2!=null){
			return false;
		}
		if(object1.equals(object2)){
			return true;
		}
		return false;
	}



}

7.使用示例:在接口上使用注解

@ChangeRecordLog(serviceclass = SysUserService.class,
            needDefaultCompare = true,
            idType = Long.class,
            tableId = "userId")
    @ApiOperation(value = "修改用户")
    @PostMapping("/edit")
    public R edit(@Validated @RequestBody SysUser user) {
        if (StringUtils.isNotNull(user.getUserId()) && user.isAdmin()) {
            return R.fail("不允许操作超级管理员用户");
        }
        if (!userService.checkUserNameUnique(user)) {
            return R.fail("修改用户'" + user.getUserName() + "'失败,登录账号已存在");
        } else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) {
            return R.fail("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
        }
        userService.updateUser(user);
        return R.ok();
    }

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
好的,关于SpringBoot自定义注解AOP的问题,我可以为您提供一些基本的介绍和示例代码。 首先,AOP(Aspect-Oriented Programming)是一种编程范式,它可以在不修改业务逻辑代码的情况下,对应用程序进行横切关注点的切面处理。而Spring AOP作为Spring框架的一部分,提供了一种基于代理模式的AOP实现。 在使用Spring AOP的过程中,自定义注解可以作为切点表达式的一部分,通过对注解的解析,实现对被注解的方法或类的切面处理。下面是一个简单的示例代码,演示如何通过自定义注解实现对方法的AOP处理: 首先,定义一个自定义注解: ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyAnnotation { String value() default ""; } ``` 然后,在需要被拦截的方法上添加该注解: ```java @Service public class MyService { @MyAnnotation("myAnnotation") public void doSomething() { System.out.println("do something..."); } } ``` 接下来,使用AspectJ的@Aspect注解定义一个切面类,并在该类中定义一个切点,用于匹配被@MyAnnotation注解的方法: ```java @Aspect @Component public class MyAspect { @Pointcut("@annotation(com.example.demo.annotation.MyAnnotation)") public void myAnnotationPointcut() {} @Before("myAnnotationPointcut()") public void beforeMyAnnotation() { System.out.println("before myAnnotation..."); } } ``` 最后,启动SpringBoot应用程序,调用MyService的doSomething方法,就可以看到输出结果: ```java before myAnnotation... do something... ``` 以上就是一个简单的SpringBoot自定义注解AOP的示例。通过使用自定义注解,可以更加方便地实现对应用程序的切面处理。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我心向阳cd

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值