Java基礎系列-靜態代理和動態代理

markriver發表於2021-09-09

1、動態代理(Dynamic Proxy)

代理分為靜態代理和動態代理,靜態代理是在編譯時就將介面、實現類、代理類一股腦兒全部手動完成,但如果我們需要很多的代理,每一個都這麼手動的去建立實屬浪費時間,而且會有大量的重複程式碼,此時我們就可以採用動態代理,動態代理可以在程式執行期間根據需要動態的建立代理類及其例項,來完成具體的功能。

其實方法直接呼叫就可以完成功能,為什麼還要加個代理呢?

原因是採用代理模式可以有效的將具體的實現與呼叫方進行解耦,透過面向介面進行編碼完全將具體的實現隱藏在內部。

2、代理實現的一般模式

其實代理的一般模式就是靜態代理的實現模式:首先建立一個介面(JDK代理都是面向介面的),然後建立具體實現類來實現這個介面,在建立一個代理類同樣實現這個介面,不同之處在於,具體實現類的方法中需要將介面中定義的方法的業務邏輯功能實現,而代理類中的方法只要呼叫具體類中的對應方法即可,這樣我們在需要使用介面中的某個方法的功能時直接呼叫代理類的方法即可,將具體的實現類隱藏在底層。

  • 第一步:定義總介面Iuser.java

package ceshi1;public interface Iuser {    void eat(String s);
}
  • 第二步:建立具體實現類UserImpl.java

package ceshi1;public class UserImpl implements Iuser {
  @Override  public void eat(String s) {
    System.out.println("我要吃"+s);
  }
}
  • 第三步:建立代理類UserProxy.java

package ceshi1;public class UserProxy implements Iuser {
  private Iuser user = new UserImpl();
  @Override  public void eat(String s) {
    System.out.println("靜態代理前置內容");
    user.eat(s);
    System.out.println("靜態代理後置內容");
  }
}
  • 第四步:建立測試類ProxyTest.java

package ceshi1;public class ProxyTest {
  public static void main(String[] args) {    
    UserProxy proxy = new UserProxy();
    proxy.eat("蘋果");
  }
}

執行結果:

靜態代理前置內容
我要吃蘋果
靜態代理後置內容

3、JDK動態代理的實現

JDK動態代理的思維模式與之前的一般模式是一樣的,也是面向介面進行編碼,建立代理類將具體類隱藏解耦,不同之處在於代理類的建立時機不同,動態代理需要在執行時因需實時建立。

  • 第一步:定義總介面Iuser.java

package ceshi1;public interface Iuser {
  void eat(String s);
}
  • 第二步:建立具體實現類UserImpl.java

package ceshi1;public class UserImpl implements Iuser {
  @Override  public void eat(String s) {
    System.out.println("我要吃"+s);
  }
}
  • 第三步:建立實現InvocationHandler介面的代理類

package ceshi1;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;public class DynamicProxy implements InvocationHandler {
  private Object object;//用於接收具體實現類的例項物件  //使用帶引數的構造器來傳遞具體實現類的物件  public DynamicProxy(Object obj){
    this.object = obj;
  }
  @Override  public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
    System.out.println("前置內容");
    method.invoke(object, args);
    System.out.println("後置內容");
    return null;
  }
}
  • 第四步:建立測試類ProxyTest.java

package ceshi1;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;public class ProxyTest {
  public static void main(String[] args) {
    Iuser user = new UserImpl();
    InvocationHandler h = new DynamicProxy(user);
    Iuser proxy = (Iuser) Proxy.newProxyInstance(Iuser.class.getClassLoader(), new Class[]{Iuser.class}, h);
    proxy.eat("蘋果");
  }
}

執行結果為:

動態代理前置內容
我要吃蘋果
動態代理後置內容

4、實現分析

4.1 首先我要說的就是介面,為什麼JDK的動態代理是基本介面實現的呢?

因為透過使用介面指向實現類的例項的多型實現方式,可以有效的將具體的實現與呼叫之間解耦,便於後期修改與維護。

再具體的說就是我們在代理類中建立一個私有成員變數(private修飾),使用介面來指向實現類的物件(純種的多型體現,向上轉型的體現),然後在該代理類中的方法中使用這個建立的例項來呼叫實現類中的相應方法來完成業務邏輯功能。

這麼說起來,我之前說的“將具體實現類完全隱藏”就不怎麼正確了,可以改成,將具體實現類的細節向呼叫方完全隱藏(呼叫方呼叫的是代理類中的方法,而不是實現類中的方法)。

這就是面向介面程式設計,利用java的多型特性,實現程式程式碼的解耦。

4.2 建立代理類的過程

如果你瞭解靜態代理,那麼你會發現動態代理的實現其實與靜態代理類似,都需要建立代理類,但是不同之處也很明顯,建立方式不同!

不同之處體現在靜態代理我們知根知底,我們知道要對哪個介面、哪個實現類來建立代理類,所以我們在編譯前就直接實現與實現類相同的介面,直接在實現的方法中呼叫實現類中的相應(同名)方法即可;而動態代理不同,我們不知道它什麼時候建立,也不知道要建立針對哪個介面、實現類的代理類(因為它是在執行時因需實時建立的)。

雖然二者建立時機不同,建立方式也不相同,但是原理是相同的,不同之處僅僅是:靜態代理可以直接編碼建立,而動態代理是利用反射機制來抽象出代理類的建立過程。

讓我們來分析一下之前的程式碼來驗證一下上面的說辭:

  • 第一點:靜態代理需要實現與實現類相同的介面,而動態代理需要實現的是固定的Java提供的內建介面(一種專門提供來建立動態代理的介面)InvocationHandler介面,因為java在介面中提供了一個可以被自動呼叫的方法invoke,這個之後再說。

  • 第二點:

private Object object;public UserProxy(Object obj){this.object = obj;}

這幾行程式碼與靜態代理之中在代理類中定義的介面指向具體實現類的例項的程式碼異曲同工,透過這個構造器可以建立代理類的例項,建立的同時還能將具體實現類的例項與之繫結(object指的就是實現類的例項,這個例項需要在測試類中建立並作為引數來建立代理類的例項),實現了靜態代理類中private Iuser user = new UserImpl();一行程式碼的作用相近,這裡為什麼不是相同,而是相近呢,主要就是因為靜態代理的那句程式碼中包含的實現類的例項的建立,而動態代理中實現類的建立需要在測試類中完成,所以此處是相近。

  • 第三點:invoke(Object proxy, Method method, Object[] args)方法,該方法是InvocationHandler介面中定義的唯一方法,該方法在呼叫指定的具體方法時會自動呼叫。其引數為:代理例項、呼叫的方法、方法的引數列表。

在這個方法中我們定義了幾乎和靜態代理相同的內容,僅僅是在方法的呼叫上不同,不同的原因與之前分析的一樣(建立時機的不同,建立的方式的不同,即反射),Method類是反射機制中一個重要的類,用於封裝方法,該類中有一個方法那就是invoke(Object object,Object...args)方法,其引數分別表示:所呼叫方法所屬的類的物件和方法的引數列表,這裡的引數列表正是從測試類中傳遞到代理類中的invoke方法三個引數中最後一個引數(呼叫方法的引數列表)中,在傳遞到method的invoke方法中的第二個引數中的(此處有點囉嗦)。

  • 第四點:測試類中的異同

靜態代理中我們測試類中直接建立代理類的物件,使用代理類的物件來呼叫其方法即可,若是別的介面(這裡指的是別的呼叫方)要呼叫Iuser的方法,也可以使用此法。

動態代理中要複雜的多,首先我們要將之前提到的實現類的例項建立(補充完整),然後利用這個例項作為引數,呼叫代理來的帶參構造器來建立“代理類例項物件”,這裡加引號的原因是因為它並不是真正的代理類的例項物件,而是建立真正代理類例項的一個引數,這個實現了InvocationHandler介面的類嚴格意義上來說並不是代理類,我們可以將其看作是建立代理類的必備中間環節,這是一個呼叫處理器,也就是處理方法呼叫的一個類,不是真正意義上的代理類,可以這麼說:建立一個方法呼叫處理器例項。

下面才是真正的代理類例項的建立,之前建立的”代理類例項物件“僅僅是一個引數:

Iuser proxy = (Iuser) Proxy.newProxyInstance(Iuser.class.getClassLoader(), new Class[]{Iuser.class}, h);

這裡使用了動態代理所依賴的第二個重要類Proxy,此處使用了其靜態方法來建立一個代理例項,其引數分別是:類載入器(可為父類的類載入器)、介面陣列、方法呼叫處理器例項。

這裡同樣使用了多型,使用介面指向代理類的例項,最後會用該例項來進行具體方法的呼叫即可。

4.3 InvocationHandler

InvocationHandler是JDK中提供的專門用於實現基於介面的動態代理的介面,主要用於進行方法呼叫模組,而代理類和例項的生成需要藉助Proxy類完成。

每個代理類的例項的呼叫處理器都是實現該介面實現的,而且是必備的,即每個動態代理例項的實現都必須擁有實現該介面的呼叫處理器,也可以這麼說,每個動態代理例項都對應一個呼叫處理器。

這裡要區分兩個概念,代理類和代理例項,呼叫處理器是在建立代理例項的時候才與其關聯起來的,所以它與代理例項是一一對應的,而不是代理類。

4.4 Proxy

Proxy類是JDK提供的用於生成動態代理類和其例項的類。

我們可以透過Proxy中的靜態方法getProxyClass來生成代理類,需要的引數為類載入器和介面列表(陣列),然後再透過反射呼叫代理類的構造器來生成代理例項,需要以一個InvocationHandler作為引數(體現出方法呼叫是與例項相關的,而非類)。

InvocationHandler handler = new MyInvocationHandler(...);
Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class);
Foo f = (Foo) proxyClass.getConstructor(InvocationHandler.class).newInstance(handler);

我們也可以直接透過Proxy中的靜態方法newProxyInstance方法來直接生產代理例項,需要提供引數為上面的三個引數,即類載入器,介面陣列,InvocationHandler。

Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),new Class<?>[] { Foo.class },handler);

4.5 總結

我們總結下JDK動態代理的實現步驟:

  • 第一步:建立介面,JDK動態代理基於介面實現,所以介面必不可少(準備工作)

  • 第二步:實現InvocationHandler介面,重寫invoke方法(準備工作)

  • 第三步:呼叫Proxy的靜態方法newProxyInstance方法生成代理例項(生成例項時需要提供類載入器,我們可以使用介面類的載入器即可)

  • 第四步:使用新生成的代理例項呼叫某個方法實現功能。

我們的動態代理實現過程中根本沒有涉及到真實類例項。
5、Cglib動態代理的實現
JDK動態代理擁有侷限性,那就是必須面向介面程式設計,沒有介面就無法實現代理,我們也不可能為了代理而為每個需要實現代理的類強行新增毫無意義的介面,這時我們需要Cglib,這種依靠繼承來實現動態代理的方式,不再要求我們必須要有介面。

  • 第一步:新增Cglib的Maven依賴

<dependency>
   <groupId>cglib</groupId>
   <artifactId>cglib</artifactId>
   <version>3.1</version></dependency>
  • 第二步:建立具體實現類User.java

public class User {    public void eat(String s){
        System.out.println("我要吃" + s);
    }
}
  • 第三步:建立實現MethodInterceptor介面的代理類

import net.sf.cglib.proxy.MethodInterceptor;import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;public class UserInterceptor implements MethodInterceptor {    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("預處理");
        Object object =  methodProxy.invokeSuper(o,objects);
        System.out.println("後處理");        return object;
    }
}
  • 第四步:建立測試類ProxyTest.java

import net.sf.cglib.proxy.Enhancer;public class ProxyTest {    public static void main(String[] args){
        Enhancer enchancer = new Enhancer();//位元組碼增強器
        enchancer.setSuperclass(User.class);//設定被代理類為父類
        enchancer.setCallback(new UserInterceptor());//設定回撥
        User user = (User)enchancer.create();//建立代理例項
        user.eat("葡萄");
    }
}

執行結果:

預處理
我要吃葡萄
後處理



作者:qe2592008
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/818/viewspace-2821309/,如需轉載,請註明出處,否則將追究法律責任。

相關文章