写在前面
在学习苍穹外卖过程中,弹幕常有 “为什么我打不开?为什么我没有输出?”的疑问,针对这些我也在学习过程中同样遇到的问题,万分感激在弹幕中找到了答案,并作出这系列汇总。本文内容是基于弹幕对苍穹外卖项目的实施与补充,仅供学习与分享之用,如有侵权请联系删除~
2024-09-12
目录
execution(* com.sky.mapper.*.*(..))
@annotation(com.sky.annotation.AutoFill)
拓展:
哪些是公共字段?
发现问题
不难发现,在前面实现新增 OR 修改数据时,往往需要编写一段代码,以此为该数据设置上修改/创建该条数据的时间和人员id。
发现特点
① 时间数据根据系统定义、人员根据Threadlocal中获取的id对应,这些数据是公共的,所有方法都可以获取。
② 每次都是在 插入,修改 数据时,需要设置这些字段。
③ 实现的代码相似,重复性高。
思考:
那么,有没有办法可以减少这种冗余?增强代码的可维护性和模块化?适合解决这个问题?
什么是AOP(面向切面编程)?
概念:
程序监测某个方法,不修改原始执行代码逻辑,由程序动态地执行某些额外的功能,对原有的方法做增强,这就叫做面向切面编程。被监测的执行方法,称之为切入点。
名词理解:
Aspect(切面)
- 切面是 AOP 的核心,包含横切逻辑的模块。一个切面可以应用到多个连接点(Join Point),以增强其行为。通常,切面是通过拦截器或者声明式的方式实现的。
- 示例:事务管理、日志记录、异常处理等都是切面。
Join Point(连接点)
- 连接点是程序执行中的某个明确点,如方法调用或异常抛出等。每个连接点都是一个执行点,AOP 可以在这个点上应用增强逻辑。
- 在 Spring AOP 中,连接点主要是方法的执行(不能应用于字段或构造函数)。
Advice(通知)
- 通知是定义在切面中的具体动作,描述了在连接点处需要执行的代码。Spring AOP 提供了多种通知类型,如前置通知(Before)、后置通知(After)、环绕通知(Around)等。
- 通知类型:
- Before Advice:在方法执行之前执行一段代码。
- After Advice:在方法执行之后执行代码。
- Around Advice:包裹方法执行,代码可以在方法执行前、后以及代替方法执行。
- After Returning Advice:在方法成功返回结果后执行代码。
- After Throwing Advice:在方法抛出异常时执行。
Pointcut(切入点)
- 切入点定义了在哪些连接点上应用增强逻辑,即选择连接点的规则。通过切入点表达式,AOP 可以选择要在哪些方法或类上织入切面逻辑。
- 示例:定义某个包下的所有
public
方法作为切入点,或者某些带有特定注解的方法。
Weaving(织入)
- 织入是将切面应用到目标对象的过程,实现在连接点处动态增强目标对象的方法。织入可以发生在编译时、类加载时、或运行时。
- Spring AOP 是通过运行时的动态代理进行织入的。
Target Object(目标对象)
- 被增强的类或方法,AOP 切面会增强它的某些功能。目标对象即业务逻辑的主体。
意义:
- Spring 框架的中心宗旨之一是非侵入性,AOP 可以方便地在某些场景实现特定的功能。
- AOP 设计的功能代码可以复用,代码耦合性更低,代码更加整洁。
看到这里,和上面的思考似乎可以对上,对吧?接下来深入想想怎么用AOP实现?
再思考两个问题:
- 待解决问题有什么关联的操作? ==> insert、update
- 待解决问题出现的时机? ==> 需要执行 insert、update 操作时
当需要执行 insert、update 操作时,我们就(有选择性的)统一处理 ==> 定义为切入点
AOP实现思路:
统一处理、切入点处理 ==> 定义注解(即标识) ==> 拦截有标识的方法执行定义的操作
自定义注解AutoFill:
用于标识需要进行公共字段自动填充的方法 => 创造一个切入点,整合 insert、update 两个操作。
自定义切面类AutoFillAspect:
统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值。 => 在切面类定义切入点、通知
加入 AutoFill 注解
在 Mapper 的需要执行公共字段填充的 insert、update 操作方法上添加注解 => 织入
技术点:枚举、注解、AOP、反射
代码开发
AutoFill (前期准备工作)
创建注解路径:src/main/java/com/sky/annotation/AutoFill.java
AutoFill 代码:
添加注解
元注解说明
@Target - 定义应用的元素
@Target 元注解用于指定自定义注解可以应用的 Java 元素类型。例如:
- ElementType.METHOD: 注解应用于方法。
- ElementType.FIELD: 注解应用于字段。
- ElementType.TYPE: 注解应用于类、接口(包括注解类型)或枚举声明。
@Retention - 定义保留策略
@Retention 元注解用于指定自定义注解的保留策略。例如:
- RetentionPolicy.SOURCE: 注解只在源码中存在,编译时会被丢弃。
- RetentionPolicy.CLASS: 注解在编译后的字节码文件中存在,但运行时不会被加载到 JVM 中。
- RetentionPolicy.RUNTIME: 注解在运行时也存在,可以通过反射机制获取。
是否必须添加元注解?
- @Target:不是必须的,但建议添加。默认情况下,注解可以应用于任何元素,指定 @Target 可以限制注解的应用范围,提高代码的可读性和安全性。
- @Retention:不是必须的,但建议添加。默认情况下,注解的保留策略是 RetentionPolicy.CLASS。明确指定 @Retention 可以确保注解在预期的生命周期内存在,并且可以通过反射机制在运行时获取到注解信息。
定义注解属性
定义注解属性,指定类型为 OperationType数据库的操作类型,通过枚举来指定。
即在使用@AutoFill注解时,需要传参数(枚举中的某一值) 👇
@AutoFill(value = OperationType.?)
OperationType(枚举类)
了解枚举类 👉 跳转
整合了两种操作,在对应的切入点传对应的参数值
- OperationType.UPDATE
- OperationType.INSERT
AutoFillAspect(定义切入点&通知)
添加注解
定义切入点
@Pointcut
:
- 表示这个方法定义了一个切入点。切入点描述的是哪些方法将被拦截,并且可以在其他地方被引用。
execution(* com.sky.mapper.*.*(..))
- 匹配
com.sky.mapper
包及其子包下所有类的所有方法。
第一个 *
表示任意的返回类型。com.sky.mapper
= 包名,第二个*
表示该包下的所有类。第三个 *
表示该类中的所有方法。( .. )
表示任意参数列表的方法。@annotation(com.sky.annotation.AutoFill)
- 表示只拦截那些标注了
@AutoFill
注解的方法。
public void autoFillPointCut()
:
- 无实现逻辑,作为标记来定义切入点。它的名字可以任意起,用于在切面中引用这个切入点。
执行通知
进入前置通知:
相关代码:
@Before("autoFillPointCut()")
- 前置通知:在标记前执行通知。
public void autoFill(JoinPoint joinPoint)
:
autoFill
方法是前置通知的逻辑。JoinPoint
是 AOP 提供的对象,它包含了当前被拦截的目标方法的相关信息log.info("开始进行公共字段自动填充..");
- 当进入到前置通知时,记录日志,标志自动填充字段过程的开始。
AOP获取拦截信息:
思路:
需要获取拦截的是insert还是update方法 => 通过
JoinPoint获取
当前被拦截的目标方法的相关信息 => 运用反射,通过字节码文件获取@AutoFill
注解对象 => 获取注解上的value。
- 通过
JoinPoint
的getSignature();方法 => 获取被拦截方法的签名、注解和参数。- 通过
signature.getMethod()
=> 获取当前被拦截的方法- 再调用
getAnnotation(AutoFill.class)
=> 获取方法上标注的@AutoFill
注解对象通过@AutoFill
注解的value()
方法,它返回该注解的属性值。在此它表示数据库操作的类型(比如INSERT
或UPDATE
)。相关代码:
拓展:
- 看看
JoinPoint源码有啥:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package org.aspectj.lang; import org.aspectj.lang.reflect.SourceLocation; public interface JoinPoint { String METHOD_EXECUTION = "method-execution"; String METHOD_CALL = "method-call"; String CONSTRUCTOR_EXECUTION = "constructor-execution"; String CONSTRUCTOR_CALL = "constructor-call"; String FIELD_GET = "field-get"; String FIELD_SET = "field-set"; String STATICINITIALIZATION = "staticinitialization"; String PREINITIALIZATION = "preinitialization"; String INITIALIZATION = "initialization"; String EXCEPTION_HANDLER = "exception-handler"; String SYNCHRONIZATION_LOCK = "lock"; String SYNCHRONIZATION_UNLOCK = "unlock"; String ADVICE_EXECUTION = "adviceexecution"; String toString(); String toShortString(); String toLongString(); Object getThis(); Object getTarget(); Object[] getArgs(); Signature getSignature(); SourceLocation getSourceLocation(); String getKind(); JoinPoint.StaticPart getStaticPart(); public interface EnclosingStaticPart extends JoinPoint.StaticPart { } public interface StaticPart { Signature getSignature(); SourceLocation getSourceLocation(); String getKind(); int getId(); String toString(); String toShortString(); String toLongString(); } }
- 调用
getSignature()方法
返回了Signature对象,因为通知实际作用于方法上,所以可以向下强转为MethodSignature报错注意:
- 此处可能有导包问题导致的报错
获取实体对象(参数):
思路:
- 获取被拦截的方法的参数列表(所有参数中只提取实体对象)。
- 用Object 数组(因为注解作用在多个方法上,无法确定参数的实体类)
- 判断参数列表是否为空(防止空指针),如果没有参数,直接返回,不执行后续逻辑。
- 取出第一个参数(约定:将实体对象放在第一个参数位置),用于后续的字段填充。
相关代码:
执行的操作的逻辑
思路:
准备赋值的数据 => 判断当前的操作类型(INSERT需要设置创建人&创建时间, UPDATE不用) => 通过反射获取setter方法来赋值
相关代码:
① 准备赋值的数据:
LocalDateTime now = LocalDateTime.now(); Long currentId = BaseContext.getCurrentId();
now
记录当前时间,用于为时间字段(如createTime
、updateTime
)赋值。currentId
获取当前用户的 ID。BaseContext.getCurrentId()
在day2已介绍。② 操作类型判断:
if (operationType == OperationType.INSERT) { // 为4个公共字段赋值 } else if (operationType == OperationType.UPDATE) { // 为2个公共字段赋值 }
③ 反射获取方法并赋值:
- INSERT 操作:
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); // 设置创建用户ID setUpdateTime.invoke(entity, now); // 设置更新时间 setUpdateUser.invoke(entity, currentId); // 设置更新用户ID
- entity.getClass()获取上文实体对象的字节码文件 => 例如新增员工时获取 Employee
getDeclaredMethod(方法名,参数类型列表)
,用于获取entity类的某个特定方法。- 获取setCreateTime、setCreateUser、setUpdateTime、setUpdateUser4个方法
- 通过
invoke
方法执行这些 setter 方法,为实体对象赋值。
- UPDATE 操作:
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); // 设置更新用户ID
- 获取setUpdateTime、setUpdateUser 2个方法
④ 异常处理:
(编译错误时将鼠标放置在红线上,可以看它会抛什么异常)咱们直接捕获大异常
拓展:
为了防止敲字符串(如:setCreateTime)敲错导致获取方法名出错,定义了常量