1. 程式人生 > 程式設計 >Java 靜態代理、動態代理以及cglib代理

Java 靜態代理、動態代理以及cglib代理

代理模式介紹

代理模式是一種設計模式,顧名思義是對某個事物的代理,外界所有對該事物的訪問或操作,都會經過該代理。舉個例子,如果你有法律方面的糾紛,那麼你必然要找律師,此時對於律師而言你就是他的委託人,而對於你而言律師是你的代理人,也就是我們常說的代理律師

在程式設計世界中,我們可以用委託類和代理類來描述二者之間的關係

代理模式的作用: 提供了對委託類的額外訪問方式,即通過代理類訪問委託類,這樣就可以在不修改委託類前提下,提供額外的功能操作,從而擴充套件委託類的功能。

簡而言之,代理模式就是設定一箇中間代理來控制所有對委託類的訪問,以達到增強委託類的功能。

為了保持行為的一致性,代理類和委託類通常會實現相同的介面,所以在訪問者看來兩者沒有絲毫的區別。通過代理類這中間一層,能有效控制對委託類物件的直接訪問,也可以很好地隱藏和保護委託類物件,同時也為實施不同控制策略預留了空間,從而在設計上獲得了更大的靈活性

代理模式分類

講解代理模式的分類之前,我們先準備些類和介面

    // DataService介面提供了save方法
    public interface DataService {
      public void save();
    }

    // DataServiceImpl類實現了DataService介面,並重寫了save方法
    public class DataServiceImpl implements DataService{
      @Override
      public void save() {
        System.out.println("DataServiceImpl method save is called"
); } } 複製程式碼

靜態代理(包裝)

    // 代理類
    public class DataServiceProxy implements DataService{
    	// 委託類
      DataService dataService = new DataServiceImpl();
    
      @Override
      public void save() {
        System.out.println("DataServiceProxy method save is called");
        dataService.save();
      }
    }
複製程式碼

就上述程式碼分析而言,DataServiceProxy為代理類,DataServiceImpl為委託類

靜態代理優缺點分析:

  • 優點
    • 可以在不修改委託類的前提下擴充套件委託類的功能
  • 缺點
    • 代理類和委託類要實現同樣的介面
    • 硬編碼,一旦委託類增加了方法,代理類同樣要修改

JDK動態代理

利用JDK API動態的在記憶體中構建代理類的例項物件

動態代理類(以下簡稱為代理類)是一個實現在建立類時在執行時指定的介面列表的類,該類具有下面描述的行為:

  • 代理介面 是代理類實現的一個介面。
  • 代理例項 是代理類的一個例項。
  • 每個代理例項都有一個關聯的呼叫處理程式 物件,它可以實現介面 InvocationHandler。通過其中一個代理介面的代理例項上的方法呼叫將被指派到例項的呼叫處理程式的 Invoke 方法,並傳遞代理例項、識別呼叫方法的 java.lang.reflect.Method 物件以及包含引數的 Object 型別的陣列。呼叫處理程式以適當的方式處理編碼的方法呼叫,並且它返回的結果將作為代理例項上方法呼叫的結果返回

要了解 Java 動態代理的機制,首先需要了解以下相關的類或介面:

  • java.lang.reflect.Proxy: 這是 Java 動態代理機制的主類,它提供了一組靜態方法來為一組介面動態地生成代理類及其物件

Proxy 的靜態方法

    // 方法 1: 該方法用於獲取指定代理物件所關聯的呼叫處理器
    static InvocationHandler getInvocationHandler(Object proxy) 
     
    // 方法 2:該方法用於獲取關聯於指定類裝載器和一組介面的動態代理類的類物件
    static Class getProxyClass(ClassLoader loader,Class[] interfaces) 
     
    // 方法 3:該方法用於判斷指定類物件是否是一個動態代理類
    static boolean isProxyClass(Class cl) 
     
    // 方法 4:該方法用於為指定類裝載器、一組介面及呼叫處理器生成動態代理類例項
    static Object newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)
複製程式碼
  • java.lang.reflect.InvocationHandler:這是呼叫處理器介面,它自定義了一個 invoke 方法,用於集中處理在動態代理類物件上的方法呼叫,通常在該方法中實現對委託類的代理訪問

InvocationHandler 的核心方法

    /**
     * 該方法負責集中處理動態代理類上的所有方法呼叫。第一個引數既是,第二個引數是
     * proxy: 代理類例項
     * method: 被呼叫的方法物件
     * args: 呼叫引數
     * 呼叫處理器根據這三個引數進行預處理或分派到委託類例項上發射執行
     */
    Object invoke(Object proxy,Method method,Object[] args)
複製程式碼

每次生成動態代理類物件時都需要指定一個實現了該介面的呼叫處理器物件(參見 Proxy 靜態方法 4 的第三個引數)。

  • java.lang.ClassLoader: 這是類載入器,負責將位元組碼載入到Java虛擬機器器(JVM)中併為其定義類物件(Class),然後該類才能被使用。通過Proxy.newProxyInstance 生成的動態代理類同樣需要類載入器來進行載入才能使用.它與普通類的唯一區別就是其位元組碼是由JVM在執行時動態生成的而非存在於某個具體的.class檔案中

動態代理機制及其特點

首先讓我們來瞭解一下如何使用 Java 動態代理。具體有如下四步驟:

  1. 通過實現 InvocationHandler 介面並實現invoke方法來建立自己的呼叫處理器
  2. 通過為 Proxy 類指定 ClassLoader 物件和一組 interface 來建立動態代理類
  3. 通過反射機制獲得動態代理類的建構函式,其唯一引數型別是呼叫處理器介面型別
  4. 通過建構函式建立動態代理類例項,構造時呼叫處理器物件作為引數被傳入
    // InvocationHandlerImpl 實現了 InvocationHandler 介面,並能實現方法呼叫從代理類到委託類的分派轉發
    // 其內部通常包含指向委託類例項的引用,用於真正執行分派轉發過來的方法呼叫
    InvocationHandler handler = new InvocationHandlerImpl(..); 
     
    // 通過 Proxy 為包括 Interface 介面在內的一組介面動態建立代理類的類物件
    Class clazz = Proxy.getProxyClass(classLoader,new Class[] { Interface.class,... }); 
     
    // 通過反射從生成的類物件獲得建構函式物件
    Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class }); 
     
    // 通過建構函式物件建立動態代理類例項
    Interface Proxy = (Interface)constructor.newInstance(new Object[] { handler });
複製程式碼

實際使用過程更加簡單,因為 Proxy 的靜態方法 newProxyInstance 已經為我們封裝了步驟 2 到步驟 4 的過程,所以簡化後的過程如下

    // InvocationHandlerImpl 實現了 InvocationHandler 介面,並能實現方法呼叫從代理類到委託類的分派轉發
    InvocationHandler handler = new InvocationHandlerImpl(..); 
     
    // 通過 Proxy 直接建立動態代理類例項
    Interface proxy = (Interface)Proxy.newProxyInstance( classLoader,new Class[] { Interface.class },handler );
複製程式碼

具體程式碼實現:

呼叫處理程式類

    public class DataServiceInvovationHandler implements InvocationHandler {
      DataService dataService;
    
      public DataServiceInvovationHandler(DataService dataService) {
        this.dataService = dataService;
      }
    
      @Override
      public Object invoke(Object proxy,Object[] args) throws Throwable {
        System.out.println(method.getDeclaringClass() + " " +  method.getName() + "() is called");
        method.invoke(dataService,args);
        return null;
      }
    }
複製程式碼

動態代理類

    public class DataServiceDynamicProxy {
      DataService dataService = new DataServiceImpl();
      public void save() {
        DataService dataServiceDynamicProxy =
                (DataService) Proxy.newProxyInstance(dataService.getClass().getClassLoader(),new Class[]{DataService.class},new DataServiceInvovationHandler(dataService));
        dataServiceDynamicProxy.save();
      }
    }
複製程式碼

靜態代理與動態代理的區別主要在:

  • 靜態代理在編譯時就已經實現,編譯完成後代理類是一個實際的class檔案
  • 動態代理是在執行時動態生成的,即編譯完成後沒有實際的class檔案,而是在執行時動態生成類位元組碼,並載入到JVM中

cglib代理

cglib (Code Generation Library )是一個第三方程式碼生成類庫,執行時在記憶體中動態生成一個子類物件從而實現對目標物件功能的擴充套件

cglib特點

  • JDK的動態代理有一個限制,就是使用動態代理的物件必須實現一個或多個介面。如果想代理沒有實現介面的類,就可以使用CGLIB實現。
  • CGLIB是一個強大的高效能的程式碼生成包,它可以在執行期擴充套件Java類與實現Java介面。它廣泛的被許多AOP的框架使用,例如Spring AOP和dynaop,為他們提供方法的interception(攔截)。
  • CGLIB包的底層是通過使用一個小而快的位元組碼處理框架ASM,來轉換位元組碼並生成新的類。不鼓勵直接使用ASM,因為它需要你對JVM內部結構包括class檔案的格式和指令集都很熟悉。

cglib與動態代理最大的區別就是

  • 使用動態代理的物件必須實現一個或多個介面
  • 使用cglib代理的物件則無需實現介面,達到代理類無侵入

使用cglib需要引入cglibjar包,cglibmaven座標如下:

    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.2.5</version>
    </dependency>
複製程式碼

程式碼實現

委託類

    public class DataService {
      public void save() {
        System.out.println("class DataService method save is called");
      }
    }
複製程式碼

代理類

    public class CglibProxy implements MethodInterceptor {
      private Object target; // 委託類
    
      public CglibProxy(Object target) {
        this.target = target;
      }
    
      //為委託類生成代理物件
      public Object getProxyInstance() {
        // 工具類
        Enhancer enhancer = new Enhancer();
        // 設定委託類為父類
        enhancer.setSuperclass(target.getClass());
        // 設定回撥函式
        enhancer.setCallback(this);
        // 建立子類物件代理
        return enhancer.create();
      }
    
    
      @Override
      public Object intercept(Object o,Object[] objects,MethodProxy methodProxy) throws Throwable {
        System.out.println("DataService cglib  proxy start...");
        // 通過反射執行委託類的方法
        method.invoke(target,objects);
        System.out.println("DataService cglib proxy end...");
        return null;
      }
    }
複製程式碼

測試結果

總結

  1. 靜態代理實現較簡單,只要代理物件對目標物件進行包裝,即可實現增強功能,但靜態代理只能為一個目標物件服務,如果目標物件過多,則會產生很多代理類。
  2. JDK動態代理需要目標物件實現業務介面,代理類只需實現InvocationHandler介面。
  3. 靜態代理在編譯時產生class位元組碼檔案,可以直接使用,效率高。
  4. 動態代理必須實現InvocationHandler介面,通過反射代理方法,比較消耗系統效能,但可以減少代理類的數量,使用更靈活。
  5. cglib代理無需實現介面,通過生成類位元組碼實現代理,比反射稍快,不存在效能問題,但cglib會繼承目標物件,需要重寫方法,所以目標物件不能為final