在前几篇文章中,已经讲解了关于AOP技术
的基本使用和核心概念,知道了AOP程序
的各部分构成,及其底层是通过动态代理技术
对目标对象的原始方法进行增强的。本文主要讲解AOP技术
的进阶部分。
AOP进阶
在讲解AOP
进阶之前,还是先看一个示例代码:
@Around("execution(* com.wzb.service.impl.EmpServiceImpl.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
// 记录方法开始时间
Long begin = System.currentTimeMillis();
// 调用原始方法
Object result = pjp.proceed();
// 记录方法结束时间
Long end = System.currentTimeMillis();
// 计算方法耗时
log.info("执行方法耗时:{}ms", end - begin);
return result;
}
这个Aspect切面
仍然是前面提到的统计方法执行用时的切面,本文将围绕这个示例代码进行讲解。
通知类型
在示例程序当中,该切面使用了一个功能最为强大的通知类型——Around环绕通知
:
@Around("execution(* com.wzb.service.impl.EmpServiceImpl.*(..))")
@Around注解
是代表通知类型,Around
表示当前通知是一个环绕通知。除了环绕通知意外,AOP
还有很多通知类型。
@Around环绕通知
环绕通知,使用@Around注解
,使用环绕通知的通知方法会在目标方法执行前后都被执行。
@Before前置通知
前置通知,使用@Before注解
,使用前置通知的通知方法会在目标方法执行前执行。
@After后置通知
后置通知,使用@After注解
,使用后置通知的通知方法会在目标方法执行后执行。并且,无论目标方法执行是否有异常,后置通知都会执行。
@AfterReturning返回后通知
返回后通知,使用@AfterReturning注解
,使用返回后通知的通知方法会在目标方法执行成功
之后执行,如果方法执行有异常,则返回后通知不执行。这和后置通知有所区别。
@AfterThrowing异常通知
异常通知,使用AfterThrowing注解
,使用异常通知的通知方法会在目标方法执行发生异常
之后执行,如果方法成功执行,则异常通知不执行,异常通知和返回后通知不可能同时执行。
可以使用这个代码来对不同类型的通知类型
进行测试:
/**
* AOP通知类型测试
*/
@Slf4j
@Aspect
@Component
public class MyAspect1 {
/**
* 抽取切入点表达式
* 抽取到切入点方法中
*/
@Pointcut("execution(* com.wzb.service.impl.EmpServiceImpl.*(..))")
private void pt() {}
/**
* Before 前置通知,在方法运行前执行
* @param joinPoint 连接点
*/
@Before("pt()")
public void before(JoinPoint joinPoint) {
log.info("before 前置通知");
}
/**
* Around 环绕通知,在方法运行前后都会执行
* @param pjp 连接点
* @return 方法返回值
* @throws Throwable 原始方法运行时抛出异常
*/
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("before around");
// 调用目标对象的原始方法执行
Object result = pjp.proceed();
log.info("目标方法执行结束");
// 假如原始方法执行时有异常,那么环绕通知的后置代码就不会执行
log.info("after around");
return result;
}
/**
* After 后置通知方法运行后执行
* @param joinPoint 连接点
*/
@After("pt()")
public void after(JoinPoint joinPoint) {
log.info("after 后置通知");
}
/**
* 返回后通知 目标方法必须正常运行才会执行返回后通知
* @param joinPoint 连接点
*/
@AfterReturning("pt()")
public void afterReturning(JoinPoint joinPoint) {
log.info("afterReturning 返回后通知");
}
/**
* 异常通知 目标方法必须抛出异常,执行出错才会执行异常通知,异常通知和返回后通知互斥
* @param joinPoint 连接点
*/
@AfterThrowing("pt()")
public void afterThrowing(JoinPoint joinPoint) {
log.info("afterThrowing 异常通知");
}
}
启动服务,发起请求进行测试:
如控制台输出所示,在目标方法执行之前,前置通知和环绕前通知就先于目标方法执行了,并且环绕前通知会先于前置通知执行,当目标方法执行结束之后,返回后通知、后置通知、环绕后通知也相继执行,并且环绕后通知是最后执行的。由于目标方法的执行过程中没有
抛出异常
,所以说异常通知不会执行。现在将目标方法中添加一个异常,再次发起请求:
如图所示,异常通知执行了,但是由于目标方法执行异常,所以说返回后通知不会执行,而后置通知是无论如何都要执行的。并且,在目标方法执行异常之后,环绕后通知也不会执行。(因为环绕通知调用了原始方法执行,但是原始方法执行已经出现异常了,程序中断)
在使用@Around
环绕通知的时候,需要调用ProceedingJoinPoint.proceed()
来让原始方法执行,其它通知类型不需要考虑目标方法执行。并且 Around
环绕通知的返回值必须指定为Object
用于接收原始方法执行的返回值,否则是无法获取原始方法执行的返回值的。
并且我们还可以将切入点表达式抽取出来,使用@Pointcut注解
将切入点表达式直接抽取到一个方法上,方法不需要方法体,只需要使用注解,获取切入点表达式,这样可以减少重复的切入点表达式,在使用时,只需要在通知类型中引用这个方法即可:
/**
* 抽取切入点表达式
* 抽取到切入点方法中
*/
@Pointcut("execution(* com.wzb.service.impl.EmpServiceImpl.*(..))")
private void pt() {}
// 直接在通知类型中引用方法获取切入点表达式
@Before("pt()")
如果想要在其他AOP类
中使用这个切入点表达式,必须在前面添加全类名
,并且方法的权限修饰符应该为public
。
通知顺序
讲解了通知类型之后,接下来需要研究一下通知执行顺序,假如在一个项目中定义了多个切面类,多个切面类中的切入点都匹配到了同一个目标方法,那么当目标方法执行的时候,这些切面类中的通知都会执行。那么这些切面类中的通知顺序是什么,此时就需要编写多个切面类进行验证。
/**
* 测试AOP
*/
@Slf4j
@Aspect
@Component
public class MyAspect2 {
/**
* 切入点方法
*/
@Pointcut("execution(* com.wzb.service.impl.EmpServiceImpl.addEmp(..)) || " +
"execution(* com.wzb.service.impl.EmpServiceImpl.login(..))")
public void pt() {}
/**
* 前置通知,在目标方法运行前执行
* @param joinPoint 连接点
*/
@Before("pt()")
public void before(JoinPoint joinPoint) {
log.info("MyAspect2 before...");
}
/**
* 环绕通知,在目标方法运行前后都会执行
* @param pjp 连接点
* @return 目标方法运行返回值
* @throws Throwable 目标方法运行抛出异常
*/
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("MyAspect2 around before...");
Object result = pjp.proceed();
// 假如目标方法运行报错,则不会执行方法后面的代码
log.info("MyAspect2 around after...");
return result;
}
/**
* 后置通知,在目标方法运行后执行,不论目标方法是否成功运行,后置通知都会执行
* @param joinPoint 连接点
*/
@After("pt()")
public void after(JoinPoint joinPoint) {
log.info("MyAspect2 after...");
}
/**
* 返回后通知,在目标方法运行成功后执行,若运行失败则不执行
* @param joinPoint 连接点
*/
@AfterReturning("pt()")
public void afterReturning(JoinPoint joinPoint) {
log.info("MyAspect2 afterReturning...");
}
/**
* 异常通知,在目标方法运行失败,抛出异常后执行,和AfterReturning通知不可能同时执行
* @param joinPoint 连接点
*/
@AfterThrowing("pt()")
public void afterThrowing(JoinPoint joinPoint) {
log.info("MyAspect2 afterThrowing");
}
}
此时项目中同时存在两个AOP类
,其中的切面全部匹配到了同一个目标对象,此时启动服务:
观察控制台输出,可以得到结论:在不同的切面类中,假如切入点匹配到同一个目标方法,则通知执行顺序默认按照切面类名
字母排序
,并且在目标方法执行前,排序靠前的切面类中通知先执行;在目标方法执行后,排序靠后的切面类通知先执行。想要控制通知执行顺序,除了修改切面类名之外(这种方式一般不用,类名必须见名知意),还可以使用@Order
注解控制执行顺序,比如Order(1)
代表这个切面类中的通知先执行,此时不再考虑类名。
切入点表达式
上述示例代码中一直使用切入点表达式来描述切入点,下面来具体介绍一下切入点表达式。
切入点表达式
是描述切入点方法的一种方式,主要用来决定项目中哪些方法需要通知
,其常见形式主要是:execution(...),根据方法签名(返回值、包名、类名、参数等信息)进行匹配
和 @annotation(...)
根据注解进行匹配。
execution
execution
方式主要是根据方法签名进行匹配,其语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带?
的部分可以省略(不建议省略包名和类名
),throws异常
可以省略(throws
是方法签名抛出的异常,并不是方法实际运行时抛出的异常),比如:
@Around("execution(* com.wzb.service.impl.EmpServiceImpl.*(..))")
这个切入点表达式就省略了权限修饰符
和 方法抛出异常
,下列是更加详细的规则:
- 方法的权限修饰符可以省略。
- 方法的返回值可以使用
*
代替,表示任意返回值类型。 - 包名可以使用
*
代替,表示任意包,每一个层级的包都需要使用*
。 - 可以使用
..
配置包名,表示当前包及其所有子包,如com..
表示com包
下的所有子包。 - 类名可以使用
*
代替,表示该包下的任意类。 - 方法名可以使用
*
代替,表示该类下的任意方法。 - 参数类型可以使用
*
代替,表示一个任意类型的参数。 - 参数类型也可以使用
..
代替,表示任意个任意类型的参数。
不仅如此,还可以使用逻辑运算符组合更加复杂的切入点表达式,比如:
@Around("execution(* com.wzb.service.impl.EmpServiceImpl.*(..)) || execution(* com.wzb.service.impl.DeptServiceImpl.*(..))")
切入点表达式的书写建议:
- 所有业务方法必须命名规范,以便于切入点表达式匹配,必须见名知意。
- 建议切入点表达式不直接指定
实现类
,而是指定对应的接口,可以增强扩展性。 - 满足业务需求的前提下,尽量缩小切入点匹配范围,因为通知执行是需要消耗资源的。
@annotation
除了使用execution
编写切入点表达式之外,对于复杂的切入点匹配,还可以使用@annotation注解
的方式简化切入点表达式书写。
首先需要自定义一个注解:
/**
* 自定义注解,用于日志记录操作
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {
}
然后在想要执行通知的原始方法上使用这个注解:
@LogOperation
@Override
public LoginInfo login(Emp emp) {
然后编写切入点表达式:
@Around("@annotation(com.wzb.annotation.LogOperation)")
这样所有使用了LogOperation
注解的方法都会被切入点匹配。假如遇到命名不规范的方法或者特殊需求,建议使用@annotation
的方式定义切入点表达式。
总结
AOP
中有五种通知类型,其中Around
通知类型是使用最为广泛的,会在目标方法执行前和方法执行成功之后都执行。假如有多个切面类
匹配到了同一个目标方法,其默认是按照类名的字母顺序进行执行的,但可以使用@Order
注解指定其执行顺序。切入点表达式可以使用execution
直接编写,也可以使用@annotation
的方式,使用注解进行指定。