AOP 进阶
AOP 主要分为 3 个部分:
- 通知类型
- 通知顺序
- 切入点表达式
1. 通知类型
类型 | 说明 |
---|---|
@Around | 环绕通知,此注解标注的通知方法在目标方法前、后都被执行 |
@Before | 前置通知,此注解标注的通知方法在目标方法前被执行 |
@After | 最终通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行 |
@AfterReturning | 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行 |
@AfterThrowing | 异常后通知,此注解标注的通知方法发生异常后执行 |
@Slf4j
@Component
@Aspect
public class MyAspect1 {
// 环绕通知
@Around("execution(* com.tlias.service.impl.EmpServiceImpl.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
// 调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
// 原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("around after ...");
return result;
}
// 前置通知
@Before("execution(* com.tlias.service.impl.EmpServiceImpl.*(..))")
public void before(JoinPoint joinPoint){
log.info("before ...");
}
// 最终通知
@After("execution(* com.tlias.service.impl.EmpServiceImpl.*(..))")
public void after(JoinPoint joinPoint){
log.info("after ...");
}
// 返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("execution(* com.tlias.service.impl.EmpServiceImpl.*(..))")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}
// 异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("execution(* com.tlias.service.impl.EmpServiceImpl.*(..))")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
程序发生异常的情况下:
@AfterReturning
标识的通知方法不会执行,@AfterThrowing
标识的通知方法执行了@Around
环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了 (因为原始方法调用已经出异常了)
在使用通知时的注意事项:
@Around
环绕通知需要自己调用ProceedingJoinPoint.proceed()
来让原始方法执行,其他通知不需要考虑目标方法执行@Around
环绕通知方法的返回值,必须指定为 Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。
1.1 代码优化
由于可能会为一个类、方法等设置多种通知类型,如同上述代码一样,每一个注解都使用一样的切面表达式,此时代码中存在大量重复性的代码,此时就可以使用 Spring 提供的 @PointCut
注解对代码进行优化。
@Slf4j
@Component
@Aspect
public class MyAspect1 {
// 切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.tlias.service.impl.EmpServiceImpl.*(..))")
public void pt(){}
// 环绕通知
@Around("pt()") // 使用公共切面表达式
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
// ...
}
// 前置通知
@Before("pt()") // 使用公共切面表达式
public void before(JoinPoint joinPoint){
log.info("before ...");
}
// ...
}
需要注意的是,切入点方法的使用范围受其修饰符的影响。
2. 通知顺序
当在项目开发当中,我们定义了多个切面类,而多个切面类中多个切入点都匹配到了同一个目标方法。此时当目标方法在运行的时候,这多个切面类当中的这些通知方法都会运行。
此时我们就有一个疑问,这多个通知方法到底哪个先运行,哪个后运行?
默认按照切面类的类名字母排序:
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行
如果想要手动控制其执行顺序,可以使用以下两种方式:
- 修改切面类的类名(这种方式非常繁琐、而且不便管理)
- 使用 Spring 提供的
@Order
注解
这里主要说明以下以注解的方式修改其顺序。
使用 @Order
注解,控制通知的执行顺序:
@Slf4j
@Component
@Aspect
@Order(2) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2 {
@Pointcut("execution(* com.tlias.service.impl.EmpServiceImpl.*(..))")
public void pt(){}
//前置通知
@Before("pt()")
public void before(){
log.info("MyAspect2 -> before ...");
}
//后置通知
@After("pt()")
public void after(){
log.info("MyAspect2 -> after ...");
}
}
@Slf4j
@Component
@Aspect
@Order(3) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect3 {
//前置通知
@Before("pt()") // 由于切入点方法是使用 public 修饰,可以在同一个包下共用
public void before(){
log.info("MyAspect3 -> before ...");
}
//后置通知
@After("pt()")
public void after(){
log.info("MyAspect3 -> after ...");
}
}
重启 SpringBoot 项目后,无论这么修改数字的大小,可以发现数字小的一定先执行,因此可以得出结论:
- 前置通知:数字越小先执行;
- 后置通知:数字越小越后执行
3. 切入点表达式
前面代码我们一直在使用切入点表达式,那什么又是切入点表达式哪?
切入点表达式:描述切入点方法的一种表达式
- 作用:主要用来决定项目中的哪些方法需要加入通知
- 常见形式:
execution(...)
:根据方法的签名来匹配@annotation(...)
:根据注解匹配
3.1 execution
execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带 ?
的表示可以省略的部分
- 访问修饰符:可省略(比如: public、protected)
包名.类名
: 可省略throws 异常
:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
可以使用通配符描述切入点:
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
举例说明使用通配符描述切入点:
-
省略方法的修饰符号
execution(void com.tlias.service.impl.EmpServiceImpl.delete(java.lang.Integer))
完整写法,通常用于指定一个具体的方法
-
使用
*
代替返回值类型execution(* com.tlias.service.impl.EmpServiceImpl.delete(java.lang.Integer))
表示任意返回值的方法
-
使用
*
代替包名(一层包使用一个*
)execution(* com.tlias.*.*.EmpServiceImpl.delete(java.lang.Integer))
表示任意返回值、com.tlias 包下的任意两层包下的 EmpServiceImpl 类下的方法
-
使用
..
省略包名execution(* com..EmpServiceImpl.delete(java.lang.Integer))
表示任意返回值、com 包下的任意层包下的 EmpServiceImpl 类下的方法
-
使用
*
代替类名execution(* com..*.delete(java.lang.Integer))
表示任意返回值、com 包下的任意层包下的任意类中的方法
-
使用
*
代替方法名execution(* com..*.*(java.lang.Integer))
表示任意返回值、com 包下的任意层包下的任意类中的任意的方法
-
使用
*
代替参数execution(* com.tlias.service.impl.EmpServiceImpl.delete(*))
表示任意返回值且有一个参数的指定包下的方法
-
使用
..
省略参数execution(* com..*.*(..))
表示任意返回值、com 包下的任意层包下的任意类中的任意且有 0 个或多个参数的方法(基本上不会使用)
注意事项:
-
根据业务需要,可以使用 且(
&&
)、或(||
)、非(!
) 来组合比较复杂的切入点表达式。 -
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是 update 开头
-
匹配 EmpServiceImpl 类中以 find 开头的方法
execution(* com.tlias.service.impl.EmpServiceImpl.find*(..))
-
描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
-
在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用
..
,使用*
匹配单个包
3.2 @annotation
实现步骤:
- 编写自定义注解
- 在业务类要做为连接点的方法上添加自定义注解
自定义注解 LogOperation
:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{ }
在需要标注的方法上使用自定义注解:
@Override
@LogOperation // 自定义注解(表示:当前方法属于目标方法)
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
return deptList;
}
@Override
@LogOperation // 自定义注解(表示:当前方法属于目标方法)
public void delete(Integer id) {
//1. 删除部门
deptMapper.delete(id);
}
配置切面类
@Slf4j
@Component
@Aspect
public class MyAspect6 {
//前置通知
@Before("@annotation(com.itheima.anno.LogOperation)")
public void before(){
log.info("MyAspect6 -> before ...");
}
//后置通知
@After("@annotation(com.itheima.anno.LogOperation)")
public void after(){
log.info("MyAspect6 -> after ...");
}
}
3.3 总结
常见的切入点表达式:
- execution 切入点表达式
- 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式
- 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过 execution 切入点表达式描述比较繁琐
- annotation 切入点表达式
- 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了
根据业务需要,可以使用 &&
、||
、!
来组合比较复杂的切入点表达式。