用大白話講Java動態代理的原理

DMingO發表於2020-07-29

動態代理是什麼

首先說下代理模式,代理模式是常見的一種java設計模式,特徵是代理類委託類實現了同樣的介面,代理類主要負責為委託類預處理、過濾、轉發,以及事後處理等。代理類與委託類之間通常會存在關聯關係,一個代理類的例項與它的委託類的例項是關聯的。代理類的例項本身是並不真正關心被呼叫方法的內部邏輯,而是會通過內部訪問呼叫 委託類的例項真正實現了的方法,來為呼叫者提供服務。

有代理的話,在訪問實際物件時,是通過代理例項來訪問、呼叫委託類方法的,代理模式就是在訪問實際物件時引入一定程度的間接性,因為這種間接性,可以附加多種用途。

動態代理對比靜態代理,最大的特點是代理類是在程式執行時生成的,並非在編譯期生成,能做的事情也多了,自然風險也高了。

動態代理最簡單的用法

用一個比較接近生活的例子:中午,餓了的室友 委託 持家有道的你 去點外賣

Hungry.java :介面

public interface Hungry {
     void callLunch();
}

Roommate.java :Hungry介面的實現類,也就是委託類

public class Roommate implements Hungry{
    private String name;
    public Roommate(String name) {
        this.name = name;
    }

    @Override
    public void callLunch() {
        System.out.println("好餓,今天午飯點外賣吧");
    }
}

public class RoommateInvocationHandler<T> implements InvocationHandler {

    private T rommate;

    public RoommateInvocationHandler(T roommate){
        this.rommate = roommate;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("下單前,我先幫你看下有沒有平臺優惠券吧");
        Object result = method.invoke(rommate , args);
        return result;
    }
}

InvocationHandler是一個介面,由代理例項內部的invocation handler實現的介面。每個代理例項都有一個關聯的invocation handler。當代理例項上呼叫方法時,method.invoke(baseImpl, args),此方法將被編碼並織入到代理例項內部的 invocation handler實現的invoke方法中。

利用 Proxy 的方式實現動態代理,呼叫 委託類介面 的方法,完成午餐點外賣這個操作

public static void main(String[] args) {
	    Roommate roommate = new Roommate("zhangsan");
        Hungry proxyInstance = (Hungry) Proxy.newProxyInstance(
                roommate.getClass().getClassLoader(),
                roommate.getClass().getInterfaces(),
                new RoommateInvocationHandler<Roommate>(roommate)
        );
        proxyInstance.callLunch();
}
//輸出結果
下單前,我先幫你看下有沒有平臺優惠券吧
好餓,今天午飯點外賣吧

代理例項proxyInstance的型別是Hungry,所以只能呼叫Hungry裡規定的方法。Roommate作為介面實現類,不是來自介面的其他的方法,是無法通過動態代理呼叫的。

可以看到代理例項在呼叫委託類實現的方法時,可以很方便地在呼叫方法的前後執行一些操作,在示例程式碼中則是在呼叫方法前簡單輸出了一行: System.out.println("下單前,我先幫你看下有沒有平臺優惠券吧"),還可以有其他用途,例如記錄這個方法的耗時時間,對方法的引數或者返回結果進行修改等等。這也是Spring,Dagger進行AOP程式設計的原理。

那為什麼繼承InvocationHandler介面和持有委託類引用的RoommateInvocationHandler呼叫來自Hungry介面的callLunch()方法時可以呼叫到委託類對callLunch()的邏輯實現呢,看看它的背後原理:

動態代理的實現原理

Proxy.newProxyInstance() 入手,逐步分析 InvocationHandler 如何建立代理例項和委託例項的關聯:

public static Object newProxyInstance(ClassLoader loader ,  Class<?>[] interfaces,
                               InvocationHandler h) throws IllegalArgumentException {
    	//InvocationHandler必須非空,說明是個重要角色
        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.
         * 利用指定的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);
        }
    }

來到這一步好像就停下了,那麼接下來探究 cl 這個例項建立過程發生了什麼:

在上面示例程式碼main函式的後面接著補充。利用ProxyGenerator.generateProxyClass生成這個動態生成的類檔案,寫入了指定路徑的class檔案內

$Proxy0 是 代理類在系統內部的編號,在示例程式碼只生成了一個代理類所以編號是 $Proxy0

        byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0",Roommate.class.getInterfaces());
        String filePath = "C:\\Users\\ODM\\Desktop\\RoommateProxy.class";
        try(FileOutputStream fos = new FileOutputStream(filePath)) {
            fos.write(classFile);
            fos.flush();
        }catch (IOException e){
            e.printStackTrace();
            System.out.println("error:寫入檔案");
        }

使用反編譯工具,我這裡用的是jd-gui反編譯,這個$Proxy0類,實現了Proxy類,繼承了和委託類相同的介面

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

public final class $Proxy0 extends Proxy implements Hungry{
  private static Method m1;
  private static Method m3; //由下方靜態程式碼塊得知,m3代表callLunch()這一個方法
  private static Method m2;
  private static Method m0;
  
  /*
  * 父類Proxy的構造器,其中 h 屬性為 InvocationHandler引用
  * protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }
  */
  public $Proxy0(InvocationHandler paramInvocationHandler) throws {
    super(paramInvocationHandler);
  }
 //關鍵!可供外界呼叫,方法名與委託類實現介面的方法相同,利用 InvocationHandler呼叫invoke
  public final void callLunch() throws {
    try{
      this.h.invoke(this, m3, null);
      return;
    }
    catch (Error|RuntimeException localError){
      throw localError;
    }
    catch (Throwable localThrowable){
      throw new UndeclaredThrowableException(localThrowable);
    }
  }
    
  public final boolean equals(Object paramObject) throws {}
  public final String toString() throws {...}
  public final int hashCode() throws {...}
  
  static{
    try{
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m3 = Class.forName("proxy_test.Hungry").getMethod("callLunch", new Class[0]);
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      return;
    }
    catch (NoSuchMethodException localNoSuchMethodException){
      throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
    }
    catch (ClassNotFoundException localClassNotFoundException){
      throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
    }
  }
}

事情逐漸明朗起來,從這個動態類的原始碼,可以分析出: $Proxy0 ,在構建這個類時,會呼叫了父類Proxy的構造方法,將InvocationHandler引用傳遞給了父類Proxy的 h 屬性,於是當我們在外界使用 代理例項 呼叫了 callLunch() 這個方法時,就會來到這一句 this.h.invoke(this, m3, null); 由於h屬性其實是InvocationHandler引用,呼叫了它的invoke,也就導致了上面示例程式碼中的RoommateInvocationHandler類的重寫過的invoke方法也就被呼叫了,RoommateInvocationHandler也持有委託類的引用,所以委託類的方法也被呼叫起來了。

Java的繼承機制是單繼承,多介面。代理類因為必須要繼承Proxy類,所以java的動態代理只能對介面進行代理,無法對一個class類進行動態代理。

動態代理原理總結

用大白話的方式講:

有一個類InvocationHandler,它的性質類似一箇中介,中介類構建時持有了委託物件,所以可以在它的invoke方法中呼叫了委託物件實現介面的具體方法。當外部呼叫這個InvocationHandler的invoke方法時,對 invoke 的呼叫最終都轉為對委託物件的方法呼叫。

建立明面上負責代理的代理例項時,在記憶體中動態生成的類不但繼承了Proxy,也實現了與委託物件相同的介面,因此代理例項可以呼叫此介面的方法,然後通過持有的中介類物件來呼叫中介類物件的invoke方法,最終達到代理例項執行了委託者的方法。

相關文章