Spring核心:AOP(3):AOP进阶

在前几篇文章中,已经讲解了关于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.*(..))")

这个切入点表达式就省略了权限修饰符 方法抛出异常,下列是更加详细的规则:

  1. 方法的权限修饰符可以省略。
  2. 方法的返回值可以使用*代替,表示任意返回值类型。
  3. 包名可以使用*代替,表示任意包,每一个层级的包都需要使用*
  4. 可以使用..配置包名,表示当前包及其所有子包,如 com.. 表示com包下的所有子包。
  5. 类名可以使用*代替,表示该包下的任意类。
  6. 方法名可以使用*代替,表示该类下的任意方法。
  7. 参数类型可以使用*代替,表示一个任意类型的参数。
  8. 参数类型也可以使用..代替,表示任意个任意类型的参数。

不仅如此,还可以使用逻辑运算符组合更加复杂的切入点表达式,比如:

@Around("execution(* com.wzb.service.impl.EmpServiceImpl.*(..)) || execution(* com.wzb.service.impl.DeptServiceImpl.*(..))")

切入点表达式的书写建议:

  1. 所有业务方法必须命名规范,以便于切入点表达式匹配,必须见名知意。
  2. 建议切入点表达式不直接指定实现类,而是指定对应的接口,可以增强扩展性。
  3. 满足业务需求的前提下,尽量缩小切入点匹配范围,因为通知执行是需要消耗资源的。
@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的方式,使用注解进行指定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值