目录
为什么需要公共字段自动填充?
步骤1 自定义注解AutoFill
步骤2 自定义切面AutoFillAspect
步骤3 在Mapper接口的方法上加入AutoFill注解
@Before("autoFillPointCut()")
JoinPoint
你能通过 JoinPoint 获取哪些信息?
例子中的 JoinPoint
获取方法签名和注解
获取被拦截方法的参数
反射
什么是反射
获取 Class 对象
获取 Method 对象
动态调用方法——invoke()
避免手动重复操作:每次插入、更新数据库时,都要手动设置 createTime
、updateTime
、createUser
、updateUser
等字段,容易出现遗漏或不一致的问题。自动填充可以保证这些公共字段在插入或更新时自动赋值,无需手动干预。
提高代码的可维护性:如果在每个数据插入和更新的地方都写上手动的字段赋值代码,当需求变化时,需要逐一修改所有相关的代码。这不仅增加了维护成本,还增加了出错的几率。通过自动填充,这些字段可以集中管理,方便维护。
保证数据一致性:自动填充可以保证所有数据记录的时间戳和用户信息是一致的,并且可以通过统一的逻辑进行约束,减少人为错误带来的数据不一致问题。
/**
* 数据库操作类型
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
/**
* 自定义注解 用于表示某个方法需要进行功能字段自动填充处理
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型 update insert
OperationType value();
}
/**
* 自定义切面 实现公共字段自动填充处理逻辑
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
/**
* 前置通知 在通知中进行公共字段的赋值
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始进行公共字段的填充...");
//获取到当前被拦截的方法上的数据库操作类型
MethodSignature signature =(MethodSignature)joinPoint.getSignature();//方法签名对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获取方法上的注解对象
OperationType operationType = autoFill.value();//获得数据库操作类型
//获取当前被拦截到的方法参数——实体对象
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0){
return;
}
Object entity = args[0];
//准备赋值数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//根据当前不同的操作类型 为属性赋值 通过反射
if(operationType == OperationType.INSERT){
//为四个公共字段赋值
try {
Method setCreatTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreatUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setCreatTime.invoke(entity,now);
setCreatUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}else if(operationType == OperationType.UPDATE){
//为2个公共字段赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod("setUpdateTime", LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod("setUpdateUser", Long.class);
//通过反射为对象属性赋值
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* 根据主键来动态修改属性
* @param employee
*/
@AutoFill(value = OperationType.UPDATE)
void update(Employee employee);
/**
* 插入员工数据
* @param employee
*/
@Insert("insert into employee (name, username, password, phone, sex, id_number, status, create_time, update_time, create_user, update_user) " +
"values (" +
"#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})"
)
@AutoFill(value = OperationType.INSERT)
void insert(Employee employee);
/**
* 根据id修改分类
* @param category
*/
@AutoFill(value = OperationType.UPDATE)
void update(Category category);
/**
* 插入数据
* @param category
*/
@AutoFill(value = OperationType.INSERT)
@Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
" VALUES" +
" (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
void insert(Category category);
@Before("autoFillPointCut()")
@Before("autoFillPointCut()")
中的参数 "autoFillPointCut()"
用来指定前置通知的切入点。它告诉 AOP 框架,这个前置通知(autoFill()
方法)应该在什么地方执行。
"autoFillPointCut()"
是一个切入点表达式,引用了之前用 @Pointcut
注解定义的切入点方法 autoFillPointCut()
。
@Pointcut
定义的 autoFillPointCut()
方法:
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
这个方法并没有实际的代码执行,它仅仅是一个标识符,用来描述一个切入点,指定了哪些方法应该被拦截。它定义了拦截 com.sky.mapper
包下所有带有 @AutoFill
注解的方法。
然后,@Before("autoFillPointCut()")
表示:
在所有符合 autoFillPointCut()
所定义的切入点表达式的目标方法之前,执行 autoFill()
方法。
JoinPoint
JoinPoint
是 AOP(面向切面编程)中的一个核心概念,它代表了在程序执行过程中的某个连接点。在 Spring AOP 中,JoinPoint
通常指的是拦截的方法调用,你可以通过 JoinPoint
获取很多与当前执行方法有关的信息,比如方法名、参数、目标对象等。
在 Spring AOP 中,JoinPoint
表示一个拦截点,即某个被拦截的方法执行时的上下文信息。你可以把 JoinPoint
看作一个对象,里面存储了和当前拦截方法相关的各种数据。通过 JoinPoint
,我们可以访问到很多和当前方法执行有关的详细信息。
JoinPoint
获取哪些信息?JoinPoint
在代码中,JoinPoint
被传递到了 autoFill()
方法里。这个 JoinPoint
表示当前拦截的数据库操作方法(如插入或更新操作)。我们通过 JoinPoint
来获取方法的签名、注解、以及被调用的方法的参数。
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 方法签名对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); // 获取方法上的注解对象
OperationType operationType = autoFill.value(); // 获得数据库操作类型
joinPoint.getSignature()
:获取当前被拦截方法的签名信息。Signature
是方法的签名对象,包含方法名、返回类型、参数类型等信息。
MethodSignature
:是 Signature
的子类,提供了更多关于方法的信息。这里通过类型转换,将 Signature
转为 MethodSignature
。
signature.getMethod().getAnnotation(AutoFill.class)
:获取方法上的 @AutoFill
注解对象。通过这个注解对象可以获取到注解的元数据,比如 value
(数据库操作类型)。
autoFill.value()
:获取 @AutoFill
注解中的 value
属性,它表示数据库操作的类型(如 INSERT
或 UPDATE
)。
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
return;
}
Object entity = args[0];
joinPoint.getArgs()
:获取当前被拦截方法的参数列表(数组形式)。如果参数为空或参数长度为 0,说明没有实体对象需要填充,直接返回。
Object entity = args[0];
:假设被拦截的方法的第一个参数是需要填充的实体对象,将其取出用于后续操作。
反射是 Java 提供的一种功能,允许在运行时去动态获取类的信息,并且可以操作这些类的信息,比如获取类的字段、方法、构造函数,甚至调用方法。它的关键在于灵活性,因为我们可以在编译时不知道类的细节,但在运行时操作它们。
通常情况下,我们编写代码时,类、方法、属性等信息都是在编译时就已经确定好的。但是在某些情况下,我们需要编写更加通用的代码,让代码在不知道具体类型的情况下,仍然能够操作这些对象。这种需求就可以通过反射实现。
Class
对象反射的核心在于获取类的**Class
对象**,通过这个对象可以获取类的各种信息。你有三种常用方式获取 Class
对象:
1.通过类名:
Class> clazz = Class.forName("com.example.MyClass");
Class.forName()
通过类的全限定名(包名+类名)来获取类的 Class
对象。这种方式适用于你知道类名(可能从配置文件或数据库中读取)的情况。
2.通过类的实例:
MyClass obj = new MyClass();
Class> clazz = obj.getClass();
通过一个对象实例来获取该对象的 Class
对象。这种方式适用于你已经有该类的实例对象的情况。
3.通过类的字面量: (本次公共字段技术就是通过类的字面量)
Class> clazz = MyClass.class;
直接通过类名加 .class
获取 Class
对象。这种方式适用于在代码中直接指定类的情况。
Method
对象一旦你有了 Class
对象,你可以通过**getDeclaredMethod()
** 方法来获取类中的某个方法。getDeclaredMethod()
需要两个参数:
第一个参数:方法名(字符串形式)。
第二个参数:方法的参数类型(可以是多个,如果方法有多个参数)。
Method method = clazz.getDeclaredMethod("setCreateTime", LocalDateTime.class);
这个代码的作用是通过类的 Class
对象 clazz
获取名为 setCreateTime
的方法,该方法接受一个 LocalDateTime
类型的参数。
方法名和参数类型必须完全匹配,否则会抛出 NoSuchMethodException
。所以在使用时,你需要确保方法名称和参数类型一致。
invoke()
Method
对象不仅仅能用来描述类中的某个方法,它还提供了一个功能强大的方法——invoke()
,用来在运行时调用方法。
invoke()
方法需要两个参数:
假设有如下代码:
public class User {
private LocalDateTime createTime;
private Long createUser;
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public void setCreateUser(Long createUser) {
this.createUser = createUser;
}
}
我们希望通过反射调用 setCreateTime
和 setCreateUser
方法:
// 获取 User 类的 Class 对象
Class> clazz = user.getClass();
// 获取 setCreateTime 方法对象
Method setCreateTime = clazz.getDeclaredMethod("setCreateTime", LocalDateTime.class);
// 获取 setCreateUser 方法对象
Method setCreateUser = clazz.getDeclaredMethod("setCreateUser", Long.class);
// 创建要传入的方法参数
LocalDateTime now = LocalDateTime.now();
Long currentUserId = 12345L;
// 通过反射调用 setCreateTime 方法,给 createTime 字段赋值
setCreateTime.invoke(user, now);
// 通过反射调用 setCreateUser 方法,给 createUser 字段赋值
setCreateUser.invoke(user, currentUserId);
在这里,我们通过反射动态调用了 user
对象的 setCreateTime
和 setCreateUser
方法,分别给 createTime
和 createUser
字段赋值。