1. 程式人生 > >Java,JDK動態代理的原理分析

Java,JDK動態代理的原理分析

1. 代理基本概念:

  以下是代理概念的百度解釋:代理(百度百科)

       總之一句話:三個元素,資料--->代理物件--->真實物件;複雜一點的可以理解為五個元素:輸入資料--->代理物件--->真實物件--->代理物件--->輸出資料。

2. JDK的動態代理概念:

  JDK的動態代理和正常的代理邏輯有些區別。

  首先先明確一下術語:類 class ,介面 interface。

  JDK動態代理是基於 interface 建立的,而不是真正的物件;也就是說,即使沒有真正的物件,JDK依然可以建立代理物件。下面用程式碼來解釋:

public class JDKProxy implements InvocationHandler{
    public Object getObject(TestInterface ref){
        return Proxy.newProxyInstance(getClass().getClassLoader(), ref.getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable { return null; } }

  當然,實際中使用的情況是有真正的物件的,像下面這樣: 

public class JDKProxy implements InvocationHandler{
    TestInterface ref;
    public Object getObject(TestInterface ref){
        this.ref = ref;
        return Proxy.newProxyInstance(getClass().getClassLoader(), ref.getClass().getInterfaces(), this
); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("doBefore"); Object o = method.invoke(ref, args); System.out.println("doAfter"); return o; } }

  那麼,和正常的代理邏輯區別就在這裡了,JDK的動態代理多依賴一個元素,就是被代理物件ref所實現的介面。如果ref物件沒有實現任何介面,那麼這個物件是無法被代理的。 

  那麼問題來了: 為什麼Java自帶的動態代理 選擇 要基於介面 ?基於什麼考慮,或者說Java如果 選擇 直接 代理真正的物件會有什麼問題?

3. 進入正題:JDK動態代理是如何實現的?(基於JDK1.8)

  Java中涉及到的關鍵先生:InvocationHandler , Proxy

  3.1 使用方法及引數詳細解釋

    程式碼使用方法:

     

//代理類,實現InvocationHandler
public
class JDKProxy implements InvocationHandler{ private UserService userServiceRef;
   //獲取代理物件
public UserService getProxy(UserService userServiceRef){ this.userServiceRef = userServiceRef;
     //Proxy生成代理物件
return (UserService) Proxy.newProxyInstance(getClass().getClassLoader(), userServiceRef.getClass().getInterfaces(), this); }
   //代理物件做的事 @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("someone is logining"); Object returnObject = method.invoke(userServiceRef, args); System.out.println("someone login success"); return returnObject; } }  

    下面是InvocationHandler的介面描述:

   package java.lang.reflect;
   public interface InvocationHandler {
       public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
   }

    其中的引數:

      proxy: 代理物件本身,也就是 getProxy(UserService userServiceRef) 獲取到的物件。大家思考一下,為什麼要把這個代理物件作為引數傳進來?

              我個人覺得這是個完全沒有必要的引數。

      method:userServiceRef中的方法。

      args: method方法的引數。

    再來看一下Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)方法引數:

      loader:類載入器,用來載入代理類的,即Proxy.newProxyInstance()的返回結果的類位元組碼。

      interfaces:代理物件所實現的介面。 這裡介面是陣列引數,通常被代理只實現一個介面。那實現多個介面時使用代理物件有什麼問題?其實也沒問題,就是呼叫不同介面的方法前需要先強轉為對應的介面類,麻煩。

      h:實現InvocationHandler的類,也就是示例程式碼中的JDKProxy類 。

    測試程式碼:

    public static void main(String[] args) {
          UserService userService = new UserServiceImpl();
          JDKProxy proxy = new JDKProxy();
          UserService userServiceProxy = proxy.getProxy(userService);
          userServiceProxy.login();
      }

  3.2 實現的原理與細節:

      1.代理物件的建立過程:

    建立代理物件的方法:Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

    通過這個方法的引數其實可以看到一些眉目,loader用來載入代理類位元組碼,interfaces作為代理類實現的介面,h為代理物件實際呼叫的方法(即invoke方法)。

    建立過程大致分為幾步:

    • 從快取中獲取代理物件,獲取到則直接返回;

      快取由WeakCache中的ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map來儲存;

      為什麼是兩級Map? 想一想,Java類的唯一性由ClassLoader+Class決定,所以Key Object是ClassLoader,Key Object是所有介面組成的物件().

       WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory()); 

      其中KeyFactory的作用就是將interfaces轉換為Key Object

    • 生成代理物件的類名proxyName;

      由 private static final class ProxyClassFactory 來完成.

      private static final String proxyClassNamePrefix = "$Proxy"; //ProxyClassFactory
      // 每次使用時 自增1
      private static final AtomicLong nextUniqueNumber = new AtomicLong();//ProxyClassFactory
      ReflectUtil.PROXY_PACKAGE = "com.sun.proxy";//RelectionUtil

      proxyName = PROXY_PACKAGE + proxyClassNamePrefix nextUniqueNumber

    • 生成proxyName類的位元組碼;

      由 sun.misc.ProxyGenerator.generateProxyClass(String proxyName, Class<?>[] interfaces, int accessFlags) ,方法返回二進位制位元組碼。

      引數: 類名,需要實現的介面,訪問標誌。

    • 將位元組碼載入到虛擬機器中,即方法區記憶體(jdk1.7之前是永久代,jdk8之後的元資料區);

      由 java.lang.reflect.Proxy.defineClass0(ClassLoader loader, String proxyName, byte[] proxyClass, int offset, int length) 完成。

       private static native Class<?> defineClass0(ClassLoader loader, String name, byte[] b, int off, int len); 

      這是一個本地方法,通過JNI呼叫,返回代理的Class物件。

    • 生成代理Class物件的例項;構造器例項化

      生成代理物件的三個引數中的 interfaces, classLoader都使用過了,還有一個InvocationHandler 沒有使用。

      通過反射獲取代理Class的引數為InvocationHandler的構造器,通過 Constructor.newInstance(new Object[]{h}); 返回最後的代理例項物件。

    建立代理的過程就完成了。

      2. 代理物件的位元組碼分析:

    還是以上面3.1的例子分析,測試程式碼稍作修改,如下:

   public static void main(String[] args) {
       System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); //生成的代理物件位元組碼儲存到.class檔案中。
          JDKProxy proxy = new JDKProxy();
         proxy.getProxy(new UserServiceImpl()); //生成代理物件
     }

    執行測試程式碼之後,user.dir目錄下會多出一個目錄:com/sun/proxy,開啟後可以看到$Proxy0.class檔案。

    jd-gui反編譯該class檔案:為方便閱讀,我把方法裡的try catch全部移掉了。

   package com.sun.proxy;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.lang.reflect.UndeclaredThrowableException;
    import myproxy.UserService;
    public final class $Proxy0 extends Proxy implements UserService {
      private static Method m1;
      private static Method m2;
      private static Method m3;
      private static Method m0;
      
      public $Proxy0(InvocationHandler paramInvocationHandler){
        super(paramInvocationHandler); //h引用在父類Proxy中
      }
      public final boolean equals(Object paramObject){
          return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
      }
      public final String toString(){
          return (String)this.h.invoke(this, m2, null);
      }
      public final boolean login(){
          return ((Boolean)this.h.invoke(this, m3, null)).booleanValue(); //boolean存在包裝和解包裝
      }
      public final int hashCode(){
          return ((Integer)this.h.invoke(this, m0, null)).intValue();
      } 
      static{
          m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
          m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
          m3 = Class.forName("myproxy.UserService").getMethod("login", new Class[0]);
          m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
          return;
      }
    }

    一目瞭然。主要五部分內容:

    代理物件繼承了Proxy類,並實現了目標介面。

    生成了以InvocationHandler為引數的構造器,例項化時將我們的自定JDKProxy(實現了InvocationHandler,並持有被代理物件)傳遞進去;

    生成了Object中的三個方法:equals, hashCode, toString;

    生成了介面中的所有方法,全部呼叫InvocationHandler物件的invoke方法;

    生成了對應方法的Method物件屬性,傳遞給invoke方法。

  3 .提示細節:

    • 對於有參和無參方法,都是通過invoke方法呼叫,無參方法會直接傳入null,所以在invoke方法中使用args引數時一定要先進行null的判斷;
    • 對於原始資料型別(int,boolean等8種),代理物件的方法中引數和返回值都進行了包裝和解包裝。
    • 代理物件生成過程中用到了反射,生成位元組碼時,反射Object物件方法和反射介面方法;生成例項時,反射獲取代理物件的構造器;代理物件方法呼叫過程中是沒有使用反射的。
    • 有沒有感覺跟 裝飾器模式 有一些 異曲同工 呢?

4. 以上就是個人總結分享的JDK動態代理的內容,原創內容,轉載請註明出處。

 

個人水平有限,有誤的地方歡迎評論中指正。

  

 

    

  

    

 

  

byte[]