
公共字段: 这里指的是在不同的业务表中,有相同的字段,如:创建时间,创建人,更新时间.......如果我们按照不同的业务逻辑去挨个实现,这就造成了代码的冗余繁杂,由此我们想到了一些解决办法。
首先我们需要知道在什么时机我们需要修改这些字段,以便对这些字段进行归类,如创建人和创建按时间都是在执行insert语句时进行修改的,更新时间是在执行insert和update语句时修改的。这时我们可以通过设置切面来统一的拦截mapper这一层,统一的为公共字段来统一的赋值。同时持久层mapper有许多字段,并不是每一个字段都需要被拦截,我们需要通过设置注解的方式来进行筛选(自定义注解AutoFill,用于表示需要进行公共字段填充的方法),之后我们使用自定义切面类AutoFillAspect来统一的拦截加入了AutoFill的方法,,通过反射为公共字段赋值。

OperationType value();这行代码的作用就是:
OperationType类型的值
就像我们设计一张表格,上面有一个必填的选项框,让使用者勾选“新增”还是“修改”。AOP切面看到这张表格,就能根据勾选的选项,决定该填哪些字段。
这就是自定义注解中参数定义的标准写法,也是让注解变得灵活实用的关键所在!
并在里面设置切入点和通知
@Aspect @Component @Slf4j public class AutoFillAspect { /** * 切入点 */ @Pointcut("execution(* com.sky.mapper.*.*(..))&& @annotation(com.sky.annotation.AutoFill)") public void autoFilePointCut(){} /** * 设置通知 */ @Before("autoFilePointCut()") public void autoFill(JoinPoint joinPoint){ log.info("开始进行公共字段填充"); } }//获取当前被拦截方法的数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
AutoFill annotation = signature.getMethod().getAnnotation(AutoFill.class);//获取方法上的注解对象
OperationType value = annotation.value();//获取操作类型
//获取到当前被拦截的方法参数---实体对象
Object[] args = joinPoint.getArgs();
if (args==null||args.length==0){
return;
}
Object entity=args[0];//将实体放在第一位,用object接收
//准备赋值的数据
LocalDate now = LocalDate.now();
Long currentId = BaseContext.getCurrentId();
//根据不同的操作类型,为对应的属性进行赋值
if (value== OperationType.INSERT){
//为四个公共字段进行赋值
try {
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDate.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDate.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke( entity,currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}else if (value==OperationType.UPDATE){
//为两个公共字段进行赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDate.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);
}代码部分 | 含义 | 比喻 |
|---|---|---|
entity | 当前要操作的对象 | 一张具体的表格(可能是菜品卡/员工卡) |
.getClass() | 获取这个对象的类信息 | 看看这张表格是什么类型(菜品卡模板/员工卡模板) |
.getDeclaredMethod() | 获取这个类中声明的方法 | 在这个模板上找到叫某个名字的功能 |
AutoFillConstant.SET_CREATE_TIME | 要找的方法名("setCreateTime") | 要找的功能叫"填写创建时间" |
LocalDate.class | 这个方法需要的参数类型 | 这个功能需要传入"日期"类型的数据 |
方法 | 作用 | 区别 |
|---|---|---|
getMethod() | 获取public方法(包括父类的) | 只能拿public的 |
getDeclaredMethod() | 获取本类声明的所有方法 | 可以拿private的,但不包括父类 |
在我们的代码中用
getDeclaredMethod()是合适的,因为setter方法都在本类中定义。 解释: execution(* com.sky.mapper.*.*(..)) 匹配 com.sky.mapper 包下所有类、所有方法 • && @annotation(com.sky.annotation.AutoFill) 并且 方法上标了 @AutoFill 这个注解 最终意思:只有mapper里加了注解的方法,才会被增强。 @Before:在 目标方法执行之前 执行 • JoinPoint:可以拿到当前执行的方法、参数(实体对象) 这里真正要做的是: 1. 拿到方法参数里的 实体对象 2. 反射给它设置:createTime、updateUser 等字段
因为我们要做的是:在执行 SQL 之前,把字段值设置好 流程是这样的: 1. Controller 调用 Service 2. Service 调用 Mapper 3. Mapper 准备执行 SQL 公共字段必须在 SQL 执行之前就填进去 @Before = 在 mapper 方法执行之前填充字段,这样 SQL 才能拿到正确的值插入/更新 • @After:方法执行完了才填充 → 没用,数据库已经写完了 • @Around:可以,但没必要,@Before 更简单清晰
// 1. 有一个对象,但不知道具体类型
Object entity = new Dish(); // 实际上是Dish对象
// 2. 获取它的类信息
Class<?> clazz = entity.getClass(); // clazz = Dish.class
// 3. 在Dish.class中查找名为"setCreateTime"的方法
// 这个方法需要一个LocalDate类型的参数
Method setCreateTime = clazz.getDeclaredMethod("setCreateTime", LocalDate.class);
// 4. 找到了!setCreateTime变量现在代表dish.setCreateTime()这个方法
// 5. 调用这个方法,把now传进去
setCreateTime.invoke(entity, now); // 相当于 dish.setCreateTime(now)【编译时】(你写的代码)
Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime", LocalDate.class);
【运行时】(JVM实际做的)
1. entity对象指向 → [堆内存中的Dish对象: id=1, name="宫保鸡丁"]
2. entity.getClass() → 找到Dish类的元数据 [方法区中的Dish.class]
3. getDeclaredMethod() → 在Dish.class的方法表中搜索:
✓ setCreateTime(LocalDate) → 找到了!内存地址0x7F3A2B
✗ setCreateTime(String) → 没有
✗ setCreateTime(Date) → 没有
4. 返回Method对象,它内部保存了0x7F3A2B这个地址
【调用时】
setCreateTime.invoke(entity, now)
→ JVM通过Method对象保存的地址0x7F3A2B,直接跳转到setCreateTime方法的代码位置执行
→ 执行时,entity作为this对象,now作为参数传入通过注解@AutoFill(value=OperationType.UPDATE).

┌─────────────────────────────────────────────────────────────┐
│ 员工登录系统 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ThreadLocal记录当前用户ID │
│ BaseContext.setCurrentId(1001) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Mapper方法执行(如insert(dish) 或 update(employee)) │
│ 方法上有 @AutoFill(INSERT/UPDATE) 注解 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ AOP切面拦截 │
│ 1. 获取注解类型:INSERT 或 UPDATE │
│ 2. 获取实体对象:dish 或 employee │
│ 3. 从ThreadLocal获取当前用户ID │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 根据操作类型执行填充 │
│ │
│ 如果是INSERT: 如果是UPDATE: │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │setCreateTime(now)│ │setUpdateTime(now)│ │
│ │setCreateUser(1001)│ │setUpdateUser(1001)│ │
│ │setUpdateTime(now)│ └─────────────────┘ │
│ │setUpdateUser(1001)│ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 反射执行setter方法 │
│ dish.setCreateTime(now) → Dish对象被赋值 │
│ employee.setUpdateUser(1001) → Employee对象被赋值 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 执行原Mapper方法 │
│ insert(dish) 或 update(employee) → 入库 │结语:感谢大家观看到最后,如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!