Spring AOP¶
约 2633 个字 157 行代码 1 张图片 预计阅读时间 11 分钟
AOP介绍¶
AOP(Aspect Oriented Programming)即面向切面编程,是Spring框架的第二大核心(第一大核心是IoC)。它是一种编程思想,旨在将横切关注点(如日志记录、权限校验、事务管理等)与业务逻辑代码分离,实现对某一类特定问题的集中处理。在程序运行期间,AOP可以在不修改源代码的基础上对已有方法进行功能增强,具有无侵入性、解耦的特点。
对于AOP的实现,常见的有:
- Spring AOP
- AspectJ
- CGLIB
Spring AOP引入¶
引入依赖:
| XML | |
|---|---|
1 2 3 4 | |
创建一个自定义的时间记录的切面类:
| Java | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Spring AOP相关概念¶
核心概念¶
- 连接点:满足切点表达式规则、可以被AOP控制的方法
- 切点:用于定义一组规则,告诉程序需要对哪些方法进行功能增强,一般使用AspectJ切点表达式语言来描述
- 通知:具体要执行的重复共性功能,最终以方法形式体现,是抽取出来的公共增强逻辑
- 切面:由切点、连接点、通知组成,既包含了需要增强的范围规则,也包含了增强逻辑的定义,一般是被
@Aspect注解标识的类。通过切面可以描述需要对哪些方法,在什么时候执行什么样的增强操作
通知类型¶
@Around:环绕通知,此注解标注的通知方法会在目标方法执行前后都执行@Before:前置通知,此注解标注的通知方法会在目标方法执行前执行@After:后置通知,此注解标注的通知方法会在目标方法执行后执行,无论目标方法是否发生异常都会执行@AfterReturning:返回后通知,此注解标注的通知方法会在目标方法正常返回后执行,发生异常时不会执行@AfterThrowing:异常后通知,此注解标注的通知方法会在目标方法发生异常后执行
不同的通知类型的执行顺序:
当程序正常执行,未发生异常时,通知的执行顺序为:
@Around前置逻辑 →@Before前置通知 → 目标方法执行 →@AfterReturning返回后通知 →@After后置通知 →@Around后置逻辑
当目标方法执行过程中发生异常时,通知的执行顺序为:
@Around前置逻辑 →@Before前置通知 → 目标方法执行抛出异常 →@AfterThrowing异常后通知 →@After后置通知→@Around后置逻辑
Note
整个执行顺序有点像栈结构,优先级越高的切面,其前置逻辑越先执行,后置逻辑越晚执行,符合"先进后出"的特点
对于环绕通知有两个需要注意的点:
- 必须调用
ProceedingJoinPoint.proceed()来触发原始目标方法执行,其他通知不需要手动触发目标方法 @Around修饰的目标方法如果有返回值,则对应的切面也可以有返回值,用来接收原始方法的返回值,否则调用方无法获取到目标方法的返回结果
ProceedingJoinPoint常用方法¶
ProceedingJoinPoint 是 环绕通知(@Around) 的专属接口,它继承了 JoinPoint,额外提供了 proceed() 方法用来"放行"执行目标方法。
proceed() —— 执行目标方法¶
大白话:拦住请求,做完你的前置逻辑后,调用这个方法让原方法继续执行。
| Java | |
|---|---|
1 2 3 4 5 6 7 | |
Warning
注意:proceed() 必须调用,且通常只调用一次。如果调用了两次,原方法会执行两次(幂等性操作慎用)
proceed(Object[] args) —— 修改参数后执行¶
大白话:把方法的参数偷偷换掉,再用新参数去执行原方法。
| Java | |
|---|---|
1 2 3 4 5 6 | |
getArgs() —— 获取方法参数¶
大白话:拿到调用方法时传进来的所有实参。
| Java | |
|---|---|
1 2 3 4 | |
getSignature() —— 获取方法签名¶
大白话:拿到方法的名字、返回类型、参数类型等元信息。
| Java | |
|---|---|
1 2 3 4 5 | |
Tip
常用于日志记录:UserService.updateUser() 这种格式。
getTarget() —— 获取被代理的原始对象¶
大白话:拿到真正干活的那个对象(不是Spring生成的代理对象)。
| Java | |
|---|---|
1 2 | |
getThis() —— 获取代理对象本身¶
大白话:拿到Spring给你生成的代理对象(通常是CGLIB或JDK动态代理的实例)。
| Java | |
|---|---|
1 2 3 | |
getKind() —— 获取连接点类型¶
大白话:告诉你当前拦截的是什么类型的连接点(方法执行、字段访问等)。
| Java | |
|---|---|
1 | |
getStaticPart() —— 获取静态连接点信息¶
大白话:获取不包含运行时参数的方法签名信息。
| Java | |
|---|---|
1 | |
切点规则¶
Spring提供了专门的@Pointcut注解,用来定义和提取公共的切点表达式,避免在多个通知中重复编写相同的匹配规则。
如果不同类之间的切点调用,则需要带上切点定义方法的全限定类名,格式为:全限定类名.方法名()。但是被调用的切点定义方法,必须将访问修饰符从private修改为public,否则其他类无法正常访问该切点
相同切面的优先级¶
当多个切面同时匹配同一个目标方法时,默认会按照切面类的类名字母排序,字母排名靠前的切面优先级更高。可以通过@Order注解来指定切面的优先级,数值越小优先级越高。
切点表达式¶
常见的切点表达式有两种写法,分别用来应对不同的匹配场景:
execution():根据方法的签名信息匹配目标方法,是最常用的切点表达式@annotation():根据方法上的自定义注解匹配目标方法,适合匹配无规则的指定方法
方法的签名信息匹配¶
execution表达式的基础语法格式为:
| Java | |
|---|---|
1 | |
其中方法参数和异常可以省略,表达式支持两个通配符来简化匹配规则:
*:匹配任意字符,只匹配一个元素(可以是返回类型、包名、类名、方法名或单个方法参数)..:匹配多个连续的任意符号,可以通配任意层级的包,或任意个数、任意类型的方法参数
访问修饰符如果省略,则不匹配private方法,但是会匹配public、protected以及默认包访问权限的方法
以下是常见的execution切点表达式示例:
TestController 下的 public修饰,返回类型为String 方法名为t1,无参方法:
| Java | |
|---|---|
1 | |
省略访问修饰符:
| Java | |
|---|---|
1 | |
匹配所有返回类型:
| Java | |
|---|---|
1 | |
匹配TestController下的所有无参方法:
| Java | |
|---|---|
1 | |
匹配TestController下的所有方法:
| Java | |
|---|---|
1 | |
匹配controller包下所有的类的所有方法:
| Java | |
|---|---|
1 | |
匹配所有包下面的TestController:
| Java | |
|---|---|
1 | |
匹配com.example.demo包下,子孙包下的所有类的所有方法:
| Java | |
|---|---|
1 | |
自定义注解匹配¶
如果需要对多个无规则的指定方法进行增强,使用execution表达式会比较繁琐,此时可以使用@annotation()切点表达式,通过匹配方法上的自定义注解来定位需要增强的方法
使用方式如下:
- 编写自定义注解
- 使用
@annotation表达式(注解全权限定名称)定义切点 - 在需要增强的目标方法上添加自定义注解
Spring AOP实现原理¶
代理模式¶
代理模式,也叫委托模式。它的定义是:为其他对象提供一种代理以控制对这个对象的访问。通过代理类,我们在调用目标方法时不再直接调用,而是通过代理对象间接调用,从而可以在调用前后添加额外的逻辑
代理根据创建时期分为两种:
- 静态代理:在程序运行前,代理类的
.class文件就已经存在了,由程序员创建代理类或特定工具自动生成 - 动态代理:在程序运行时,运用反射机制动态创建代理对象
静态代理¶
以房屋租赁为例,定义一个租房接口和房东实现类,然后创建中介代理类,在代理类中调用房东的租房方法前后添加额外的服务逻辑。例如下面的代码:
房东和中介都要进行一个共同的操作(出租和售卖):
| Java | |
|---|---|
1 2 3 4 5 6 7 | |
房东实现自己的出租和售卖操作,只有简单的行为:
| Java | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
中介需要在售卖或者出租之前进行一些额外的操作,然后再售卖或者出租:
| Java | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
中介自己没房子,需要房东先提供给中介房源。当有租客或者买家来看房时,租客找到中介进行后续步骤:
| Java | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
这种方式虽然实现了功能增强,但每个被代理对象(例如HouseOwnerSubject)都需要单独编写一个代理类(例如HouseProxySubject),当需要代理的类较多时,会导致代码冗余和维护困难。
动态代理¶
相比于静态代理,动态代理不需要为每个目标对象单独编写代理类,而是在程序运行时动态创建代理对象。Java提供了两种常见的动态代理实现方式:JDK动态代理和CGLIB动态代理。下面以JDK动态代理演示:
先创建一个JDK代理器:
| Java | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
然后让JDK帮忙生成代理对象:
| Java | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
除了使用JDK进行动态代理,还可以使用CGLIB进行动态代理,二者最大的区别就是JDK动态代理不能代理没有实现接口的类,而CGLIB则没有这种限制
Spring AOP的实现原理¶
Spring AOP 是基于动态代理实现的。Spring 在运行时通过 ProxyFactory 创建代理对象,DefaultAopProxyFactory 中的 createAopProxy 方法会根据目标对象的类型决定使用哪种代理方式:如果目标类实现了接口,则使用 JDK 动态代理;否则使用 CGLIB 动态代理。此外,还可以通过 proxyTargetClass 属性强制使用 CGLIB 代理,Spring Boot 2.X 开始默认使用 CGLIB 代理。
对于proxyTargetClass,有下面几种情况:
proxyTargetClass为false且目标类实现了接口时,使用 JDK 动态代理proxyTargetClass为false且目标类未实现接口时,使用 CGLIB 动态代理proxyTargetClass为true时,无论目标类是否实现接口,都使用 CGLIB 动态代理。
常见面试题¶
- 如何理解Spring AOP
- AOP的实现方式有哪些
- 代理
- 注解
- 配置
- AOP原理(动态代理)
