阿里四面:你知道Spring AOP建立Proxy的過程嗎?

2021-09-22 08:00:01

因為Spring在執行期把切面中的程式碼邏輯動態「織入」到了容器物件方法內,讓開發者能無感知地在容器物件方法前後任意新增程式碼片段,所以AOP其實就是個代理模式。凡是代理,由於程式碼不可直接閱讀,是 bug 的重災區。

案例

某遊戲系統,含負責點券充值的類CouponService,它含有一個充值方法deposit():

deposit()會使用微信支付充值。因此在這個方法中,加入pay()。

由於微信支付是第三方介面,需記錄介面呼叫時間。
引入 @Around 增強 ,分別記錄在pay()方法執行前後的時間,並計算pay()執行耗時。

Controller:

存取介面,會發現這段計算時間的切面並沒有執行到,輸出紀錄檔如下:

切面類明明定義了切面對應方法,但卻沒執行到。說明在類的內部,通過this呼叫的方法,不會被AOP增強。

解析

  • this對應的物件就是一個普通CouponService物件:
  • 而在Controller層中自動裝配的CouponService物件:

    是個被Spring增強過的Bean,所以執行deposit()時,會執行記錄介面呼叫時間的增強操作。而this對應的物件只是一個普通的物件,並無任何額外增強。

為什麼this參照的物件只是一個普通物件?
要從Spring AOP增強物件的過程來看。

實現

AOP的底層是動態代理,建立代理的方式有兩種:

  • JDK方式
    只能對實現了介面的類生成代理,不能針對普通類
  • CGLIB方式
    可以針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法,來實現代理物件。

針對非Spring Boot程式,除了新增相關AOP依賴項外,還會使用 @EnableAspectJAutoProxy 開啟AOP功能。
這個註解類引入AspectJAutoProxyRegistrar,它通過實現ImportBeanDefinitionRegistrar介面完成AOP相關Bean準備工作。

現在來看下建立代理物件的過程。先來看下呼叫棧:

  • 建立代理物件的時機
    建立一個Bean時

建立的的關鍵工作由AnnotationAwareAspectJAutoProxyCreator完成

AnnotationAwareAspectJAutoProxyCreator

一種BeanPostProcessor。所以它的執行是在完成原始Bean構建後的初始化Bean(initializeBean)過程中

AbstractAutoProxyCreator#postProcessAfterInitialization
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
   if (bean != null) {
      Object cacheKey = getCacheKey(bean.getClass(), beanName);
      if (this.earlyProxyReferences.remove(cacheKey) != bean) {
         return wrapIfNecessary(bean, beanName, cacheKey);
      }
   }
   return bean;
}

關鍵方法wrapIfNecessary:在需要使用AOP時,它會把建立的原始Bean物件wrap成代理物件,作為Bean返回。

AbstractAutoProxyCreator#wrapIfNecessary

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
   // 省略非關鍵程式碼
   Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
   if (specificInterceptors != DO_NOT_PROXY) {
      this.advisedBeans.put(cacheKey, Boolean.TRUE);
      Object proxy = createProxy(
            bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
      this.proxyTypes.put(cacheKey, proxy.getClass());
      return proxy;
   }
   // 省略非關鍵程式碼 
}

createProxy

建立代理物件的關鍵:

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
      @Nullable Object[] specificInterceptors, TargetSource targetSource) {
  // ...
  // 1. 建立一個代理工廠
  ProxyFactory proxyFactory = new ProxyFactory();
  if (!proxyFactory.isProxyTargetClass()) {
   if (shouldProxyTargetClass(beanClass, beanName)) {
      proxyFactory.setProxyTargetClass(true);
   }
   else {
      evaluateProxyInterfaces(beanClass, proxyFactory);
   }
  }
  Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
  // 2. 將通知器(advisors)、被代理物件等資訊加入到代理工廠
  proxyFactory.addAdvisors(advisors);
  proxyFactory.setTargetSource(targetSource);
  customizeProxyFactory(proxyFactory);
   // ...
   // 3. 通過代理工廠獲取代理物件
  return proxyFactory.getProxy(getProxyClassLoader());
}

經過這樣一個過程,一個代理物件就被建立出來了。我們從Spring中獲取到的物件都是這個代理物件,所以具有AOP功能。而之前直接使用this參照到的只是一個普通物件,自然也就沒辦法實現AOP的功能了。

修正

只有參照的是被動態代理建立出來的物件,才會被Spring增強,具備AOP該有的功能。
什麼樣的物件具備這樣條件?

被@Autowired註解

通過 @Autowired,在類的內部,自己參照自己:

直接從AopContext獲取當前Proxy

AopContext,就是通過一個ThreadLocal來將Proxy和執行緒繫結起來,這樣就可以隨時拿出當前執行緒繫結的Proxy。

使用該方案有個前提,需要在 @EnableAspectJAutoProxy 加設定項 exposeProxy = true ,表示將代理物件放入到ThreadLocal,這才可以直接通過

AopContext.currentProxy()

獲取到,否則報錯:

於是修改程式碼:

勿忘修改EnableAspectJAutoProxyexposeProxy屬性: