Spring AOP 知识整理

时间:2019-11-05
本文章向大家介绍Spring AOP 知识整理,主要包括Spring AOP 知识整理使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

通过一个多月的 Spring AOP 的学习,掌握了 Spring AOP 的基本概念。AOP 是面向切面的编程(Aspect-Oriented Programming),是基于 OOP(面向对象的编程,Object-Oriented Programming)开发的一套程序架构。
微服务架构体系下,有时候要求一段通用性的事务使用在软件中的许多模块,比如日志模块,外部业务逻辑等。以日志模块举例,根据 OOP 的思想,我可以建立一个日志类,然后在每一个需要记录日志的类中初始化日志类,达到日志记录的目的。缺点是,你必须在所有即将使用的业务代码中,实例化日志类,然后执行日志类的方法。如果采用 AOP 的思想,可以将日志类作为切面,然后使用代理的方式将这个日志类切入到需要记录日志的类中。日志类方法的执行只需要写一次,代码更容易维护。
在实践的过程中,使用 Spring AOP 的思想,为 PressSystem 项目加入了日志记录模块。日志采用 MySQL 数据库表进行记录,数据库连接采用 MyBatis。

AOP 基本概念

下面给出 AOP 中自己理解后的基本概念。如果想看原文,可以点击文末的 Spring AOP 官方文档。

  • Aspect(切面):一种切入多个类的模块化机制。Spring AOP 中,aspect 有两种实现方式:
    • 使用常规的类,然后使用 基于 XML Schema 方式
    • 使用常规的类,然后加上 @Aspect 注解,也就是 @AspectJ 方式
  • JoinPoint(连接点):可以理解为一个方法,就是切面要切入的那个方法。

  • Advice(通知):通知就是切面中的方法,这个方法将从JoinPoint切入。

  • Pointcut(切点):切点就是表达式,通知将会切入符合切点表达式的JoinPoint 中。

  • Introduction(引入):引入机制可以向一个能被切入的对象发明新的接口,并实现它。其实就是把能被切入的对象强制转换成另一个类,这样就可以执行另一个类的方法了。

  • Target Object(目标对象):就是能被切入的对象。Spring AOP 中,目标对象永远是可被代理的对象。

  • AOP Proxy(AOP 代理):AOP Proxy 就是 通过 AOP 框架生成出来的一个对象,这个对象实现了切面的功能,是目标类被切入以后的结果。Spring AOP 代理分两种:

    • JDK 动态代理
    • CGLIB 代理
  • Weaving(织入):织入就是把切面与目标类连接的过程。过程的产物就是一个被切入的目标。

通知的类型

简单地来说,通知可以有前置通知、后置通知、环绕通知等等。比如,前置通知就是通知在切入点执行之前执行;后置通知就是在切入点执行之后执行。在我的 Spring AOP 示例程序中,列举了以下几个通知的实现方式。

  • Before advice(前置通知):在连接点之前执行。除非是前置通知抛出了异常,否则前置通知没有能力影响执行流流向连接点。

  • After returning advice(返回后通知):在连接点的方法正常执行完之后再执行的通知。所谓的正常执行完,比如说连接点方法没有抛出异常。

  • After throwing advice(抛出后通知):如果方法因为抛出了异常而终止,那么就执行抛出后通知。

  • After (finally) advice(后置通知):不管连接点方法的退出情况如何,是正常还是有异常,后置通知都会执行。

  • Around advice(环绕通知):环绕通知是包围住连接点的通知,可以在连接点的前面或后面执行自定义的方法。环绕通知可以决定是否执行连接点的方法,或者绕过连接点的方法。绕过的方式是返回环绕通知自己的值或者抛出一个异常。

PressSystem 项目中 Spring AOP 的基本组件

以下提到的组件,是实现 Spring AOP 的组件最小子集。

  • XuanTiController.java
    在 PressSystem 项目中,用户操作的是 jsp 页面,jsp 页面会发送一个异步请求给 XuanTiController,在 XuanTiController 中,会调用切入点的方法。所以可以把 XuanTiController 看作入口。

  • XuanTiService.java
    采用接口与实现分离的设计原则,XuanTiService 就是接口,它的实现是 XuanTiServiceImpl,XuanTiServiceImpl 就是切入点。

  • XuanTiServiceImpl.java
    XuanTiServiceImpl 就是切入点,其中的函数被通知切入了。

  • LogAspect.java
    LogAspect 就是切面,其中有一些通知,这些通知将会切入到切入点中。

  • spring-common.xml
    这是切入点和切面的配置文件。

PressSystem 项目中 Spring AOP 的切入机制

  1. 总的来说,是 LogAspect 这个 bean 切入了 XuanTiServiceImpl 这个 bean

  2. 在 spring-common.xml 配置文件中,声明了 XuanTiServiceImpl 和 LogAspect 2个 bean
    <bean class="com.tgb.service.impl.XuanTiServiceImpl" />
    <bean id="logAspect" class="com.tgb.utils.LogAspect" />

  3. 在 LogAspect 中,有很多配置,看如下的3行。这些配置指定了
    saveLogInsert
    updateLogInsert
    deleteLogInsert
    这三个通知要切入 XuanTiServiceImpl
    @AfterReturning(pointcut="execution(* com.tgb.service.impl.*.save(..))", argNames="returnValue", returning="returnValue")
    @AfterReturning(pointcut="execution(* com.tgb.service.impl.*.update(..))", argNames="returnValue", returning="returnValue")
    @Around("execution(* com.tgb.service.impl.*.delete(..)) && args(id, table_name)")
    注意,每个通知,有不同类型的通知的 @AspectJ 方式的标记,例如 @AfterReturning, @Around
    另外,每个通知,可以有 pointcut 配置,即每个通知要切入到哪里

  4. AOP 的实现方式是通过代理实现的。所以在 spring-common.xml 中指定了 autoproxy,为 XuanTiServiceImpl 创建代理,如下所示:
    <aop:aspectj-autoproxy />
    注释:如果Spring决定一个bean要被切入,那么Spring就会为这个 bean 自动生成代理来插入外来的方法,保证通知被执行

  5. 在 XuanTiServiceImpl 中有如下3个方法,这3个方法符合 pointcut 的描述,因此具体地来说,这3个方法就是切入点,它们被顺利地切入了
    public void save(XuanTi xuanTi) {...}
    public boolean update(XuanTi xuanTi) {...}
    public boolean delete(String id, String table_name) {...}

PressSystem 项目中 Spring AOP 机制的调用过程

以下描述,主要涉及2点,可以概括为 @Aspect 和 @Autowired
@Aspect 就是“切入机制”,@AfterReturning, @Around, pointcut, <aop:aspectj-autoproxy /> 等知识点都属于这个知识体系
@Autowired 是“自动绑定机制”,@Component 注解,广义的 bean 等知识点属于 @Autowired 的基础知识体系

另外,下面所提到的调用过程,是 Spring AOP 相关的过程最小子集

  1. XuanTiController 被调用了,具体来说,其中的 xuanTiService.save(xuanTi); 被调用了

  2. 因为在 XuanTiController 有标注为 @Autowired 的 xuanTiService,所以 Application 会去查找 xuanTiService 这个 bean

  3. 至于为什么会去查找 xuanTiService 这个 bean 呢,是因为 spring-common.xml 中的配置
    <context:annotation-config />
    注释:启用注解配置方式,比如说启用了 @Autowired 的识别。

  4. XuanTiService 只是一个 interface,实现它的是 XuanTiServiceImpl

  5. XuanTiServiceImpl 这个 bean 找到了,因为在 spring-common.xml 中有配置
    <bean class="com.tgb.service.impl.XuanTiServiceImpl" />

  6. xuanTiService.save(xuanTi); 被执行了,具体来说,是 XuanTiServiceImpl 中的 public void save(XuanTi xuanTi) {…} 被执行了

  7. 根据上一章节“切入机制”的描述,在执行 public void save(XuanTi xuanTi) {...} 的时候,会伴随着执行 LogAspect 中的 saveLogInsert 方法

  8. 在 LogAspect 中,有标注为 @Autowired 的 logService,所以 Application 会去查找 logService 这个 bean

  9. 查找的原因,就是第3点所描述的

  10. LogService 只是一个 interface,实现它的是 LogServiceImpl

  11. XuanTiServiceImpl 这个 bean 找到了,因为在 spring-common.xml 中有配置
    <bean id="LogService" class="com.tgb.service.impl.LogServiceImpl" />
    注释:日志记录业务逻辑对象

  12. logService.log(log); 被执行了,具体来说,是 LogServiceImpl 中的 public void log(Log log) {…} 被执行了

知识拓展

在本章节,我将会介绍另外一些 Spring AOP Docs 中提到的基础知识。这些基础知识没有使用在 PressSystem 项目中,但是作为 Spring AOP Docs 中的基础知识,应当有所了解。

五种类型的通知的实现方式

在前面的章节中,提到过五种类型的通知。这五种类型的通知,在我的 aop_demo 示例程序中都有使用。注意,声明通知的方法有以下两种方式。在我的示例程序中,都有展示:

  • @Aspectj
  • Schema-based

通知参数的使用方法

声明一个通知的时候,是可以传入切入点的参数,在通知中使用这个参数的,例如上文中提到的 delete 通知:

@Around("execution(* com.tgb.service.impl.*.delete(..)) && args(id, table_name)")

当我传入了 (id, table_name) 参数后,可以在通知中使用 id 和 table_name。delete 通知的目的是,当 delete 方法执行的时候,写下日志。在日志中我要知道删除的 id 和 table_name。传入了这两个参数之后,就可以记录了。
通知的参数可以很复杂,用于满足实际需要。想要了解更多通知参数的使用方法,可以查看 Spring AOP Docs。

Pointcut - 切点表达式的使用

Pointcut 是 Advice 的具体配置,指定了一个 Advice 将会切入到哪些切入点中,这是由切点表达式决定的。Pointcut 是一种十分重要的机制,在 PressSystem 的 LogAspect 中有简单的应用。想要知道更多使用方法,可以参考 Spring AOP Docs。

Introductions 介绍

以下是 Spring AOP Docs 中的原版描述:

Introductions (known as inter-type declarations in AspectJ) enable an aspect to declare that advised objects implement a given interface, and to provide an implementation of that interface on behalf of those objects.

相关的代码,可以参看 com.spring.demo08 和 com.spring demo14。其中:

  • com.spring.demo08 是 Schema 方式实现的
  • com.spring.demo14 是 @AspectJ 方式实现的

根据我的理解,Introduction 的意义就是接口类型强转,但是看到一句注释说,这是“接口动态实现”,觉得里面还有其他的花头我没有理解。。。

AOP Proxy 原理简介

Spring 的动态代理有两种:一是 JDK 的动态代理;另一个是 cglib 动态代理(通过修改字节码来实现代理)。可以以 JDK 动态代理的方式为例,来粗浅地探知 AOP Proxy 的究竟。
JDK的代理方式主要就是通过反射跟动态编译来实现的,主要涉及到java.lang.reflect包中的两个类:Proxy 和 InvocationHandler。其中 InvocationHandler 是一个接口,可以通过实现该接口定义横切逻辑,在并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编织在一起。 将会在另一篇文章中,重点来谈一下 Spring AOP 的底层实现技术:JDK 动态代理。

小结

这篇博客写了很久,一方面是由于差不多是利用业余时间在学习,另一方面是由于 Spring AOP 的知识点有很多。
在面向切面的编程的概念中,比较重要的是切面类和目标类。切面类中有切面方法,目标类中有切入点。通过正确的配置,可以使切面方法切入到目标类的切面点中。
这篇博客首先介绍了 AOP 的概念,然后介绍了通知的5种类型。接下来,以 PressSystem 为例,讲述了 PressSystem 中的 AOP 的基本组件,切入机制和调用过程。
最后,在知识拓展中,十分简单地提到了五种类型的通知的实现方式,通知参数的使用方法,Pointcut - 切点表达式的使用,Introductions 简单介绍。最重要的一点,我认为是 AOP Proxy 原理简介。有关 AOP Proxy 原理简介,请参看另一篇博客:Spring AOP 中的 JDK 动态代理。

参考资料

  1. 10. Aspect Oriented Programming with Spring (这是总纲,可以作为知识点的引入)
  2. AOP 那点事儿 (这篇文章写得并不好,太随性,思路不清楚)
  3. Spring AOP Example Tutorial – Aspect, Advice, Pointcut, JoinPoint, Annotations, XML Configuration (Spring AOP demo)
  4. Spring AOP 完成日志记录 (PressSystem 的日志功能实现,参考了这篇文章)
  5. Spring 容器AOP的实现原理——动态代理 (Spring AOP 动态代理的学习)
  6. Spring AOP的底层实现技术—JDK动态代理 (Spring AOP 动态代理的学习)

创作时间:2016-05-14 星期六 6:32:24 PM

原文地址:https://www.cnblogs.com/gkmeteor/p/11799214.html