通过Spring提供的AOP切面编程,实现项目中公共字段的自动填充

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的菜鸟,欢迎互关,一起搬砖~~~

  • 23
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring AOP,可以使用切面编程实现动态数据源的切换。下面是一个简单的示例: 首先,创建一个注解类`DataSource`,用于标识需要切换数据源的方法: ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface DataSource { String value() default "default"; } ``` 然后,创建一个切面类`DataSourceAspect`,在该类定义切点和切面逻辑: ```java @Aspect @Component public class DataSourceAspect { @Around("@annotation(dataSource)") public Object switchDataSource(ProceedingJoinPoint joinPoint, DataSource dataSource) throws Throwable { try { // 获取要切换的数据源名称 String dataSourceName = dataSource.value(); // 根据数据源名称切换数据源 switchDataSource(dataSourceName); // 执行目标方法 return joinPoint.proceed(); } finally { // 切换回默认数据源 switchDataSource("default"); } } // 实际切换数据源的逻辑 private void switchDataSource(String dataSourceName) { // 根据传入的数据源名称进行数据源切换逻辑的实现 // ... } } ``` 在上述代码,`@Around("@annotation(dataSource)")`表示拦截带有`@DataSource`注解的方法,并执行切面逻辑。在切面逻辑,首先获取切换的数据源名称,然后根据该名称进行数据源的切换操作。在目标方法执行完毕后,切面逻辑会将数据源切换回默认的数据源。 最后,使用`@DataSource`注解标识需要切换数据源的方法: ```java @Service public class UserService { @DataSource("db1") public void addUser(User user) { // 执行添加用户的逻辑 } @DataSource("db2") public void updateUser(User user) { // 执行更新用户的逻辑 } } ``` 在上述示例,`addUser`方法使用名为"db1"的数据源,`updateUser`方法使用名为"db2"的数据源。 通过以上步骤,就可以使用Spring AOP实现动态数据源的切换。当调用带有`@DataSource`注解的方法时,切面会根据注解指定的数据源名称进行数据源切换,从而实现动态切换数据源的效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值