Core Java

Implementing an annotation interface

Using annotation is every day task for a Java developer. If nothing else simple @Override annotation should ring the bell. Creating annotations is a bit more complex. Using the “home made” annotations during run-time via reflection or creating a compile time invoked annotation processor is again one level of complexity. But we rarely “implement” an annotation interface. Somebody secretly, behind the scenes certainly does for us.

When we have an annotation:

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

then a class annotated with this annotation

@AnnoWithDefMethod("my default value")
public class AnnotatedClass {
}

and finally we when get the annotation during runtime executing

AnnoWithDefMethod awdm = AnnotatedClass.class.getAnnotation(AnnoWithDefMethod.class);

then what do we get into the variable awdm? It is an object. Objects are instances of classes, not interfaces. Which means that somebody under the hood of the Java runtime has “implemented” the annotation interface. We can even print out features of the object:

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());
        }

to get a result something like

my default value
60e53b93
class com.sun.proxy.$Proxy1
interface AnnoWithDefMethod
value
equals
toString
hashCode
annotationType

So we do not need to implement an annotation interface but we can if we wanted. But why would we want that? So far I have met one situation where that was the solution: configuring guice dependency injection.

Guice is the DI container of Google. The configuration of the binding is given as Java code in a declarative manner as it is described on the documentation page. You can bind a type to an implementation simply declaring

bind(TransactionLog.class).to(DatabaseTransactionLog.class);

so that all TransactionLog instance injected will be of DatabaseTransactionLog. If you want to have different imlpementation injected to different fields in your code you should some way signal it to Guice, for example creating an annotation, putting the annotation on the field, or on the constructor argument and declare the

bind(CreditCardProcessor.class)
        .annotatedWith(PayPal.class)
        .to(PayPalCreditCardProcessor.class);

This requires PayPal to be an annotation interface and you are required to write an new annotation interface acompaniing each CreditCardProcessor implementation or even more so that you can signal and separate the implementation type in the binding configuration. This may be an overkill, just having too many annotation classes.

Instead of that you can also use names. You can annotate the injection target with the annotation @Named("CheckoutPorcessing") and configure the binding

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

This is a tehnique that is well known and widely used in DI containers. You specify the type (interface), you create the implementations and finally you define the binding type using names. There is no problem with this, except that it is hard to notice when you type porcessing instead of processing. Such a mistake remains hidden until the binding (run-time) fails. You can not simply use a final static String to hold the actual value because it can not be used as the annotation parameter. You could use such a constant field in the binding definition but it is still duplication.

The idea is to use something else instead of String. Something that is checked by the compiler. The obvious choice is to use a class. To implement that the code can be created learning from the code of NamedImpl, which is a class implementing the annotation interface. The code is something like this (Note: Klass is the annotation interface not listed here.):

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
    }
}

The actual binding will look something like

@Inject
  public RealBillingService(@Klass(CheckoutProcessing.class) CreditCardProcessor processor,
      TransactionLog transactionLog) {
    ...
  }
 
    bind(CreditCardProcessor.class)
        .annotatedWith(Klass.klass(CheckoutProcessing.class))
        .to(CheckoutCreditCardProcessor.class);

In this case any typo is likely to be discovered by the compiler. What happens actually behind the scenes, and why were we requested to implement the annotation interface?

When the binding is configured we provide an object. Calling Klass.klass(CheckoutProcessing.class) will create an instance of KlassImpl and when Guice tries to decide if the actual binding configuration is valid to bind CheckoutCreditCardProcessor to the CreditCardProcessor argument in the constructor of RealBillingService it simply calls the method equals() on the annotation object. If the instance created by the Java runtime (remember that Java runtime creates an instance that had a name like class com.sun.proxy.$Proxy1) and the instance we provided are equal then the binding configuration is used otherwise some other binding has to match.

There is another catch. It is not enough to implement equals(). You may (and if you are a Java programmer (and you are why else you read this article (you are certainly not a lisp programmer)) you also should) remember that if you override equals() you have to override also hashCode(). And actually you should provide an implementation that does the same calculation as the class created by the Java runtime. The reason for this is that the comparison may not directly be performed by the application. It may (and it does) happen that Guice is looking up the annotation objects from a Map. In that case the hash code is used to identify the bucket in which the comparing object has to be and the method equals() is used afterwards to check the identity. If the method hashCode() returns different number in case of the Java runtime created and out objects they will not even match up. equals() would return true, but it is never invoked for them because the object is not found in the map.

The actual algorithm for the method hashCode is described on the documentation of the interface java.lang.annotation. I have seen this documentation before but understood the reason why the algorithm is defined when I first used Guice and implemented a similar annotation interface implementing class.

The final thing is that the class also has to implement annotationType(). Why? If I ever figure that out I will write about that.

Reference: Implementing an annotation interface from our JCG partner Peter Verhas at the Java Deep blog.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button