實現一個註解接口

我的網站


對於 Java 程序員來說使用註解就是日常任務,先不說別的,@Override 註解那是再熟悉不過了,不過創建倒是有點小複雜的。在運行時通過反射使用底層註解或者創建創建一個編譯時調用的註解處理器這又是另一個級別的複雜度。不過我們很少實現一個註解接口,因爲有人祕密地爲我們實現了。


當我們有這樣一個註解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface AnnoWithDefMethod {
    String value() default "default value string";
}

然後在一個類上使用該註解:
@AnnoWithDefMethod("my default value")
public class AnnotatedClass {
}

在運行時我們執行下邊代碼得到這個註解:
AnnoWithDefMethod awdm = AnnotatedClass.class.getAnnotation(AnnoWithDefMethod.class);

那我們最終在變量 awdm 中存儲的是什麼呢?這是個對象。對象是類的實例,而不是接口的,這意味着在 Java 運行時的底層已經實現了註解接口。我們是可以輸出對象的一些特性的:
System.out.println(awdm.value());
System.out.println(Integer.toHexString(System.identityHashCode(awdm)));
System.out.println(awdm.getClass());
System.out.println(awdm.annotationType());
for (Method m : awdm.getClass().getDeclaredMethods()) {
   System.out.println(m.getName());
}

相應的輸出結果:
my default value
60e53b93
class com.sun.proxy.$Proxy1
interface AnnoWithDefMethod
value
equals
toString
hashCode
annotationType

這裏可以看出我們不需要實現註解接口,不過如果需要的話我們也可以實現。那爲什麼我們需要實現呢?這近我就遇到了這種需要我們自己實現接口的場景:配置 Guice 的依賴注入。


Guice 是 Google 的 DI 容器。文檔中介紹這種綁定的配置是使用類似 Java 代碼的聲明形式。你只需要像下邊一樣簡單生命來將一個類型綁定到一個實現上:
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
因此所有的 TransactionLog 實例的注入都將是 DatabaseTransactionLog。如果你想在你的代碼裏邊爲不同的屬性注入不同的實現那你就需要某種方式來告訴 Guice,比如,創建一個註解將其注入到一個屬性上或者構造器的參數裏然後聲明:
bind(CreditCardProcessor.class)
        .annotatedWith(PayPal.class)
        .to(PayPalCreditCardProcessor.class);

這種方式下 PayPal 是一個註解接口,而對於不同的 CreditCardProcessor 就需要創建一個新的註解接口,這樣在綁定配置裏就能夠區分相應的實現類型。這種方式就是個坑,需要創建許多不同的註解類。


作爲更好的選擇我們可以使用 names。我們可以用註解 @Named("CheckoutProcessing") 來註解注入目標然後再配置綁定:

bind(CreditCardProcessor.class)
        .annotatedWith(Names.named("CheckoutProcessing"))
        .to(CheckoutCreditCardProcessor.class);

這個是在 DI 容器中有名並且廣泛使用的技術。我們指定類型(接口),然後創建相應的實現再用 names 來綁定類型。這種方式基本上沒啥問題除了在拼寫上,porcessing 和 processing 有時難以識別。這種錯誤很難發現也只有到綁定(運行時)的時候會失敗。我們也不能簡單的使用 final static String 來存儲實際的值,因爲它是不能作爲註解的參數的。我們也可以選擇在綁定定時使用常量屬性但是這仍然是多餘的。


替換的方法就是使用能夠代替 String 的值,並且是能夠被編譯器檢查的。很明顯是實用類。要實現這樣的類我們可以學 NamedImpl 的代碼,這個類正是一個實現了註解接口的類。代碼長的大概如下(注意 Klass 是一個未列出來的註解接口):
class KlassImpl implements Klass {
    Class<? extends Annotation> annotationType() {
        return Klass.class
    }
    static Klass klass(Class value){
        return new KlassImpl(value: value)
    }
    public boolean equals(Object o) {
        if(!(o instanceof Klass)) {
            return false;
        }
        Klass other = (Klass)o;
        return this.value.equals(other.value());
    }
    public int hashCode() {
        return 127 * "value".hashCode() ^ value.hashCode();
    }
 
     Class value
    @Override
    Class value() {
        return value
    }
}

實際的綁定如同下面:
@Inject
  public RealBillingService(@Klass(CheckoutProcessing.class) CreditCardProcessor processor,
      TransactionLog transactionLog) {
    ...
  }
 
    bind(CreditCardProcessor.class)
        .annotatedWith(Klass.klass(CheckoutProcessing.class))
        .to(CheckoutCreditCardProcessor.class);

在這種情況下任何一種類型都會被編譯器檢查。那麼在這種情況下實際發生了什麼,爲什麼我們要去實現註解接口呢?


當綁定被配置的時候我們提供了一個對象。調用 Klass.klass(CheckoutProcessing.class) 創建一個 KlassImpl 的實例,當 Guice 嘗試判斷實際的綁定配置是否合法地將 CheckoutCreditCardProcessor 綁定到 RealBillingService 的構造函數參數 CreditCardProcessor 上時,它會簡單調用註解對象的方法 equals() 。如果 Java 運行時創建的實例(記住 Java 運行時會創建一個名字爲 class com.sun.proxy.$Proxy1 的實例)以及我們所提供的實例相等時那麼相應的綁定配置就會被使用否則將會去匹配別的綁定。


這裏還有另外一個注意點,只實現 equals() 是不夠的。你應該記得你覆蓋 equals() 的同時你還必須覆蓋 hashCode()。實際上 Java 運行時創建的一個類是同樣需要提供一個具有同等功能的方法,有可能對象的比較不適直接郵應用執行的。有可能(實際上也是) Guice 是從一個 Map 裏邊查詢註解對象的。這種情況下 hash 值是用來確定被比較對象所處在的 bucket 的位置而方法 equals() 是隨後用來比較對象相等性的。如果方法 hashCode() 在 Java 運行時創建的時候返回了不同的值那麼那些 bucket 的值是不可能與要查找值相同的。


在接口 java.lang.annotation 的文檔中有詳細描述方法 hashCode 的實現算法。我在之前就見過了這個文檔不過也只是在第一次使用 Guice 的時候才明白這個算法的實現,然後實現了一個相似註解接口的類。


最後一件事是註解實現類必須實現 annotationType() 方法。爲什麼?在我瞭解後我會把它寫出來。

翻譯原文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章