Java反射与AOP实战:公共字段自动填充

🔥个人主页:北极的代码(欢迎来访)
🎬作者简介:java后端学习者
❄️个人专栏:苍穹外卖日记,SSM框架深入,JavaWeb
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:前面我们完成了员工管理(新增员工和分页查询)和员工编辑等业务功能,同时因为业务逻辑一样,对于菜品的分类,我们直接导入了代码,下面我们主要实现的是公共字段的填充和新增菜品操作,以及难点和原理。
公共字段:
这里指的是在不同的业务表中,有相同的字段,如:创建时间,创建人,更新时间.......如果我们按照不同的业务逻辑去挨个实现,这就造成了代码的冗余繁杂,由此我们想到了一些解决办法。
实现思路——面向切面编程
首先我们需要知道在什么时机我们需要修改这些字段,以便对这些字段进行归类,如创建人和创建按时间都是在执行insert语句时进行修改的,更新时间是在执行insert和update语句时修改的。这时我们可以通过设置切面来统一的拦截mapper这一层,统一的为公共字段来统一的赋值。同时持久层mapper有许多字段,并不是每一个字段都需要被拦截,我们需要通过设置注解的方式来进行筛选(自定义注解AutoFill,用于表示需要进行公共字段填充的方法),之后我们使用自定义切面类AutoFillAspect来统一的拦截加入了AutoFill的方法,,通过反射为公共字段赋值。
实现步骤:
1.我们先创建一个自定义注解类,然后在里面指定数据库操作的类型,实现我们已经定义了数据库操作的类型,create,update。

OperationType value();这行代码的作用就是:
-
定义注解的参数:让注解可以接收一个
OperationType类型的值 -
传递重要信息:告诉AOP这次操作是INSERT还是UPDATE
-
实现灵活配置:同一个注解可以通过不同参数实现不同行为
就像我们设计一张表格,上面有一个必填的选项框,让使用者勾选“新增”还是“修改”。AOP切面看到这张表格,就能根据勾选的选项,决定该填哪些字段。
这就是自定义注解中参数定义的标准写法,也是让注解变得灵活实用的关键所在!
2.创建一个切面类
并在里面设置切入点和通知
@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 等字段
为什么要用 @Before?(最重要)
因为我们要做的是:在执行 SQL 之前,把字段值设置好
流程是这样的:
1. Controller 调用 Service
2. Service 调用 Mapper
3. Mapper 准备执行 SQL
公共字段必须在 SQL 执行之前就填进去
@Before = 在 mapper 方法执行之前填充字段,这样 SQL 才能拿到正确的值插入/更新
• @After:方法执行完了才填充 → 没用,数据库已经写完了
• @Around:可以,但没必要,@Before 更简单清晰
具体分析流程(以Dish为例):
// 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作为参数传入
3.在mapper方法上加上@AutoFill注解
通过注解@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) → 入库 │
结语:感谢大家观看到最后,如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!
更多推荐




所有评论(0)