Spring的两大核心思想,是 IoC和AOP。接下来我们进入AOP的学习。
1. AOP概述
AOP,Aspect Oriented Programming (面向切面编程)
什么是面向切面编程呢?
切面就是指某一类特定问题 ,所以AOP也可以理解为面向特定方法编程。
什么是面向特定方法编程呢?
比如上个章节学习的"登录校验",就是一类特定问题,登录校验拦截器,就是对"登录校验"这类问题的统一处理。
所以,拦截器也是AOP的一种应用。AOP是一种思想,拦截器是AOP思想的一种实现。Spring框架实现了这种思想,提供了拦截器技术的相关接口。
同样的,统一数据返回格式和统一异常处理,也是AOP思想的一种实现。
简单来说:AOP是一种思想,是对某一类事情的集中处理。
什么是Spring AOP?
AOP是一种思想,它的实现方法有很多,有SpringAOP,也有AspectJ、CGLIB等。
Spring AOP是AOP的一种实现方式。
2. Spring AOP
示例:
现在有⼀个项目, 项目中开发了很多的业务功能,有一些业务的执行效率比较低,耗时较长,我们需要对接口进行优化。
第一步就需要定位出执行耗时比较长的业务方法,再针对该业务方法来进行优化
- 定位:我们就需要统计当前项目中每一个业务方法的执行耗时。
- 统计:可以在业务方法运行前和运行后,记录下方法的开始时间和结束时间,两者之差就是这个方法的耗时。
这种方法是可以解决问题的,但一个项目中会包含很多业务模块,每个业务模块又有很多接口,一个接口又包含很多方法,如果我们要在每个业务方法中都记录方法的耗时,对于程序员而言,会增加很多的工作量.
AOP就可以做到在不改动这些原始方法的基础上,针对特定的方法进行功能的增强。
- AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性:解耦)
接下来我们来看Spring AOP如何来实现
① 引入AOP依赖
在pom.xml文件中添加配置:
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
② 编写AOP程序
记录Controller中每个方法的执行时间

- @Aspect:标识这是一个切面类
- @Around:环绕通知,在目标方法的前后都会被执行.后面的表达式表示对哪些方法进行增强
- ProceedingJoinPoint.proceed()让原始方法(目标方法)执行
运行程序,观察日志:

3. Spring AOP详解
SprintAOP的概念:
- 切点
- 切面
- 通知
- 连接点

① 切点(Pointcut)
切点(Pointcut),也称之为"切入点"
Pointcut 的作用就是提供一组规则(使用AspectJ pointcut expression language来描述),告诉程序对
哪些方法来进行功能增强。

上面的表达式 execution(* com.book.demo.controller.*.*(..)) 就是切点表达式。
② 连接点(Join Point)
满足切点表达式规则的方法,就是连接点,也就是可以被AOP控制的方法。
例如上述,所有 com.book.demo.controller 路径下的方法, 都是连接点。
java
package com.book.demo.controller;
@RequestMapping("/book")
@RestController
public class BookController {
@RequestMapping("/addBook")
public Result addBook(BookInfo bookInfo) {
//...代码省略
}
@RequestMapping("/queryBookById")
public BookInfo queryBookById(Integer bookId){
//...代码省略
}
@RequestMapping("/updateBook")
public Result updateBook(BookInfo bookInfo) {
//...代码省略
}
}
上述BookController中的方法都是连接点。
切点和连接点的关系:
- 连接点是满足切点表达式的元素,切点可以看做是保存了众多连接点的一个集合,
③ 通知(Advice)
通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
比如上述程序中记录业务方法的耗时时间,就是通知

在AOP面向切面编程当中, 我们把这部分重复的代码逻辑抽取出来单独定义, 这部分代码就是通知的内容。
④ 切面(Aspect)
切面(Aspect) =切点(Pointcut)+通知(Advice)
通过切面就能够描述当前AOP程序需要针对于哪些方法,在什么时候执行什么样的操作
切面既包含了通知逻辑的定义,也包括了连接点的定义.

切面所在的类, 我们⼀般称为切面类,而 @Aspect注解就是用来标识切面类的。
⑤ 通知类型
上面我们讲了什么是通知,接下来学习通知的类型,@Around就是其中一种通知类型,表示环绕通知。
Spring中AOP的通知类型有以下几种:
- @Around:环绕通知 ,此注解标注的通知方法在目标方法前,后都被执行
- @Before:前置通知 ,此注解标注的通知方法在目标方法前被执行
- @After:后置通知 ,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning:返回后通知 ,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing:异常后通知 ,此注解标注的通知方法发生异常后执行
示例:
1. 正常运行情况

测试方法:

运行程序,观察日志:


程序正常运行的情况下,@AfterThrowing标识的通知方法不会执行。
从上图也可以看出来,@Around标识的通知方法包含两部分,一个"前置逻辑",一个"后置逻辑"。
其中"前置逻辑"会先于@Before标识的通知方法执行,"后置逻辑"会晚于@After标识的通知方法执
行。

2. 异常时的情况

运行程序,观察日志:


程序发生异常的情况下:
- @AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了
- @Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了(因为原始方法调用出异常了)

也可以通过 try-catch 捕获处理异常:就可以正常运行

此时再次运行:

注意事项:
- @Around环绕通知需要调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行.
- @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的.
- 一个切面类可以有多个切点。
⑥ @PointCut
上面代码存在一个问题,就是存在大量重复的切点表达式
execution(* com.book.demo.controller.*.*(..)),Spring提供了@Pointcut注解,把公共的切点
表达式提取出来,需要用到时引用该切入点表达式即可。
上述代码就可以修改为:

当切点定义使用 private 修饰时,仅能在当前切面类中使用,当其他切面类也要使用当前切点定义时,就需要把 private 改为 public。引用方式为:全限定类名.方法名()

其他切面类AspectDemo2:

运行结果:

⑦ @Order 切面优先级
当我们在一个项目中,定义了多个切面类时,并且这些切面类的多个切入点都匹配到了同一个目标方法。当目标方法运行的时候,这些切面类中的通知方法都会执行,那么这几个通知方法的执行顺序是什么样的呢?
我们还是通过程序来求证:
定义多个切面类:
- 为防止干扰,我们把AspectDemo这个切面先去掉(把@Component注解去掉就可以)
为简单化,只写了@Before和aAfter两个通知:



先运行程序,观察日志:

通过上述程序的运行结果,可以看出:
存在多个切面类时,默认按照切面类的类名字母排序:
- @Before通知:字母排名靠前的先执行
- @After通知:字母排名靠前的后执行
但这种方式不方便管理,我们的类名更多还是具备一定含义的。
Spring给我们提供了一个新的注解,来控制这些切面通知的执行顺序:@Order
使用方式如下:



重新运行程序:

通过上述程序的运行结果,得出结论:
@Order注解标识的切面类,执行顺序如下:
- @Before通知:数字越小先执行
- @After通知:数字越大先执行
@Order控制切面的优先级,先执行优先级较高的切面,再执行优先级较低的切面,最终执行目标方法。

⑧ 切点表达式
上面的代码中,我们一直在使用切点表达式来描述切点,下面我们来介绍一下切点表达式的语法。
切点表达式常见有两种表达方式
- execution(.....):根据方法的签名来匹配
- @annotation(.....):根据注解匹配
1. execution 表达式
execution() 是最常用的切点表达式, 用来匹配方法,语法为:
XML
execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)
其中: 访问修饰符和异常可以省略

切点表达式支持通配符表达:
-
- * :匹配任意字符,只匹配一个元素(返回类型,包,类名,方法或者方法参数)
- a.包名使用 * 表示任意包(一层包使用一个*)
- b.类名使用 * 表示任意类
- c.返回值使用 * 表示任意返回值类型
- d.方法名使用 * 表示任意方法
- e.参数使用 * 表示一个任意类型的参数
- 2... :匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数
- a.使用 **..**配置包名,标识此包以及此包下的所有子包
- b.可以使用 **..**配置参数,任意个任意类型的参数
切点表达式示例:
TestController 下的 public修饰, 返回类型为String ,方法名为t1, 无参方法
XML
execution(public String com.example.demo.controller.TestController.t1())
省略访问修饰符
XML
execution(String com.example.demo.controller.TestController.t1())
匹配所有返回类型
XML
execution(* com.example.demo.controller.TestController.t1())
匹配TestController 下的所有无参方法
XML
execution(* com.example.demo.controller.TestController.*())
匹配TestController 下的所有方法
XML
execution(* com.example.demo.controller.TestController.*(..))
匹配controller包下所有的类的所有方法
XML
execution(* com.example.demo.controller.*.*(..))
匹配所有包下面的TestController
XML
execution(* com..TestController.*(..))
匹配com.example.demo包下, 子孙包下的所有类的所有方法
XML
execution(* com.example.demo..*(..))
2. @annotation
execution表达式更适用有规则的,如果我们要匹配多个无规则的方法呢,比如:TestController中的 t1() 和 UserController中的 u1() 这两个方法。
这个时候我们使用execution这种切点表达式来描述就不是很方便了.
我们可以借助自定义注解的方式以及另一种切点表达式@annotation来描述这一类的切点
实现步骤:
- 编写自定义注解
- 使用@annotation表达式来描述切点
- 在连接点的方法上添加自定义注解
1)自定义注解 @MyAspect
创建⼀个注解类(和创建Class文件⼀样的流程, 选择Annotation就可以了)


- @Target标识了Annotation所修饰的对象范围,即该注解可以用在什么地方.
- 常用取值:
- ElementType.TYPE:用于描述类、接口(包括注解类型)或enum声明
- ElementType.METHOD:描述方法
- ElementType.PARAMETER:描述参数
- ElementType.TYPE_USE:可以标注任意类型
- @Retention指Annotation被保留的时间长短,标明注解的生命周期
- @Retention的取值有三种:
- 1.RetentionPolicy.SOURCE:表示注解仅存在于源代码中,编译成字节码后会被丢弃,这意味着在运行时无法获取到该注解的信息,只能在编译时使用.比如@suppressWarnings,以及lombok提供的注解@Data,@Slf4j
- 2.RetentionPolicy.CLASS:编译时注解,表示注解存在于源代码和字节码中,但在运行时会被丢弃,这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行时无法获取.通常用于一些框架和工具的注解
- 3.RetentionPolicy.RUNTIME:运行时注解.表示注解存在于源代码,字节码和运行时中.这意味着在编译时,字节码中和实际运行时都可以通过反射获取到该注解的信息,通常用于一些需要在运行时处理的注解,如Spring的@Controller@ResponseBody
2)切面类
使用 @annotation 切点表达式定义切点, 只对 @MyAspect 生效
切面类代码如下:

3)添加自定义注解
在TestController中的t2()和UserController中的u2()这两个方法上添加自定义注解 @MyAspect ,其 他方法不添加。


运行程序,测试接口:
可以看见,测试了 test.t2 和 user.u2,切面通知被执行了:

没有加上自定义注解@MyAspect 的其他方法,都不会执行切面通知:

⑨ Spring AOP的实现方式
- 基于注解 @Aspect (参考上述课件内容)
- 基于自定义注解 (自定义注解 @annotation)
4. Spring AOP原理
上面我们主要学习了SpringAOP的应用,接下来我们来学习SpringAOP的原理,也就是Spring是如何实现AOP的。
SpringAOP是基于动态代理来实现AOP的。
① 代理模式
代理模式,也叫委托模式。
定义:为其他对象提供一种代理以控制对这个对象的访问,它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用,
在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用,
使用代理前:

使用代理之后:

代理模式的主要角色
- Subject:业务接口类.可以是抽象类或者接口(不一定有)
- RealSubject:业务实现类,具体的业务执行,也就是被代理对象.
- Proxy:代理类.RealSubject的代理
【比如房屋租赁
Subject就是提前定义了房东做的事情,交给中介代理,也是中介要做的事情
RealSubject:房东
Proxy: 中介】
代理模式可以在不修改被代理对象 的基础上,通过扩展代理类,进行一些功能的附加与增强
根据代理的创建时期,代理模式分为静态代理和动态代理
- 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的.class文件就已经存在了.
- 动态代理:在程序运行时,运用反射机制动态创建而成。
② 静态代理
静态代理:在程序运行前,代理类的.class文件就已经存在了.(在出租房子之前,中介已经做好了相关的工作,就等租户来租房子了)
我们通过代码来加深理解,以房租租赁为例
1.定义接口(定义房东要做的事情,也是中介需要做的事情)

- 实现接口(房东出租房子)

- 代理(中介, 帮房东出租房子)

- 使用

运行结果:

上面这个代理实现方式就是静态代理
从上述程序可以看出,虽然静态代理也完成了对目标对象的代理,但是由于代码都写死了,对目标对象的每个方法的增强都是手动完成的,非常不灵活,所以日常开发几乎看不到静态代理的场景。
接下来新增需求:中介又新增了其他业务:代理房屋出售
我们需要对上述代码进行修改
1.接口定义修改

- 接口实现修改

- 代理类修改

从上述代码可以看出,我们修改接口(Subject)和业务实现类(RealSubject)时,还需要修改代理类
(Proxy)。同样的,如果有新增接口(Subject)和业务实现类(RealSubject),也需要对每一个业务实现类新增代理类(Proxy).
既然代理的流程是一样的,有没有一种办法,让他们通过一个代理类来实现呢?
这就需要用到动态代理技术了。
③ 动态代理
相比于静态代理来说,动态代理更加灵活。
我们不需要针对每个目标对象都单独创建一个代理对象,而是把这个创建代理对象的工作推迟到程序运行时由JVM来实现,也就是说动态代理在程序运行时,根据需要动态创建生成。
比如房屋中介,我不需要提前预测都有哪些业务,而是业务来了我再根据情况创建
我们还是先看代码再来理解。
Java也对动态代理进行了实现,并给我们提供了一些API,常见的实现方式有两种:
- JDK动态代理
- CGLIB动态代理
1)JDK动态代理
JDK动态代理类实现步骤
- 定义一个接口及其实现类(静态代理中的HouseSubject和RealHouseSubject)
- 自定义InvocationHandler并重写invoke方法,在invoke方法中我们会调用目标方法(被代理类的方法)并自定义一些处理逻辑
- 通过Proxy.newProxyInstance(ClassLoader loader,Class<?>\[\] interfaces,InvocationHandlerh)方法创建代理对象
定义JDK动态代理类,实现 InvocationHandler 接口

创建一个代理对象并使用

代码讲解:
**1. InvocationHandler:**InvocationHandler 接口是Java动态代理的关键接口之⼀, 它定义了⼀个单一方法 invoke() , 用于处理被代理对象的方法调用;通过实现 InvocationHandler 接⼝, 可以对被代理对象的方法进行功能增强。

- invoke 方法参数说明
- proxy:代理对象
- method:代理对象需要实现的方法,即其中需要重写的方法
- args:method所对应方法的参数
**2. Proxy:**Proxy 类中使用频率最高的方法是: newProxyInstance() , 这个方法主要用来生成⼀个代理对象

- newProxyInstance() 方法这个方法一共有3个参数:
- Loader:类加载器,用于加载代理对象。
- interfaces:被代理类实现的一些接口(这个参数的定义,也决定了JDK动态代理只能代理实现了接口的一些类)
- h:实现了InvocationHandler接口的对象
2)CGLIB动态代理
JDK动态代理 有一个最致命的问题是其只能代理实现了接口的类(只能代理接口) 。
有些场景下,我们的业务代码是直接实现的,并没有接口定义,为了解决这个问题,我们可以用CGLIB动态代理机制来解决。
CGLIB(CodeGeneration Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成.CGLIB通过继承方式实现代理,很多知名的开源框架都使用到了CGLIB.例如Spring中的AOP模块中:如果目标对象实现了接口,则默认采用JDK动态代理,否则采用CGLIB动态代理。
CGLIB动态代理类实现步骤
- 定义一个类(被代理类)
- 自定义MethodInterceptor并重写intercept方法,intercept用于增强目标方法,和JDK动态代理中的invoke方法类似
- 通过Enhancer类的create()创建代理类
接下来看下实现:
添加依赖:
和JDK动态代理不同,CGLIB(CodeGenerationLibrary)实际是属于一个开源项目,如果你要使用它
的话,需要手动添加相关依赖
XML
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
自定义 MethodInterceptor(方法拦截器),实现MethodInterceptor接口

创建代理类并使用

但是运行起来会报错:原因在于 JDK版本与CGLIB库之间的兼容性问题。

解决方式:添加 JVM 启动参数
在运行该 DynamicMain 类时,添加 --add-opens 参数,手动将 java.lang 包开放给 CGLIB。
-
点击右上角的运行配置(Run/Debug Configurations)。
-
在 VM options 输入框中填入:
XML--add-opens java.base/java.lang=ALL-UNNAMED
这样就能运行成功:

代码讲解:
1. MethodInterceptor: MethodInterceptor 和 JDK动态代理中的 InvocationHandler 类似, 它只定义了⼀个方法 intercept() , 用于增强目标方法。
参数说明:
- obj:被代理的对象
- method:目标方法(被拦截的方法,也就是需要增强的方法)
- objects:方法入参
- methodProxy:用于调用原始方法
**2. Enhancer.create():**用来生成⼀个代理对象

参数说明:
- type: 被代理类的类型(类或接⼝)
- callback: 自定义方法拦截器 MethodInterceptor