0. 引言
最近在练习项目的时候发现大多数的表都存在公共字段,比如:create_user,create_time,update_user,update_time。在每一次的增加与修改操作时,如果都在service层手动对这些字段赋值,将会使得存在大量重复的代码,冗余且难以维护。
这是项目中的三张表,可以看到都存在相同的四个字段。其他的表大多数也存在这四个字段
1. 使用的技术与实现思路
枚举、注解、AOP、反射
首先,在CRUD中,只有添加记录和修改记录会涉及到对以上四个公共字段的操作,故而需要定义枚举类来区别该次操作的类型;其次需要在持久层操作数据库之前为公共字段自动注入值,故需要定义一个前置通知;接着在前置通知中通过反射拿到具体的实参对象(用于封装数据的pojo对象),然后通过反射技术调用set方法进行赋值;最后需要自定义一个注解,将其添加到持久层的insert和update方法上,以便在前置通知中通过反射解析出该次操作的类型。
2. 实现步骤
2.1 自定义枚举类
我们知道java的枚举类型是有限的实例集合,他们是唯一的、已命名、不可更改的,故而使用枚举类来标识Insert或Update操作:
2.2 自定义注解
我们知道,在SpringAOP中可以通过注解的方式来描述切入点表达式。故而定义一个注解,将其value属性的返回值设置为之前定义的枚举类OperationType;该注解的元注解为:@Target,规定该注解只能用于方法上,@Retention,规定该注解被保留到运行阶段。
2.3 编写前置通知
首先需要定义一个切面类,在切面类中使用@PointCut注解将公共的切点表达式抽取出来,具体的切入点表达式为:
execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)
该切入点表达式匹配的方法为:com.sky.mapper包下所有添加了@AutoFill注解的类的所有方法,返回值任意,形参列表任意。
做好了以上的准备工作,是时候编写具体的通知内容了!首先需要通过反射拿到当前方法的数据库操作类型,通过joinpoint对象调用getSignature()方法,得到连接点对象的方法签名(包含该方法的返回值类型 、方法名、形参列表类型),再将其强转为MethodSignature类型的对象,再通过该对象得到注解对象,最后通过注解对象调用其value()方法得到注解信息。具体的代码为:
接着需要获得当前方法的参数,也就是实体对象,为了后续的反射操作做准备:methodSignature对象调用其getParameterTypes()方法得到形参列表的class对象数组,并约定形参实例对象位于形参列表的第一个(满足SpringBoot的约定大于配置的设计原则),故而直接取出class数组的0号索引元素为entity对象(实参对象对应类的class对象);接着joinPoint对象调用getArgs()方法得到形参列表实例数组,并拿到该数组的第一个元素为entityInstance对象。具体的代码为:
接着准备赋值数据,使用LocalDateTime类提供的静态方法now获得当前时间;在我的项目中定义了BaseContext类,其中定义了静态方法getCurrentId(),用来得到当前操作人的id。具体实现是在做Jwt校验时通过线程局部变量threadLocal调用其set()方法存入Jwt解析出来的empID(操作人id),然后在当前线程的其他地方通过get()方法得到该id。具体的代码为:
我们知道在update操作时只需要为update_user和update_yime字段赋值,而在insert操作时需要为create_user、create_time、update_user、update_time赋值;所以首先需要判断操作的类型,然后使用entity类调用getDeclaredMethod()方法得到待赋值字段的set()方法对象,通过set()方法对象的invoke()方法,将前面得到的entityInstance实例对象和赋值数据作为实参传入方法中。具体代码为:
该功能的完整源码为(使用时注意导入相应的依赖):
log.info("开始为公共字段赋值..."); Signature signature = joinPoint.getSignature(); // 通过连接点对象得到方法签名对象(方法签名是JVM提供的,包含方法名、返回值类型和方法参数) if (signature instanceof MethodSignature){ // 1.获取到当前被拦截的方法上的数据库操作类型 MethodSignature methodSignature = (MethodSignature) signature; AutoFill autoFill = methodSignature.getMethod().getAnnotation(AutoFill.class); // 获得方法上的注解对象 OperationType operationType = autoFill.value(); // 获得数据库操作类型 // 2.获取到当前被拦截的方法的参数--也就是实体对象 Class[] parameterTypes = methodSignature.getParameterTypes(); // 获得形参列表class数组 Class entity = parameterTypes[0]; // 这里得到的是该形参实例对象对应的类(calss对象) Object[] args = joinPoint.getArgs(); // 直接得到形参列表实例数组 Object entityInstance = args[0]; // 这里具体拿到了形参实例对象 // 3.准备赋值的数据 LocalDateTime now = LocalDateTime.now(); Long currentId = BaseContext.getCurrentId(); // 4.根据当前不同的操作类型,为对应的属性通过反射来赋值 if (operationType == OperationType.INSERT){ // Insert操作需要为4个字段赋值,通过反射解析出实体对象的set方法,并调用set方法对其赋值 // 可以不得到entity对象(实例对象对应的class对象),直接这样写: // entityInstance.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setCreateUser = entity.getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setCreateTime = entity.getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); Method setUpdateTime = entity.getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); setCreateUser.invoke(entityInstance,currentId); setCreateTime.invoke(entityInstance,now); setUpdateUser.invoke(entityInstance,currentId); setUpdateTime.invoke(entityInstance,now); }else if (operationType == OperationType.UPDATE){ // Update操作需要为2个字段赋值 Method setUpdateUser = entity.getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); Method setUpdateTime = entity.getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); setUpdateUser.invoke(entityInstance,currentId); setUpdateTime.invoke(entityInstance,now); }
结束语
我们知道Spring的AOP切面编程拥有相当强大的功能,巧妙运用会大大降低代码冗余;还有一个特别常见的AOP应用场景就是记录数据库操作日志,小伙伴们可以自行实现。
我是为建筑行业添砖java的菜鸟,欢迎互关,一起搬砖~~~