遙遠的橋 (A Bridge Too Far)

工程 (Engineering) | Rob Harrop | 2007 年 1 月 16 日 | ...

在我之前的文章中,我介紹了一種建立策略類別的技術,該類別可以充分利用應用程式中存在的任何泛型元數據。在那篇文章的結尾,我展示了這段程式碼片段

EntitlementCalculator calculator = new DividendEntitlementCalculator();
calculator.calculateEntitlement(new MergerCorporateActionEvent());

您會記得DividendEntitlementCalculator被定義為

public class DividendEntitlementCalculator implements EntitlementCalculator<DividendCorporateActionEvent> {

    public void calculateEntitlement(DividendCorporateActionEvent event) {

    }
}

因此,傳入一個MergerCorporateActionEvent的實例到calculateEntitlement方法的DividendEntitlementCalculator類別中是不正確的。 但是,正如我在上一篇文章中提到的那樣,該程式碼可以編譯。 為什麼? 好吧,EntitlementCalculator.calculateEntitlement()被定義為接受任何擴展CorporateActionEvent的類型,所以它應該可以編譯。 那麼在這種情況下,運行時會發生什麼,Java 如何強制執行類型安全? 好吧,正如您可能想像的那樣,運行此程式碼會產生ClassCastException表示您無法將MergerCorporateActionEvent轉換為DividendCoporateActionEvent。 透過這種方式,Java 可以為您的應用程式強制執行類型安全 - 沒有任何方法可以讓MergerCorporateActionEvent偷偷進入預期DividendCorporateActionEvent的方法中。

這裡真正要問的問題是:「那個ClassCastException從哪裡來的?」 答案很簡單 - Java 編譯器會新增程式碼以建立並拋出它,方法是引入一個橋接方法。 橋接方法是編譯器將產生並新增到您的類別中的合成方法,以確保在面對泛型類型時的類型安全。

在上面顯示的情況下EntitlementCalculator.calculateEntitlement可以使用與CorporateActionEvent類型相容的任何物件來呼叫。 然而,DividendEntitlementCalculator只接受與DividendCorporateActionEvent類型相容的物件,但是,由於您可以透過DividendEntitlementCalculator經由EntitlementCalculator介面來呼叫它,因此它也必須接受CorporateActionEvent。 那麼這在編譯後的類別檔案中會轉換成什麼? 我們有使用者提供的方法

public void calculateEntitlement(DividendCorporateActionEvent event) {
    System.out.println(event);
}

它轉換成這個位元碼

public void calculateEntitlement(bigbank.DividendCorporateActionEvent);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   getstatic       #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   aload_1
   4:   invokevirtual   #3; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
   7:   return

但我們也有一個編譯器產生方法

public void calculateEntitlement(bigbank.CorporateActionEvent);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   aload_0
   1:   aload_1
   2:   checkcast       #4; //class bigbank/DividendCorporateActionEvent
   5:   invokevirtual   #5; //Method calculateEntitlement:(Lbigbank/DividendCorporateActionEvent;)V
   8:   return

它轉換成這個 Java 程式碼

public void calculateEntitlement(CorporateActionEvent event) {
    calculateEntitlement((DividendCorporateActionEvent)event);
}

所以,在這裡您可以清楚地看到當傳入ClassCastException時,CorporateActionEvents而不是DividendCorporateActionEvents從哪裡來的 - 編譯器產生的橋接方法

現在,這當然是一個很棒的功能。 我們不希望在 Java 語言中新增泛型會破壞我們長期以來一直使用的類型安全。 然而,正如這些事情所預期的那樣 - 一切並非都很好。 目前 JDK 中實作的橋接方法的主要問題是,註釋不會從橋接方法複製到橋接方法。 當您不小心在反射中取得橋接方法並嘗試解析某些註釋時,這會導致各種問題。

你們中的一些人可能想知道您是如何錯誤地取得橋接方法的。 這是一個相當複雜的問題。 一個常見的原因(也是我們在 Spring 中最常看到的情況)是您正在建立委託給某個物件的 JDK 代理,並且您嘗試將代理介面中的方法對應到委託上的相應實作方法(通常是為了解析註釋)。 考慮這段程式碼

public static void main(String[] args) {
    EntitlementCalculator ec = createProxy(new DividendEntitlementCalculator());
    ec.calculateEntitlement(null);
}

private static EntitlementCalculator createProxy(EntitlementCalculator calculator) {
    InvocationHandler handler = new TransactionLoggingInvocationHandler(calculator);
    return (EntitlementCalculator) Proxy.newProxyInstance(calculator.getClass().getClassLoader(),
                                                                calculator.getClass().getInterfaces(), handler);
}

private static class TransactionLoggingInvocationHandler implements InvocationHandler {

    private final EntitlementCalculator delegate;

    public TransactionLoggingInvocationHandler(EntitlementCalculator delegate) {
        this.delegate = delegate;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Method delegateMethod = delegate.getClass().getMethod(method.getName(), method.getParameterTypes());
        Transactional annotation = delegateMethod.getAnnotation(Transactional.class);
        if(annotation != null) {
            System.out.println("Executing transactional method: " + delegateMethod);
        } else {
            System.out.println("Executing non-transactional method: " + delegateMethod);
        }
        return method.invoke(delegate, args);
    }
}

在這裡,我們正在為給定的EntitlementCalculator物件建立一個代理,該代理將記錄代理物件上的方法是否是事務性的。 如果我們像下面這樣註釋DividendEntitlementCalculator類別,我們可以預期代理在呼叫calculateEntitlement時記錄我們正在執行一個事務性方法.

@Transactional
public void calculateEntitlement(DividendCorporateActionEvent event) {
    System.out.println(event);
}

主函式 (main)

Executing non-transactional method: public volatile void bigbank.DividendEntitlementCalculator.calculateEntitlement(bigbank.CorporateActionEvent)

。 但是,執行上面的範例會產生這個結果DividendEntitlementCalculator請注意,這與我們正在呼叫的CorporateActionEvent上的方法不符。 當然,這顯然會是這種情況; 這裡的重點是介面方法和委託方法的簽名不同。 一個是用父類型定義的,在本例中是DividendCorporateActionEvent,另一個是用子類型定義的,在本例中是。 您還會注意到,我們實際上已經收到了橋接方法 - 因為它的簽名確實與介面方法的簽名相符(根據定義)。

也許尋找委託方法的更好解決方案是使用傳入引數的類型,而不是介面方法上的類型。 當面對使用繼承的引數時,您可以簡單地在引數的類型層次結構中向上搜尋類型匹配。 不幸的是,這種方法無法可靠地運作。 考慮一下您有以下介面的情況

public interface Foo<T> {
    void bar(T t);
}

然後是這個實作

public class FooImpl implements Foo<Number>{

    public void bar(Number t) {
    }

    public void bar(Serializable t) {
    }
}

如果您要使用傳入InvocationHandler的具體引數類型來解析委託方法,那麼當面對Integer類型的引數時,您會選擇哪個方法? 您無法(從介面方法中)判斷類型參數是Number,並且由於這兩種方法都與Integer類型相容,因此一般情況下不可能一直解析正確的方法。

解決這個問題只有兩種方法(據我所知)。 第一種方法是使用像 ASM 這樣的函式庫來讀取橋接方法的位元碼,並找出它呼叫的方法。 使用 ASM 讀取位元碼是一個很棒的解決方案,而且通常是萬無一失的。 但是,在安全環境中,它可能需要對不允許的函式庫進行讀取權限,這可能會帶來問題。 第二種解決方案是使用橋接方法中的泛型元數據來解析實作類別中的哪個方法正在被橋接。

在上面的範例中,我們可以看到介面方法是barT參數化。 我們可以使用Class.getGenericInterfaces()FooImpl的泛型介面元數據來確定T被實現為Number。 從那裡,就可以簡單地知道橋接方法是bar(Number)而不是bar(Serializable)。 不幸的是,面對涉及具有邊界的 multiple 類型參數的複雜層次結構時,此方法會變得越來越複雜。 幸運的是,此邏輯封裝在 Spring 的BridgeMethodResolver類別中。 這是一個完美的例子,說明 Spring 解決了 Java 開發人員面臨的艱難基礎架構問題,並將它們整合到應用程式堆疊中。 每當在 Spring 中執行註釋查詢時,橋接方法都會被透明地解析。

的實作BridgeMethodResolver基本上已經完成; 但是,我確信有些我們尚未考慮到的複雜案例,我很樂意聽取在此區域遇到任何問題的使用者的意見。

取得 Spring 電子報 (Get the Spring newsletter)

透過 Spring 電子報保持聯繫 (Stay connected with the Spring newsletter)

訂閱 (Subscribe)

取得領先 (Get ahead)

VMware 提供培訓和認證,以加速您的進度。(VMware offers training and certification to turbo-charge your progress.)

了解更多 (Learn more)

取得支援 (Get support)

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。(Tanzu Spring offers support and binaries for OpenJDK™, Spring, and Apache Tomcat® in one simple subscription.)

了解更多 (Learn more)

即將到來的活動 (Upcoming events)

查看 Spring 社群中所有即將到來的活動。(Check out all the upcoming events in the Spring community.)

查看全部 (View all)