1. 程式人生 > >深入理解系列之JAVA動態代理機制

深入理解系列之JAVA動態代理機制

代理的作用,就是生成代理物件使得真實物件的某些方法執行被代理物件攔截,從而在真實方法執行前、執行後新增額外的“動作”!動態代理則是指不需要修改原來的物件方法,在程式執行的過程中動態的生成代理物件,從而動態的生成這些“額外的”動作,主要從兩個方面來深入理解動態代理機制!

問題一、動態代理的基本實現是什麼?

動態代理本質上還是java中的“代理設計模式”,所以啟UML圖如下所示
這裡寫圖片描述
真正實現的時候分為以下幾個步驟:

1、建立被代理物件的介面

interface Subject{
      public void run();
    }

2、實現被代理的真實物件

class
RealSubject implements Subject{
@Override public void run(){ System.out.println("真實物件正在執行"); } }

3、建立呼叫處理器:

class SubjectInvocation implements InvocationHandler{

      private Subject subject;

      public SubjectInvocation(Subject subject){
        this.subject = subject;
      }

      @Override
      public
Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("將要代理"); Object result = method.invoke(subject,args); System.out.println("代理結束"); return null; } }

呼叫處理器物件很關鍵,我們可以看到呼叫處理器物件持有被代理物件的介面,當invoke()執行的時候,通過method.invoke()執行實際物件的具體方法,這樣就相當於攔截了真實物件的執行,從而在實際方法執行的時候新增額外的動作!

4、生成代理物件:

public class ProxyTest {

      public static void main(String[] args){

        SubjectInvocation subjectInvocation = new SubjectInvocation(new RealSubject());
        Subject proxySubject = (Subject) Proxy.newProxyInstance(Subject.class.getClassLoader(),
                new Class[]{Subject.class},subjectInvocation);
        proxySubject.run();
        }
    }

重點關注程式碼:

Subject proxySubject = (Subject)Proxy.newProxyInstance(Subject.class.getClassLoader(),
                new Class[]{Subject.class},
                subjectInvocation);

我們發現,代理物件的型別是強制轉換為的被代理的介面型別,其中代理物件的生成是呼叫Proxy類的靜態方法實現的,

public static Object newProxyInstance(
ClassLoader loader, 
Class<?>[] interfaces, 
InvocationHandler h) 
throws IllegalArgumentException{…}

引數的含義如下:
loader:定義了代理類的ClassLoder,通常是interface.class.getClassLoader獲得;
interfaces:代理類實現的介面列表
h:呼叫處理器,也就是我們上面定義的實現了InvocationHandler介面的類例項
最後當呼叫proxySubject.run()時,實際上被攔截進而執行invoke()函式。我們執行一下,看看我們的動態代理是否能正常工作。我這裡執行後的輸出為:

將要代理
真實物件正在執行
代理結束

問題二、為什麼執行proxySubject.run()之後,會被攔截從而執行invoke()函式呢?

我們看到,invoke()函式其實是呼叫處理器裡的函式,而ProxySubject是Subject型別的,他們之間有什麼關係,為什麼執行run之後會跳到invoke函式呢?解開這個謎底的第一步是去看 Proxy.newProxyInstance()方法的原始碼!
第一步:

public static Object newProxyInstance(ClassLoader loader,
                                              Class<?>[] interfaces,
                                              InvocationHandler h)
            throws IllegalArgumentException
        {
            Objects.requireNonNull(h);

            final Class<?>[] intfs = interfaces.clone();
            final SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
            }

            /*
             * Look up or generate the designated proxy class.
             */
            Class<?> cl = getProxyClass0(loader, intfs);

            /*
             * Invoke its constructor with the designated invocation handler.
             */
            try {
                if (sm != null) {
                    checkNewProxyPermission(Reflection.getCallerClass(), cl);
                }

                final Constructor<?> cons = cl.getConstructor(constructorParams);
                final InvocationHandler ih = h;
                if (!Modifier.isPublic(cl.getModifiers())) {
                    AccessController.doPrivileged(new PrivilegedAction<Void>() {
                        public Void run() {
                            cons.setAccessible(true);
                            return null;
                        }
                    });
                }
                return cons.newInstance(new Object[]{h});
            } catch (IllegalAccessException|InstantiationException e) {
                throw new InternalError(e.toString(), e);
            } catch (InvocationTargetException e) {
                Throwable t = e.getCause();
                if (t instanceof RuntimeException) {
                    throw (RuntimeException) t;
                } else {
                    throw new InternalError(t.toString(), t);
                }
            } catch (NoSuchMethodException e) {
                throw new InternalError(e.toString(), e);
            }
        }

我們把多餘的異常處理等程式碼去除,只看核心程式碼

public static Object newProxyInstance(ClassLoader loader,
                                              Class<?>[] interfaces,
                                              InvocationHandler h)
            throws IllegalArgumentException
        {

            final Class<?>[] intfs = interfaces.clone();
            Class<?> cl = getProxyClass0(loader, intfs);
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            return cons.newInstance(new Object[]{h});
         }

我們可以看到,代理物件的生成實際上是利用反射獲取建構函式,通過載入建構函式生成的,其中生成的物件持有呼叫處理器h。這裡有個很隱藏的問題:return cons.newInstance(new Object[]{h});到底返回的是什麼型別的物件?Subject?Proxy?其實都不是,我們可以打印出來該類的名稱來驗證一下,所以程式碼修改成這樣:

public class ProxyTest {

      public static void main(String[] args){
        SubjectInvocation subjectInvocation = new SubjectInvocation(new RealSubject());
        Subject proxySubject = (Subject) Proxy.newProxyInstance(Subject.class.getClassLoader(),
                new Class[]{Subject.class},subjectInvocation);
        System.out.println(proxySubject.getClass().getName());
        proxySubject.run();
        }
    }

執行輸出為:

proxySubject的型別是:$Proxy0
正在代理
真實物件正在執行
代理結束

OK,我們看到了一個意想不到的結果:Proxy0這個是什麼型別?還記得文章開始所說的代理類是動態生成的嗎?其中proxySubject就是代理類的例項物件,所以這個Proxy0就是動態生成的代理類,proxySubject就是根據這個類建立的動態代理物件!那麼這個類和Proxy、Subject又有什麼關係呢?為什麼它持有run方法而且能呼叫invoke方法呢?解開謎底的第二步就是研究Proxy0!
第二步:

按理說,生成的動態代理類是直接載入到記憶體中我們無法獲取的,直觀的方法可以通過反射還原這個類的內容,但實在複雜!但是好在JDK提供了代理類的生成函式,所以程式碼改成這樣:

public class ProxyTest {
    public static void main(String[] args){
//新增這一句
    System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
    SubjectInvocation subjectInvocation = new SubjectInvocation(new RealSubject());
    Subject proxySubject = (Subject) Proxy.newProxyInstance(Subject.class.getClassLoader(),
                new Class[]{Subject.class},subjectInvocation);
    System.out.println(proxySubject.getClass().getName());
    proxySubject.run();
} 

System.getProperties().put(“sun.misc.ProxyGenerator.saveGeneratedFiles”, “true”) 這句話執行後再進行動態代理生成就會在專案的根目錄下生成代理類的位元組碼檔案(實際路徑是包名+類名)然後再借用IDE的反編譯功能就能看到這個動態生成類的廬山真面目:

import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.reflect.UndeclaredThrowableException;

    final class $Proxy0 extends Proxy implements Subject {
      private static Method m1;
      private static Method m3;
      private static Method m2;
      private static Method m0;

      public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
      }

      public final boolean equals(Object var1) throws  {
        try {
          return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue();
        } catch (RuntimeException | Error var3) {
          throw var3;
        } catch (Throwable var4) {
          throw new UndeclaredThrowableException(var4);
        }
      }

      public final void run() throws  {
        try {
          super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
          throw var2;
        } catch (Throwable var3) {
          throw new UndeclaredThrowableException(var3);
        }
      }

      public final String toString() throws  {
        try {
          return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
          throw var2;
        } catch (Throwable var3) {
          throw new UndeclaredThrowableException(var3);
        }
      }

      public final int hashCode() throws  {
        try {
          return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue();
        } catch (RuntimeException | Error var2) {
          throw var2;
        } catch (Throwable var3) {
          throw new UndeclaredThrowableException(var3);
        }
      }

      static {
        try {
          m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
          m3 = Class.forName("proxy.Subject").getMethod("run");
          m2 = Class.forName("java.lang.Object").getMethod("toString");
          m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
          throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
          throw new NoClassDefFoundError(var3.getMessage());
        }
      }
    }

我們很明顯的看到, Proxy0繼承了Proxy實現了Subject!這樣,因為繼承Proxy,所以Proxy0就持有了呼叫處理器物件,因為實現Subject,所以Proxy0就可以擁有run()方法。這樣我們繼續看程式碼就可以輕而易舉的找出run()方法時如何聯絡上呼叫處理器的invoke()方法的了!

首先看到,$Proxy0類聲明瞭m0~m3幾個Method型別的變數:

      private static Method m1;
      private static Method m3;
      private static Method m2;
      private static Method m0;

然後看最後的靜態程式碼塊(類載入的時候,靜態程式碼塊首先執行):

static {
        try {
          m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
          m3 = Class.forName("Subject").getMethod("run");
          m2 = Class.forName("java.lang.Object").getMethod("toString");
          m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
          throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
          throw new NoClassDefFoundError(var3.getMessage());
        }
      }

可以看到,這些變數實際上是通過反射機制利用Class.forName().getMethod()動態載入的指向特定型別下的方法引用變數,這裡我們只看run()方法:

public final void run() throws  {
        try {
          super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
          throw var2;
        } catch (Throwable var3) {
          throw new UndeclaredThrowableException(var3);
        }
      }

“很巧合”的是,$Proxy0中“竟然”也有一個run()方法(實際上是因為繼承了Subject罷了),而且裡面傳入的方法引用便是m3,而執行run方法後,竟然呼叫的是h.invoke()!

到這裡基本可以說是真相大白了: ProxySubject是$Proxy0型別物件,而$Proxy0類因為繼承Proxy實現了Subject,所以持有了呼叫處理器h和run()方法,而且重寫了run方法:執行run()方法後實際執行的是invoke方法,而invoke方法因為傳入了由反射機制生成的方法引用,所以執行invoke方法後再執行method.invoke(),自然而然的執行了method也就是run方法!

讀者應該也注意到,$Proxy0除了包含run方法外還有hashcode、equals、toString方法,這是因為所有的類包括 Subject類都是繼承的Object類,$Proxy0自然也包含這些方法! 這麼說來,JDK的動態代理機制的UML圖就變成了這樣:
這裡寫圖片描述