问题背景:在我们做前后端分离项目,或者是其他项目的时候,代码的简洁成为了我们必不可少的一部分,在数据库设计的时候,有时候同样的代码或字段需要重复的去编写,这大大提高了代码的冗余度。
例如:在我们的用户表user和我们的类似于发布动态的表 难免会出现相同的字段,例如:更新时间,修改时间,创建时间等。一系列字段,需要我们每次做update或者insert时,都会重复的编写对应的代码进行赋值,这样我们的代码冗余就会体现出来。
解决方案:为此我们可以使用spring中的AOP面向切面编程对其进行简化,并且使用自定义注解进行实现这种重复赋值的情况。
项目初始化的版本
- spring版本:3.0.2
- maven版本:3.8.8
- jdk版本:java8及以上,我这里是jdk17
- Idea版本这个不是很重要:我这里是2023.x
项目依赖:
主要的就是需要引入自定义切面的一些包
<!-- 自定义切面类Aspect--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
代码编写:
创建一个自定义注解类 AutoFill
package com.sxy.recordnetwork.annotation;
import com.sxy.recordnetwork.enumeration.OperationType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解,用于标识某个方法需要进行普通自动填充,例如修改用户信息需要涉及到id我们不能直接一个个得到,id我们只能通过注解模式
* @author Administrator
*/
// 指定该注解只能加载方法上method -> METHOD
@Target(ElementType.METHOD)
/*
1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
// 指定属性,指定数据库操作的类型 定义枚举类UPDATE
OperationType value();
}
我自定义注解的类型是一个枚举类OperationType,我们定义一个枚举类用来存放UPDATE和INSERT操作
package com.sxy.recordnetwork.enumeration;
/**
* 定义更新的操作枚举类
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 新增操作
*/
INSERT
}
接着我们创建一个自定义的切面类AutoFillAspect用于配置AOP切面的切入点方法
package com.sxy.recordnetwork.aspect;
import com.sxy.recordnetwork.Utils.BaseContext;
import com.sxy.recordnetwork.annotation.AutoFill;
import com.sxy.recordnetwork.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 自定义切面类,实现公共字段的填充,例如修改用户信息使用到where条件是用户的id
*/
@Component
@Slf4j
@Aspect
public class AutoFillAspect {
/*
切入点 对哪些类的哪些方法来进行拦截进行定义
*/
// execute(返回值是所有的* 在com.sky.mapper包下 所有的类 所有的方法 (匹配所有的参数类型 ) && 满足这个方法上加入了AutoFill的注解)
@Pointcut("execution(* com.sxy.recordnetwork.mapper.*.*(..)) && @annotation(com.sxy.recordnetwork.annotation.AutoFill)")
public void autoFillPointCut() {
}
/* 前置通知 因为我们要在insert 和update语句执行之前做相关操作
在通知中进行公共字段的赋值
Before("切入点,当匹配表达式的时候执行的方法")
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
log.info("开始进行公共字段自动填充。。。");
// 1、获取到当前被拦截的方法上的数据库操作类型是INSERT 还是 UPDATE
MethodSignature signature = (MethodSignature) joinPoint.getSignature();// 方法签名对象
AutoFill annotation = signature.getMethod().getAnnotation(AutoFill.class); // 获得方法上的注解对象
OperationType operationType = annotation.value(); // 获得数据库操作类型
// 2、获取到当前被拦截的方法的参数 -- 实体对象
Object[] args = joinPoint.getArgs();
// 是否为空指针
if (args == null || args.length == 0) {
return;
}
// 通过object接受,因为实体对象不确定
Object UserUpdateDTO = args[0];
// 3、准备赋值的数据
Long currentId = BaseContext.getCurrentId(); // 当前用户id 这个ID来自于当前线程中的用户id
if (operationType == OperationType.UPDATE) { // update操作
// 为一个公共字段进行填充id
try{
Method setUpdateUser = UserUpdateDTO.getClass().getDeclaredMethod("setUserNo", Long.class);
setUpdateUser.invoke(UserUpdateDTO, currentId);
}catch(Exception e){
e.printStackTrace();
}
}else{ // insert操作
}
}
}
整体的代码我在这里解读一下:
我们这个类中有两个方法分别为:
- autoFillPointCut
- 定义切入点
- 参数列表为空
- 注解:@Pointcut:定义切入点匹配类型
- 匹配规则为:
- 返回值为所有的* 在com.sxy.recordnetwork.mapper包下.所有的类.所有的方法(匹配所有的参数类型) &&(并且) 满足这个方法上添加了AutoFill的注解)
- 匹配规则为:
- autoFill
- 逻辑处理代码
- 参数列表为JoinPoint类 用于获取拦截到的方法上的数据类型
- 注解:@Before:前置通知,在方法执行之前进行拦截通知 注解中的参数为切入点,这里的切入点就是上面的方法autoFillPointCut,满足autoFillPointCut这个方法上的切入点Pointcut注解的匹配规则进行拦截
autoFill方法内部代码解读:
@Before:前置通知,因为需要在insert和update语句执行之前做操作,在通知中进行公共字段的赋值
方法参数:JoinPoint 用于获取拦截到的方法上的数据类型是INSER还是UPDATE等或其他
首先:我们就是用这个JoinPoint 来获取方法的签名对象
接着:获得方法上的注解对象
然后:获取数据库的操作类型
紧接着:获取到当前被拦截的方法的参数 -- 实体对象,是一个Object类型数组,因为我们不知道具体的类型是什么,并且被拦截的方法列表上可能由多个参数,所以我们获取到Object类型数组
是否为空:我们判断一下是否为空指针,方法空指针错误
紧跟着:通过object类型接收,因为实体对象不确定,我们拿到object类型数组的第一个参数,我们在这里规定执行修改或添加操作时,第一个参数必须是一个实体对象,所以我们就获取到object类型数组中的第一个数据
然后:准备需要赋值的数据。我这里只是对修改的时候获取当前用户线程中的id,做的一个演示,拿到这个数据,甚至你可能还需要获取当前时间。
跟着:用上面获取到方法上注解的参数是否和我定义的枚举类OperationType.UPDATE是相等的,如果是UPDATE就执行update相关操作,否则就执行insert操作
最后:我们通过拿到实体类中的set属性方法,对应的数据类型为Long.class,然后对其赋值即可完成操作
Mapper层代码:
/** * 修改用户信息的方法 * @param userUpdateDTO */ @AutoFill(OperationType.UPDATE) void saveUser(UserUpdateDTO userUpdateDTO);
我们只需要在mapper层的某个修改方法或者新增方法上添加这个注解指定类型参数即可
总结:以上就是我们为了防止重复代码会造成一个代码冗余的方法,对其进行一个解决问题的方法,通过自定义注解和自定义AOP切面类实现的一个公共字段的填充。
如果有什么问题欢迎在评论区下面留言,各位博友们,如果觉得这篇文章写得好对你有帮助,麻烦给个评论 + 关注 + 点赞 + 收藏呗。😊😊😊