Spring/Boot/Cloud系列知識(3)— — 代理模式(中)

本文轉自:https://blog.csdn.net/yinwenjie/article/details/78008276

(接上文《Spring/Boot/Cloud系列知識(2)——代理模式》)

2.2、代理模式在Spring中的應用

那麼java中基於java.lang.reflect.Proxy的動態代理模式和Spring生態有什麼關係呢?Spring中的所有Bean實例存儲在一個名叫IOC容器(Inversion of Control 控制反轉容器)中。這個容器中存在着接口和實現類的對應關係(也可以直接存儲類的實例,無需這個類有任何接口的實現),而其中Bean實例的存儲方式都默認採用單例保存。

一般情況下我們可以通過BeanFactory(Spring IOC容器工廠)接口中getBean()方法,直接獲取到這個接口在IOC容器中對應的實例。但當我們需要爲這個Bean實例附加AOP切面操作時,這個實例就會被代理——視這個實例實現接口的情況和Spring的配置情況,又可以區別爲使用Proxy動態代理還是使用Cglib代理。如下圖所示:

  • 通常情況下當我們使用IOC容器中的Bean實例時,IOC容器將向使用者返回這個接口的具體實現,並不會代理這個Bean實例。如下圖所示:

  • 當我們爲這個Bean設定了AOP切面功能後,這個IOC容器中的Bean實例就會被代理。以下設定了一個AOP切面攔截(AOP切面的詳細講解在後續文章中進行,這裏我們只看被代理的結果就好):
// 創建一個AOP切面攔截器
@Aspect
@Component
public class MyInterceptor {
  /** 
   * 攔截yinwenjie.test.proxy.service包下面的所有類中的所有方法
   * 後續文章還會詳細講解Spring EL和AOP攔截規則 
   */    
  @Pointcut("execution(* yinwenjie.test.proxy.service..*.*(..))")  
  public void executeService() {
    System.out.println("攔截器作用!");
  }
  // 前置事件
  @Before("executeService()")  
  public void doBeforeAdvice(JoinPoint joinPoint){
    System.out.println("前置事件!");
  }
}
  • 接着我們重新執行單元測試(重新運行代碼),就會發現“MyService”接口的實現——MyServiceImpl被代理了。代理者是Spring基礎設施中aop模塊的JdkDynamicAopProxy類。

org.springframework.aop.framework.JdkDynamicAopProxy類非常重要,它實現了Java對動態代理模式的支持,既實現了java.lang.reflect.InvocationHandler接口(當然不止是實現了這個接口)。注意:需要設定您的Spring主配置文件(這裏是Spring Boot)中spring.aop.proxy-target-class=false,否則就算您依賴注入的Bean存在接口,也會使用Cglib代理。

3. Cglib代理(動態)

要使用java對動態代理的原生支持,就需要被代理的類至少實現了一個接口。但如果某個需要被代理的類沒有實現任何接口,又怎麼辦呢?這時一個比較好的選擇就是使用Cglib代理,Cglib代理並不是java的原生代理模式,而是由第三方提供的一種代理方式。Cglib代理組件是Mockito Framework的一部分,其內部封裝了一個java字節碼生成框架ASM:

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or dynamically generate classes, directly in binary form. Provided common transformations and analysis algorithms allow to easily assemble custom complex transformations and code analysis tools.

以上引用自OW2組織對ASM的描述(http://asm.ow2.org)。簡單來說就是ASM字節碼框架可在運行時動態生成Class字節碼內容,或者擴展已有的某個Class。實際上Spring生態中默認使用Cglib動態代理作爲主要的動態代理方式——不是java原生的java.lang.reflect.Proxy動態代理。無論被代理的Bean實例是否實現了任何接口,Cglib都能更好勝任代理工作任務。

3.1 Cglib組件基本使用

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.10.19</version>
</dependency>

在本小節的示例講解中,我們需要在工程的maven文件中導入Cglib組件的引用(1.10.19版本)。接下來我們可以使用一個示例,來看看Cglib組件的基本使用。Cglib組件中的基本角色包括:

  • 代理器:代理器實現了org.mockito.cglib.proxy.MethodInterceptor接口,當代理者被外部使用者調用時,就會觸發其中的intercept方法。在本示例中,我們創建了兩個代理器DefaultProxy和CglibProxy:
  • DefaultProxy代理器
    // 默認的代理器,什麼都不執行
    public class DefaultProxy implements MethodInterceptor {
      @Override
      public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // 這裏不做任何處理(也可以直接使用proxy執行父級代碼)
        return null;
      }
    }

     

  • CglibProxy代理器
/**
 * 這是一個基於Cglib的代理器
 * 它實現了org.mockito.cglib.proxy.MethodInterceptor接口
 * @author yinwenjie
 */
public class CglibProxy implements MethodInterceptor {
  // 日誌
  private static final Logger LOG = LoggerFactory.getLogger(CglibProxy.class);
  /**
   * 該方法在代理者被調用時觸發
   * @param obj 這是代理者,按照本示例就是使用Enhancer生成的子級類
   * @param method 被調用的方法
   * @param args 調用方法時可能傳入的參數
   * @param proxy cglib內部的方法代理,可以用它來執行真正的業務過程
   */
  @Override
  public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    LOG.info("通過debug觀察Enhancer生成的子級類,是否有似曾相識的感覺?");
    LOG.info("被調用的方法:" + method.getName());
    LOG.info("可以在正式調用前執行一些額外的過程");
    // 這裏返回真正的業務調用過程
    Object result = null;
    try {
      result = proxy.invokeSuper(obj, args);
    } catch (Exception e) {
      LOG.error(e.getMessage() , e);
      LOG.info("在調用執行異常後,執行一些額外的過程");
      return null;
    }
    LOG.info("在調用執行成功後,執行一些額外的過程");
    return result;
  }
}
  • 被代理者:被代理者就是負責真正業務過程處理的類。本示例中我們創建了一個TargetClass類,作爲真正業務的處理者
/**
 * 這是要被代理的業務處理類<br>
 * 這個類既沒有任何父類,也沒有實現任何接口
 * @author yinwenjie
 */
public class TargetClass {
  // 日誌
  private static final Logger LOG = LoggerFactory.getLogger(TargetClass.class);
  public void handleSomething(String param) {
    LOG.info("傳入了一個參數:" + param + ",做了一些業務處理");
  }
}
  • 字節碼增強操作器Enhancer

Enhancer是Cglib中用於動態生成java字節碼的最重要工具,其內部是對ASM框架的封裝。以下示例是Enhancer的基本使用:

public class Run {
  static {
    BasicConfigurator.configure();
  }
  public static void main(String[] args) {
    /*
     * Cglib的核心思路,是依靠一個java字節碼操作框架ASM
     * 在代碼運行時,爲被代理的Class生成擴展代碼(子級代碼)
     * 然後通過運行子級代碼,實現代理
     * 
     * Enhancer是其中用來進行代碼生成的主要類,請看如下運行示例
     * */

    Enhancer enhancer = new Enhancer();
    // 基於被代理的TargetClass,爲它創建一個子類進行代理執行
    enhancer.setSuperclass(TargetClass.class);
    // 設置代理器,這裏設置了兩個代理器DefaultProxy和CglibProxy(這裏也可以傳null)
    enhancer.setCallbacks(new Callback[]{new DefaultProxy() , new CglibProxy()});
    // 設置過濾器(很多資料都寫錯了用法,請參見本人MethodFilter中的註釋)
    enhancer.setCallbackFilter(new MethodFilter());

    // 在運行時動態創建代理類(TargetClass類的子類)
    TargetClass proxy = (TargetClass)enhancer.create();

    // 以下方法將被DefaultProxy代理
    proxy.toString();
    // 以下方法將被CglibProxy代理
    proxy.handleSomething("做一些對社會有益的事情!");
  }
  ……
}
  • 其它的角色——代理過濾器 
    代理過濾器CallbackFilter的主要工作,是根據當前外部使用者所調用的代理者的方法,來決定使用哪一個代理器:
public class Run {
  ……
  static class MethodFilter implements CallbackFilter {
    @Override
    public int accept(Method method) {
      /*
       * 在main方法中,我們一共設置了兩個代理器,DefaultProxy和CglibProxy。
       * 當被執行的方法是Object類中的,類似clone、toString、hashCode這樣的方法,則使用DefaultProxy進行代理
       * 其他的方法,就是我們真正需要被代理的業務方法了,使用CglibProxy這個代理器進行代理
       * 
       * 這裏返回的結果,就是我們在main方法中使用enhancer.setCallbacks設定的代理器順序
       * 返回0,表示使用DefaultProxy代理器;返回1,表示使用CglibProxy代理器
       * 
       * 代碼就不解釋了,很容易看懂。
       * 大家還可以參見org.mockito.cglib.proxy.Proxy.getProxyClass的源代碼,後文中我們還會提到這個Proxy類
       */
      if (method.getDeclaringClass().getName().equals("java.lang.Object")) {
        return 0;
      }
      return 1;
    }
  }
}
  • 下圖展示了debug過程中,觀察到的obj對象的結構效果。是否有似曾相似的感覺?

以下是示例代碼的執行結果:

09:27:36.939 [main] INFO yinwenjie.test.proxy.cglib.CglibProxy - 通過debug觀察Enhancer生成的子級類,是否有似曾相識的感覺?
09:27:36.964 [main] INFO yinwenjie.test.proxy.cglib.CglibProxy - 被調用的方法:handleSomething
09:27:36.964 [main] INFO yinwenjie.test.proxy.cglib.CglibProxy - 在正式調用前執行一些額外的過程
09:27:36.998 [main] INFO yinwenjie.test.proxy.cglib.target.TargetClass - 傳入了一個參數:做一些對社會有益的事情!,做了一些業務處理
09:27:36.998 [main] INFO yinwenjie.test.proxy.cglib.CglibProxy - 在調用執行成功後,執行一些額外的過程

3.2 Cglib替代Java原生動態代理

Cglib組件中有類似java原生動態代理的實現方式,可以完整替代java中原生動態代理的支持。這使得Spring默認使用Cglib動態代理作爲主要的動態代理方式有了基礎支撐。本小節我們來看看Cglib如何提供類似java原生動態代理的支持(我們使用的Cglib版本還是1.10.19)。

  • 以下是接口和接口的實現類,也是我們需要進行進行代理的業務處理類。這裏的代碼示例已經在之前的文章中出現過,這裏就不再贅述了:
// 被代理的接口
public interface TargetOneInterface {
  // 這是一個方法
  public void doSomething();
  // 這是第二個方法
  public void handleSomething();
}

......

// 接口實現,也是真正的業務處理類
public class TargetOneImpl implements TargetOneInterface {
  // 日誌
  private static final Logger LOG = LoggerFactory.getLogger(TargetOneImpl.class);
  @Override
  public void doSomething() {
    LOG.info("doSomething 方法被執行");
  }
  @Override
  public void handleSomething() {
    LOG.info("handleSomething 方法被執行");
  }
}
  • 以下我們基於Cglib提供的InvocationHandler接口,實現的代理器。注意,在Java的原生動態代理支持中,也有這個名稱相同的接口——包名不一樣:
......
import org.mockito.cglib.proxy.InvocationHandler;
......

// 代理者處理器
public class ProxyInvocationHandler implements InvocationHandler {
  /**
   * 模擬一個簡單的IOC容器<br>
   * 當然實際情況沒有這麼簡單,IOC內部有一個組織結構的
   */
  private static Map<Class<?>, Object> simulatorContainer = new HashMap<>();

  static {
    // 這是TargetOneInterface的實現
    simulatorContainer.put(TargetOneInterface.class, new TargetOneImpl());
  }

  /**
   * @param proxy 代理對象,注意是代理者,不是被代理者
   * @param method 被代理的方法
   * @param args 被執行的代理方法中傳入的參數
   */
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 被代理的接口
    Class<?> targetInterface = method.getDeclaringClass();
    // 從容器中取出執行類
    Object target = simulatorContainer.get(targetInterface);
    if(target == null) {
      return null;
    }
    // 開始調用
    return method.invoke(target, args);
  }
}

在以上這個代理器示例中,我們模擬了一個簡易的IOC容器,並根據被代理的接口從這個簡易的容器中取出接口對應的真實業務處理類,最後執行。

  • 以下是調用代碼和執行結果
public class Run {

  static {
    BasicConfigurator.configure();
  }

  public static void main(String[] args) {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    //這是Cglib代理處理器
    ProxyInvocationHandler invocationHandler = new ProxyInvocationHandler();
    Object proxy = Proxy.newProxyInstance(classLoader,new Class<?>[]{TargetOneInterface.class}, invocationHandler);

    // 開始調用(測試第一個接口)
    TargetOneInterface targetOne = (TargetOneInterface)proxy;
    targetOne.doSomething();
    targetOne.handleSomething();
  }
}

對以上代碼進行debug操作,會發現類似如下的調試信息:

從調試信息可以看出,當我們調用org.mockito.cglib.proxy.Proxy.newProxyInstance這個靜態方法時,一旦執行成功,該方法就將返回一個由Cglib組件動態生成的類。上圖中表示爲:

。後文內容中我們會閱讀org.mockito.cglib.proxy.Proxy.newProxyInstance中的源代碼,來講解其中的工作內容。以下是示例的運行結果:

[main] INFO yinwenjie.test.proxy.cglib.dproxy.target.TargetOneImpl - doSomething 方法被執行
[main] INFO yinwenjie.test.proxy.cglib.dproxy.target.TargetOneImpl - handleSomething 方法被執行

<==== 上一篇  =====>

Spring/Boot/Cloud系列知識(2)— — 代理模式

 <==== 下一篇  =====>

Spring/Boot/Cloud系列知識(4)— — 代理模式(下)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章