Spring AOP 常见错误(下)

当一个系统采用的切面越来越多时,因为执行顺序而导致的问题便会逐步暴露出来

1:错乱混合不同类型的增强

还是沿用上节课的宿舍管理系统开发场景。

这里我们先回顾下,你就不用去翻代码了。这个宿舍管理系统保护了一个电费充值模块,它包含了一个负责电费充值的类 ElectricService,还有一个充电方法 charge():

@Service
public class ElectricService {
    public void charge() throws Exception {
        System.out.println("Electric charging ...");
    }
}

为了在执行 charge() 之前,鉴定下调用者的权限,我们增加了针对于 Electric 的切面类 AopConfig,其中包含一个 @Before 增强。这里的增强没有做任何事情,仅仅是打印了一行日志,然后模拟执行权限校验功能(占用 1 秒钟)。

//省略 imports
@Aspect
@Service
@Slf4j
public class AspectService {
  @Before("execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) ")
  public void checkAuthority(JoinPoint pjp) throws Throwable {
      System.out.println("validating user authority");
      Thread.sleep(1000);
  }
}

执行后,我们得到以下 log,接着一切按照预期继续执行:

validating user authority
Electric charging ...

一段时间后,由于业务发展,ElectricService 中的 charge() 逻辑变得更加复杂了,我们需要仅仅针对 ElectricService 的 charge() 做性能统计。为了不影响原有的业务逻辑,我们在 AopConfig 中添加了另一个增强,代码更改后如下:


//省略 imports
@Aspect
@Service
public class AopConfig {
    @Before("execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) ")
    public void checkAuthority(JoinPoint pjp) throws Throwable {
        System.out.println("validating user authority");
        Thread.sleep(1000);
    }

    @Around("execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) ")
    public void recordPerformance(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        pjp.proceed();
        long end = System.currentTimeMillis();
        System.out.println("charge method time cost: " + (end - start));
    }
}

执行后得到日志如下:

validating user authority
Electric charging ...
charge method time cost 1022 (ms)

通过性能统计打印出的日志,我们可以得知 charge() 执行时间超过了 1 秒钟。然而,该方法仅打印了一行日志,它的执行不可能需要这么长时间。

因此我们很容易看出问题所在:当前 ElectricService 中 charge() 的执行时间,包含了权限验证的时间,即包含了通过 @Around 增强的 checkAuthority() 执行的所有时间。这并不符合我们的初衷,我们需要统计的仅仅是 ElectricService.charge() 的性能统计,它并不包含鉴权过程。

当然,这些都是从日志直接观察出的现象。实际上,这个问题出现的根本原因和 AOP 的执行顺序有关。针对这个案例而言,当同一个切面(Aspect)中同时包含多个不同类型的增强时(Around、Before、After、AfterReturning、AfterThrowing 等),它们的执行是有顺序的。那么顺序如何?我们不妨来解析下。

案例解析

返回当前传递的增强注解在 this.instanceOrder 中的序列值,序列值越小,则越靠前。而结合之前构造参数传递的顺序,我们很快就能判断出:最终的排序结果依次是 Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class。

结合之前的讨论,我们可以得出一个结论:同一个切面中,不同类型的增强方法被调用的顺序依次为 Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class。

从上述案例解析中,我们知道 Around 类型的增强被调用的优先级高于 Before 类型的增强,所以上述案例中性能统计所花费的时间,包含权限验证的时间,也在情理之中。

知道了原理,修正起来也就简单了。假设不允许我们去拆分类,我们可以按照下面的思路来修改:

  1. 将 ElectricService.charge() 的业务逻辑全部移动到 doCharge(),在 charge() 中调用 doCharge();
  2. 性能统计只需要拦截 doCharge();
  3. 权限统计增强保持不变,依然拦截 charge()。

ElectricService 类代码更改如下:

@Service
public class ElectricService {
 
    public void charge() {
        doCharge();
    }
    public void doCharge() {
        System.out.println("Electric charging ...");
    }
}

切面代码更改如下:

//省略 imports
@Aspect
@Service
public class AopConfig {
    @Before("execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) ")
    public void checkAuthority(JoinPoint pjp) throws Throwable {
        System.out.println("validating user authority");
        Thread.sleep(1000);
    }

    @Around("execution(* com.spring.puzzle.class6.example1.ElectricService.doCharge()) ")
    public void recordPerformance(ProceedingJoinPoint pjp) throws Throwable {
    long start = System.currentTimeMillis();
    pjp.proceed();
    long end = System.currentTimeMillis();
    System.out.println("charge method time cost: " + (end - start));
  }
}

2:错乱混合同类型增强

那学到这里,你可能还有疑问,如果同一个切面里的多个增强方法其增强都一样,那调用顺序又如何呢?我们继续看下一个案例。

这里业务逻辑类 ElectricService 没有任何变化,仅包含一个 charge():

import org.springframework.stereotype.Service;
@Service
public class ElectricService {
    public void charge() {
        System.out.println("Electric charging ...");
    }
}

切面类 AspectService 包含两个方法,都是 Before 类型增强。

第一个方法 logBeforeMethod(),目的是在 run() 执行之前希望能输入日志,表示当前方法被调用一次,方便后期统计。另一个方法 validateAuthority(),目的是做权限验证,其作用是在调用此方法之前做权限验证,如果不符合权限限制要求,则直接抛出异常。这里为了方便演示,此方法将直接抛出异常:

//省略 imports
@Aspect
@Service
public class AopConfig {
  @Before("execution(* com.spring.puzzle.class5.example2.ElectricService.charge())")
  public void logBeforeMethod(JoinPoint pjp) throws Throwable {
      System.out.println("step into ->"+pjp.getSignature());
  }
  @Before("execution(* com.spring.puzzle.class5.example2.ElectricService.charge()) ")
  public void validateAuthority(JoinPoint pjp) throws Throwable {
      throw new RuntimeException("authority check failed");
  }
}

我们对代码的执行预期为:当鉴权失败时,由于 ElectricService.charge() 没有被调用,那么 run() 的调用日志也不应该被输出,即 logBeforeMethod() 不应该被调用,但事实总是出乎意料,执行结果如下:

虽然鉴权失败,抛出了异常且 ElectricService.charge() 没有被调用,但是 logBeforeMethod() 的调用日志却被输出了,这将导致后期针对于 ElectricService.charge() 的调用数据统计严重失真。

这里我们就需要搞清楚一个问题:当同一个切面包含多个同一种类型的多个增强,且修饰的都是同一个方法时,这多个增强的执行顺序是怎样的?

案例解析

到这,答案揭晓:如果两个方法名长度相同,则依次比较每一个字母的 ASCII 码,ASCII 码越小,排序越靠前;若长度不同,且短的方法名字符串是长的子集时,短的排序靠前。

问题修正

从上述分析我们得知,在同一个切面配置类中,针对同一个方法存在多个同类型增强时,其执行顺序仅和当前增强方法的名称有关,而不是由谁代码在先、谁代码在后来决定。了解了这点,我们就可以直接通过调整方法名的方式来修正程序:

//省略 imports
@Aspect
@Service
public class AopConfig {
  @Before("execution(* com.spring.puzzle.class6.example2.ElectricService.charge())")
  public void logBeforeMethod(JoinPoint pjp) throws Throwable {
      System.out.println("step into ->"+pjp.getSignature());
  }
  @Before("execution(* com.spring.puzzle.class6.example2.ElectricService.charge()) ")
  public void checkAuthority(JoinPoint pjp) throws Throwable {
      throw new RuntimeException("authority check failed");
  }
}

我们可以将原来的 validateAuthority() 改为 checkAuthority(),这种情况下,对增强(Advisor)的排序,其实最后就是在比较字符 l 和 字符 c。显然易见,checkAuthority() 的排序会靠前,从而被优先执行,最终问题得以解决。

拆分成两个切面,用@Order或Ordered接口控制顺序。个人觉得Spring在排序设计上比较混乱,各种排序规则不统一

posted @ 2021-06-17 16:45  hochan_100  阅读(44)  评论(0编辑  收藏  举报