Some months ago I attended a presentation at which Wilfred Springer demonstrated his very cool Preon binary codec library. Defining binary file formats in Preon requires quite a lot of fairly repetitive sets of annotations, and during a chat after the talk Wilfred mentioned (in fact, he blogged about it) how much more convenient it would be if one could just define "shortcuts":

@RequiredEnumProperty(column = "AGENT")

for

@NotNull 
@Column(name = "AGENT") 
@Enumerated(EnumType.STRING)

for instance - and use those instead. Sort-of "macro annotatations" for Java, if you like.
A thought that has presumably also occurred to many frequent users of Hibernate, JAXB or other annotation-heavy frameworks.

Well, it took me rather longer than the couple of days it would probably have taken a developer of Wilfred's skill, but finally @Composite is here!

Just to dispel any misconceptions up front: here be no bytecode weaving or other runtime magic, so @Composite does not affect the semantics of the "regular" AnnotatedElement methods1. Composite annotations are instead supported via an AnnotatedElements interface, which provides all the familiar annotation-related methods, and "unpacks" registered composite annotations to their "leaf" types.
So @Composite is not (yet) drop-in magic - you will need to explicitly call the AnnotatedElements interface from your code.

Hello composite World

The AtCompositeDemo class included in the project basically looks like this:

public class AtCompositeDemo {
    ...
    // define a composite annotation    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    @CompositeAnnotation
    public @interface TargetRetentionLeafCompositeAnnotation {
        boolean runtimeRetention() default false;
    
        @LeafAnnotation
        Target targetLeafAnnotation() default @Target({ ElementType.METHOD });
    
        @LeafAnnotation(factoryClass = RetentionLeafAnnotationFactory.class)
        Retention retentionLeafAnnotation() default @Retention(RetentionPolicy.RUNTIME);
    }

    // apply the composite annotation...
    @Resource
    @TargetRetentionLeafCompositeAnnotation(runtimeRetention = true)
    private static @interface AnnotatedAnnotation {}

    // ...to two targets
    @Resource
    @TargetRetentionLeafCompositeAnnotation(runtimeRetention = false)
    private static @interface OtherAnnotatedAnnotation {}
    
    public static void main(String[] args) {
	// get a configured instance of the AnnotatedElements interface
        AnnotatedElements annotatedElements = ...
        
        log.info("Retrieving annotations from AnnotatedAnnotation.class");
        log.info(Arrays.toString(annotatedElements.getAnnotations(AnnotatedAnnotation.class)));
        
        log.info("Retrieving annotations from OtherAnnotatedAnnotation.class");
        log.info(Arrays.toString(annotatedElements.getAnnotations(OtherAnnotatedAnnotation.class)));
    }
    
}

When run, it should produce output similar to the following

Retrieving annotations from AnnotatedAnnotation.class
[@javax.annotation.Resource(shareable=true, mappedName=, description=, name=, 
type=class java.lang.Object, authenticationType=CONTAINER), @java.lang.annotation.Retention(
value=RUNTIME), @java.lang.annotation.Target(value=[METHOD])]
Retrieving annotations from OtherAnnotatedAnnotation.class
[@javax.annotation.Resource(shareable=true, mappedName=, description=, name=, 
type=class java.lang.Object, authenticationType=CONTAINER), @java.lang.annotation.Retention(
value=CLASS), @java.lang.annotation.Target(value=[METHOD])]

which demonstrates the key features of @Composite: namely, that the composite annotation is correctly "expanded" into a @Target and a @Retention annotation, and the "regular" @Resource annotation is still picked up.

Defining composite annotations

OK, so let's dive into this in a bit more detail. A composite annotation is just a regular custom annotation which is itself annotated with @CompositeAnnotation. Like any other annotation, it may have members, which may be of primitive or annotation type, amongst others. For instance

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@CompositeAnnotation
public @interface DoubleOhAgentCompositeAnnotation {
    ...
}

The "leaf" annotations - the annotations that the composite "expands" to - are simply members that are annotated with @LeafAnnotation. Of course, this only makes sense if these members return an annotation type! Note that members of a composite annotation are not automatically leaves, even if they return an annotation.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@CompositeAnnotation
public @interface DoubleOhAgentCompositeAnnotation {
    // not a leaf annotation - not even an annotation type!
    int numberOfKills();

    // also not a leaf annotation, thus rather pointless to include
    Department employer default @Department("MI6");

    @LeafAnnotation
    CodeNumber codeNumberLeafAnnotation;

    @LeafAnnotation
    Clearance clearanceLeafAnnotation default @Clearance(SECRET); 
}

As indicated in the comments, whilst there is nothing wrong with declaring non-leaf annotation members in the composite, it doesn't make much sense, in general, because the only purpose of the composite is to be expanded.

The values that will be returned for the leaf annotations are the values of the members of the composite instance that is being expanded. So the following composite instance

@DoubleOhAgentCompositeAnnotation(numberOfKills = 36, codeNumberLeafAnnotation = @CodeNumber("006"))
Agent jackGiddings;

would expand to @CodeNumber("006"), @Clearance(SECRET), and

@DoubleOhAgentCompositeAnnotation(numberOfKills = 103, codeNumberLeafAnnotation = @CodeNumber("007"), 
    clearanceLeafAnnotation = @Clearance(TOP_SECRET))
Agent jamesBond;

to @CodeNumber("007"), @Clearance(TOP_SECRET).

Specifying good defaults almost always makes sense (if nothing else, it saves on typing!), but as you can see they can always be overriden, if required.

Writing leaf annotation factories

So what about this mysterious numberOfKills member? It's not of an annotation type, so certainly can't be a leaf. Moreover, it doesn't even have a default value, which means it must be specified every time the composite is used! Annoying, or what?

Well, I'll be getting to that. For now, it's worth bearing in mind that, whilst defining leaf annotations using defaults is already convenient, it's still static - the defaults are fixed in the definition, and even overriding defaults can only be done using values known at compile-time.

But what if the value of a leaf annotation depended on some runtime property - oh, I don't know, the time of day, the phase of the moon? Or if there was some non-trivial business logic involved that we would rather not have to carry out "by hand", as we would have to in order to specify the value at compile time?

Not that these cases are likely to occur often, but when they do, it would sure be nice to be able to generate the appropriate leaf annotation value dynamically.

This is where LeafAnnotationFactory comes in2. It generates a leaf value based on the composite annotation instance the leaf is declared in - and whatever else you can lay your hands on at runtime...even the phase of the moon, if you like.

As an example, assume that we wish to calculate a "danger rating" for our 00 agents:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@CompositeAnnotation
public @interface DoubleOhAgentCompositeAnnotation {
    ...
    @LeafAnnotation
    Rating ratingLeafAnnotation; 
}

The current rules indicate that any agent with more than 100 kills - ah, yes, the numberOfKills member - is considered extremely dangerous.

Of course, we know that Maj. Jack Giddings is below and Bond above the limit, so we could just set the values at compile time.

@DoubleOhAgentCompositeAnnotation(numberOfKills = 36, codeNumberLeafAnnotation = @CodeNumber("006"),
    ratingLeafAnnotation = @Rating(DANGEROUS))
Agent jackGiddings;

@DoubleOhAgentCompositeAnnotation(numberOfKills = 103, codeNumberLeafAnnotation = @CodeNumber("007"), 
    clearanceLeafAnnotation = @Clearance(TOP_SECRET), 
    ratingLeafAnnotation = @Rating(EXTREMELY_DANGEROUS))
Agent jamesBond;

But what if Her Majesty's government, in a fit of political correctness, lowers the limit to 30? Will we remember to adjust the annotations? Will we want to do it for all of our agents?

No, better to generate the correct value for ratingLeafAnnotation at runtime.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@CompositeAnnotation
public @interface DoubleOhAgentCompositeAnnotation {
    int numberOfKills();
    ...
    /*
     * The default will be *ignored* - the factory is always called - but it is more 
     * convenient to provide it to prevent the compiler from prompting for one.
     */
    @LeafAnnotation(factoryClass = RatingLeafAnnotationFactory.class)
    Rating ratingLeafAnnotation default @Rating(DANGEROUS); 
}

class RatingLeafAnnotationFactory
        implements LeafAnnotationFactory {
    private static final int EXTREME_DANGER_THRESHOLD = 100;

    public Rating newInstance(
            DoubleOhAgentCompositeAnnotation declaringCompositeAnnotation) {
        return RuntimeConfiguredAnnotationFactory.newInstance(Rating.class, 
                MapUtils.toMap("value", 
                        (declaringCompositeAnnotation.numberOfKills() > EXTREME_DANGER_THRESHOLD) 
                        ? Rating.EXTREMELY_DANGEROUS
                        : Rating.DANGEROUS));
    }

}

// @Rating calculated based on the number of kills
@DoubleOhAgentCompositeAnnotation(numberOfKills = 36, codeNumberLeafAnnotation = @CodeNumber("006"))
Agent jackGiddings;

@DoubleOhAgentCompositeAnnotation(numberOfKills = 103, codeNumberLeafAnnotation = @CodeNumber("007"), 
    clearanceLeafAnnotation = @Clearance(TOP_SECRET))
Agent jamesBond;

Now all we need to adapt to the new rules is to modify the EXTREME_DANGER_THRESHOLD in the factory3!

Note that the default defined (somewhat counter-intuitively, perhaps) for ratingLeafAnnotation has no effect on the value: the factory is always called. But without a default the compiler will prompt for a value for the member whenever the composite is used.

Usage guidelines

In order to forestall any chance of your mind taking off on imagining all the things one might do with this, a few sobering points:

  • Firstly, remember that @Composite currently only works if annotation reflection is done via the AnnotatedElements interface. Class.getAnnotation(...) will not work here!
  • This also means that, regrettably, @Composite will not work for Hibernate, Spring or any other framework that internally uses "regular" methods to access annotation information.4
  • For the same reasons, @Composite should not be used to provide annotations which are used by Java itself, e.g. @Target or @Retention, as these won't be visible to the compiler! So please don't go about copying the demo ๐Ÿ˜‰

Restrictions

Apart from the above guidelines, there are a number of intentional limitations in the use of composites, whose aim is to make sure that @Composite does not allow you to work around restrictions on the use of "regular" annotations. This ensures that annotation semantics remain consistent with regular Java.

Which means that, amongst others:

  • There may also only be one leaf annotation of a given type per composite.
  • The target of a leaf annotation must match that of the composite annotation in which it is declared.
  • There may not be more than one annotation of the same type on a given element, whatever the regular and composite annotations on that element.

For the full list, see LeafAnnotation and CompositeAnnotation.

Using the validation processor

In most of the above cases, @Composite will fail early, at the moment the AnnotatedElements instance is created. Still, it would be nicer if invalid configurations - and as we have seen, there are quite a few ways of creating those - could be detected before we even get to runtime.

To help accomplish this, @Composite includes a validation processor, which is a Java 6 annotation processor5. If you're running on Java 6, the processor will automatically run at compile-time6 and fail, with hopefully useful error messages, if @Composite is not being used correctly. You can also add validation processing to your Eclipse project - see the CompositeAnnotationValidationProcessor Javadoc for details.

For a demonstration of the validation processor, check out the at-composite-validator-demo project (http://aphillips.googlecode.com/svn/at-composite-validator-demo/trunk/) and try to build it!

Integrating with Spring

@Composite wouldn't be a self-respecting Java project if it didn't offer some kind of Spring integration now, would it7? Thankfully, there's not much to do here - simply create an instance of
AtCompositeAnnotatedElements (which can be a singleton), passing a list of the composite annotation types to be supported.

AtCompositeDemo already demonstrates how to use @Composite with Spring, and more information can be found in the AtCompositeAnnotatedElements Javadoc.

Getting @Composite

The @Composite source code, and that for non-standard dependencies, is available at Google Code.

Maven

If you're using Maven, the relevant dependency is

  
    com.qrmedia.pattern
    at-composite
    1.0-SNAPSHOT
  

and you'll need to add the following repository to your POM.

  
    qrmedia-releases
    http://qrmedia.com/mvn-repository
  
Footnotes

  1. Although that would probably make an interesting follow-up project!
  2. I studiously ignored its appearance in the demo, but I'm pretty sure I didn't manage to sneak it past all of you ๐Ÿ˜‰
  3. RuntimeConfiguredAnnotationFactory is just a convenient way of generating a runtime annotation instance, but well worth a look...if only to see that it's probably simpler than one might expect!
  4. I suspect, though, that it wouldn't be too hard to adapt such frameworks to use @Composite, since it is fully "backwards-compatible" with regular Java annotations. With judicious use of aspects, there might even be a way of "retro-fitting" this onto existing code.
  5. Testing the annotation processor proved rather less straightforward than I first imagined, but that's a story for another blog post.
  6. You can suppress annotation processing by passing a -proc:none argument to the compiler. See the javac docs.
  7. Although I have to confess to secret Guice leanings of late ๐Ÿ˜‰