(苍穹外卖 DAY4)AOP!!!实现公共字段填充

写在前面

在学习苍穹外卖过程中,弹幕常有 “为什么我打不开?为什么我没有输出?”的疑问,针对这些我也在学习过程中同样遇到的问题,万分感激在弹幕中找到了答案,并作出这系列汇总。本文内容是基于弹幕对苍穹外卖项目的实施与补充,仅供学习与分享之用,如有侵权请联系删除~ 

2024-09-12

目录

写在前面

哪些是公共字段?

发现问题

发现特点

思考:

什么是AOP(面向切面编程)?

概念:

名词理解:

Aspect(切面)

Join Point(连接点)

Advice(通知)

Pointcut(切入点)

Weaving(织入)

Target Object(目标对象)

意义:

AOP实现思路:

自定义注解AutoFill:

自定义切面类AutoFillAspect:

加入 AutoFill 注解

代码开发

AutoFill (前期准备工作)

AutoFill 代码:

添加注解

定义注解属性

 OperationType(枚举类)

AutoFillAspect(定义切入点&通知)

添加注解

定义切入点

@Pointcut:

execution(* com.sky.mapper.*.*(..))

@annotation(com.sky.annotation.AutoFill)

执行通知

进入前置通知:

AOP获取拦截信息: 

思路:

相关代码:​编辑

拓展:

报错注意:

获取实体对象(参数):

思路:

相关代码:

执行的操作的逻辑

思路:

相关代码:

拓展:


哪些是公共字段?

发现问题

不难发现,在前面实现新增 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 切面会增强它的某些功能。目标对象即业务逻辑的主体

意义:

  1. Spring 框架的中心宗旨之一是非侵入性,AOP 可以方便地在某些场景实现特定的功能
  2. AOP 设计的功能代码可以复用,代码耦合性更低,代码更加整洁。

看到这里,和上面的思考似乎可以对上,对吧?接下来深入想想怎么用AOP实现?

再思考两个问题:

  • 待解决问题有什么关联的操作? ==>  insertupdate
  • 待解决问题出现的时机? ==>  需要执行 insertupdate 操作时

需要执行 insertupdate 操作时,我们就(有选择性的)统一处理 ==> 定义为切入点


AOP实现思路:

统一处理、切入点处理 ==>  定义注解(即标识) ==>  拦截有标识的方法执行定义的操作

自定义注解AutoFill:

用于标识需要进行公共字段自动填充的方法 => 创造一个切入点,整合 insertupdate 两个操作。

自定义切面类AutoFillAspect:

统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值。 =>  在切面类定义切入点、通知

加入 AutoFill 注解

Mapper 的需要执行公共字段填充的 insertupdate 操作方法上添加注解 => 织入


技术点:枚举、注解、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() 方法,它返回该注解的属性值。在此它表示数据库操作的类型(比如 INSERTUPDATE)。
相关代码:
拓展:
  • 看看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
报错注意:
  • 此处可能有导包问题导致的报错
获取实体对象(参数)
思路:
  1. 获取被拦截的方法的参数列表(所有参数中只提取实体对象)。
    1. 用Object 数组(因为注解作用在多个方法上,无法确定参数的实体类)
  2. 判断参数列表是否为空(防止空指针),如果没有参数,直接返回,不执行后续逻辑。
  3. 取出第一个参数(约定:将实体对象放在第一个参数位置),用于后续的字段填充。
相关代码:

执行的操作的逻辑
思路:
准备赋值的数据 => 判断当前的操作类型(INSERT需要设置创建人&创建时间, UPDATE不用) => 通过反射获取setter方法来赋值
相关代码:

① 准备赋值的数据:

LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
  • now 记录当前时间,用于为时间字段(如 createTimeupdateTime)赋值。
  • 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)敲错导致获取方法名出错,定义了常量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值