(點選上方公眾號,可快速關註)
來源:光閃 ,
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
return demoMapper.selectAll();
}
}
/**
* mapper類
*/
@Mapper
public interface DemoMapper {
@Select(“SELECT * FROM demo”)
@Demo
List
}
/**
* 切入的註解
*/
@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版本修複了這個問題。
影響範圍
原因已經查明,那麼根據原因我們推算一下影響範圍
-
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版本是無解的。
運算式參考如下連結:
-
Spring 之AOP AspectJ切入點語法詳解(最全面、最詳細。)
https://blog.csdn.net/zhengchao1991/article/details/53391244
-
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技能