歡迎光臨
每天分享高質量文章

介面方法上的註解無法被 @Aspect 宣告的切麵攔截的原因分析

(點選上方公眾號,可快速關註)


來源:光閃 ,

my.oschina.net/guangshan/blog/1808373

前言

在Spring中使用MyBatis的Mapper介面自動生成時,用一個自定義的註解標記在Mapper介面的方法中,再利用@Aspect定義一個切麵,攔截這個註解以記錄日誌或者執行時長。但是驚奇的發現這樣做之後,在Spring Boot 1.X(Spring Framework 4.x)中,並不能生效,而在Spring Boot 2.X(Spring Framework 5.X)中卻能生效。

這究竟是為什麼呢?Spring做了哪些更新產生了這樣的變化?此文將帶領你探索這個秘密。

案例

核心程式碼

@SpringBootApplication

public class Starter {

  public static void main(String[] args) {

    SpringApplication.run(DynamicApplication.class, args);

  }

}

 

@Service

public class DemoService {

 

    @Autowired

    DemoMapper demoMapper;

 

    public List

> selectAll() {

        return demoMapper.selectAll();

    }

}

 

/**

 * mapper類

 */

@Mapper

public interface DemoMapper {

 

  @Select(“SELECT * FROM demo”)

  @Demo

  List

> selectAll();

 

}

 

/**

 * 切入的註解

 */

@Target({ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface Demo {

  String value() default “”;

}

 

/**

 * aspect切麵,用於測試是否成功切入

 */

@Aspect

@Order(-10)

@Component

public class DemoAspect {

 

  @Before(“@annotation(demo)”)

  public void beforeDemo(JoinPoint point, Demo demo) {

    System.out.println(“before demo”);

  }

 

  @AfterDemo(“@annotation(demo)”)

  public void afterDemo(JoinPoint point, Demo demo) {

    System.out.println(“after demo”);

  }

}

測試類

@RunWith(SpringRunner.class) 

@SpringBootTest(classes = Starter.class)

public class BaseTest {

 

    @Autowired

    DemoService demoService;

 

    @Test

    public void testDemo() {

        demoService.selectAll();

    } 

}

在Spring Boot 1.X中,@Aspect裡的兩個println都沒有正常列印,而在Spring Boot 2.X中,都列印了出來。

除錯研究

已知@Aspect註解宣告的攔截器,會自動切入符合其攔截條件的Bean。這個功能是透過@EnableAspectJAutoProxy註解來啟用和配置的(預設是啟用的,透過AopAutoConfiguration),由@EnableAspectJAutoProxy中的@Import(AspectJAutoProxyRegistrar.class)可知,@Aspect相關註解自動切入的依賴是AnnotationAwareAspectJAutoProxyCreator這個BeanPostProcessor。在這個類的postProcessAfterInitialization方法中打上條件斷點:beanName.equals(“demoMapper”)

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

    if (bean != null) {

        // 快取中嘗試獲取,沒有則嘗試包裝

        Object cacheKey = getCacheKey(bean.getClass(), beanName);

        if (!this.earlyProxyReferences.contains(cacheKey)) {

            return wrapIfNecessary(bean, beanName, cacheKey);

        }

    }

    return bean;

}

在wrapIfNecessary方法中,有自動包裝Proxy的邏輯:

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {

    // 如果是宣告的需要原始Bean,則直接傳回

    if (beanName != null && this.targetSourcedBeans.contains(beanName)) {

        return bean;

    }

    // 如果不需要代理,則直接傳回

    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {

        return bean;

    }

    // 如果是Proxy的基礎元件如Advice、Pointcut、Advisor、AopInfrastructureBean則跳過

    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {

        this.advisedBeans.put(cacheKey, Boolean.FALSE);

        return bean;

    }

 

    // Create proxy if we have advice.

    // 根據相關條件,查詢interceptor,包括@Aspect生成的相關Interceptor。

    // 這裡是問題的關鍵點,Spring Boot 1.X中這裡傳回為空,而Spring Boot 2.X中,則不是空

    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);

    if (specificInterceptors != DO_NOT_PROXY) {

        // 傳回不是null,則需要代理

        this.advisedBeans.put(cacheKey, Boolean.TRUE);

        // 放入快取

        Object proxy = createProxy(

                bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));

        // 自動生成代理實體

        this.proxyTypes.put(cacheKey, proxy.getClass());

        return proxy;

    }

 

    this.advisedBeans.put(cacheKey, Boolean.FALSE);

    return bean;

}

除錯發現,Spring Boot 1.X中specificInterceptors傳回為空,而Spring Boot 2.X中則不是空,那麼這裡就是問題的核心點了,檢視原始碼:

protected Object[] getAdvicesAndAdvisorsForBean(Class > beanClass, String beanName, TargetSource targetSource) {

    List advisors = findEligibleAdvisors(beanClass, beanName);

    if (advisors.isEmpty()) {

        // 如果是空,則不代理

        return DO_NOT_PROXY;

    }

    return advisors.toArray();

}

protected List findEligibleAdvisors(Class > beanClass, String beanName) {

    // 找到當前BeanFactory中的Advisor

    List candidateAdvisors = findCandidateAdvisors();

    // 遍歷Advisor,根據Advisor中的PointCut判斷,傳回所有合適的Advisor

    List eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);

    // 擴充套件advisor串列,這裡會預設加入一個ExposeInvocationInterceptor用於暴露動態代理物件,之前文章有解釋過

    extendAdvisors(eligibleAdvisors);

    if (!eligibleAdvisors.isEmpty()) {

        // 根據@Order或者介面Ordered排序

        eligibleAdvisors = sortAdvisors(eligibleAdvisors);

    }

    return eligibleAdvisors;

}

protected List findAdvisorsThatCanApply(

        List candidateAdvisors, Class > beanClass, String beanName) {

    ProxyCreationContext.setCurrentProxiedBeanName(beanName);

    try {

        // 真正的查詢方法  

        return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);

    }

    finally {

        ProxyCreationContext.setCurrentProxiedBeanName(null);

    }

}

這裡的核心問題在於AopUtils.findAdvisorsThatCanApply方法,這裡的傳回在兩個版本是不一樣的,由於這裡程式碼過多就不貼上來了,說明下核心問題程式碼是這段:

// AopProxyUtils.java

public static List findAdvisorsThatCanApply(List candidateAdvisors, Class > clazz) {

    // … 省略

    for (Advisor candidate : candidateAdvisors) {

        if (canApply(candidate, clazz, hasIntroductions)) {

            eligibleAdvisors.add(candidate);

        }

    }

    // … 省略

}

public static boolean canApply(Advisor advisor, Class > targetClass, boolean hasIntroductions) {

    if (advisor instanceof IntroductionAdvisor) {

        return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);

    }

    else if (advisor instanceof PointcutAdvisor) {

        // 對於@Aspect的切麵,是這段程式碼在生效

        PointcutAdvisor pca = (PointcutAdvisor) advisor;

        return canApply(pca.getPointcut(), targetClass, hasIntroductions);

    }

    else {

        // It doesn’t have a pointcut so we assume it applies.

        return true;

    }

}

基本定位了問題點,看下最終呼叫的canApply方法,Spring Boot 1.X與2.X這裡的程式碼是不一樣的

Spring Boot 1.X中原始碼,即Spring AOP 4.X中原始碼

/**

 * targetClass是com.sun.proxy.$Proxy??即JDK動態代理生成的類

 * hasIntroductions是false,先不管

 */

public static boolean canApply(Pointcut pc, Class > targetClass, boolean hasIntroductions) {

    Assert.notNull(pc, “Pointcut must not be null”);

    // 先判斷class,這裡兩個版本都為true

    if (!pc.getClassFilter().matches(targetClass)) {

        return false;

    }

     

    MethodMatcher methodMatcher = pc.getMethodMatcher();

    // 如果method是固定true,即攔截所有method,則傳回true。這裡當然為false

    if (methodMatcher == MethodMatcher.TRUE) {

        // No need to iterate the methods if we’re matching any method anyway…

        return true;

    }

 

    // 特殊型別,做下轉換,Aspect生成的屬於這個型別

    IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;

    if (methodMatcher instanceof IntroductionAwareMethodMatcher) {

        introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;

    }

 

    // 取到標的class的所有介面

    Set> classes = new LinkedHashSet>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));

    // 再把標的calss加入遍歷串列

    classes.add(targetClass);

    for (Class > clazz : classes) {

        Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);

        // 遍歷每個類的每個方法,嘗試判斷是否match

        for (Method method : methods) {

            if ((introductionAwareMethodMatcher != null &&

                    introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||

                    methodMatcher.matches(method, targetClass)) {

                return true;

            }

        }

    }

 

    return false;

}

Spring Boot 2.X中原始碼,即Spring AOP 5.X中原始碼

public static boolean canApply(Pointcut pc, Class > targetClass, boolean hasIntroductions) {

    Assert.notNull(pc, “Pointcut must not be null”);

    if (!pc.getClassFilter().matches(targetClass)) {

        return false;

    }

 

    MethodMatcher methodMatcher = pc.getMethodMatcher();

    if (methodMatcher == MethodMatcher.TRUE) {

        // No need to iterate the methods if we’re matching any method anyway…

        return true;

    }

 

    IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;

    if (methodMatcher instanceof IntroductionAwareMethodMatcher) {

        introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;

    }

 

    Set> classes = new LinkedHashSet<>();

    // 這裡與1.X版本不同,使用Jdk動態代理Proxy,先判斷是否是Proxy,如果不是則加入使用者Class,即被動態代理的class,以便查詢真正的Class中是否符合判斷條件

    // 因為動態代理可能只把被代理類的方法實現了,被代理類的註解之類的沒有複製到生成的子類中,故要使用原始的類進行判斷

    // JDK動態代理一樣不會為動態代理生成類上加入介面的註解

    // 如果是JDK動態代理,不需要把動態代理生成的類方法遍歷串列中,因為實現的介面中真實的被代理介面。

    if (!Proxy.isProxyClass(targetClass)) {

        classes.add(ClassUtils.getUserClass(targetClass));

    }

    classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));

 

    for (Class > clazz : classes) {

        Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);

        for (Method method : methods) {

            // 比1.X版本少遍歷了Proxy生成的動態代理類,但是遍歷內容都包含了真實的介面,其實是相同的,為什麼結果不一樣呢?

            if ((introductionAwareMethodMatcher != null &&

                    introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||

                    methodMatcher.matches(method, targetClass)) {

                return true;

            }

        }

    }

 

    return false;

}

除錯資訊圖

上面的程式碼執行結果不同,但是區別隻是少個動態代理生成的類進行遍歷,為什麼少一個遍歷內容結果卻是true呢?肯定是introductionAwareMethodMatcher或者methodMatcher的邏輯有改動,其中methodMatcher和introductionAwareMethodMatcher是同一個物件,兩個方法邏輯相同。看程式碼:

/** AspectJExpressionPointcut.java

 * method是上面介面中遍歷的方法,targetClass是標的class,即生成的動態代理class

 */

public boolean matches(Method method, @Nullable Class > targetClass, boolean beanHasIntroductions) {

    obtainPointcutExpression();

    Method targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);

    ShadowMatch shadowMatch = getShadowMatch(targetMethod, method);

 

    // Special handling for this, target, @this, @target, @annotation

    // in Spring – we can optimize since we know we have exactly this class,

    // and there will never be matching subclass at runtime.

    if (shadowMatch.alwaysMatches()) {

        return true;

    }

    else if (shadowMatch.neverMatches()) {

        return false;

    }

    else {

        // the maybe case

        if (beanHasIntroductions) {

            return true;

        }

        // A match test returned maybe – if there are any subtype sensitive variables

        // involved in the test (this, target, at_this, at_target, at_annotation) then

        // we say this is not a match as in Spring there will never be a different

        // runtime subtype.

        RuntimeTestWalker walker = getRuntimeTestWalker(shadowMatch);

        return (!walker.testsSubtypeSensitiveVars() ||

                (targetClass != null && walker.testTargetInstanceOfResidue(targetClass)));

    }

}

這段程式碼在Spring Boot 1.X和2.X中基本是相同的,但是在AopUtils.getMostSpecificMethod(method, targetClass);這一句的執行結果上,兩者是不同的,1.X傳回的是動態代理生成的Class中重寫的介面中的方法,2.X傳回的是原始介面中的方法。

而在動態代理生成的Class中重寫的介面方法裡,是不會包含介面中的註解資訊的,所以Aspect中條件使用註解在這裡是拿不到匹配資訊的,所以傳回了false。

而在2.X中,因為傳回的是原始介面的方法,故可以成功匹配。

問題就在於AopUtils.getMostSpecificMethod(method, targetClass)的邏輯:

// 1.X

public static Method getMostSpecificMethod(Method method, Class > targetClass) {

    // 這裡傳回了targetClass上的重寫的method方法。

    Method resolvedMethod = ClassUtils.getMostSpecificMethod(method, targetClass);

    // If we are dealing with method with generic parameters, find the original method.

    return BridgeMethodResolver.findBridgedMethod(resolvedMethod);

}

 

// 2.X

public static Method getMostSpecificMethod(Method method, @Nullable Class > targetClass) {

    // 比1.X多了個邏輯判斷,如果是JDK的Proxy,則specificTargetClass為null,否則取被代理的Class。

    Class > specificTargetClass = (targetClass != null && !Proxy.isProxyClass(targetClass) ?

            ClassUtils.getUserClass(targetClass) : null);

    // 如果specificTargetClass為空,直接傳回原始method。

    // 如果不為空,傳回被代理的Class上的方法

    Method resolvedMethod = ClassUtils.getMostSpecificMethod(method, specificTargetClass);

    // If we are dealing with method with generic parameters, find the original method.

    // 獲取真實橋接的方法,泛型支援

    return BridgeMethodResolver.findBridgedMethod(resolvedMethod);

}

至此原因已經完全明瞭,Spring在AOP的5.X版本修複了這個問題。

影響範圍

原因已經查明,那麼根據原因我們推算一下影響範圍

  1. Bean是介面動態代理物件時,且該動態代理物件不是Spring體系生成的,介面中的切麵註解無法被攔截

  2. Bean是CGLIB動態代理物件時,該動態代理物件不是Spring體系生成的,原始類方法上的切麵註解無法被攔截。

  3. 可能也影響基於類名和方法名的攔截體系,因為生成的動態代理類路徑和類名是不同的。

如果是Spring體系生成的,之前拿到的都是真實類或者介面,只有在生成動態代理後,才是新的類。所以在建立動態代理時,獲取的是真實的類。

介面動態代理多見於ORM框架的Mapper、RPC框架的SPI等,所以在這兩種情況下使用註解要尤為小心。

有些同學比較關心@Cacheable註解,放在Mapper中是否生效。答案是生效,因為@Cacheable註解中使用的不是@Aspect的PointCut,而是CacheOperationSourcePointcut,其中雖然也使用了getMostSpecificMethod來獲取method,但是最終其實又從原始方法上嘗試獲取了註解:

// AbstractFallbackCacheOperationSource.computeCacheOperations

if (specificMethod != method) {

    //  Fallback is to look at the original method

    opDef = findCacheOperations(method);

    if (opDef != null) {

        return opDef;

    }

    // Last fallback is the class of the original method.

    opDef = findCacheOperations(method.getDeclaringClass());

    if (opDef != null && ClassUtils.isUserLevelMethod(method)) {

        return opDef;

    }

}

看似不受影響,其實是做了相容。

可以參考後面的內容,有提到Spring相關的issue

解決方案

如何解決這個問題呢?答案是在Spring Boot 1.X中沒有解決方案。。因為這個類太基礎了,除非切換版本。

使用其他Aspect運算式也可以解決此問題,使用註解方式在1.X版本是無解的。

運算式參考如下連結:

  1. Spring 之AOP AspectJ切入點語法詳解(最全面、最詳細。)

    https://blog.csdn.net/zhengchao1991/article/details/53391244

  2. Spring Aspect的Execution運算式

    https://blog.csdn.net/lang_niu/article/details/51559994

本來以為在註解Demo中加入@Inherited可解決的,結果發現不行,因為這個@Inherited只在類註解有效,在介面中或者方法上,都是不能被子類或者實現類繼承的,看這個@Inherited上面的註釋

/**

 * Indicates that an annotation type is automatically inherited.  If

 * an Inherited meta-annotation is present on an annotation type

 * declaration, and the user queries the annotation type on a class

 * declaration, and the class declaration has no annotation for this type,

 * then the class’s superclass will automatically be queried for the

 * annotation type.  This process will be repeated until an annotation for this

 * type is found, or the top of the class hierarchy (Object)

 * is reached.  If no superclass has an annotation for this type, then

 * the query will indicate that the class in question has no such annotation.

 *

 *

Note that this meta-annotation type has no effect if the annotated

 * type is used to annotate anything other than a class.  Note also

 * that this meta-annotation only causes annotations to be inherited

 * from superclasses; annotations on implemented interfaces have no

 * effect.

 * 上面這句話說明瞭只在父類上的註解可被繼承,介面上的都是無效的

 *

 * @author  Joshua Bloch

 * @since 1.5

 */

@Documented

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.ANNOTATION_TYPE)

public @interface Inherited {

}

擴充套件閱讀

問題及可能的影響範圍已經詳細分析完了,下麵我們好奇一下,這個核心問題類AopUtils.java的提交記錄中,作者有寫什麼嗎

AopUtils.java類GitHub頁面

https://github.com/spring-projects/spring-framework/blob/master/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java

檢視這個類的歷史記錄,註意Commits on Apr 3, 2018這個日期的提交,其中提到:

Consistent treatment of proxy classes and interfaces for introspection

 

Issue: SPR-16675

Issue: SPR-16677

針對proxy classes做了內省配置,相關issue是SPR-16677,我們看下這個issue。

Spring Framework/SPR-16677

https://jira.spring.io/browse/SPR-16677

這個issue詳細描述了這次提交的原因及目的。

讀者感興趣的話可以詳細的閱讀。

註意AopUtils.java的最新提交,又做了一些最佳化,可以研究一下。

擴充套件知識

上面的示例程式碼依賴於資料庫,現做一個模擬Mapper類的改進,可以直接無任何依賴的重現該問題:

已知Mybatis的Mapper介面是透過JDK動態代理生成的邏輯,而Mapper介面相關的Bean生成,是透過AutoConfiguredMapperScannerRegistrar自動註冊到BeanFactory中的,註冊進去的是MapperFactoryBean這個工廠Bean型別。

而MapperFactoryBean的getObject方法,則是透過getSqlSession().getMapper(this.mapperInterface)生成的,mapperInterfact是mapper介面。

底層是透過Configuration.getMapper生成的,再底層是mapperRegistry.getMapper方法,程式碼如下

public T getMapper(Class type, SqlSession sqlSession) {

    final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type);

    if (mapperProxyFactory == null) {

        throw new BindingException(“Type ” + type + ” is not known to the MapperRegistry.”);

    }

    try {

        // 呼叫下麵的方法生成代理實體

        return mapperProxyFactory.newInstance(sqlSession);

    } catch (Exception e) {

        throw new BindingException(“Error getting mapper instance. Cause: ” + e, e);

    }

}

public T newInstance(SqlSession sqlSession) {

    // 建立MapperProxy這個InvocationHandler實體

    final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);

    return newInstance(mapperProxy);

}

protected T newInstance(MapperProxy mapperProxy) {

    // 呼叫jdk動態代理生成實體,代理的InvocationHandler是MapperProxy

    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);

}

可以看到底層是透過JDK動態代理Proxy生成的,InvocationHandler是MapperProxy類。

清楚原理之後,我們對上面的實體做下改造,把Mybatis的取用簡化。

@Configuration

public class DemoConfiguraion {

     

    @Bean

    public FactoryBean getDemoMapper() {

        return new FactoryBean() {

            @Override

            public DemoMapper getObject() throws Exception {

                InvocationHandler invocationHandler = (proxy, method, args) -> {

                    System.out.println(“呼叫動態代理方法” + method.getName());

                    return Collections.singletonList(new HashMap());

                };

                return (DemoMapper) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] {DemoMapper.class}, invocationHandler);

            }

            @Override

            public Class > getObjectType() {

                return DemoMapper.class;

            }

            @Override

            public boolean isSingleton() {

                return true;

            }

        };

    }

}

上面的程式碼可達到與Mapper同樣的效果,大家可以本地隨便玩哈。

看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂