前言
在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<Map<String, Object>> selectAll() { return demoMapper.selectAll(); } } /** * mapper類 */ @Mapper public interface DemoMapper { @Select("SELECT * FROM demo") @Demo List<Map<String, Object>> 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<Advisor> advisors = findEligibleAdvisors(beanClass, beanName); if (advisors.isEmpty()) { // 如果是空,則不代理 return DO_NOT_PROXY; } return advisors.toArray(); } protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) { // 找到當前BeanFactory中的Advisor List<Advisor> candidateAdvisors = findCandidateAdvisors(); // 遍歷Advisor,根據Advisor中的PointCut判斷,返回所有合適的Advisor List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); // 擴展advisor列表,這里會默認加入一個ExposeInvocationInterceptor用於暴露動態代理對象,之前文章有解釋過 extendAdvisors(eligibleAdvisors); if (!eligibleAdvisors.isEmpty()) { // 根據@Order或者接口Ordered排序 eligibleAdvisors = sortAdvisors(eligibleAdvisors); } return eligibleAdvisors; } protected List<Advisor> findAdvisorsThatCanApply( List<Advisor> 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<Advisor> findAdvisorsThatCanApply(List<Advisor> 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; } }
1.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<Class<?>> classes = new LinkedHashSet<Class<?>>(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; }
2.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<Class<?>> 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版本修復了這個問題。
影響范圍
原因已經查明,那么根據原因我們推算一下影響范圍
-
Bean是接口動態代理對象時,且該動態代理對象不是Spring體系生成的,接口中的切面注解無法被攔截
-
Bean是CGLIB動態代理對象時,該動態代理對象不是Spring體系生成的,原始類方法上的切面注解無法被攔截。
-
可能也影響基於類名和方法名的攔截體系,因為生成的動態代理類路徑和類名是不同的。
如果是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版本是無解的。
表達式參考如下鏈接:
https://blog.csdn.net/zhengchao1991/article/details/53391244
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. * * <p>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頁面
查看這個類的歷史記錄,注意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。
這個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> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) 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<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } protected T newInstance(MapperProxy<T> 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<DemoMapper> getDemoMapper() { return new FactoryBean<DemoMapper>() { @Override public DemoMapper getObject() throws Exception { InvocationHandler invocationHandler = (proxy, method, args) -> { System.out.println("調用動態代理方法" + method.getName()); return Collections.singletonList(new HashMap<String, Object>()); }; 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; } }; } }
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<Class<?>> 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;
}