我的學習筆記——Java靜態代理和動態代理

靜態代理與JDK動態代理

  所謂就是代理是通過代理對象訪問目標對象,用代理對象來代表目標對象的功能,也可以目標方法進行增強。Java代理分爲靜態代理和動態代理。在這裏主要描述靜態代理和JDK動態代理。

1.靜態代理

  所謂靜態代理,就是程序在編譯時就確定了代理類與被代理類的關係。
假設有一接口TakeBooks,接口內有一抽象方法takeBooksFromOffice。老師類Teacher實現了這一接口,現在有一代理類Student也實現了TakeBooks接口。
  現在要讓學生代理老師去辦公室取書,也就是說我們要讓學生(Student)類去代理老師(Teacher)類實現takeBooksFromOffice方法。
以下是靜態代理的實現

  • TakeBooks接口
public interface TakeBooks {
   public void takeBooksFromOffice();
}
  • 目標類 Teacher類
public class Teacher implements TakeBooks{

    @Override
    public void takeBooksFromOffice() {
        System.out.println(  "去辦公室取書" );
    }

}
  • 代理類 Student類
public class Student implements TakeBooks{
    //持有目標對象
    private Teacher teacher;

    public Student(Teacher teacher){
        this.teacher = teacher;
    }

    @Override
    public void takeBooksFromOffice() {
        //實現目標對象的方法
        teacher.takeBooksFromOffice();
        //這裏也可以寫其他內容進行對目標方法的增強
    }

}
  • 測試類Test
public class Test  {
    public static void main(String[] args) {
        //目標類Teacher
        Teacher teacher = new Teacher();
        //代理類Student
        Student proxy = new Student(teacher);
        //學生代理老師去辦公室取書
        proxy.takeBooksFromOffice();
    }
}
//Output:
//去辦公室取書

  從上述的例子我們可以看出,實現對一個目標類的靜態代理
我們只需要:
1.創建一個代理類也實現目標類的接口;
public class Student implements TakeBooks
2.使代理類持有目標類的引用;
private Teacher teacher;
3.代理類接口方法實現爲 持有的目標類引用.接口方法 或者再加上其他的增強方法;
teacher.takeBooksFromOffice();


  觀察上述,我們也能發現靜態代理也存在不可避免的缺陷。假設在上面的例子中,我今天讓學生代理去取書,我明天讓保潔阿姨代理老師去取書,後天讓校門警衛代理老師去取書,那麼我們就要在代碼中再添加多個代理類。假如老師去學校要司機代理開車,去食堂喫飯要食堂阿姨代理打飯,這樣一個目標類的不同方法還需要定義不同的代理類去實現。可見,靜態代理的重用性比較差


2.JDK動態代理

  JDK動態代理是java.lang.reflect.*包提供的方式,這種方法必須要藉助一個接口才能產生一個代理對象。我們仍然使用上述老師取書的例子,代理類與目標類的共用接口TakeBooks和目標類Teacher沒有變。

  • TakeBooks接口
public interface TakeBooks {
   public void takeBooksFromOffice();
}
  • 目標類 Teacher類
public class Teacher implements TakeBooks{

    @Override
    public void takeBooksFromOffice() {
        System.out.println(  "去辦公室取書" );
    }

}
  • 現在我們創建能夠建立代理對象和目標對象的聯繫的JDK代理類
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JDKproxy implements InvocationHandler {
	//JDK代理類持有目標類的引用
    private Object target;
	//建立代理類與目標類的聯繫
    public Object bind(Object target){
        this.target = target;
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    //InvocationHandler接口裏抽象方法的具體實現,就是代理邏輯方法的實現
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    	//這行代碼相當於調度了目標對象的方法
    	//只不過靜態代理我們直接使用this.teacher.takeBooksFromOffice()調用
    	//而動態代理運用了反射實現調用而已
        Object object = method.invoke(target, args);
        return object;
    }
}
  • 測試類Test
public class Test {
    public static void main(String[] args) {
    	//創建JDK代理類實例 
        JDKproxy jdKproxy = new JDKproxy();
        //目標類Teacher
        Teacher teacher = new Teacher();
        //建立接口引用的代理類,由於bind方法返回值是Object類型,我們需要強制向下轉型
        //bind傳入參數爲目標類,返回一個代理類
        TakeBooks proxy = (TakeBooks)jdKproxy.bind(teacher);
        //代理者幫老師去取書
        proxy.takeBooksFromOffice();
    }
}//Output:
//去辦公室取書

  從上述的例子我們可以看出,實現對一個目標類的動態代理
  我們只需要:
1.創建一個實現InvocationHandler接口JDK代理類
2.在JDK代理類中持有一個目標類的引用,並創建一個方法爲這個引用賦值,以此來建立目標對象和代理對象的聯繫(這裏用了bind方法,其實爲JDK代理類創建一個有參構造方法也可以實現);
3.在invoke方法中實現代理邏輯方法,invoke方法是JDK代理類所實現的InvocationHandler接口中的抽象方法。
  在JDK動態代理中我們發現JDK代理類中持有的目標類和方法返回的代理類均爲Object類型,這意味着不僅學生可以給老師取書,甚至還能幫保潔阿姨倒水,幫校園門衛站崗,這取決於我們在main方法中做出怎樣的向下轉型(前提是合法的向下轉型)。


  不過相信大家瀏覽完後可能會和我提出一樣的問題:

  • InvocationHandler接口是什麼;
  • Proxy.newProxyInstance方法參數什麼,返回了什麼;
  • invoke方法傳入參數是什麼;
  • main函數裏沒有顯式地調用invoke方法,InvocationHandler中的invoke()方法是怎麼自動實現的呢;

  1)InvocationHandler接口中invoke方法包含3個參數
public Object invoke(Object proxy, Method method, Object[] args)

參數 含義
Object proxy proxy對象即爲newProxyInstance方法中生成的對象
Method method method 用來獲取當前調度的方法信息
Object[] args args就是當前調度方法的參數列表

  2)Proxy中的靜態方法newProxyInstance包含三個參數

參數 含義
ClassLoader loader 類加載器,這裏我採用了target的類加載器
Class<?>[] interfaces 表示生成的動態代理對象處在哪個接口之下,我們直接獲取target的接口實現列表然後傳入方法中
InvocationHandler h 表示實現了代理方法邏輯的JDK代理類(也就是實現了InvocationHandler接口的那個類),我們是在JDK代理類中調用的newProxyInstance方法,所以要將自身傳入該方法中,所以此處傳入this。

  newProxyInstance方法的返回值就是JDK動態生成的代理類。
  實際上newProxyInstance也可以在main函數中調用,在main函數調用時我們應該這樣聲明:

TakeBooks proxy = Proxy.newProxyInstance(teacher.getClass().getClassLoader(), teacher.getClass().getInterfaces(), jdkproxy);

  這樣的話我們就不需要bind方法了,可以將bind方法改成JDK代理類的有參構造器。
  3)既然我們想知道代理類是如何實現目標類方法的,那麼我們要首先了解代理類中的內容。我們現在在invoke方法裏面再加一個語句:

 @Override
@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    	//添加一句可以輸出newProxyInstance方法幫助我們自動生成的代理類的類名的語句
    	//proxy.getClass().getName()中的proxy就是上面invoke方法的第一個參數裏面的proxy
        System.out.println( proxy.getClass().getName() );
        Object object = method.invoke(target, args);
        return object;
    }

  這樣輸出的結果就變成了:


com.sun.proxy.$Proxy0
去辦公室取書
Process finished with exit code 0


  很明顯 $ Proxy0類就是編譯器爲我們生成的代理類的類名,我們可以讓編譯器生成$ Proxy0的.class文件再經過反編譯獲得其類型信息,具體步驟爲:

1)在main方法的首部添加

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

注意,此處的是"true"被雙引號包圍的
2)按照控制檯輸出的路徑對應手動創建文件夾
我的控制檯輸出是:com.sun.proxy.$Proxy0
我的項目名是jdkproxy,那麼我創建的文件夾應該爲

C:\Users\14583\IdeaProjects\jdkProxy\com\sun\proxy

也就是我們在項目的根目錄下,根據控制檯輸出的路徑創建文件夾

  這樣當我們運行了main方法時,在我們新建的這個文件目錄下就會生成$ Proxy0.class文件了,我使用的JD-GUI這個軟件反編譯的.class文件,得到了$ Proxy0類的源代碼,下面是其一部分:

//代理類是Proxy的子類並實現了我們定義的TakeBooks接口
public final class $Proxy0
  extends Proxy
  implements TakeBooks
{
  //......源代碼相當的長,我只選取了一個部分
  private static Method m3;
  //......
  public final void takeBooksFromOffice()
  {
    try
    {
      this.h.invoke(this, m3, null);
      return;
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
    //.......
    static
  {
    try
    {
      //......
      m3 = Class.forName("jdkProxy.TakeBooks").getMethod("takeBooksFromOffice", new Class[0]);
      //......
      return;
    }
    catch (NoSuchMethodException localNoSuchMethodException)
    {
      throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
    }
    catch (ClassNotFoundException localClassNotFoundException)
    {
      throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
    }
  }
  }

  通過對源代碼的閱讀,我明白了我在main方法中調用proxy.takeBooksFromOffice(); 這一語句時,我操作的proxy引用所指向的其實是一個$ Proxy0類的實例;
我調用的這個takeBooksFromOffice本質上是$ Proxy0源代碼中的final void takeBooksFromOffice 這一方法,它進而調用this.h.invoke(this, m3, null); 方法,這裏的 this.h,其實是代理類父類Proxy中的一個保護變量,查看Proxy類的源代碼後,我發現其中有這樣一個信息:

protected InvocationHandler h;

但是這個h究竟是什麼值呢?
在$ Proxy0類中有這樣的構造方法:

public $Proxy0(InvocationHandler paramInvocationHandler)
  {
    super(paramInvocationHandler);
  }

$ Proxy0的構造器內部引用了父類Proxy的構造方法,我們再查找父類Proxy的源代碼發現:

 protected Proxy(InvocationHandler h) {
 		//這句是用來判斷傳入的h是否爲空的
        Objects.requireNonNull(h);
        this.h = h;
    }

  而我們定義的JDKproxy類恰好是InvocationHandler的實現類。在建立目標類與代理類聯繫的時候,我們向生成代理類的newProxyInstance方法傳入了三個參數,分別爲:1.目標類加載器2.目標類所實現的接口3.JDKproxy實例本身,那麼這裏的"this.h"的值很可能就是在main方法中定義的jdkProxy.
  我們再看$ Proxy0的源代碼,我們發現這樣的一部分靜態代碼塊:

 static
  {
    try
    {
      //......
      m3 = Class.forName("jdkProxy.TakeBooks").getMethod("takeBooksFromOffice", new Class[0]);
      //......
      return;
    }
    catch (NoSuchMethodException localNoSuchMethodException)
    {
      throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
    }
    catch (ClassNotFoundException localClassNotFoundException)
    {
      throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
    }
  }

其中有這樣的一條語句:

  m3 = Class.forName("jdkProxy.TakeBooks").getMethod("takeBooksFromOffice", new Class[0]);

再看main方法中的

JDKproxy jdKproxy = new JDKproxy();
Teacher teacher = new Teacher();
TakeBooks proxy = (TakeBooks)jdKproxy.bind(teacher);
proxy.takeBooksFromOffice();

  也就是說當我們將目標類的真實類型傳入bind方法並進行對其結果進行強制向下轉型的時候,代理類就已經加載了目標類的真實類型,JDK代理類型的實例jdkProxy以及目標類所實現的接口類型,我們調用proxy.takeBooksFromOffice() 時編譯器會執行this.h.invoke,也就是jdkProxy.invoke方法,而通過反射實現的調度真實對象的方法就封裝在jdkProxy.invoke中了,即Object object = method.invoke(target, args);
  InvocationHandler中的invoke()方法是怎麼自動實現的呢?代理類的實例又是怎樣知道目標類有takeBooksFromOffice方法呢?
  當我們將目標類的類加載器和接口列表傳給newProxyInstance時,代理類$ Proxy0就已經通過靜態代碼塊完成了對需要代理的類(即目標類)類型和方法的加載;當我們再將實現了InvocationHandler接口的JDK代理類實例jdkProxy傳入時,由於代理類$ Proxy0持有由InvocationHandler類型的變量聲明,它就可以通過接口變量調度被JDKproxy實現的接口方法invoke。而當接口變量調用了被類實現的接口中方法時,也是在通知相應的對象調用接口的方法,這種層間協作的過程也被稱爲接口的回調。

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