一. 为什么需要AOP?
首先,我们先来用一个例子来说明 AOP 存在的意义。就简单地以统计各个接口方法的执行耗时为例,根据我们之前学习的知识来说,我们需要在每个业务方法前后分别统计开始时间、结束时间并计算出所使用的时间(即开始时间 - 结束时间),并在日志中打印出来。但是,这样子实现这个功能的话就会引入一系列相应的问题:在每个接口方法中都存在有大量的重复代码,写起来吃力不说,后续如果要修改的话还需每个接口方法都要进行修改,十分费力。为了解决这个问题,我们就要引入 AOP 的思想。
二. AOP 概述
2.1 什么是 AOP
AOP,全称为 : Aspect Oriented Programming,意为面向切面编程。要理解 AOP 这个思想,我们首先要先明白切面是什么,切面指的就是抽出来的公共模块,也可以理解为某一类特定的问题,比如说 用户登录验证啦、方法耗时统计啦等等,都可以被认为是一个切面。而 AOP 就是针对相同类型的切面进行一些统一的处理的思想,而前文中所提到的拦截器就是 AOP 思想的一种体现,简单来说,AOP 就是一种针对某一类事情进行统一处理的一种思想。
2.2 AOP 的作用与优势
1. 可以在不修改原代码的基础上对已有的接口方法进行处理
2. 可以降低代码的耦合度,无侵入性
3. 减少了重复代码,提高了开发效率,代码维护起来方便
2.3 Spring AOP 是什么
上文中我们已经提到过,AOP 是一种思想,那么对于这个思想就会有很多实现方式,就比如说 Spring AOP 、AspectJ 和 CGLIB 就是几个实现了 AOP 思想的方式。而 Spring AOP 又是 Spring 框架的两大核心之一(IOC 和 Spring AOP),自然也是十分重要的部分,接下来,我们就来介绍如何使用 Spring AOP
三. Spring AOP 快速入门
在本小节中,我们将使用统一统计方法耗时为例来让我们对 Spring AOP 进行一个快速的入门
3.1 引入 AOP 依赖
要想在 Spring 项目中使用 Spring AOP,我们首先就要引入相应的依赖,所需的依赖如下所示:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc-test</artifactId> <scope>test</scope> </dependency>
直接将以上内容复制到Spring 项目中的 pom 文件当中并刷新 Maven 即可
3.2 编写 AOP 程序
在编写 AOP 程序时,我们会用到以下几个核心注解 : @Aspect、@Around,接下来,我们先来介绍这几个注解的作用
@Aspect :标记当前这个 Bean 是一个 AOP 切面类
@Around:环绕通知
切点表达式:"execution(* com.example.springaopdemo.controller.*.*(..))"
在此小节中,我们暂不用深究其概念问题,先理解代码的书写,在下一小节中我们会依次来讲解着其中的概念
该计时器的代码如下所示
@Slf4j @Aspect @Component public class AspectDemo1 { @Around("execution(* com.example.springaopdemo.controller.*.*(..))") public Object recordTime(ProceedingJoinPoint pjp) { log.info("目标方法执行前...."); //执行目标方法 Object result = null; try { result = pjp.proceed(); } catch (Throwable e) { log.error("do Around throwing...."); } log.info("目标方法执行后...."); return result; } }代码分析:
方法执行前的逻辑:记录开始时间
目标方法执行的时机:pjp.proceed()
方法执行后的逻辑:记录结束时间 、计算出耗时并打印日志
当我们完成了这个代码后,只要是符合切点表达式的接口方法执行时,就会统一执行上述的计时器代码,这也体现了 AOP 的优点 : 无侵入、大大减少了重复代码 和 维护起来方便
四. Spring AOP 的核心概念
4.1 切点(Pointcut)
也称之为 “切入点”,提供一组规则,告诉程序对哪些方法进行功能增强
例如:"execution(* com.example.springaopdemo.controller.*.*(..))"
4.2 连接点(JoinPoint)
满足切点表达式规则的方法,就是连接点,也就是可以被 AOP 控制的方法
4.3 通知(Advice)
具体要做的工作,指重复的逻辑,也就是共性功能,最终体现为一个方法
4.4 切面(Aspect)
切面 = 切点 + 通知
切面所在类一般称为切面类,被 @Aspect 注解标识
4.5 切点与连接点的关系
连接点是满足切点表达式的元素
切点可以看做是保存了众多连接点的一个集合
五. Spring AOP 的通知类型
5.1 五种通知类型
@Around:环绕通知,目标方法前后都执行(分执行前的逻辑 和 执行后的逻辑)
@Before:前置通知,目标方法前执行
@After:后置通知,目标方法后执行,无论是否有异常都会执行
@AfterRunning:返回后通知,目标方法后执行,有异常不会执行
@AfterThrowing:异常后通知,目标方法发生异常后执行
5.2 未出现异常的通知的执行逻辑
@Around 前置逻辑 → @Before → 目标方法 → @AfterRunning → @After → @Around 后置逻辑
我们可以通过打印的日志内容来验证上述内容
5.3 出现异常的通知的执行逻辑
Around 前置逻辑 → @Before → @AfterThrowing → @After
同样的,我们也可以通过打印的日志内容来验证上述内容
六、切点表达式与 @Pointcut
6.1 execution 表达式
语法:execution(< 访问修饰符 > < 返回类型 > < 包名 . 类名 . 方法 ( 方法参数 )> < 异常 >)
其中,访问修饰符 和 异常 这两部分可以省略
所支持的通配符:* 匹配一个元素 ,.. 匹配多个连续的任意符号
1.*
- a. 包名使用
*:表示任意包(一层包使用一个*) - b. 类名使用
*:表示任意类 - c. 返回值使用
*:表示任意返回值类型 - d. 方法名使用
*:表示任意方法 - e. 参数使用
*:表示一个任意类型的参数
2...
- a. 使用
..配置包名:标识此包以及此包下的所有子包 - b. 可以使用
..配置参数:任意个任意类型的参数
示例:execution(* com.example.springaopdemo.controller.*.*(..))
6.2 @Pointcut 提取公共切点表达式
定义方式:在 @Pointcut 注解中加入 execution 表达式
例如:@Pointcut("execution(* com.example.springaopdemo.controller.*.*(..))")
public void pt(){};
后续在需要用到 切点表达式 的地方直接用 pt 代替即可
具体代码演示:
@Slf4j @Aspect @Component public class AspectDemo1 { //将切点表达式提取出来 @Pointcut("execution(* com.example.springaopdemo.controller.*.*(..))") public void pt(){}; @Around("pt()") public Object recordTime(ProceedingJoinPoint pjp) throws Throwable{ log.info("目标方法执行前...."); Object result = pjp.proceed(); log.info("目标方法执行后...."); return result; } }6.3 @annotation 与自定义注解
上文中讲到的 execution 切点表达式适合用于匹配有规则的方法(比如说是都在 xx 包下的方法),而如果我们要匹配的是无规则的方法时,execution 切点表达式就不太适用了。为了解决这个问题,spring 引入了@annotation 注解 和 自定义注解
适用场景:匹配多个无规则的方法
实现方式:编写自定义注解 → 使 @annotation 表达式描述切点 → 在连接点方法上添加自定义注解
具体代码示例如下所示
自定义注解:
@Target(ElementType.METHOD) //注解生效的地方 @Retention(RetentionPolicy.RUNTIME) //注解生效的时机 //适用于无规则的情况 public @interface MyAspect { }描述切点 和 要执行的逻辑
@Slf4j @Aspect @Component public class MyAspectDemo { @Around("@annotation(com.example.springaopdemo.aspect.MyAspect)") public Object recordTime(ProceedingJoinPoint pjp) { log.info("目标方法执行前...."); //执行目标方法 Object result = null; try { result = pjp.proceed(); } catch (Throwable e) { log.error("do Around throwing...."); } //记录方法执行结束时间 log.info("目标方法执行后...."); return result; } }其中,@annotation 表达式 所描述的是 具有 @MyAspect 注解的这个方法就为目标方法,当我们完成了以上两个类之后,在其它类的方法中添加上 @MyAspect 注解,那么每当这个方法执行时,就会统一执行上述的通知逻辑
七、多个切面的执行顺序:@Order
在一个 Spring 项目中,我们可能会同时存在有多个切面,而切面之间是存在 执行顺序的,默认是按照切面类的类名字母排序,当然,Spring 也提供了设置各个切面的执行顺序的 注解 @Order,在 该注解后添加的数字越小,代表着该切面的执行顺序越快(添加的数字可以为负数)
八、Spring AOP 的实现方式
Spring AOP 共有四种实现方式,分别是:
基于注解 @Aspect
基于自定义注解
基于 Spring API,通过 XML 配置
基于代理来实现
九、Spring AOP 原理与动态代理
9.1 代理模式
定义:为其他对象提供一种代理以用来控制对这个对象的访问。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。在某些情况下,一个对象并不适合直接去调用另一个类中的方法,而是要通过调用代理类来间接调用目标类中的方法,而代理类在其中就扮演了一个中介的角色。
主要角色:Subject(业务接口类)、RealSubject(业务实现类)、Proxy(代理类)
Subject(业务接口类):可以是抽象类或接口(不一定存在)
RealSubject(业务实现类):具体的业务逻辑的实现,也就是被代理的对象
Proxy(代理类):RealSubject 的代理类
意义:代理模式可以在不修改被代理对象的基础上,通过扩展代理类进行功能附加与增强
9.2 静态代理
创建时机:程序运行前,代理类的 .class 文件就已经存在
缺点:代码写死,不够灵活
代码示例
接下来,我们就以房东让中介代理 出售/出租 房子为例来演示一个简单的静态代理代码逻辑
首先,我们先来定义Subject(业务接口类),房东和中介都要实现这个接口,具体代码如下所示
public interface HouseSubject { void saleHouse(); void rentHouse(); }然后,我们要去完成 RealSubject(业务实现类),也就是房东类,该类实现了 Subject(业务接口类)并重写了 出售/出租 房子这两个方法,具体代码如下所示
public class RealHouseSubject implements HouseSubject{ @Override public void saleHouse() { System.out.println("我是房东, 我卖房"); } @Override public void rentHouse() { System.out.println("我是房东, 我出租房子"); } }接下来,我们就要接着去完成 Proxy(代理类),也就是中介类,对于中介类来说,他需要帮助房东去 出售/出租 房子,那么,首先在 中介类 中就要创建有一个 房东对象 来存储 其代理的房东是谁,并在后续的 出售/出租 房子中进行代理。具体代码如下所示
public class HouseProxy implements HouseSubject{ private HouseSubject realHouseSubject; public HouseProxy(HouseSubject realHouseSubject) { this.realHouseSubject = realHouseSubject; } @Override public void saleHouse() { System.out.println("我是中介, 我帮房东开始代理"); realHouseSubject.saleHouse(); System.out.println("我是中介, 我帮房东结束代理"); } @Override public void rentHouse() { System.out.println("我是中介, 我帮房东开始代理"); realHouseSubject.rentHouse(); System.out.println("我是中介, 我帮房东结束代理"); } }通过以上的三个代码,我们就简单地完成了一个静态代理的逻辑
9.3 动态代理
动态代理有两种实现方式,分别是 JDK 动态代理 和 CGLIB 动态代理
9.3.1 JDK 动态代理
接下来,我们来通过一个具体的代码示例(房东将房子代理给中介进行 出售/出租)来理解 JDK 动态代理(理解即可,无需太过注意代码内容)
要使用 JDK 动态代理,我们要按照以下两个步骤来使用
1. 编写“中介”的办事逻辑
该中介要实现InvocationHandler 接口
public class JDKInvocation implements InvocationHandler { //目标对象, 被代理对象 private Object target; public JDKInvocation(Object target) { this.target = target; } //调用目标方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("我开始代理..."); Object invoke = method.invoke(target, args); System.out.println("我结束代理"); return invoke; } }invoke 方法:
这是整个 JDK 动态代理最核心的方法。 当代理对象(中介)的任何方法被调用时,请求都会被转发到这个 invoke 方法里
参数:proxy 表示的是生成的代理对象,method 表示的是目标方法,args 表示的是目标方法执行所需要的参数
method.invoke 方法:
这是“中介”真正干活的地方。method 是接口的方法,target 是真实的房东。这句话的意思是:“执行 target 对象上的 method 方法,参数是 args”。如果没有这一步,代理就只是空壳,无法执行真实的业务逻辑
参数:target 表示的是被代理的对象(也就是来执行目标方法的对象) ,args 表示的是目标方法执行所需要的参数
2. 动态生成中介并进行调用
public class Main { public static void main(String[] args) { HouseSubject target = new RealHouseSubject(); //JDK动态代理 //只能代理接口,不能代理类 //动态创建一个代理类 HouseSubject houseSubject = (HouseSubject) Proxy.newProxyInstance(target.getClass().getClassLoader(), new Class[]{HouseSubject.class},new JDKInvocation(target)); houseSubject.saleHouse(); } }Proxy.newProxyInstance 方法:
在程序运行时动态生成代理类和代理对象实例,返回值为所生成的代理对象
参数:target.getClass().getClassLoader() 表示的是代理对象的类加载器,new Class[]{HouseSubject.class} 表示的是你要代理的接口是哪个,new JDKInvocation(target) 表示的是要执行的逻辑是什么
注意:JDK 动态代理只能够代理接口,不能够代理类 !!!
9.3.2 CGLIB 动态代理
JDK动态代理有⼀个最致命的问题是其只能代理实现了接口的类。但在有些场景下,我们的业务代码是直接实现的,并没有接口定义。为了解决这个问题,我们可以使用第三方实现的 CGLIB 动态代理机制来解决。
还是和之前讲解 JDK 动态代理时一样,我们还是通过一个具体的代码示例(房东将房子代理给中介进行 出售/出租)来理解 CGLIB 动态代理
要使用 JDK 动态代理,我们要按照以下三个步骤来使用
1. 引入依赖
由于 CGLIB 动态代理 是由第三方实现的,我们想要在 Spring 项目中使用前要先引入一个第三方的依赖,引入的依赖如下所示
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
值得注意的是,CGLIB 动态代理有多个实现方式,比如说 Spring 和 第三方等等,在我们使用第三方实现的 CGLIB 动态代理时,由于在 JDK 9 之后加了一些模块的原因,有一些方法不能再被反射访问,我们需要给 GVM 配置一个启动参数,如果使用的是 Spring 实现的动态代理则无需此步
添加方式如下图所示
添加的启动参数为
--add-opens java.base/java.lang=ALL-UNNAMED
2. 编写“中介”的办事逻辑
public class CGlibMethodInterceptor implements MethodInterceptor { private Object target; public CGlibMethodInterceptor(Object target) { this.target = target; } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("我开始代理"); Object invoke = method.invoke(target, args); System.out.println("我结束代理"); return invoke; } }CGLIB 动态代理 在这个代码示例上是和 JDK 动态代理十分相似的,其中 CGLIB 动态代理中的 intercept 方法也是和 JDK 动态代理 invoke 方法类似,参数方面只是多了一个 method 的代理对象 MethodProxy proxy,在执行目标方法是时可以通过 method.invoke 或 proxy.invoke 来执行
3. 动态生成中介并进行调用
public class Main { public static void main(String[] args) { HouseSubject target = new RealHouseSubject(); HouseSubject houseSubject = (HouseSubject) Enhancer.create(target.getClass(), new CGlibMethodInterceptor(target)); houseSubject.rentHouse(); } }9.4 Spring AOP 的代理选择
再讲解 Spring AOP 的代理选择前,我们要先知道 JDK 动态代理 和 CGLIB 动态代理的区别: JDK 动态代理只能够代理接口而不能代理类,CGLIB 则既可以代理接口也可以代理类
而 Spring AOP 是选择了哪个代理呢?答案是,两个都要。那么,这就会出现一个新的问题,什么时候应该使用 JDK 动态代理?什么时候应该使用 CGLIB 动态代理呢?
为了解决这个问题,我们就要去探究 Spring AOP 的源码,在源码中我们可以发现,在代理工厂中有一个重要的属性——proxyTargetClass,其默认值为 false (这是针对在 Spring Framework 的场景下,在 Spring Boot 2.X 中默认值也为 false,而在 Spring Boot 3.X 中默认值则为 true,即默认使用 CGLIB 动态代理)。该属性与Spring AOP 所选择的代理模式的对应关系如下图所示
简单来说,Spring AOP 的代理选择可以概括为 proxyTargetClass = false 时,如果目标类实现了接口,优先使用 JDK 动态代理;如果目标类没有接口,则使用 CGLIB 动态代理。proxyTarget = true时,则强制使用 CGLIB 动态代理。在 Spring Boot 3.x 中,默认值为true,也就是默认使用 CGLIB 动态代理。理解这一点,也就能理解为什么 Spring AOP 既能代理接口,也能代理普通类
十、总结
到这里,Spring AOP 的核心内容就基本介绍完了。本文从“为什么需要 AOP”开始,以接口耗时统计为例,说明了 AOP 是为了解决横切逻辑重复、分散、难以维护的问题;然后介绍了 Spring AOP 的快速入门,包括 @Aspect、@Around和切点表达式;接着梳理了切点、连接点、通知、切面等核心概念,以及五种通知类型、通知执行顺序、@Pointcut、@annotation 自定义注解和 @Order;最后深入到代理模式,分析了静态代理、JDK 动态代理、CGLIB 动态代理,以及 Spring AOP 在两者之间的选择。
一句话总结:AOP 是一种“把公共逻辑抽出来,在合适的位置统一织入”的思想;Spring AOP 则是基于动态代理对这一思想的落地实现。它让我们在不修改原有业务代码的前提下,完成日志、耗时统计、权限校验、事务管理等统一处理