Java架構-Java JDK 動態代理

陌霖Java架構發表於2018-11-11

代理模式是常用的java設計模式,他的特徵是代理類與委託類有同樣的介面,代理類主要負責為委託類預處理訊息、過濾訊息、把訊息轉發給委託類,以及事後處理訊息等。代理類與委託類之間通常會存在關聯關係,一個代理類的物件與一個委託類的物件關聯,代理類的物件本身並不真正實現服務,而是通過呼叫委託類的物件的相關方法,來提供特定的服務。

下圖就是代理模式的UML類圖:

Java架構-Java JDK 動態代理

從圖中可以看出,代理介面(Subject)、代理類(ProxySubject)、委託類(RealSubject)形成一個“品”字結構。 根據代理類的生成時間不同可以將代理分為靜態代理和動態代理兩種。

1)靜態代理:由程式設計師建立或特定工具自動生成原始碼,再對其編譯。在程式執行前,代理類的.class檔案就已經存在了。

2)動態代理:在程式執行時,運用反射機制動態建立而成。

1、靜態代理

由程式設計師建立或工具生成代理類的原始碼,再編譯代理類。所謂靜態也就是在程式執行前就已經存在代理類的位元組碼檔案,代理類和委託類的關係在執行前就確定了。

介面 :

/**
 * 定義一個介面
 * Created by Carl on 2016/8/14.
 */
public interface BookService {

    void buyBook();

}
複製程式碼

實現類:

/**
 * 實現類
 * Created by Carl on 2016/8/14.
 */
public class BookServiceImpl implements BookService {
    @Override
    public void buyBook() {
        System.out.println("買一本書...");
    }
}
複製程式碼

代理類:

/**
 * 代理類
 * Created by Carl on 2016/8/14.
 */
public class BookServiceProxy implements BookService {

    private BookService bookService;

    public BookServiceProxy(BookService bookService){
        this.bookService = bookService;
    }

    @Override
    public void buyBook() {
        prepareMoneyForBuyBook();
        bookService.buyBook();
        readBookAfterBuy();
    }

    private void prepareMoneyForBuyBook(){
        System.out.println("為買本準備好錢...");
    }

    private void readBookAfterBuy(){
        System.out.println("終於可以看自己喜歡的書了...");
    }
}
複製程式碼

Client類:

/**
 * Created by Carl on 2016/8/14.
 */
public class StaticProxyTest {

    public static void main(String[] args) {
        BookService bookService = new BookServiceProxy(new BookServiceImpl());
        bookService.buyBook();
    }

}
複製程式碼

執行結果如下所示:

Java架構-Java JDK 動態代理

靜態代理類優缺點

優點:

業務類只需要關注業務邏輯本身,保證了業務類的重用性。這是代理的共有優點。

缺點:

1)代理物件的一個介面只服務於一種型別的物件,如果要代理的方法很多,勢必要為每一種方法都進行代理,靜態代理在程式規模稍大時就無法勝任了。 2)如果介面增加一個方法,除了所有實現類需要實現這個方法外,所有代理類也需要實現此方法。增加了程式碼維護的複雜度。

2、動態代理實現示例

建立自己的呼叫處理器

/**
 * 動態代理類對應的呼叫處理程式類
 * Created by Carl on 2016/8/14.
 */
public class SubjectInvocationHandler implements InvocationHandler {

    private Object delegate;

    public SubjectInvocationHandler(Object bookService){
        this.delegate = bookService;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        prepareMoneyForBuyBook();
        Object obj = method.invoke(delegate, args);
        readBookAfterBuy();
        return obj;
    }

    private void prepareMoneyForBuyBook(){
        System.out.println("為買本準備好錢...");
    }

    private void readBookAfterBuy(){
        System.out.println("終於可以看自己喜歡的書了...");
    }
}
複製程式碼

客戶端程式碼:

/**
 * 動態代理客戶類 並生成代理類的二進位制檔案用於下面分析
 * Created by Carl on 2016/8/14.
 */
public class DynamicProxyTest {

    public static void main(String[] args) {
        BookService bookService = new BookServiceImpl();
        BookService dynamicProxy = (BookService) Proxy.newProxyInstance(BookService.class.getClassLoader(),
                new Class[]{BookService.class}, new SubjectInvocationHandler(bookService));
        dynamicProxy.buyBook();

        // write proxySubject class binary data to file
        createProxyClassFile();
    }

    public static void createProxyClassFile()
    {
        String name = "ProxySubject";
        byte[] data = ProxyGenerator.generateProxyClass( name, new Class[] { Subject.class } );
        try
        {
            FileOutputStream out = new FileOutputStream( name + ".class" );
            out.write( data );
            out.close();
        }
        catch( Exception e )
        {
            e.printStackTrace();
        }
    }

}
複製程式碼

3、動態代理相關API

動態代理類的原始碼是在程式執行期間由JVM根據反射等機制動態的生成,所以不存在代理類的位元組碼檔案。代理類和委託類的關係是在程式執行時確定。

先看看與動態代理緊密關聯的Java API。

1)java.lang.reflect.Proxy 這是 Java 動態代理機制生成的所有動態代理類的父類,它提供了一組靜態方法來為一組介面動態地生成代理類及其物件。

Proxy類的靜態方法

// 方法 1: 該方法用於獲取指定代理物件所關聯的呼叫處理器  
static InvocationHandler getInvocationHandler(Object proxy)   

// 方法 2:該方法用於獲取關聯於指定類裝載器和一組介面的動態代理類的類物件  
static Class getProxyClass(ClassLoader loader, Class[] interfaces)   

// 方法 3:該方法用於判斷指定類物件是否是一個動態代理類  
static boolean isProxyClass(Class cl)   

// 方法 4:該方法用於為指定類裝載器、一組介面及呼叫處理器生成動態代理類例項  
static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)  
複製程式碼

2)java.lang.reflect.InvocationHandler

這是呼叫處理器介面,它自定義了一個 invoke 方法,用於集中處理在動態代理類物件上的方法呼叫,通常在該方法中實現對委託類的代理訪問。每次生成動態代理類物件時都要指定一個對應的呼叫處理器物件。

InvocationHandler的核心方法 :

// 該方法負責集中處理動態代理類上的所有方法呼叫。第一個引數既是代理類例項,第二個引數是被呼叫的方法物件  
// 第三個方法是呼叫引數。呼叫處理器根據這三個引數進行預處理或分派到委託類例項上反射執行  
Object invoke(Object proxy, Method method, Object[] args)  
複製程式碼

3)java.lang.ClassLoader 這是類裝載器類,負責將類的位元組碼裝載到 Java 虛擬機器(JVM)中併為其定義類物件,然後該類才能被使用。Proxy 靜態方法生成動態代理類同樣需要通過類裝載器來進行裝載才能使用,它與普通類的唯一區別就是其位元組碼是由 JVM 在執行時動態生成的而非預存在於任何一個 .class 檔案中。

每次生成動態代理類物件時都需要指定一個類裝載器物件

4、動態代理內部實現

首先來看看類Proxy的程式碼實現 Proxy的主要靜態變數

// 對映表:用於維護類裝載器物件到其對應的代理類快取
private static Map loaderToCache = new WeakHashMap(); 

// 標記:用於標記一個動態代理類正在被建立中
private static Object pendingGenerationMarker = new Object(); 

// 同步表:記錄已經被建立的動態代理類型別,主要被方法 isProxyClass 進行相關的判斷
private static Map proxyClasses = Collections.synchronizedMap(new WeakHashMap()); 

// 關聯的呼叫處理器引用
protected InvocationHandler h;
複製程式碼

Proxy的構造方法

// 由於 Proxy 內部從不直接呼叫建構函式,所以 private 型別意味著禁止任何呼叫
private Proxy() {} 

// 由於 Proxy 內部從不直接呼叫建構函式,所以 protected 意味著只有子類可以呼叫
protected Proxy(InvocationHandler h) {this.h = h;} 
複製程式碼

我們可以先看一看Proxy代理類生成的呼叫過程:

Java架構-Java JDK 動態代理

Proxy靜態方法newProxyInstance

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    // 檢查h不為空,否則拋異常
    Objects.requireNonNull(h);
    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }
    /*
     * 獲得與指定類裝載器和一組介面相關的代理類型別物件
     */
    Class<?> cl = getProxyClass0(loader, intfs);

    /*
     * 呼叫指定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);
    }
}
複製程式碼

類Proxy的getProxyClass方法呼叫ProxyGenerator的generateProxyClass方法產生ProxySubject.class的二進位制資料:

public static byte[] generateProxyClass(final String name, Class[] interfaces)
複製程式碼

我們可以import sun.misc.ProxyGenerator,呼叫 generateProxyClass方法產生binary data,然後寫入檔案,最後通過反編譯工具來檢視內部實現原理。 反編譯後的ProxySubject.java Proxy靜態方法newProxyInstance在上面的動態代理測試類DynamicProxyTest已經生成了ProxySubject這個.class檔案。具體程式碼如下:

import java.lang.reflect.*;   
public final class ProxySubject extends Proxy   
    implements Subject   
{   
    private static Method m1;   
    private static Method m0;   
    private static Method m3;   
    private static Method m2;   
    public ProxySubject(InvocationHandler invocationhandler)   
    {   
        super(invocationhandler);   
    }   
    public final boolean equals(Object obj)   
    {   
        try  
        {   
            return ((Boolean)super.h.invoke(this, m1, new Object[] {   
                obj   
            })).booleanValue();   
        }   
        catch(Error _ex) { }   
        catch(Throwable throwable)   
        {   
            throw new UndeclaredThrowableException(throwable);   
        }   
    }   
    public final int hashCode()   
    {   
        try  
        {   
            return ((Integer)super.h.invoke(this, m0, null)).intValue();   
        }   
        catch(Error _ex) { }   
        catch(Throwable throwable)   
        {   
            throw new UndeclaredThrowableException(throwable);   
        }   
    }   
    public final void buyBook()   
    {   
        try  
        {   
            super.h.invoke(this, m3, null);   
            return;   
        }   
        catch(Error _ex) { }   
        catch(Throwable throwable)   
        {   
            throw new UndeclaredThrowableException(throwable);   
        }   
    }   
    public final String toString()   
    {   
        try  
        {   
            return (String)super.h.invoke(this, m2, null);   
        }   
        catch(Error _ex) { }   
        catch(Throwable throwable)   
        {   
            throw new UndeclaredThrowableException(throwable);   
        }   
    }   
    static    
    {   
        try  
        {   
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {   
                Class.forName("java.lang.Object")   
            });   
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);   
            m3 = Class.forName("Subject").getMethod("buyBook", new Class[0]);   
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);   
        }   
        catch(NoSuchMethodException nosuchmethodexception)   
        {   
            throw new NoSuchMethodError(nosuchmethodexception.getMessage());   
        }   
        catch(ClassNotFoundException classnotfoundexception)   
        {   
            throw new NoClassDefFoundError(classnotfoundexception.getMessage());   
        }   
    }   
}  
複製程式碼

ProxyGenerator內部是如何生成class二進位制資料,可以參考原始碼。

private byte[] generateClassFile() {   
  /*  
   * Record that proxy methods are needed for the hashCode, equals,  
   * and toString methods of java.lang.Object.  This is done before  
   * the methods from the proxy interfaces so that the methods from  
   * java.lang.Object take precedence over duplicate methods in the  
   * proxy interfaces.  
   */  
  addProxyMethod(hashCodeMethod, Object.class);   
  addProxyMethod(equalsMethod, Object.class);   
  addProxyMethod(toStringMethod, Object.class);   
  /*  
   * Now record all of the methods from the proxy interfaces, giving  
   * earlier interfaces precedence over later ones with duplicate  
   * methods.  
   */  
  for (int i = 0; i < interfaces.length; i++) {   
      Method[] methods = interfaces[i].getMethods();   
      for (int j = 0; j < methods.length; j++) {   
    addProxyMethod(methods[j], interfaces[i]);   
      }   
  }   
  /*  
   * For each set of proxy methods with the same signature,  
   * verify that the methods' return types are compatible.  
   */  
  for (List<ProxyMethod> sigmethods : proxyMethods.values()) {   
      checkReturnTypes(sigmethods);   
  }   
  /* ============================================================  
   * Step 2: Assemble FieldInfo and MethodInfo structs for all of  
   * fields and methods in the class we are generating.  
   */  
  try {   
      methods.add(generateConstructor());   
      for (List<ProxyMethod> sigmethods : proxyMethods.values()) {   
    for (ProxyMethod pm : sigmethods) {   
        // add static field for method's Method object   
        fields.add(new FieldInfo(pm.methodFieldName,   
      "Ljava/lang/reflect/Method;",   
       ACC_PRIVATE | ACC_STATIC));   
        // generate code for proxy method and add it   
        methods.add(pm.generateMethod());   
    }   
      }   
      methods.add(generateStaticInitializer());   
  } catch (IOException e) {   
      throw new InternalError("unexpected I/O Exception");   
  }   
  /* ============================================================  
   * Step 3: Write the final class file.  
   */  
  /*  
   * Make sure that constant pool indexes are reserved for the  
   * following items before starting to write the final class file.  
   */  
  cp.getClass(dotToSlash(className));   
  cp.getClass(superclassName);   
  for (int i = 0; i < interfaces.length; i++) {   
      cp.getClass(dotToSlash(interfaces[i].getName()));   
  }   
  /*  
   * Disallow new constant pool additions beyond this point, since  
   * we are about to write the final constant pool table.  
   */  
  cp.setReadOnly();   
  ByteArrayOutputStream bout = new ByteArrayOutputStream();   
  DataOutputStream dout = new DataOutputStream(bout);   
  try {   
      /*  
       * Write all the items of the "ClassFile" structure.  
       * See JVMS section 4.1.  
       */  
          // u4 magic;   
      dout.writeInt(0xCAFEBABE);   
          // u2 minor_version;   
      dout.writeShort(CLASSFILE_MINOR_VERSION);   
          // u2 major_version;   
      dout.writeShort(CLASSFILE_MAJOR_VERSION);   
      cp.write(dout);   // (write constant pool)   
          // u2 access_flags;   
      dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);   
          // u2 this_class;   
      dout.writeShort(cp.getClass(dotToSlash(className)));   
          // u2 super_class;   
      dout.writeShort(cp.getClass(superclassName));   
          // u2 interfaces_count;   
      dout.writeShort(interfaces.length);   
          // u2 interfaces[interfaces_count];   
      for (int i = 0; i < interfaces.length; i++) {   
    dout.writeShort(cp.getClass(   
        dotToSlash(interfaces[i].getName())));   
      }   
          // u2 fields_count;   
      dout.writeShort(fields.size());   
          // field_info fields[fields_count];   
      for (FieldInfo f : fields) {   
    f.write(dout);   
      }   
          // u2 methods_count;   
      dout.writeShort(methods.size());   
          // method_info methods[methods_count];   
      for (MethodInfo m : methods) {   
    m.write(dout);   
      }   
             // u2 attributes_count;   
      dout.writeShort(0); // (no ClassFile attributes for proxy classes)   
  } catch (IOException e) {   
      throw new InternalError("unexpected I/O Exception");   
  }   
  return bout.toByteArray(); 
複製程式碼

5、總結

一個典型的動態代理建立物件過程可分為以下四個步驟:

  1. 通過實現InvocationHandler介面建立自己的呼叫處理器 IvocationHandler handler = new InvocationHandlerImpl(…);
  2. 通過為Proxy類指定ClassLoader物件和一組interface建立動態代理類 Class clazz = Proxy.getProxyClass(classLoader,new Class[]{…});
  3. 通過反射機制獲取動態代理類的建構函式,其引數型別是呼叫處理器介面型別 Constructor constructor = clazz.getConstructor(new Class[]{InvocationHandler.class});
  4. 通過建構函式建立代理類例項,此時需將呼叫處理器物件作為引數被傳入 Interface Proxy = (Interface)constructor.newInstance(new Object[] (handler)); 為了簡化物件建立過程,Proxy類中的newInstance方法封裝了2~4,只需兩步即可完成代理物件的建立。 生成的ProxySubject繼承Proxy類實現Subject介面,實現的Subject的方法實際呼叫處理器的invoke方法,而invoke方法利用反射呼叫的是被代理物件的的方法(Object result=method.invoke(proxied,args))

簡化後的動態代理實現

// InvocationHandlerImpl 實現了 InvocationHandler 介面,並能實現方法呼叫從代理類到委託類的分派轉發  
InvocationHandler handler = new InvocationHandlerImpl(..);   

// 通過 Proxy 直接建立動態代理類例項  
Interface proxy = (Interface)Proxy.newProxyInstance( classLoader,   
     new Class[] { Interface.class },  handler );  
複製程式碼

6、美中不足

誠然,Proxy已經設計得非常優美,但是還是有一點點小小的遺憾之處,那就是它始終無法擺脫僅支援interface代理的桎梏,因為它的設計註定了這個遺憾。

回想一下那些動態生成的代理類的繼承關係圖,它們已經註定有一個共同的父類叫Proxy。Java的繼承機制註定了這些動態代理類們無法實現對class的動態代理,原因是多繼承在Java中本質上就行不通。有很多條理由,人們可以否定對class代理的必要性,但是同樣有一些理由,相信支援class動態代理會更美好。

介面和類的劃分,本就不是很明顯,只是到了Java中才變得如此的細化。如果只從方法的宣告及是否被定義來考量,有一種兩者的混合體,它的名字叫抽象類。實現對抽象類的動態代理,相信也有其內在的價值。此外,還有一些歷史遺留的類,它們將因為沒有實現任何介面而從此與動態代理永世無緣。如此種種,不得不說是一個小小的遺憾。但是,不完美並不等於不偉大,偉大是一種本質,Java動態代理就是佐例。

為什麼某些人會一直比你優秀,是因為他本身就很優秀還一直在持續努力變得更優秀,而你是不是還在滿足於現狀內心在竊喜! 

合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!
複製程式碼
To-陌霖Java架構

分享網際網路最新文章 關注網際網路最新發展
複製程式碼

相關文章