介绍
背景
- 典型字段:创建时间(create_time)、创建人ID(create_user)、修改时间(update_time)、修改人ID(update_user)
- 问题本质:多个业务表(员工表、分类表、菜品表等)存在相同字段导致代码冗余
- 维护痛点:字段变更时需要修改多处代码,维护成本高且容易遗漏
- 操作类型区分:
- 创建字段:仅在insert操作时赋值
- 修改字段:在insert和update操作时均需赋值
- 技术方案:
- 注解标记:自定义注解标识需要自动填充的方法
- AOP拦截:通过切面统一处理带注解的Mapper方法
- 反射赋值:利用反射机制动态设置字段值
- 技术要点:使用枚举、注解、AOP和反射技术实现
公共字段自动填充是在软件开发中,特别是涉及数据库操作的应用里,自动为记录添加通用信息(如创建时间、修改时间、创建者、修改者等)的机制。其原理综合运用了面向切面编程、线程本地存储、注解、反射等技术:
1.面向切面编程(AOP)
- 原理:AOP 能将横切关注点(如日志记录、事务管理、公共字段填充)从业务逻辑中分离出来。通过定义切面(Aspect),确定在哪些连接点(Join Point,如方法调用)执行特定的通知(Advice,如前置通知、后置通知)。
- 应用示例:在Java开发中,使用Spring AOP框架,开发者可定义一个切面,指定在所有数据持久化方法(如保存、更新操作)执行前,触发前置通知,完成公共字段的填充。例如,在保存菜品信息到数据库前,通过切面自动填充创建时间、创建人等字段。
2.线程本地存储(ThreadLocal)
- 原理:ThreadLocal为每个线程提供独立的变量副本,使得在多线程环境下,每个线程都能独立操作自己的变量,避免线程安全问题。在公共字段填充场景中,常用于存储和传递与当前线程相关的上下文信息,如当前登录用户。
- 应用示例:在一个Web应用中,当用户登录后,将用户信息存入ThreadLocal。在后续处理业务逻辑(如订单创建、商品更新等)时,不同的业务方法可能在不同的类和方法调用层次,但通过ThreadLocal都能获取到当前登录用户信息,用于填充公共字段(如创建人、修改人)。
大致的项目结构如下所示:
实现思路
- 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
- 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
- 在Mapper 的方法上加入 AutoFill 注解
sky-server
├── src/main/java/com/sky
│ ├── annotation
│ │ └── AutoFill.java # 自定义注解
│ ├── aspect
│ │ └── AutoFillAspect.java # 切面类
│ ├── mapper
│ │ ├── CategoryMapper.java # 添加 @AutoFill 注解
│ │ ├── DishMapper.java # 格式调整
│ │ └── EmployeeMapper.java # 添加 @AutoFill 注解
│ ├── service/impl
│ │ ├── CategoryServiceImpl.java # 修改:移除手动填充代码
│ │ └── EmployeeServiceImpl.java # 修改:移除手动填充代码
│ ├── constant
│ │ └── AutoFillConstant.java # 公共字段自动填充相关常量
│ ├── context
│ │ └── BaseContext.java # 全局上下文
│ └── enumeration
│ ├── OperationType.java # 引用:数据库操作类型枚举
│ └── StatusConstant.java # 引用:状态常量枚举- @Target(ElementType.METHOD):指定注解只能加在方法上
- @Retention(RetentionPolicy.RUNTIME):运行时保留
- 包含OperationType value()属性,指定数据库操作类型,OperationType枚举定义在com.sky.enumeration包,包含UPDATE和INSERT两种操作类型,不包含DELETE和SELECT,因为这些操作不需要填充公共字段
package com.sky.annotation;
/**
* 自定义注解,标识某个方法需要进行功能字段自动填充
*/
@Target(ElementType.METHOD) // 作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时
public @interface AutoFill {
// 数据库操作类型
OperationType value();
}package com.sky.enumeration;
/**
* 数据库操作类型(枚举)
*/
public enum OperationType {
UPDATE,
INSERT
}package com.sky.constant;
/**
* 公共字段自动填充相关常量
*/
public class AutoFillConstant {
/**
* 实体类中的方法名称
*/
public static final String SET_CREATE_TIME = "setCreateTime";
public static final String SET_UPDATE_TIME = "setUpdateTime";
public static final String SET_CREATE_USER = "setCreateUser";
public static final String SET_UPDATE_USER = "setUpdateUser";
}/**
* 插入数据
* @param category
*/
@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})")
@AutoFill(value = OperationType.INSERT)
void insert(Category category);AutoFillAspect 切面类工作原理
1. 切面定义和切入点
@Aspect // 标识这是一个切面类
@Component // 作为Spring组件管理
@Slf4j // 日志记录
public class AutoFillAspect {
// 定义切入点:拦截mapper包下所有方法且有@AutoFill注解的方法
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut() {
}
}2. 前置通知处理逻辑
在方法执行前,通过 @Before("autoFillPointCut()") 拦截目标方法:
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
log.info("开始进行公共字段自动填充...");
// 获取方法签名和@AutoFill注解中的操作类型(INSERT或UPDATE)
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
AutoFill autoFill = methodSignature.getMethod().getAnnotation(AutoFill.class);
OperationType operationType = autoFill.value();
// 获取方法参数(实体对象)
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
return;
}
Object entity = args[0];
// 准备填充数据:当前时间和当前用户ID
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
// 根据操作类型进行字段填充
if (operationType == OperationType.INSERT) {
// INSERT操作填充所有4个字段
// createTime, createUser, updateTime, updateUser
// ...
} else {
// UPDATE操作只填充updateTime和updateUser
// ...
}
}3. 字段填充的具体实现
使用反射机制调用实体对象的setter方法:
// INSERT操作
if (operationType == OperationType.INSERT) {
try {
// 通过反射获取实体类的setter方法
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = 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);
// 调用setter方法进行赋值
setCreateTime.invoke(entity, now);
setCreateUser.invoke(entity, currentId);
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
// UPDATE操作只更新更新时间和更新人
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}4. 核心组件说明
- @AutoFill 注解:标记在 Mapper 接口方法上,指定操作类型(INSERT 或 UPDATE)
- OperationType 枚举:定义操作类型(INSERT 和 UPDATE)
- AutoFillConstant 常量类:定义实体类中公共字段的 setter 方法名
- BaseContext:通过 ThreadLocal 存储当前用户 ID,用于填充 createUser 和 updateUser 字段
5. 工作流程总结
- 当执行带有 @AutoFill 注解的 Mapper 方法时,触发切面逻辑
- 切面获取方法上的注解,判断操作类型(INSERT 或 UPDATE)
- 获取方法参数中的实体对象
- 获取当前时间和当前用户 ID(从 ThreadLocal 中获取)
- 根据操作类型,通过反射调用实体对象的 setter 方法填充相应字段:
- INSERT 操作:填充创建时间、创建人、更新时间、更新人四个字段
- UPDATE 操作:只填充更新时间、更新人两个字段
这种设计实现了在数据持久化时自动填充公共字段,避免了在业务代码中重复设置这些字段,提高了代码的整洁性和维护性。
package com.sky.aspect;
// 完整代码点击标题
/**
* 切面类,实现公共字段自动填充处理逻辑
*/
@Aspect // 表示当前类是一个切面类
@Component // 表示当前类是一个组件,会被Spring容器扫描到并管理
@Slf4j // 表示自动注入log对象,log对象表示日志记录器,可以记录日志信息
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("开始进行公共字段自动填充...");
// 获取到当前被拦截的方法上的数据库操作类型(UPDATE INSERT)
// 获取到当前被拦截的方法签名对象
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 获取到当前被拦截的方法上的AutoFill注解对象
AutoFill autoFill = methodSignature.getMethod().getAnnotation(AutoFill.class);
// 获取到当前被拦截的方法上的数据库操作类型(UPDATE INSERT)
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();
// 判断当前数据库操作类型,然后通过反射进行赋值
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
// 通过反射为实体对象中的属性赋值
setUpdateUser.invoke(entity, currentId);
}
}