@Async分析exposeProxy=true不生效原因

每篇一句

技術的發展總會實在掌聲中,伴隨着噓聲中前進。因此,需要有一顆擁抱變革的心態~

前言

本文標題包含有'靚麗'的字眼:Spring框架bug。相信有的小夥伴心裏小九九就會說了:又是一篇標題黨文章。
鑑於此,此處可以很負責任的對大夥說:本人所有文章絕不譁衆取寵,除了乾貨只剩乾貨。

相信關注過我的小夥伴都是知道的,我只遞送乾貨,絕不標題黨來浪費大家的時間和精力~那無異於謀財害命(說得嚴重了,不喜勿噴)
關於標題黨的好與壞、優與劣,此處我不置可否

本篇文章能讓你知道exposeProxy=true真實作用和實際作用範圍,從而能夠在開發中更精準的使用到它。

背景

本來一切本都那麼平靜,直到我用了@Async註解,好多問題都接踵而至(上篇文章已經解決大部分了)。在上篇文章中,爲了解決@Async同類方法調用問題我提出了兩個方向的解決方案:

  1. 自己注入自己,然後再調用接口方法(當然此處的一個變種是使用編程方式形如:AInterface a = applicationContext.getBean(AInterface.class);這樣子手動獲取也是可行的~~~本文不討論這種比較直接簡單的方式)
  2. 使用AopContext.currentProxy();方式

方案一上篇文章已花筆墨重點分析,畢竟方案一我認爲更爲重要些。本文分析使用方案二的方式,它涉及到AOP、代理對象的暴露,因此我認爲本文的內容對你平時開發的影響是不容小覷,可以重點瀏覽咯~

我相信絕大多數小夥伴都遇到過這個異常:

 java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.
	at org.springframework.aop.framework.AopContext.currentProxy(AopContext.java:69)
	at com.fsx.dependency.B.funTemp(B.java:14)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:206)
	at com.sun.proxy.$Proxy44.funTemp(Unknown Source)
	...

    然後當你去靠度娘搜索解決方案時,發現無一例外都教你只需要這麼做就成:

    @EnableAspectJAutoProxy(exposeProxy = true)
    

      本文我想說的可能又是一個技術敏感性問題,其實絕大多數情況下你按照這麼做是可行的,直到你遇到了@Async也需要調用本類方法的時候,你就有點絕望了,然後本文或許會成爲了你的救星~

      本以爲加了exposeProxy = true就能順風順水了,但它卻出問題了:依舊報如上的異常信息。如果你看到這裏也覺得不可思議,那麼本文就更能體現它的價值所在~

      此問題我個人把它歸類爲Spring的bug我覺得是無可厚非的,因爲它的語義與實際表現出來的結果想悖了,so我把定義爲Spring框架的bug
      對使用者來說,標註了exposeProxy = true,理論上就應該能夠通過AopContext.currentProxy()拿到代理對象,可惜Spring這裏卻掉鏈子了,有點名不副實之感~

      示例

      本文將以多個示例來模擬不同的使用case,首先從直觀的結果上先了解@EnableAspectJAutoProxy(exposeProxy = true)的作用以及它存在的問題。

      備註:下面所有示例都建立在@EnableAspectJAutoProxy(exposeProxy = true)已經開啓的前提下,形如:

      @Configuration
      @EnableAspectJAutoProxy(exposeProxy = true) // 暴露當前代理對象到當前線程綁定
      public class RootConfig {
      }
      

        示例一

        @Service
        public class B implements BInterface {
        
            @Transactional
            @Override
            public void funTemp() {
                ...
        
                // 希望調用本類方法  但是它拋出異常,希望也能夠回滾事務
                BInterface b = BInterface.class.cast(AopContext.currentProxy());
                System.out.println(b);
                b.funB();
            }
        
            @Override
            public void funB() {
                // ... 處理業務屬於  
                System.out.println(1 / 0);
            }
        }
        

        結論:能正常work,事務也會生效~

        示例二

        同類內方法調用,希望異步執行被調用的方法(希望@Async生效)

        @Service
        public class B implements BInterface {
            @Override
            public void funTemp() {
                System.out.println("線程名稱:" + Thread.currentThread().getName());
                // 希望調用本類方法  但是希望它去異步執行~
                BInterface b = BInterface.class.cast(AopContext.currentProxy());
                System.out.println(b);
                b.funB();
            }
            @Async
            @Override
            public void funB() {
                System.out.println("線程名稱:" + Thread.currentThread().getName());
            }
        }
        

        結論:執行即報錯

        java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.
        

          示例三

          同類內方法調用,希望異步執行被調用的方法,並且在入口方法處使用事務

          @Service
          public class B implements BInterface {
              @Transactional
              @Override
              public void funTemp() {
                  System.out.println("線程名稱:" + Thread.currentThread().getName());
                  // 希望調用本類方法  但是希望它去異步執行~
                  BInterface b = BInterface.class.cast(AopContext.currentProxy());
                  System.out.println(b);
                  b.funB();
              }
              @Async
              @Override
              public void funB() {
                  System.out.println("線程名稱:" + Thread.currentThread().getName());
              }
          }
          

          結論:正常work沒有報錯,@Async異步生效、事務也生效

          示例四

          示例三的唯一區別是把事務註解@Transactional標註在被調用的方法處(和@Async同方法):

          @Service
          public class B implements BInterface {
              @Override
              public void funTemp() {
                  System.out.println("線程名稱:" + Thread.currentThread().getName());
                  // 希望調用本類方法  但是希望它去異步執行~
                  BInterface b = BInterface.class.cast(AopContext.currentProxy());
                  System.out.println(b);
                  b.funB();
              }
              @Transactional
              @Async
              @Override
              public void funB() {
                  System.out.println("線程名稱:" + Thread.currentThread().getName());
              }
          }
          

          結論:同示例三

          示例五

          @Async標註在入口方法上:

          @Service
          public class B implements BInterface {
              @Transactional
              @Async
              @Override
              public void funTemp() {
                  System.out.println("線程名稱:" + Thread.currentThread().getName());
                  BInterface b = BInterface.class.cast(AopContext.currentProxy());
                  System.out.println(b);
                  b.funB();
              }
              @Override
              public void funB() {
                  System.out.println("線程名稱:" + Thread.currentThread().getName());
              }
          }
          

          結論:請求即報錯

          java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.
          	at org.springframework.aop.framework.AopContext.currentProxy(AopContext.java:69)
          

          示例六

          偷懶做法:直接在實現類裏寫個方法(public/private)然後註解上@Async

          我發現我司同事有大量這樣的寫法,所以專門拿出作爲示例,以儆效尤~

          @Service
          public class B implements BInterface {
          	...
              @Async
              public void fun2(){
                  System.out.println("線程名稱:" + Thread.currentThread().getName());
              }
          }
          

          結論:因爲方法不在接口上,因此肯定無法通過獲取代理對象調用它

          需要注意的是:即使該方法不屬於接口方法,但是標註了@Async所以最終生成的還是B的代理對象~(哪怕是private訪問權限也是代理對象)

          可能有的小夥伴會想通過context.getBean()獲取到具體實現類再調用方法行不行。咋一想可行,實際則不是不行的。
          這裏再次強調一次,若你是AOP是JDK的動態代理的實現,這樣100%報錯的:

          BInterface bInterface = applicationContext.getBean(BInterface.class); // 正常獲取到容器裏的代理對象
          applicationContext.getBean(B.class); //報錯  NoSuchBeanDefinitionException
          // 原因此處不再解釋了,若是CGLIB代理,兩種獲取方式均可~
          

          備註:雖說CGLIB代理方式用實現類方式可以獲取到代理的Bean,但是強烈不建議依賴於代理的具體實現而書寫代碼,這樣移植性會非常差的,而且接手的人肯定也會一臉懵逼、二臉懵逼…

          因此當你看到你同事就在本類寫個方法標註上@Async然後調用,請制止他吧,做的無用功~~~(關鍵自己還以爲有用,這是最可怕的深坑~

          原因大剖析

          找錯的常用方法:逆推法。
          首先我們找到報錯的最直接原因:AopContext.currentProxy()這句代碼報錯的,因此有必要看看AopContext這個工具類

          // @since 13.03.2003
          public final class AopContext {
          	private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy");
          	private AopContext() {
          	}
          	// 該方法是public static方法,說明可以被任意類進行調用
          	public static Object currentProxy() throws IllegalStateException {
          		Object proxy = currentProxy.get();
          		// 它拋出異常的原因是當前線程並沒有綁定對象
          		// 而給線程版定對象的方法在下面:特別有意思的是它的訪問權限是default級別,也就是說只能Spring內部去調用~
          		if (proxy == null) {
          			throw new IllegalStateException("Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.");
          		}
          		return proxy;
          	}
          	// 它最有意思的地方是它的訪問權限是default的,表示只能給Spring內部去調用~
          	// 調用它的類有CglibAopProxy和JdkDynamicAopProxy
          	@Nullable
          	static Object setCurrentProxy(@Nullable Object proxy) {
          		Object old = currentProxy.get();
          		if (proxy != null) {
          			currentProxy.set(proxy);
          		} else {
          			currentProxy.remove();
          		}
          		return old;
          	}
          }
          

          從此工具源碼可知,決定是否拋出所示異常的直接原因就是請求的時候setCurrentProxy()方法是否被調用過。通過尋找發現只有兩個類會調用此方法,並且都是Spring內建的類且都是代理類的處理類CglibAopProxyJdkDynamicAopProxy

          說明:本文所有示例,都基於接口的代理,所以此處只以JdkDynamicAopProxy作爲代表進行說明即可

          我們知道在執行代理對象的目標方法的時候,都會交給InvocationHandler處理,因此做事情的在invoke()方法裏:

          final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
          	...
          	@Override
          	@Nullable
          	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          		...
          			if (this.advised.exposeProxy) {
          				// Make invocation available if necessary.
          				oldProxy = AopContext.setCurrentProxy(proxy);
          				setProxyContext = true;
          			}
          		...
          		finally {
          			if (setProxyContext) {
          				// Restore old proxy.
          				AopContext.setCurrentProxy(oldProxy);
          			}
          		}
          	}
          }
          

          so,最終決定是否會調用set方法是由this.advised.exposeProxy這個值決定的,因此下面我們只需要關心ProxyConfig.exposeProxy這個屬性值什麼時候被賦值爲true的就可以了。

          ProxyConfig.exposeProxy這個屬性的默認值是false。其實最終調用設置值的是同名方法Advised.setExposeProxy()方法,而且是通過反射調用的

          @EnableAspectJAutoProxy(exposeProxy = true)的作用

          此註解它導入了AspectJAutoProxyRegistrar,最終設置此註解的兩個屬性的方法爲:

          public abstract class AopConfigUtils {
          	public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) {
          		if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
          			BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
          			definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE);
          		}
          	}
          	public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) {
          		if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
          			BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
          			definition.getPropertyValues().add("exposeProxy", Boolean.TRUE);
          		}
          	}
          }
          

          看到此註解標註的屬性值最終都被設置到了internalAutoProxyCreator身上,也就是進而重要的一道菜:自動代理創建器。

          在此各位小夥伴需要先明晰的是:@Async的代理對象並不是由自動代理創建器來創建的,而是由AsyncAnnotationBeanPostProcessor一個單純的BeanPostProcessor實現的。

          示例結論分析

          本章節在掌握了一定的理論的基礎上,針對上面的各種示例進行結論性分析

          示例一分析

          本示例目的是事務,可以參考開啓事務的註解@EnableTransactionManagement。該註解向容器注入的是自動代理創建器InfrastructureAdvisorAutoProxyCreator,所以exposeProxy = true對它的代理對象都是生效的,因此可以正常work~

          備註:@EnableCaching注入的也是自動代理創建器~so exposeProxy = true對它也是有效的

          示例二分析

          很顯然本例是執行AopContext.currentProxy()這句代碼的時候報錯了。報錯的原因相信我此處不說,小夥伴應該個大概了。

          @EnableAsync給容器注入的是AsyncAnnotationBeanPostProcessor,它用於給@Async生成代理,但是它僅僅是個BeanPostProcessor並不屬於自動代理創建器,因此exposeProxy = true對它無效。
          所以AopContext.setCurrentProxy(proxy);這個set方法肯定就不會執行,so但凡只要業務方法中調用AopContext.currentProxy()方法就鐵定拋異常~~

          示例三分析

          這個示例的結論,相信是很多小夥伴都沒有想到的。僅僅只是加入了事務,@Asycn竟然就能夠完美的使用AopContext.currentProxy()獲取當前代理對象了。

          爲了便於理解,我分步驟講述如下,不出意外你肯定就懂了:

          1. AsyncAnnotationBeanPostProcessor在創建代理時有這樣一個邏輯:若已經是Advised對象了,那就只需要把@Async的增強器添加進去即可。若不是代理對象纔會自己去創建
          public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSupport implements BeanPostProcessor {
          	@Override
          	public Object postProcessAfterInitialization(Object bean, String beanName) {
          		if (bean instanceof Advised) {
          			advised.addAdvisor(this.advisor);
          			return bean;
          		}
          		// 上面沒有return,這裏會繼續判斷自己去創建代理~
          	}
          }
          
          1. 自動代理創建器AbstractAutoProxyCreator它實際也是個BeanPostProcessor,所以它和上面處理器的執行順序很重要~~~
          2. 兩者都繼承自ProxyProcessorSupport所以都能創建代理,且實現了Ordered接口
            1. AsyncAnnotationBeanPostProcessor默認的order值爲Ordered.LOWEST_PRECEDENCE。但可以通過@EnableAsync指定order屬性來改變此值。 執行代碼語句:bpp.setOrder(this.enableAsync.<Integer>getNumber("order"));
            2. AbstractAutoProxyCreator默認值也同上。但是在把自動代理創建器添加進容器的時候有這麼一句代碼:beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); 自動代理創建器這個處理器是最高優先級
          3. 由上可知因爲標註有@Transactional,所以自動代理會生效,因此它會先交給AbstractAutoProxyCreator把代理對象生成好了,再交給後面的處理器執行
          4. 由於AbstractAutoProxyCreator先執行,所以AsyncAnnotationBeanPostProcessor執行的時候此時Bean已經是代理對象了,由步驟1可知,此時它會沿用這個代理,只需要把切面添加進去即可~

          從上面步驟可知,加上了事務註解,最終代理對象是由自動代理創建器創建的,因此exposeProxy = true對它有效,這是解釋它能正常work的最爲根本的原因。

          示例四分析

          同上。

          @Transactional只爲了創建代理對象而已,所在放在哪兒對@Async的作用都不會有本質的區別

          示例五分析

          此示例非常非常有意思,因此我特意拿出來講解一下。

          咋一看其實以爲是沒有問題的,畢竟正常我們會這麼思考:執行funTemp()方法會啓動異步線程執行,同時它會把Proxy綁定在當前線程中,所以即使是新起的異步線程也有能夠使用AopContext.currentProxy()纔對。

          但有意思的地方就在此處:它報錯了,正所謂你以爲的不一定就是你以爲的。
          解釋:根本原因就是關鍵節點的執行時機問題。在執行代理對象funTemp方法的時候,綁定動作oldProxy = AopContext.setCurrentProxy(proxy);在前,目標方法執行(包括增強器的執行)invocation.proceed()在後。so其實在執行綁定的還是在主線程裏而並非是新的異步線程,所以在你在方法體內(已經屬於異步線程了)執行AopContext.currentProxy()那可不就報錯了嘛~

          示例六分析

          略。(上已分析)

          解決方案

          對上面現象原因可以做一句話的總結:@Async要想順利使用AopContext.currentProxy()獲取當前代理對象來調用本類方法,需要確保你本Bean已經被自動代理創建器提前代理

          在實際業務開發中:只要的類標註有@Transactional或者@Caching等註解,就可以放心大膽的使用吧

          知曉了原因,解決方案從來都是信手拈來的事。
          不過如果按照如上所說需要隱式依賴這種方案我非常的不看好,總感覺不踏實,也總感覺報錯遲早要來。(比如某個同學該方法不要事務了/不要緩存了,把對應註解摘掉就瞬間報錯了,到時候你可能哭都沒地哭訴去~)

          備註:墨菲定律在開發過程中從來都沒有不好使過~~~程序員兄弟姐妹們應該深有感觸吧

          下面根據我個人經驗,介紹一種解決方案中的最佳實踐:

          遵循的最基本的原則是:顯示的指定比隱式的依賴來得更加的靠譜、穩定

          @Component
          public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
              @Override
              public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
                  BeanDefinition beanDefinition = beanFactory.getBeanDefinition(TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME);
                  beanDefinition.getPropertyValues().add("exposeProxy", true);
              }
          }
          

          這樣我們可以在@AsyncAopContext.currentProxy()就自如使用了,不再對別的啥的有依賴性~

          其實我認爲最佳的解決方案是如下兩個(都需要Spring框架做出修改):
          1、@Async的代理也交給自動代理創建器來完成
          2、@EnableAsync增加exposeProxy屬性,默認值給false即可(此種方案的原理同我示例的最佳實踐~

          總結

          通過6組不同的示例,演示了不同場景使用@Async,並且對結論進行解釋,不出意外,小夥伴們讀完之後都能夠掌握它的來龍去脈了吧。

          最後再總結兩點,小夥伴們使用的時候稍微注意下就行:

          1. 請不要在異步線程裏使用AopContext.currentProxy()
          2. AopContext.currentProxy()不能使用在非代理對象所在方法體內
          發表評論
          所有評論
          還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
          相關文章