首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 動態代理機制分析與擴展(第 2 部分):突破介面限制,支援類別代理

     編輯:Java 相關知識

本文旨在將 Java 動態代理機制從介面擴展至類別,使類別也能享有類似介面的動態代理支援。

設計與特點

新擴展類別命名為 ProxyEx,直接繼承 java.lang.reflect.Proxy,並宣告與原 Proxy 類別同名的 public 靜態方法,目的在保持與原代理機制使用方法完全一致。

ProxyEx 類別繼承圖

與原代理機制最大差別在於:動態產生的代理類別不再從 Proxy 繼承,改為繼承被代理類別。因 Java 單一繼承原則,擴展代理機制支援類別數目不超過一個,但可宣告實作多個介面。套件管理機制與原類似:不支援超過一個類別與介面同時為非 public;若僅有一個非 public 類別或介面,假設其套件為 PackageA,動態產生代理類別將位於 PackageA;否則位於被代理類別所在套件。產生代理類別同樣賦予 final 與 public 存取屬性,命名規則類似為「父類別名 + ProxyN」(N 為遞增阿拉伯數字)。異常處理與原機制完全一致。

動態產生代理類別繼承圖

模板

透過對 Java 動態代理機制推演,獲得通用方法模板。預期透過模板定製與引導代理類別程式碼生成是可行方法。主要使用兩個模板:類別模板與方法模板。

類別模板

package &Package;
final public class &Name &Extends &Implements
{
    private java.lang.reflect.InvocationHandler handler = null;
    &Constructors
    &Methods
}

類別模板定製代理類別程式碼框架。帶「&」前綴標籤位用於引導相應程式碼替換。預留套件(&Package)、類別名(&Name)、類別繼承(&Extends)、介面實作(&Implements)、建構子集(&Constructors)、方法集(&Methods)標籤位。類別模板同時宣告私有型別調用處理器物件作為類別成員。

方法模板

&Modifiers &ReturnType &MethodName (&Parameters) &Throwables
{
    java.lang.reflect.Method method = null;
    try {
        method = &Class.getMethod( "&MethodName", &ParameterTypes );
    }
    catch(Exception e){
    }
    Object r = null;
    try{
        r = handler.invoke( this, method, &ParameterValues );
    }&Exceptions
    &Return
}

方法模板定製代理類別方法集合中各方法程式碼框架,同樣帶「&」前綴標籤位引導替換。預留修飾詞(&Modifiers)、回傳型別(&ReturnType)、方法名(&MethodName)、參數列表(&Parameters)、異常列表(&Throwables)、方法宣告類別(&Class)、參數型別列表(&ParameterTypes)、調用處理器參數值列表(&ParameterValues)、異常處理(&Exceptions)、回傳值(&Return)標籤位。

程式碼生成

有了類別與方法模板,程式碼生成過程有章可循。基本過程分三步:1)生成代理類別方法集;2)生成代理類別建構子;3)生成完整代理類別。

生成代理類別方法集

第一步,透過反射取得被代理類別所有 public 或 protected 且非 static 的 Method 物件列表,這些方法將被涵蓋因它們可被其他類別存取。

第二步,遍歷 Method 物件列表,對每個物件進行程式碼生成工作。

String declTemplate = "&Modifiers &ReturnType &MethodName(&Parameters) &Throwables";
String bodyTemplate = "&Declaration &Body";
// 方法宣告
String declare = declTemplate.replaceAll("&Modifiers", getMethodModifiers(method))
    .replaceAll("&ReturnType", getMethodReturnType(method))
    .replaceAll("&MethodName", method.getName())
    .replaceAll("&Parameters", getMethodParameters(method))
    .replaceAll("&Throwables", getMethodThrowables(method));
// 方法宣告與實作
String body = bodyTemplate.replaceAll("&Declaration", declare)
    .replaceAll("&Body", getMethodEntity(method));

涉及 ProxyEx 私有輔助函數如 getMethodModifiers、getMethodReturnType 等,透過反射取得所需資訊動態生成各部分程式碼。getMethodEntity 較重要,調用其他輔助函數生成程式碼並替換標籤位。

private static String getMethodEntity(Method method) {
    String template = "\n{"
        + "\n java.lang.reflect.Method method = null;"
        + "\n try{"
        + "\n method = &Class.getMethod( \"&MethodName\", &ParameterTypes );"
        + "\n }"
        + "\n catch(Exception e){"
        + "\n }"
        + "\n Object r = null;"
        + "\n try{"
        + "\n r = handler.invoke( this, method, &ParameterValues );"
        + "\n }&Exceptions"
        + "\n &Return"
        + "\n}";
    String result = template.replaceAll("&MethodName", method.getName())
        .replaceAll("&Class", method.getDeclaringClass().getName() + ".class")
        .replaceAll("&ParameterTypes", getMethodParameterTypesHelper(method))
        .replaceAll("&ParameterValues", getMethodParameterValuesHelper(method))
        .replaceAll("&Exceptions", getMethodParameterThrowablesHelper(method))
        .replaceAll("&Return", getMethodReturnHelper(method));
    return result;
}

為 Class 型別物件生成對應字元程式碼時可能涉及陣列型別,推演過程需遞歸生成程式碼,此部分由 getTypeHelper 方法提供:

private static String getTypeHelper(Class type) {
    if (type.isArray()) {
        Class c = type.getComponentType();
        return getTypeHelper(c) + "[]";
    } else {
        return type.getName();
    }
}

第三步,將生成方法儲存進 Map 表,記錄鍵值對(方法宣告、方法實作)。因類別多型性,父類別方法可能被子類別覆寫,遍歷所得方法列表會出現重複方法物件,維護此表自然達到避免重複生成目的。

生成代理類別建構子

代理類別透過建構子反射生成,建構時傳入唯一參數為調用處理器物件。為保持一致性,新代理類別建構子同樣只有一個調用處理器參數。模板簡單如下:

public &Constructor(java.lang.reflect.InvocationHandler handler)
{
    super(&Parameters);
    this.handler = handler;
}

特別說明 super 方法參數值列表 &Parameters 生成:借鑑 Mock 思想,側重追求物件建構成功,未過多分析尋求最準確最有意義賦值。讀者可能產生疑慮,稍後會提及改進方法。

生成完整代理類別

透過上述步驟,建構子與所有需代理方法程式碼已生成,接著生成完整代理類別。過程直觀:取得相關資訊對類別模板各標籤位替換,輕鬆完成代理類別程式碼生成。

被遺忘的角落:類別變數

似乎遺忘了什麼?從呼叫者角度,期望代理類別能作為被代理類別如實代表呈現,包含內部狀態,這些狀態通常由類別變數體現,涉及類別變數代理問題。

解決此問題,首先思考何時兩者類別變數可能出現不一致?回答此問題即找到解決思路。回顧代理類別建構子,我們以粗糙方式建構代理類別實例,可能一開始就不一致。每次方法呼叫也可能導致兩者類別變數不一致。如何解決?直觀想法:1)建構時需設法同步;2)方法呼叫前後也需同步。這樣能有效避免代理與被代理類別變數不一致問題。

但如何取得被代理類別實例?目前設計無法做到。既然如此,繼續擴展之旅。這次擴展對象是調用處理器介面,在擴展介面加入取得被代理類別物件方法,且擴展調用處理器介面以 static 與 public 形式定義在 ProxyEx 類別中。

public static interface InvocationHandlerEx extends InvocationHandler {
    // 回傳指定 stubClass 參數對應被代理類別實體物件
    Object getStub(Class stubClass);
}

新調用處理器介面具備取得被代理類別物件能力,為實現類別變數同步打開通道。接下來需執行類別變數同步的 sync 方法,每個動態產生代理類別都會悄悄加入此私有方法供呼叫。每次方法分派轉發至調用處理器執行前後,sync 方法都會被呼叫,保證類別變數雙向即時更新。相應方法模板需更新支援新特性。

Object r = null;
try{
    // 代理類別到被代理類別方向變數同步
    sync(&Class, true);
    r = handler.invoke( this, method, &ParameterValues );
    // 被代理類別到代理類別方向變數同步
    sync(&Class, false);
}&Exceptions
&Return

sync 方法還會在建構子尾部被呼叫,將被代理類別物件變數資訊同步至代理類別物件,實現類似複製建構子等價效果。建構子模板相應更新:

public &Name(java.lang.reflect.InvocationHandler handler)
{
    super(&Parameters);
    this.handler = handler;
    // 被代理類別到代理類別方向變數同步
    sync(null, false);
}

sync 方法實作

思想:首先取得被代理類別所有 Field 物件列表,透過擴展調用處理器取得方法宣告類別對應 stub 物件,遍歷 Field 列表對各變數執行複製同步。

private synchronized void sync(java.lang.Class clazz, boolean toStub) {
    // 判斷是否為擴展調用處理器
    if (handler instanceof InvocationHandlerEx) {
        java.lang.Class superClass = this.getClass().getSuperclass();
        java.lang.Class stubClass = (clazz != null ? clazz : superClass);
        // 透過擴展調用處理器取得 stub 物件
        Object stub = ((InvocationHandlerEx)handler).getStub(stubClass);
        if (stub != null) {
            // 取得所有需同步類別成員列表,遍歷並同步
            java.lang.reflect.Field[] fields = getFields(superClass);
            for (int i = 0; fields != null && i < fields.length; i++) {
                try {
                    fields[i].setAccessible(true);
                    // 執行代理與被代理類別變數同步
                    if (toStub) {
                        fields[i].set(stub, fields[i].get(this));
                    } else {
                        fields[i].set(this, fields[i].get(stub));
                    }
                } catch (Throwable e) {}
            }
        }
    }
}

涉及取得類別所有 Field 物件列表靜態輔助方法 getFields。為提高頻繁查詢效能,配合靜態 fieldsMap 物件記錄已查詢類別所含 Field 列表,再次查詢能迅速回傳對應列表。類別模板相應更新:

package &Package;
final public class &Name &Extends &Implements
{
    private static java.util.HashMap fieldsMap = new java.util.HashMap();
    private java.lang.reflect.InvocationHandler handler = null;
    &Constructors
    &Methods
}
private static java.lang.reflect.Field[] getFields(java.lang.Class c) {
    if (fieldsMap.containsKey(c)) {
        return (java.lang.reflect.Field[])fieldsMap.get(c);
    }
    java.lang.reflect.Field[] fields = null;
    if (c == java.lang.Object.class) {
        fields = c.getDeclaredFields();
    } else {
        java.lang.reflect.Field[] fields0 = getFields(c.getSuperclass());
        java.lang.reflect.Field[] fields1 = c.getDeclaredFields();
        fields = new java.lang.reflect.Field[fields0.length + fields1.length];
        System.arraycopy(fields0, 0, fields, 0, fields0.length);
        System.arraycopy(fields1, 0, fields, fields0.length, fields1.length);
    }
    fieldsMap.put(c, fields);
    return fields;
}

動態編譯與載入

程式碼生成後需經編譯產生 JVM 可識別位元組碼,位元組碼再透過類別載入器載入 JVM 才能真正使用。闡述動態編譯與載入。

動態編譯由 ProxyEx 類別 getProxyClassCodeSource 函數完成,分三步:1)儲存原始碼至 .java 檔;2)編譯該檔案;3)從輸出 .class 檔讀取位元組碼。

private static byte[] getProxyClassCodeSource(String pkg, String className, String declare) throws Exception {
    // 將類別原始碼儲存進名為類別名加 ".java" 本地檔案
    File source = new File(className + ".java");
    FileOutputStream fos = new FileOutputStream(source);
    fos.write(declare.getBytes());
    fos.close();
    // 呼叫 com.sun.tools.javac.Main 靜態方法 compile 進行動態編譯
    int status = com.sun.tools.javac.Main.compile(new String[] {
        "-d", ".", source.getName() });
    if (status != 0) {
        source.delete();
        throw new Exception("Compiler exit on " + status);
    }
    // 編譯位元組碼輸出至與套件結構相同本地目錄,檔名為類別名加 ".class"
    String output = ".";
    int curIndex = -1, lastIndex = 0;
    while ((curIndex = pkg.indexOf('.', lastIndex)) != -1) {
        output = output + File.separator + pkg.substring(lastIndex, curIndex);
        lastIndex = curIndex + 1;
    }
    output = output + File.separator + pkg.substring(lastIndex);
    output = output + File.separator + className + ".class";
    // 從輸出檔讀取位元組碼存入位元組陣列
    File target = new File(output);
    FileInputStream f = new FileInputStream(target);
    byte[] codeSource = new byte[(int)target.length()];
    f.read(codeSource);
    f.close();
    // 刪除暫存檔
    source.delete();
    target.delete();
    return codeSource;
}

取得代理類別位元組碼,接著動態載入該類別。由 ProxyEx 類別 defineClassHelper 函數完成,分兩步:1)透過反射取得父類別 Proxy 私有靜態方法 defineClass0;2)傳入位元組碼陣列與其他相關資訊反射呼叫該方法完成類別動態載入。

private static Class defineClassHelper(String pkg, String cName, byte[] codeSource) throws Exception {
    Method defineClass = Proxy.class.getDeclaredMethod("defineClass0",
        new Class[] { ClassLoader.class, String.class, byte[].class, int.class, int.class });
    defineClass.setAccessible(true);
    return (Class)defineClass.invoke(Proxy.class,
        new Object[] { ProxyEx.class.getClassLoader(),
            pkg.length()==0 ? cName : pkg+"."+cName,
            codeSource, new Integer(0), new Integer(codeSource.length) });
}

效能改進

原動態代理機制中對介面陣列有有趣特點:介面順序差異在一定程度上導致生成新代理類別,即使實無必要。原因是快取表以介面名稱列表為關鍵字,不同順序意味不同關鍵字,若對應關鍵字不存在會生成新但作用重複代理類別。ProxyEx 類別透過主動排序避免類似問題,提高動態生成代理類別效率。且若發現陣列全為介面型別,直接呼叫父類別 Proxy 靜態方法 getProxyClass 生成代理類別,否則透過擴展動態代理機制生成,這也一定程度上改進效能。

相容性問題

需考慮與原代理機制相容性。Proxy 還有兩個靜態方法:isProxyClass 與 getInvocationHandler,分別用於判斷 Class 物件是否為動態代理類別、從 Object 物件取得對應調用處理器(若可能)。

static boolean isProxyClass(Class cl)
static InvocationHandler getInvocationHandler(Object proxy)

相容性問題主要涉及 ProxyEx 與父類別 Proxy 關於動態生成代理類別資訊如何保持同步。Proxy 類別有私有 Map 物件 proxyClasses 專門保存所有動態生成代理類別型別。isProxyClass 透過查詢該表確定某 Class 物件是否動態代理類別,需做的就是將 ProxyEx 生成代理類別型別也保存入該表。此工作由 ProxyEx 靜態方法 addProxyClass 輔助完成。

private static void addProxyClass(Class proxy) throws IllegalArgumentException {
    try {
        // 透過反射取得父類別私有 proxyClasses 變數並更新
        Field proxyClasses = Proxy.class.getDeclaredField("proxyClasses");
        proxyClasses.setAccessible(true);
        ((Map)proxyClasses.get(Proxy.class)).put(proxy, null);
    } catch (Exception e) {
        throw new IllegalArgumentException(e.toString());
    }
}

相對而言,Proxy 靜態方法 getInvocationHandler 實作相當簡單:先判斷是否代理類別,若是則直接型別轉換至 Proxy 回傳調用處理器成員。擴展代理類別不從 Proxy 繼承,故取得調用處理器物件方法需調整。由 ProxyEx 同名靜態方法 getInvocationHandler 完成。

public static InvocationHandler getInvocationHandler(Object proxy) throws IllegalArgumentException {
    // 若 Proxy 實例,直接呼叫父類別方法
    if (proxy instanceof Proxy) {
        return Proxy.getInvocationHandler(proxy);
    }
    // 若非代理類別,拋異常
    if (!Proxy.isProxyClass(proxy.getClass())) {
        throw new IllegalArgumentException("Not a proxy instance");
    }
    try {
        // 透過反射取得擴展代理類別調用處理器物件
        Field invoker = proxy.getClass().getDeclaredField("handler");
        invoker.setAccessible(true);
        return (InvocationHandler)invoker.get(proxy);
    } catch (Exception e) {
        throw new IllegalArgumentException("Suspect not a proxy instance");
    }
}

坦言:也有局限

受限 Java 類別繼承機制,擴展動態代理機制也有局限,不支援:

  • 宣告為 final 的類別
  • 宣告為 final 的函式
  • 建構子均為 private 型別的類別

實例示範

以 2010 世博門票售票代理為模型示範。

定義售票員抽象類別 TicketSeller

public abstract class TicketSeller {
    protected String theme;
    protected TicketSeller(String theme) { this.theme = theme; }
    public String getTicketTheme() { return this.theme; }
    public void setTicketTheme(String theme) { this.theme = theme; }
    public abstract int getTicketPrice();
    public abstract int buy(int ticketNumber, int money) throws Exception;
}

實作 2010 世博門票售票代理類別 Expo2010TicketSeller

public class Expo2010TicketSeller extends TicketSeller {
    protected int price;
    protected int numTicketForSale;
    public Expo2010TicketSeller() {
        super("World Expo 2010");
        this.price = 180;
        this.numTicketForSale = 200;
    }
    public int getTicketPrice() { return price; }
    public int buy(int ticketNumber, int money) throws Exception {
        if (ticketNumber > numTicketForSale) {
            throw new Exception("There is no enough ticket available for sale, only " + numTicketForSale + " ticket(s) left");
        }
        int charge = money - ticketNumber * price;
        if (charge < 0) {
            throw new Exception("Money is not enough. Still needs " + (-charge) + " RMB.");
        }
        numTicketForSale -= ticketNumber;
        return charge;
    }
}

購票者類別 TicketBuyer 模擬購票示範擴展動態代理機制

public class TicketBuyer {
    public static void main(String[] args) {
        // 建立真正 TicketSeller 物件,作為 stub 實體
        final TicketSeller stub = new Expo2010TicketSeller();
        // 建立擴展調用處理器物件
        InvocationHandler handler = new InvocationHandlerEx() {
            public Object getStub(Class stubClass) {
                // 僅對可接受 Class 型別回傳 stub 實體
                if (stubClass.isAssignableFrom(stub.getClass())) return stub;
                return null;
            }
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object o;
                try {
                    System.out.println(" >>> Enter method: " + method.getName());
                    o = method.invoke(stub, args);
                } catch (InvocationTargetException e) {
                    throw e.getCause();
                } finally {
                    System.out.println(" <<< Exit method: " + method.getName());
                }
                return o;
            }
        };
        // 透過 ProxyEx 建構動態代理
        TicketSeller seller = (TicketSeller) ProxyEx.newProxyInstance(
            TicketBuyer.class.getClassLoader(),
            new Class[] { TicketSeller.class },
            handler);
        // 顯示代理類別型別
        System.out.println("Ticket Seller Class: " + seller.getClass() + "\n");
        // 直接存取 theme 變數,驗證代理類別變數在物件建構時同步有效性
        System.out.println("Ticket Theme: " + seller.theme + "\n");
        // 函式存取 price 資訊
        System.out.println("Query Ticket Price...");
        System.out.println("Ticket Price: " + seller.getTicketPrice() + " RMB\n");
        // 模擬票務交易
        buyTicket(seller, 1, 200);
        buyTicket(seller, 1, 160);
        buyTicket(seller, 250, 30000);
        // 直接更新 theme 變數
        System.out.println("Updating Ticket Theme...\n");
        seller.theme = "World Expo 2010 in Shanghai";
        // 函式存取 theme 資訊,驗證擴展動態代理機制對變數同步有效性
        System.out.println("Query Updated Ticket Theme...");
        System.out.println("Updated Ticket Theme: " + seller.getTicketTheme() + "\n");
    }
    // 購票函式
    protected static void buyTicket(TicketSeller seller, int ticketNumber, int money) {
        try {
            System.out.println("Transaction: Order " + ticketNumber + " ticket(s) with " + money + " RMB");
            int charge = seller.buy(ticketNumber, money);
            System.out.println("Transaction: Succeed - Charge is " + charge + " RMB\n");
        } catch (Exception e) {
            System.out.println("Transaction: Fail - " + e.getMessage() + "\n");
        }
    }
}

執行輸出

Ticket Seller Class: class com.demo.proxy.test.TicketSellerProxy0

Ticket Theme: World Expo 2010

Query Ticket Price...
>>> Enter method: getTicketPrice
<<< Exit method: getTicketPrice
Ticket Price: 180 RMB

Transaction: Order 1 ticket(s) with 200 RMB
>>> Enter method: buy
<<< Exit method: buy
Transaction: Succeed - Charge is 20 RMB

Transaction: Order 1 ticket(s) with 160 RMB
>>> Enter method: buy
<<< Exit method: buy
Transaction: Fail - Money is not enough. Still needs 20 RMB.

Transaction: Order 250 ticket(s) with 30000 RMB
>>> Enter method: buy
<<< Exit method: buy
Transaction: Fail - There is no enough ticket available for sale, only 199 ticket(s) left

Updating Ticket Theme...

Query Updated Ticket Theme...
>>> Enter method: getTicketTheme
<<< Exit method: getTicketTheme
Updated Ticket Theme: World Expo 2010 in Shanghai
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved