@Composite - Macro Annotations for Java

Andrew Phillips

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

Comments (6)

  1. Wilfred Springer - Reply

    June 23, 2009 at 9:59 am

    Totally cool. I'm just wondering if it would be possible to have a general purpose LeafAnnotationFactory that's built on top of some sort of EL. (Which would pull the derivation rule back into an annotation attribute.)

  2. Andrew Phillips - Reply

    June 23, 2009 at 10:12 am

    NB: Trying to build this on a few different machines has thrown up some unexpected test failures in CompositeAnnotationValidationProcessorTest. This uses the Java Compiler API and doesn't appear to be exactly bullet-proof yet - the tests in this class can be @Ignored, and I'll try to figure out what's going on asap.

    Also, running eclipse:eclipse or eclipse:m2eclipse against the project will leave you with a

    <classpathentry kind="src" path="src/test/java/com/qrmedia/pattern/compositeannotation/validation/sample" output="target/test-classes/com/qrmedia/pattern/compositeannotation/validation/sample" excluding="**/*.java"/>

    entry in your .classpath. This is a (seemingly unavoidable) result of the way the test resources are configured in the POM - again, required for the validation processor tests - and needs to be removed.
    Or right-click on the source folder in Eclipse and select Build Path -> Remove from Build Path.

  3. fuzebest - Reply

    June 26, 2009 at 9:34 am

    why did you abandon the idea of bytecode or runtime instrumentation?
    Nowadays it is nothing wrong with it.

  4. Andrew Phillips - Reply

    June 26, 2009 at 10:25 pm

    @fuzebest "abandoned" is perhaps an unfortunate choice of phrase - it was intentionally not considered for a number of reasons, chief among which is that it would limit factory classes to once again essentially producing static annotations. The "leaf" annotation(s) would namely have to be generated at the moment the bytecode were instrumented, as opposed to when the lookup is made.

    But it is definitely an avenue worth pursuing, and certainly a possibility with regard to trying to integrate composite annotations with existing code.

  5. The sceptic - Reply

    August 16, 2009 at 2:01 pm

    Hi Andrew,

    sorry, but I didn't get the point. You just started to provide a shortcut for the tedious task of writing annotation triples (see your motivation example) and you end up with a composite-wrapper that contains literaly exactly that three annotations requiring to type even more characters for the wrapping-stuff? "Yes, you can" save typing effort in cases were you only want to use defaults for the members of a composite...but in any other case using the composition scheme is no better than using the original source annotations. You might argue that in cases I want to change all occurences of a certain scheme than I just have to change the composite. Yes, true, but then I would argue that it's probably never going to happen if you work with a set of stable annotations. And even if there's change then I would rather favour a text-processing tool. What you really wanted to do was to provide a c-style preprocessor, but disgiused it in some strange annotation scheme. I guess ant is capable of doing preprocessing stuff...
    As I am "The sceptic" I have been waiting ever since annotations arrived for the first projects doing funny stuff with it: Now here's another example. When is "jakarta commons annotations" going to arrive, which is the (meta) annotation wrapper (like this wrapper for logging frameworks...). And I like the Meta-Annotations of webbeans or what was the name of the next big thing. What I wanted to say with this rant is that you should concider using the right tool for a task, in oyur case a (macro) preprocessor...

    My suggstions:
    1.) Sun/Java starts providing a preprocessor api (I don't get the point why anybody refuses to concider a preprocessor, but any other funny stuff is discussed in epic breadth and blind enthusiasm, for instance making java functional...)

    2.) The Java framework itself should provide a plugable SPI-API for resolving annotations. Annotations should be allowed to have any number of arguments. A Resolver-SPI-Plugin (SPI function) could analyze the source annotations and provide an interpretation/tranformation into a target set. We would require

    2.1 Declaration
    @AnnotationBlaBla
    @AnnotationSPI(id=FunnyMapper)
    public @interface FunnyInterpretedAnnotation {
    String useCaseID;
    AnnotaionParamList nameValueVarargs;
    }

    2.2 How to use the Annotation
    @FunnyInterpretedAnnotation(UseCaseID="Bond")
    Agent ofYourMajesty;

    2.3 The SPI trafo function
    function: SPI( @FunnyInterpretedAnnotation(UseCaseID="Bond") ) -> { @NotNull, @CodeNumber("007"), @Clearance("Clarence") }

    2.4 SPI function configuration
    And then we need some sort of configuration, mapping UseCaseIDs into sets of target annotations...well let's use xml for that. Oh, wait a minute! annotations were invented partially to overcome xml-hell. Well, seems to be a silly suggeestion then...

  6. Andrew Phillips - Reply

    August 19, 2009 at 4:42 pm

    @The sceptic Replacing three annotations on an element with a "special" annotation that contains those three annotations would indeed be rather pointless. However, it's not uncommon, to see a group of annotations repeated many times throughout a codebase. This aside from the potential benefits (and drawbacks, of course) of making the annotations runtime-configurable, for instance

    > Sun/Java starts providing a preprocessor api
    Something in addition to the instrumentation API, or replacing it?

    The SPI concept is interesting, and would certainly offer a much cleaner way of making the annotation system pluggable, i.e. integrating it with "standard" Java annotation reflection.

Add a Comment