In the java world we have been using and getting used to annotations since Java 1.5. Although there were some critical voices at first, I think most of us have come around and are using annotations now quite extensively. In my experience annotations are mostly used on POJO domain classes to configure frameworks like Hibernate, Spring and Seam and many other frameworks to be able to handle the custom objects correctly.
There are as many different approaches to this as there are implementations. In this blog I try to identify a few of the better approaches and a few of the poorer ones. The blog is not so much meant as a critique on the frameworks that the examples are taken from, but more as a guide to designing your own annotations whenever you might be faced with that task.
Prime examples: JPA annotations
The best examples of using annotations for configuration – both good and bad – can be found in JPA Entity classes.
[java]
@Entity
@Table(name="person")
public class Person {
@Id
@GeneratedValue(strategy=SEQUENCE)
@Column(name = "personId")
private Long id;
@Valid @NotEmpty
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "personId", nullable = false)
@org.hibernate.annotations.Cascade(CascadeType.DELETE_ORPHAN)
@org.hibernate.annotations.IndexColumn(name = "position")
private List<Address> addresses = new ArrayList<Address>();
…
}
[/java]
What a wealth of annotations! And in such a few lines of code. Anybody that has dealt with this type of code before however, will testify that this example is not overly complex at all. You’ll find this ind of code all over.
To start with, let’s say that I think this type of coding has been extremely successful. So regardless whether we find the usage of annotations in this example recommendable or not, if you work with Java code, you’ll be reading and writing a lot of code like this, so understanding it is imperative. However success is just one of the indicators for quality and it is always good to think about how things can be improved.
A perfect annotation
Of course there is no such thing as a perfect annotation. But as a general rule of thumb a good annotation should describe the annotated element and be meaningful outside its intended context. This last requirement is a little bit vague. I hope it will become more clear once we look at some examples.
Annotations are very often used as a convenient means of configuration. This type of usage breaks the above rule for ‘perfect annotations’, but they are very convenient and very widely used. Drawbacks of this type of usage are:
- To change the configuration the source code needs to be recompiled.
- The annotations (and thus their containing packages and often all of the framework code) need to be on the classpath for compilation and running.
- Only one configuration can (usually) be specified. Most frameworks allow for a different means of configuration to circumvent this.
…but these drawbacks do not mean that this type of usage has been less successful in the wild. On the contrary! There are however ways to design frameworks so that they can be configured by annotations that are not impacted by these drawbacks.
Let’s look at the example above in the light of these
- The @Entity annotation specifies that instances of this class should contains data that is to be persisted. It describes how the class is intended. It also allows several frameworks to operate on it.
- The @Table annotation is pure configuration. It does not describe anything about this class and is tightly bound to its context: Databases.
- The @Valid and @NotEmpty annotations describe features of valid instances of this class. They are however quite tightly bound to their context: data validation.
For each of the annotations in the example such a analysis can be made. It is interesting to think about how useful the annotations are in relation how perfect they are: The @Entity annotation, although being almost perfect is, is actually superfluous, because it will never be used without the @Table annotation. The @Valid and @NotEmpty annotations are much more useful: They allow very powerful cross-cutting concerns to be implemented with very little code, configuration or dependency of application code on framework code, making it easy to test and reuse this code.
Configuration annotation patterns in the wild
The Worst
All this dribble about perfect annotations is nice and dandy, but where does it lead us? Maybe it is better to look at the other end of the spectrum, where we find the very worst of annotations:
[java]
@XmlJavaTypeAdapter(CurrencyAdapter.class)
private BigDecimal price;
[/java]
This JAXB annotation, used to configure the way a value is represented in XML, ties the implementation of a framework interface to the field on the domain class we’re trying to configure the framework for. We could as well implement the interface in the class itself!
Better… Level of indirection
A better pattern can be found in the seam framework for configuring interceptors. The following code configures an interceptor around a business method to measure the time it takes to complete.
[java]
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Interceptors(SeamTimingInterceptor.class)
public @interface MeasureCalls {}
[/java]
[java]
…
@MeasureCalls
public void someBusinessMethod() {
…
}
…
[/java]
[java]
@Interceptor(around = {
BijectionInterceptor.class,
…
SeamInterceptor.class,
})
public class SeamTimingInterceptor {
@AroundInvoke
public Object timeCall(InvocationContext invocation) throws Exception {
long t0 = System.nanoTime();
try {
return invocation.proceed();
} finally {
long dt = System.nanoTime() – t0;
// Log time.
}
}
}
[/java]
The first annotation just defines a cross-cutting feature that we would like to implement in our application. We can easily use it in our application as shown by the second fragment.
Coming to the last code fragment, the actual implementation of the interceptor, i feel that the framework designers went a little overboard and were too annotation-happy: The interceptor could have simply implemented an interface:
[java]
public interface AroundMethodInterceptor {
Object aroundInvoke(InvocationContext invocation);
}
[/java]
This would have made the contract for an interceptor much clearer.
It could also help solve the other problem with this implementation: Now, the business class needs the annotation to compile, the annotation needs the interceptor and the interceptor needs the framework classes. From this perspective, the extra level of indirection has not helped a bit.
Best… Classpath scanning and interfaces
Another approach that is quite elegant is the JAX-RS approach to extending the framework through Providers and the RESTEasy implementation for it. The code below automatically registers an exception mapper, which integrates with the framework and is used to handle EJB exceptions coming from resource methods.
[java]
@Provider
public class EJBExceptionMapper implements ExceptionMapper<EJBException> {
@Context
private Providers providers;
@Override
public Response toResponse(EJBException exception) {
while (exception.getCause() != null
&& exception.getCause() instanceof EJBException) {
exception = (EJBException) exception.getCause();
}
Exception cause = exception.getCausedByException();
ExceptionMapper delegate = providers.getExceptionMapper(cause.getClass());
if (delegate != null) {
return delegate.toResponse(cause);
} else {
return ExceptionUtil.serverError(cause.getMessage());
}
}
}
[/java]
The good thing here is that the framework will scan for the annotation and then register the class according to its interface it implements.
Another interesting aspect is the @Context annotation, that basically works as an injector point.
Extensions for custom annotations
This mechanism can be used to configure the RESTEasy framework to handle custom annotations for custom serialization. Jackson has a similar mechanism like JAXB for configuring the way a field is seriaized with the @JsonSerializer annotation. Unfortunately this has the same drawbacks. A better approach can be implemented using the Provider mechanism on the Jackson framework itself.
[java]
@Provider
public class ObjectMapperResolver implements ContextResolver<ObjectMapper> {
private AnnotationIntrospector annotationIntrospector;
public ObjectMapper getContext(Class<?> type) {
return new CustomObjectMapper();
}
class CustomObjectMapper extends ObjectMapper {
/**
- Constructor.
*/
public CustomObjectMapper() {
super(null, null, null,
new SerializationConfig(
DEFAULT_INTROSPECTOR,
pair(annotationIntrospector,DEFAULT_ANNOTATION_INTROSPECTOR),
STD_VISIBILITY_CHECKER, null),
new DeserializationConfig(DEFAULT_INTROSPECTOR,
pair(annotationIntrospector,DEFAULT_ANNOTATION_INTROSPECTOR),
STD_VISIBILITY_CHECKER, null)
);
}
}
}
[/java]
The above class ‘provides’ the Jackson framework with a custom ObjectMapper. This is done automatically, because the class is annotated with @Provider and implements ContextResolver<ObjectMapper>. Now the RESTEasy framework recognizes this as a part of the framework that will provide a ObjectMapper to inject into a @Context injection point. The Jackson framework uses the same injection point to find a ObjectMapper to use for serialization.
Now how do we get it to recognize our own annotations? The answer is given by the code below, which is used in the custom ObjectMapper.
[java]
public class CustomAnnotationInspector extends NopAnnotationIntrospector {
private Map<Class<? extends Annotation>, String> serializerComponents =
new HashMap<Class<? extends Annotation>, String>();
@Override
public Object findSerializer(Annotated am, BeanProperty bp) {
if (property != null) {
for (Annotation annotation : bp.getMember().getAnnotated().getAnnotations()) {
String name = serializerComponents.get(annotation.annotationType());
if (name != null) {
return Component.getInstance(name);
}
}
}
return super.findSerializer(am, bp);
}
}
[/java]
This can be used as shown below, when a serializer for the @Currency is configured in the seriliazerComponents map. (We use this in combination with the Seam framework for configuration).
[java]
@Target({FIELD, METHOD})
@Retention(RUNTIME)
public @interface Currency {}
…
@Currency
BigInteger price;
…
[/java]Conclusion
In this blog I’ve shown a number of patterns for configuring frameworks using annotations. I’ve also shown some useful classes for extending the RESTEasy and Jackson frameworks with support for custom annotations. I hope these examples will inspire you to write your own perfect and useful annotations.