苍穹外卖 - Day03
一、公共字段自动填充:AOP的妙用
在我们的项目中,许多数据表(如 employee
, dish
, category
等)都有一些共通的字段,比如 create_time
(创建时间)、update_time
(修改时间)、create_user
(创建人ID)、update_user
(修改人ID)。每次执行插入或更新操作时,如果都在业务代码中手动为这些字段赋值,不仅繁琐重复,而且容易遗漏。
为了优雅地解决这个问题,“苍穹外卖”项目采用了面向切面编程 (AOP) 的思想,结合自定义注解和Java反射机制来实现这些公共字段的自动填充。
1.1 核心技术点:AOP、自定义注解、反射与枚举
在深入具体实现之前,我们先对涉及的核心概念做个提纲挈领的介绍:
-
AOP (Aspect-Oriented Programming - 面向切面编程):
-
核心思想: 将那些贯穿于多个业务模块中的通用功能(如日志记录、权限校验、事务管理、以及我们这里的公共字段填充)从主业务逻辑中分离出来,形成可重用的“切面”。这样可以降低业务逻辑的复杂度,提高代码的模块化和可维护性。
(通俗点说:) AOP 就像给你的代码加装了一些“监控探头”和“自动处理装置”。你规定好在哪些地方(切点)装探头,探头发现特定动作(方法执行)时,就自动触发相应的处理(通知/增强逻辑),而不需要在每个地方都手动写一遍这些处理代码。
-
-
注解 (Annotation):
-
核心思想: 注解是附加在代码(类、方法、字段等)上的一种元数据(metadata),它本身不直接影响代码的执行逻辑,但可以被编译器或运行时环境读取,并据此执行某些特定的操作或提供额外信息。
(打个比方:) 注解就像给代码元素贴上的“标签”。比如,
@Override
标签告诉编译器检查这个方法是不是真的覆盖了父类的方法。我们也可以自定义标签,然后写一些代码来识别这些标签并执行特定逻辑。 -
-
反射 (Reflection):
-
核心思想: Java 反射机制允许程序在运行时动态地获取任意一个类的信息(如方法、字段)并能操作它们。
(简单来说:) 反射就是让Java程序在运行时能“看透”和“操纵”它自己的代码结构,即使在编译时这些结构是未知的或者不可直接访问的(比如私有字段)。
-
-
枚举 (Enumeration):
-
核心思想: 枚举用于定义一组固定的常量集合。它比使用普通的静态
final
常量更类型安全,也更具可读性。
(举个例子:) 我们需要表示数据库操作的类型,比如“插入”和“更新”。用枚举
OperationType.INSERT
和OperationType.UPDATE
就比用整数0
和1
要清晰得多,也不容易出错。 -
1.2 “苍穹外卖”项目中的实现方案
基于以上概念,“苍穹外卖”项目通过以下步骤实现公共字段的自动填充:
-
定义操作类型枚举 (
OperationType.java
):-
创建一个枚举类,明确标识出数据库操作的类型,例如:
public enum OperationType {INSERT,UPDATE }
-
-
创建自定义注解 (
@AutoFill.java
):-
定义一个注解,比如
@AutoFill
,用来标记哪些 Mapper 层的方法在执行时需要进行公共字段的自动填充。 -
这个注解可以带一个
value
成员变量,其类型就是我们上面定义的OperationType
枚举,用来指明当前被标记的方法执行的是插入操作还是更新操作。import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;@Target(ElementType.METHOD) // 注解作用于方法上 @Retention(RetentionPolicy.RUNTIME) // 注解在运行时保留,以便通过反射读取 public @interface AutoFill {OperationType value(); // 通过value属性指定数据库操作类型是INSERT还是UPDATE }
-
-
创建切面类 (
AutoFillAspect.java
):-
这是一个使用
@Aspect
注解标记的类,它包含了我们的横切逻辑(即自动填充公共字段的逻辑)。 -
定义切点 (Pointcut): 使用切点表达式指定哪些方法会被拦截。这里我们希望拦截所有被
@AutoFill
注解标记的方法。-
切点表达式可能类似于:
@Pointcut("@annotation(com.sky.annotation.AutoFill)")
(假设@AutoFill
注解在com.sky.annotation
包下) -
或者更精确地指定到 Mapper 包下的方法:
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
-
-
定义通知 (Advice): 通常使用
@Before
(前置通知),表示在目标方法(被@AutoFill
标记的Mapper方法)执行之前,执行我们的自动填充逻辑。-
获取操作类型: 在通知方法中,可以通过连接点 (
JoinPoint
) 获取到目标方法上的@AutoFill
注解,从而得到当前操作是INSERT
还是UPDATE
。 -
获取实体对象: Mapper 方法的参数通常就是要操作的实体对象(比如
Employee
、Dish
等)。通知方法需要从连接点的参数中获取到这个实体对象。(要注意的是:) Mapper方法可能有多个参数,实体对象不一定是第一个。需要约定好实体对象在参数列表中的位置,或者通过参数类型来判断。
-
通过反射为公共字段赋值:
-
获取当前时间 (
LocalDateTime.now()
)。 -
获取当前登录用户的ID (通常从
ThreadLocal
中获取,例如BaseContext.getCurrentId()
)。 -
根据操作类型(
INSERT
或UPDATE
)和实体对象:-
如果是
INSERT
操作:-
需要为
createTime
,updateTime
,createUser
,updateUser
这四个字段赋值。 -
使用反射获取实体对象中名为
setCreateTime
,setUpdateTime
,setCreateUser
,setUpdateUser
的方法。 -
通过
Method.invoke()
方法调用这些setter方法,将当前时间和当前用户ID设置进去。
-
-
如果是
UPDATE
操作:-
只需要为
updateTime
和updateUser
这两个字段赋值。 -
同样使用反射获取并调用
setUpdateTime
和setUpdateUser
方法。
-
-
(这里为什么用反射?) 因为这个切面要处理项目中所有可能需要自动填充的实体(Employee, Dish, Category等),这些实体类各不相同,但它们都约定了具有如
setCreateTime
这样的方法。反射使得我们可以在不知道具体实体类型的情况下,动态地查找并调用这些公共的setter方法。 -
-
-
-
在 Mapper 接口的方法上使用
@AutoFill
注解:-
对于那些执行插入或更新操作的 Mapper 方法,在其声明上添加
@AutoFill
注解,并指明操作类型。 -
例如,在
EmployeeMapper.java
中:@Mapper public interface EmployeeMapper {@AutoFill(OperationType.INSERT) // 标记为插入操作,需要自动填充void insert(Employee employee);@AutoFill(OperationType.UPDATE) // 标记为更新操作,需要自动填充void update(Employee employee); }
-
工作流程总结:
-
当调用被
@AutoFill
注解标记的 Mapper 方法时(例如employeeMapper.insert(employee)
)。 -
Spring AOP 机制会侦测到这个注解,
AutoFillAspect
切面中定义的@Before
通知会在目标方法执行前被触发。 -
通知方法获取到
@AutoFill
注解的值(INSERT
或UPDATE
)和传入的实体对象。 -
根据操作类型,通知方法通过 Java 反射机制,动态地找到实体对象中对应的
setCreateTime
,setUpdateTime
等方法,并用当前时间或当前用户ID调用它们,完成公共字段的赋值。 -
然后,目标 Mapper 方法(如
insert
)继续执行,此时实体对象中的公共字段已经被填充好了,可以直接持久化到数据库。
通过这种方式,公共字段的填充逻辑被集中到了一个切面中统一处理,业务代码(Service层和Mapper层)不再需要关心这些字段的设置,使得代码更加简洁、健壮,并且易于维护。