day607.aop不同类型增强顺序&同类型增强顺序问题-spring编程常见错误

阿昌喜欢吃黄桃      2022-05-06     387

关键词:

Aop不同类型增强顺序&同类型增强顺序问题

Day606的文章中,咋们聊了记录了一下aop的两个使用错误的问题案例【this 调用的当前类方法无法被拦截、通过代理类访问被代理类的成员属性抛空指针异常】。

那这次我们记录讨论,一个当一个系统采用的切面越来越多时,因为执行顺序而导致的问题便会逐步暴露出来,内容如下。


前置条件

一个系统有一个电费充值模块,它包含了一个负责电费充值的类 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 等),它们的执行是有顺序的。那么顺序如何?


Spring 初始化单例类的一般过程:

基本都是 getBean()->doGetBean()->getSingleton()

如果发现 Bean 不存在,则调用 createBean()->doCreateBean() 进行实例化。

而如果我们的代码里使用了 Spring AOP,doCreateBean() 最终会返回一个代理对象。至于代理对象如何创建,(参考 AbstractAutoProxyCreator#createProxy):

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
      @Nullable Object[] specificInterceptors, TargetSource targetSource) 
   //省略非关键代码
   Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
   proxyFactory.addAdvisors(advisors);
   proxyFactory.setTargetSource(targetSource);
   //省略非关键代码
   return proxyFactory.getProxy(getProxyClassLoader());

其中 advisors 就是增强方法对象,它的顺序决定了面临多个增强时,到底先执行谁。

而这个集合对象本身是由 specificInterceptors 构建出来的,而 specificInterceptors 又是由 AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean 方法构建:

@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
      Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) 
   List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
   if (advisors.isEmpty()) 
      return DO_NOT_PROXY;
   
   return advisors.toArray();
  

简单说,其实就是根据当前的 beanClass、beanName 等信息,结合所有候选的 advisors,最终找出匹配(Eligible)的 Advisor,为什么如此?

毕竟 AOP 拦截点可能会配置多个,而我们执行的方法不见得会被所有的拦截配置拦截

寻找匹配 Advisor 的逻辑参考 AbstractAdvisorAutoProxyCreator#findEligibleAdvisors:

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) 
   //寻找候选的 Advisor
   List<Advisor> candidateAdvisors = findCandidateAdvisors();
   //根据候选的 Advisor 和当前 bean 算出匹配的 Advisor
   List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
   extendAdvisors(eligibleAdvisors);
   if (!eligibleAdvisors.isEmpty()) 
      //排序
      eligibleAdvisors = sortAdvisors(eligibleAdvisors);
   
   return eligibleAdvisors;

最终 Advisors 的顺序是由两点决定:

  • candidateAdvisors 的顺序;
  • sortAdvisors 进行的排序。

重点看下对本案例起关键作用的 candidateAdvisors 排序。

实际上,它的顺序是在 @Aspect 标记的 AopConfig Bean 构建时就决定了。具体而言,就是在初始化过程中会排序自己配置的 Advisors,并把排序结果存入了缓存(BeanFactoryAspectJAdvisorsBuilder#advisorsCache)。

排序是在 Bean 的构建中进行的,而最后排序执行的关键代码位于下面的方法中(参考 ReflectiveAspectJAdvisorFactory#getAdvisorMethods):

private List<Method> getAdvisorMethods(Class<?> aspectClass) 
   final List<Method> methods = new ArrayList<>();
   ReflectionUtils.doWithMethods(aspectClass, method -> 
      // Exclude pointcuts
      if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) 
         methods.add(method);
      
   , ReflectionUtils.USER_DECLARED_METHODS);
   // 排序
   methods.sort(METHOD_COMPARATOR);
   return methods;

methods.sort(METHOD_COMPARATOR) 方法是重点。


METHOD_COMPARATOR 的代码,会发现它是定义在 ReflectiveAspectJAdvisorFactory 类中的静态方法块,代码如下:

static 
   Comparator<Method> adviceKindComparator = new ConvertingComparator<>(
         new InstanceComparator<>(
               Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
         (Converter<Method, Annotation>) method -> 
            AspectJAnnotation<?> annotation =
               AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
            return (annotation != null ? annotation.getAnnotation() : null);
         );
   Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);
   //合并上面两者比较器
   METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator);

最终会调用的基准比较器,以下是它的关键实现代码:

new InstanceComparator<>(
      Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class)

构造方法也是较为简单的,只是将传递进来的 instanceOrder 赋予了类成员变量,继续查看 InstanceComparator 比较器核心方法 compare 如下,也就是最终要调用的比较方法:

public int compare(T o1, T o2) 
   int i1 = getOrder(o1);
   int i2 = getOrder(o2);
   return (i1 < i2 ? -1 : (i1 == i2 ? 0 : 1));

一个典型的 Comparator,代码逻辑按照 i1、i2 的升序排列,即 getOrder() 返回的值越小,排序越靠前

查看 getOrder() 的逻辑如下:

private int getOrder(@Nullable T object) 
   if (object != null) 
      for (int i = 0; i < this.instanceOrder.length; i++) 
         //instance 在 instanceOrder 中的“排号”
         if (this.instanceOrder[i].isInstance(object)) 
            return i;
         
      
   
   return this.instanceOrder.length;

返回当前传递的增强注解在 this.instanceOrder 中的序列值,序列值越小,则越靠前

而结合之前构造参数传递的顺序,我们很快就能判断出:最终的排序结果依次是 Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class

到此为止,答案也呼之欲出:this.instanceOrder 的排序,即为不同类型增强的优先级,排序越靠前,优先级越高

结合之前的讨论,我们可以得出一个结论:

同一个切面中,不同类型的增强方法被调用的顺序依次为 Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class


针对上面案例的解决方式如下:

将 ElectricService.charge() 的业务逻辑全部移动到 doCharge(),在 charge() 中调用 doCharge();性能统计只需要拦截 doCharge();

权限统计增强保持不变,依然拦截 charge()。

@Service
public class ElectricService 
    @Autowired
    ElectricService electricService;
    public void charge() 
        electricService.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));
  


二、同类型增强顺序问题

这里业务逻辑类 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() 不应该被调用,但事实总是出乎意料,执行结果如下:

step into ->void com.spring.puzzle.class6.example2.Electric.charge()
Exception in thread “main” java.lang.RuntimeException: authority
check failed

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

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


你应该还记得上述代码中,定义 METHOD_COMPARATOR 的静态代码块吧。

METHOD_COMPARATOR 本质是一个连续比较器,而上个案例中我们仅仅只看了第一个比较器,细心的你肯定发现了这里还有第二个比较器 methodNameComparator,任意两个比较器都可以通过其内置的 thenComparing() 连接形成一个连续比较器,从而可以让我们按照比较器的连接顺序依次比较:

static 
   //第一个比较器,用来按照增强类型排序
   Comparator<Method> adviceKindComparator = new ConvertingComparator<>(
         new InstanceComparator<>(
               Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
         (Converter<Method, Annotation>) method -> 
            AspectJAnnotation<?> annotation =
               AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
            return (annotation != null ? annotation.getAnnotation() : null);
         )
   //第二个比较器,用来按照方法名排序
   Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);
   METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator);

第 2 个比较器 methodNameComparator 依然使用的是 ConvertingComparator,传递了方法名作为参数。我们基本可以猜测出该比较器是按照方法名进行排序的,这里可以进一步查看构造器方法及构造器调用的内部 comparable():

public ConvertingComparator(Converter<S, T> converter) 
   this(Comparators.comparable(), converter);

// 省略非关键代码
public static <T> Comparator<T> comparable() 
   return ComparableComparator.INSTANCE;

上述代码中的 ComparableComparator 实例其实极其简单,代码如下:

public class ComparableComparator<T extends Comparable<T>> implements Comparator<T> 
   public static final ComparableComparator INSTANCE = new ComparableComparator();

   @Override
   public int compare(T o1, T o2) 
      return o1.compareTo(o2);
   

答案和我们的猜测完全一致,methodNameComparator 最终调用了 String 类自身的 compareTo(),代码如下:

public int compareTo(String anotherString) 
    int len1 = value.length;
    int len2 = anotherString.value.length;
    int lim = Math.min(len1, len2);
    char v1[] = value;
    char v2[] = anotherString.value;

    int k = 0;
    while (k < lim) 
        char c1 = v1[k];
        char c2 = v2[k];
        if (c1 != c2) 
            return c1 - c2;
        
        k++;
    
    return len1 - len2;

到这,答案揭晓:如果两个方法名长度相同,则依次比较每一个字母的 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() 的排序会靠前,从而被优先执行,最终问题得以解决。


三、总结

  • 在同一个切面配置中,如果存在多个不同类型的增强,那么其执行优先级是按照增强类型的特定顺序排列,依次的增强类型为 Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class;
  • 在同一个切面配置中,如果存在多个相同类型的增强,那么其执行优先级是按照该增强的方法名排序,排序方式依次为比较方法名的每一个字母,直到发现第一个不相同且 ASCII 码较小的字母。

java基础day03-2(代码片段)

...va基础day03-2方法的重载规则方法名必须相同参数列表必须不同(个数不同/类型不同/参数排列顺序不同等)方法的返回值类型可以相同或不同publicstaticvoidmain(String[]args)intmax=max(10,21);//doublemax=max(10,20); variablemaxisalreadydefinedinthescope//... 查看详情

什么类型可以保存 C++ 中不同类的成员函数指针?

】什么类型可以保存C++中不同类的成员函数指针?【英文标题】:Whattypecanholdmember-function-pointersofdifferenceclassesinC++?【发布时间】:2010-03-1803:23:25【问题描述】:我需要一个数组来保存不同类的成员函数指针。如何定义数组?代... 查看详情

创建不同对象的实例列表

】创建不同对象的实例列表【英文标题】:Creatinginstancelistofdifferentobjects【发布时间】:2012-06-1920:57:37【问题描述】:我正在尝试创建一个包含不同类实例的数组列表。如何在不定义类类型的情况下创建列表?(&lt;Employee&gt;... 查看详情

方法的重载设计

...方法允许存在一个以上的同名方法,只要他们的参数列表不同即可。方法重载的作用:屏蔽了同一功能的方法由于参数不同所导致的方法名称不同的差异。方法重载判断原则:“两同一不同”两同:同类中,方法名相同;一不同... 查看详情

day9.集合

作用:去重,关系运算定义:由不同元素组成的集合,集合中是一组无序排列的可hash值(不可变类型),可以作为字典的key   集合的目的是将不同的值存放到一起,不同的集合间用来做关系运算,无需纠结于集合中单个值... 查看详情

day1方法的重载

...和参数的数量)方法的重载:方法名称相同但是方法参数不同(1.参数类型不同2.参数类型相同但是参数个数不同3.类型和个数都不同)例如:调用Console.WtiteLine(); 可以传递double类型的参数,可以传递int类型的参数,可以传递s... 查看详情

java进阶路线

...机基础Java入门学习 Day2:注释、标识符、关键字/数据类型/类型转换/变量常量/运算符/包机制、JavaDoc/2023-1-12Java基础语法 Day3:Scanner对象/顺序结构/选择结构/循环结构/break&continue/练习流程控制和方法 Day4:何为方法/方... 查看详情

day02-数据类型:数字

...数据?  x=10,10是我们要存储的数据二、为何数据要分不同的类型  数据是用来表示状态的,不同的状态就应该用不同的类型的数据去表示三、数据类型  数字(整型,浮点型,复数)  字符串  列表  元组  字... 查看详情

将变量指向来自不同类类型的成员函数

】将变量指向来自不同类类型的成员函数【英文标题】:Pointingavariabletoamemberfunctionfromadifferentclasstype【发布时间】:2012-04-1321:35:24【问题描述】:在C++中,如何使用指针从B类调用A类的方法成员?顺便说一下A类和B类是不同的类... 查看详情

11多态&动态静态绑定(java)

...后一个概念,也是最重要的知识点。多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)实现多态的技术称为:动态绑定(dynamicbi... 查看详情

day3:数据类型

一、数据分不同类型的原因:因为数据是用来表示状态的,不同状态需要用不同类型的数据去表示。二、数据类型的分类:数字、字符串、列表、元组、字典、集合。 三、基础数据类型:1、数字int:主要用于计算。  #bit_... 查看详情

c#:数组、arraylist、list、链表、栈、队列,hashset的不同day0816

...个不用,不安全)容量不固定,类型不固定ArrayList中插入不同类型的数据是允许的。ArrayListal=ne 查看详情

day604.@value注入问题&集合类型注入问题-spring编程常见错误(代码片段)

@Value注入问题&集合类型注入问题讲到Spring的反转注入,必然知道他的强大,当这次今天阿昌总结的两种问题,当@Value和Spring注入集合类型有可能会发生的问题如下:一、@Value没有注入预期的值当使用&#... 查看详情

day462.mysql数据类型&约束-mysql(代码片段)

MySQL数据类型精讲1.MySQL中的数据类型类型类型举例整数类型TINYINT、SMALLINT、MEDIUMINT、INT(或INTEGER)、BIGINT浮点类型FLOAT、DOUBLE定点数类型DECIMAL位类型BIT日期时间类型YEAR、TIME、DATE、DATETIME、TIMESTAMP文本字符串类型CHAR、VARCHAR、TINYTEXT... 查看详情

day462.mysql数据类型&约束-mysql(代码片段)

MySQL数据类型精讲1.MySQL中的数据类型类型类型举例整数类型TINYINT、SMALLINT、MEDIUMINT、INT(或INTEGER)、BIGINT浮点类型FLOAT、DOUBLE定点数类型DECIMAL位类型BIT日期时间类型YEAR、TIME、DATE、DATETIME、TIMESTAMP文本字符串类型CHAR、VARCHAR、TINYTEXT... 查看详情

day9-面向对象和面向过程(代码片段)

一、面向对象与面向过程  面向对象与面向过程是两种不同的编程范式,编程范式指的是按照什么方式去编程,去实现一个功能。不同的编程范式本质上代表对各种类型的任务采取不同的解决问题的思路。1、面向过程编程 ... 查看详情

TestNG 执行顺序 - 它混合了来自不同类的测试

】TestNG执行顺序-它混合了来自不同类的测试【英文标题】:TestNGexecutionorder-it\'smixingtestsfromdifferentclasses【发布时间】:2019-06-2609:25:31【问题描述】:TestNG在执行时混合了来自不同类的测试。每个班级都有一些测试。而不是像这... 查看详情

day708.tomcat和jetty有哪些不同-深入拆解tomcat&jetty(代码片段)

Tomcat和Jetty有哪些不同Hi,我是阿昌,今天学习记录的是关于Tomcat和Jetty有哪些不同。概括一下Tomcat和Jetty两者最大的区别。大体来说,Tomcat的核心竞争力是成熟稳定,因为它经过了多年的市场考验,应用也相当... 查看详情