AOP - 面向切面编程
1. 基础
1.1 概述
AOP:Aspect Oriented Programming (面向切面编程),即面向特定方法编程。
场景:
- 统计业务方法的执行耗时
- 记录操作日志
- 权限管理
- 事务管理
- ……
AOP的作用: 在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)
AOP的优势:
- 代码无侵入
- 减少重复代码
- 提高开发效率
- 维护方便
1.2 SpringAOP开发步骤
需求:统计各个业务层方法执行耗时
实现步骤:
- 导入依赖:在项目pom.xml中导入AOP的依赖
1 | <dependency> |
- 编写AOP程序:针对于特定方法根据业务需要进行编程
1 |
|
1.3 核心概念
- 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
- 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
- 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
- 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
- 目标对象:Target,通知所应用的对象
1.4 执行流程
Spring的AOP底层是基于动态代理技术来实现的。也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。
2. 进阶
2.1 通知类型
Spring中AOP的通知类型:
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
在使用通知时的注意事项:
- @Around 环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
- @Around 环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的
当代码中存在大量重复的切入点表达式时,Spring提供了@PointCut
注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可,具体形式如下:
1 |
|
需要注意的是:当切入点方法使用private
修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把private
改为public
,而在引用的时候,具体的语法为:全类名.方法名()
,具体形式如下:
1 |
|
2.2 通知顺序
当在项目开发当中,我们定义了多个切面类,而多个切面类中多个切入点都匹配到了同一个目标方法。此时当目标方法在运行的时候,这多个切面类当中的这些通知方法都会运行。
在不同切面类中,默认按照切面类的类名字母排序:
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行
如果我们想控制通知的执行顺序有两种方式:
-
修改切面类的类名(这种方式非常繁琐、而且不便管理)
-
使用Spring提供的@Order注解,形如:
1
2
3
4
5
6
7
8
9
10
11
// 切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2 {
public void before(){
log.info("MyAspect2 -> before ...");
}
……
}
2.3 切入点表达式
切入点表达式:
-
描述切入点方法的一种表达式
-
作用:主要用来决定项目中的哪些方法需要加入通知
-
常见形式:
-
execution(……):根据方法的签名来匹配
-
@annotation(……) :根据注解匹配
-
2.3.1 execution
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
1 | execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?) |
其中带?
的表示可以省略的部分
- 访问修饰符:可省略(比如: public、protected)
- 包名.类名: 可省略
- throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
可以使用通配符描述切入点
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
注意事项:
-
根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。形如:
1
execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))
切入点表达式的书写建议:
- 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头
- 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
- 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 …,使用 * 匹配单个包
2.3.2 @annotation
如果我们要匹配多个无规则的方法,比如:list()和 delete()这两个方法。这个时候我们基于execution这种切入点表达式来描述就不是很方便了。此时,我们可以借助于另一种切入点表达式annotation来描述这一类的切入点,从而来简化切入点表达式的书写。
实现步骤:
-
编写自定义注解
-
在业务类要作为连接点的方法上添加自定义注解
自定义注解:MyLog
1 | // 作用域 |
业务类:DeptServiceImpl
1 |
|
切面类
1 |
|
2.3.3 总结
- execution切入点表达式
- 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式
- 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐
- annotation切入点表达式
- 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了
2.4 连接点
在Spring中用JoinPoint
抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
-
对于@Around通知,获取连接点信息只能使用
ProceedingJoinPoint
类型 -
对于其他四种通知,获取连接点信息只能使用
JoinPoint
,它是ProceedingJoinPoint
的父类型
1 |
|