对于 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() 方法。为什么?在我了解后我会把它写出来。
翻译原文