什么是 AOP?
面向切面编程
AOP 是在不改动原有业务代码的前提下,对方法做增强。
可以理解成:
在某个方法执行前/后,自动插入一段公共逻辑。
在苍穹外卖中 AOP 起什么作用呢?
- 拦截 Mapper 的
insert方法 → 自动设置createTime、updateTime、createUser、updateUser - 拦截
update方法 → 自动设置updateTime、updateUser
这部分的伪代码大概如下:
@Aspect
@Component
public class AutoFillAspect {
@Before("@annotation(autoFill)")
public void autoFill(JoinPoint joinPoint, AutoFill autoFill) {
Object entity = joinPoint.getArgs()[0]; // 拿到要插入/更新的对象
if (autoFill.value() == OperationType.INSERT) {
setField(entity, "createTime", LocalDateTime.now());
setField(entity, "updateTime", LocalDateTime.now());
setField(entity, "createUser", CurrentUserUtil.getUserId());
setField(entity, "updateUser", CurrentUserUtil.getUserId());
}
if (autoFill.value() == OperationType.UPDATE) {
setField(entity, "updateTime", LocalDateTime.now());
setField(entity, "updateUser", CurrentUserUtil.getUserId());
}
}
}
然后在 Mapper 方法上加注解:
@AutoFill(OperationType.INSERT)
void insert(User user);
@AutoFill(OperationType.UPDATE)
void update(User user);
因为 insert 和 update 不可避免的需要添加 time 和 user 了,这样就不用在 service 层中反复写 setCreateTime 和 setCreateUser 了
具体的在苍穹外卖中如何实现 AOP 呢(主要代码分析)?
自定义注解
AutoFill.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
OperationType value(); // INSERT / UPDATE
}
给 Mapper 方法打标记,告诉程序这个方法是新增和修改操作
AOP 切面会拦截带有该注解的 Mapper 方法,在方法执行前自动填充公共字段(createTime、updateTime、createUser、updateUser)。
枚举
OperationType.java
package com.sky.annotation;
/**
* 数据库操作类型枚举
*/
public enum OperationType {
INSERT,
UPDATE
}
Mapper
@AutoFill(OperationType.INSERT) //标记这是新增操作,触发AOP填充 @Insert("insert into dish values(#{id},#{name},#{categoryId},#{price},#{image},#{description},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})")
void insert(Dish dish);
切面(AOP 核心)
AutoFillAspect.java
/**
* 自定义切面,实现公共字段自动填充
*/
@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) throws NoSuchMethodException {
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 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);
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e){
e.printStackTrace();
}
} else if (operationType==OperationType.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){
e.printStackTrace();
}
}
}
}
值得注意的点
切入点说明:
@Pointcut 定义切入点表达式:
execution(* com.sky.mapper.*.*(..)):匹配com.sky.mapper包下所有类、所有方法&& @annotation(com.sky.annotation.AutoFill):并且方法上带有 @AutoFill 自定义注解
合起来含义:只有 mapper 包下,并且标记了 @AutoFill 注解的方法,才会被 AOP 拦截
比如:
@AutoFill(OperationType.INSERT)
void insert (Dish dish);- 而 autoFillPointCut() 是空方法,只是一个切入点签名,用来被通知引用,方法体不会执行
//准备赋值数据
LocalDateTime now=LocalDateTime.now();
Long currentId= BaseContext.getCurrentId();
BaseContext.getCurrentId():从 ThreadLocal 获取当前登录用户 id(员工 id)
判断是新增还是更新,用反射调用 setter 赋值
// 方法名字符串
String methodName = "setCreateTime";
// entity 可能是 Dish、Employee、Setmeal,不确定是哪个实体类
Method setCreateTime = entity.getClass().getDeclaredMethod(methodName, LocalDateTime.class);
// 执行这个set方法,等价于 entity.setCreateTime(now)
setCreateTime.invoke(entity, now);
第二行代码就是在反射:在运行时,拿到对象的字节码,根据方法名字符串去类里面查找对应的方法。
getDeclaredMethod(方法名,参数类型)
entity.getClass():反射入口,拿到这个对象对应的 Class 字节码对象
如果 entity 是 Dish 对象 → 拿到 Dish 的 Class
如果 entity 是 Employee 对象 → 拿到 Employee 的 Class
getDeclaredMethod(方法名, 参数类型):去这个 Class 里面搜索,找一个名字叫 setCreateTime、参数是 LocalDateTime 的方法。
第三行是 invoke 反射调用方法:执行刚才找到的这个方法
invoke(实例对象,传给方法的参数)
完整执行流程回顾
- Service 调用
dishMapper.insert(dish) insert方法上有@AutoFill(OperationType.INSERT)- AOP 切面匹配切入点,执行
@Before通知 - JoinPoint 拿到方法、注解、dish 实体对象
- 反射调用 dish 的 set 方法填充 4 个公共字段
- 填充完成,执行 MyBatis Insert SQL