Java 8 動態代理的新技巧(1):為什麼使用動態代理?

ImportNew發表於2015-09-22

動態代理(Dynamic proxies)是 Java 1.3 引入的特性,在 J2EE 的遠端呼叫中應用非常廣泛。給定一個抽象介面以及這個介面的具體實現,就可以通過建立兩個額外的類來實現這個介面的遠端呼叫了(如,跨JVM)。首先,在源JVM上實現相應的介面,並將呼叫細節序列化後通過網路傳輸。然後,在目標JVM上,獲取到序列化後的呼叫的細節,並分配給具體的的類去呼叫。

沒有動態代理和反射,開發者不得不為每個遠端介面提供兩個類。一個動態代理是執行時產生的類,實現一個或多個介面,介面中每個方法的呼叫都會自動轉換為 java.runtime.InvocationHandler 提供的方法呼叫:

Java 8動態代理的新技巧(1):為什麼使用動態代理?

public interface InvocationHandler {
    Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

InvocationHandler決定如何處理呼叫,如何在執行時使用方法的有效資訊,包括註解、引數型別及方法的返回型別。這樣就可以實現一個通用邏輯來定義方法呼叫的分發。一旦你寫好了一個InvocationHandler,就可以呼叫代理類的 handler 來完成所有介面中的方法,而不是為每一個介面寫一個單獨的實現。

遠端呼叫最近幾年裡已經沒那麼受歡迎了,因為開發者需要明白方法呼叫分發與網路請求傳送在語義和失敗模式上的本質區別,但是動態代理仍保留在語言當中。在這篇文章中,我將討論動態代理其他方面的作用。在下一篇文章中,將討論動態代理新的實現技術,這些技術是由於 Java 8 引入 lambda 表示式和預設方法而產生的。

魔法匹配器

這些年來,我一直在使用一個“Magic”物件,以便能夠寫出簡潔的流式測試。我定義了一個“magic”的介面,然後通過一個動態代理來實現目標行為。比較特別的是,在測試時候用”magic builders”來生成測試值,然後用“magic matchers”來表述斷言屬性測試的結果。我們這裡只關注匹配器。

我們有一個Person支撐類,這是一個典型的bean——成員變數是私有的,通過getter和setter方法暴露。

public class Person {

    private String name;
    private int age;

    // insert getters and setters here
}

使用一個簡單Hamcrest類,我們有兩種方式來斷言該類的例項。一種方法是單獨抽取每個值,分開斷言。

assertThat(person.getName(), containsString("Smith"));
assertThat(person.getAge(), greaterThan(30));

另一種方式是使用allOf和hasProperty方法,將物件作為一個整體,通過一組期望值來匹配。

assertThat(person, allOf(
    hasProperty("name", containsString("Smith")),
    hasProperty("age", greaterThan(30)));

這樣能很好的工作,但是這種方式對 Hamcrest 描述整體匹配和錯誤匹配並沒有什麼幫助。

Expected: (hasProperty("name", a string containing "Putey") and hasProperty("age", a value greater than <43>))
but: hasProperty("age", a value greater than <43>) property 'age' <42> was less than <43>

hasProperty的匹配在型別一致性的檢測也是非常弱的:我們可以寫成 hasProperty(“age”, containsString(“Smith”)),這樣型別檢測也不會拒絕。

我們真正想要的是一個流式API,能夠像下面一樣使用:

assertThat(person, aPerson()
    .withName("Arthur Putey")
    .withAge(greaterThan(43)));

並且能夠很好且易於理解地報告錯誤的匹配:

Expected:
name: a string containing "Putey"
age: a value greater than <43>
    but:
age: <42> was less than <43>

很容易寫一個上述功能的自定義匹配器,但是不得不很乏味地寫很多次。幸運的是,可以通過動態代理來幫我們解決。首先,我們定義一個流式介面,該介面包含如下方法:

interface PersonMatcher extends Matcher<Person> {
    PersonMatcher withName(String expected);
    PersonMatcher withName(Matcher<? super String> matching);
    PersonMatcher withAge(int expected);
    PersonMatcher withAge(Matcher<Integer> matching);
}

然後,我們使用在一個名為 MagicMatcher 的類上的靜態方法來獲取動態代理,該代理實現了這個介面,然後通過方法呼叫來獲取調節表示式:

static PersonMatcher aPerson() {
    return MagicMatcher.proxying(PersonMatcher.class);
}

每個方法的呼叫都通過代理類的“interpreted”方法來實現,該代理從方法(“withAge”)中獲取屬性(“age”),並指定呼叫匹配物件上的(“getAge”)方法來獲取屬性值。屬性的名稱以及匹配中對應的值將會被儲存,直到代理類的 match 或 describeMismatch 方法被呼叫(這就是為什麼介面需要繼承 Matcher)。在呼叫的時候需要抽取並測試物件的屬性,如果有必要,會建立錯誤匹配報告。

這種方式是輕量級的,我們可以引入任何新的自定義的介面,並在測試中重用,這樣,是非常有利於編寫自定義Hamcrest匹配器的,因為不再需要編寫介面的實現。所有需要生成的在介面中定義的匹配器行為,都只需要實現一次,我們通過一個合適的 InvocationHandler 來完成邏輯功能的實現。

下一篇文章中,我將建立一個很小的,但是很有用的庫,我們使用 Java 8 的動態代理來完成各項功能,並演示一些用於實現各種代理行為的方式,包括介面及”magic”物件的生成。這個庫的原始碼,包括這篇文章中討論的 MagicMatcher 類的實現,都可以在 github 上找到。

相關文章