Java 8 – Annotation duplication

A new core language feature in Java 8 is the ability to add multiple annotations of the same type to a declaration. I can’t say I’d noticed you couldn’t, which probably suggests this isn’t a feature I’ll be heavily using.

Annotations, introduced in Java 1.5, have become pretty pervasive in the Java world. You won’t have covered too many miles without seeing, or needing to use, these little @ labels on classes, fields, methods or their parameters.

They’re changing in Java 8, at least in an apparently small way. Java 8 now allows you to repeat the same annotation on a given declaration.

It’s pretty obvious when you look at the code that supports annotations that this hasn’t previously been the case. The getAnnotation(Class<T>) method in the reflection API, for example, takes a specific annotation type and returns one instance and one only. And lest you try to add a second annotation of the same type to a declaration in some Java code your IDE will be screaming wriggly red lines at you in a heartbeat.

But then most annotations, such as JUnit’s @Test or JAXB’s @XmlRootElement, are pretty one-shot anyway and so you could be forgiven for not having noticed you’ve been unable to have more than one. You could also be forgiven for scratching your head a little as to where you’d want to.

Rifling through the many libraries in my current project’s codebase I can see that Apache’s CXF has a number of annotation cases where multiple instances are useful – such as @Policy which allows WS-Policy details to be associated with various aspects of a web-service. They handle the limitation by defining a parent @Policies annotation to contain the list:-

@Policies({
    @Policy(uri = "policy1.xml"),
    @Policy(uri = "policy2.xml")
})

Thinking back over my recent code-monkeying miles for an example to try this out with, I can only think of one place I might have wanted to do it…

Stuck

For a lightweight, but somewhat sprawling web-site, the owner wanted some values a user had previously entered to be prefilled on subsequent pages. My solution to this was to annotate fields on various form objects with a “sticky field” type and then run all submissions through a session based cache manager which would update the sticky field cache with any relevant values entered on that page. All page generation was then run through the same cache manager to pre-fill appropriate fields.

To illustrate, we’ll define an enumeration identifying some types of sticky field:-

public enum StickyField {
    NAME,
    HOME_EMAIL,
    WORK_EMAIL
}

We’ll define a simple @Sticky annotation to allow us to associate one of these values with a form field:-

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Sticky {
    StickyField id();
}

Let’s say we have a simple “My details” form on our web-site which accepts some of these values. We annotate those fields with the appropriate sticky field type so they get retained in the cache when the page is submitted:-

public class MyDetailsForm {
    @Sticky(id=StickyField.NAME)
    String name;
    String address;
    @Sticky(id=StickyField.WORK_EMAIL)
    String workEmail;
    @Sticky(id=StickyField.HOME_EMAIL)
    String homeEmail;

    // Getters and Setters

}

Another “Leave a Message” form allows the user to contact the web-site owner by email. This form accepts a subject and message along with the user’s name and email address which we can now pre-fill if they’ve been previously entered in their session. Since we want only a preferred email address here it would be nice to associate that field with both WORK_EMAIL and HOME_EMAIL so we can take either. Before Java 8 though, this code wouldn’t compile:-

public class LeaveAMessageForm {
    String subject;
    String message;
    @Sticky(id=StickyField.NAME)
    String yourName;
    @Sticky(id=StickyField.WORK_EMAIL)
    @Sticky(id=StickyField.HOME_EMAIL)
    String yourEmail;

    // Getters and Setters

}

With Java 8 it will, right?

Wrong.

In order to repeat an annotation in this way, the first thing you need to do is mark it as repeatable by, er, annotating it with @Repeatable. Only it’s not quite that simple as the Repeatable annotation requires a parameter specifying the class of a container annotation. Under-the-hood this works rather like the CXF @Policies/@Policy annotation highlighted earlier. It just hides the need for the annotated declaration to explicitly include the container annotation.

So, though we needn’t directly use it, we need to define an aggregating annotation which contains an array value of our repeatable annotation type:-

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Stickies {
    Sticky[] value();
}

Now we can update our Sticky annotation to mark it repeatable:-

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Repeatable(value = Stickies.class)
public @interface Sticky {
    StickyField id();
}

Our code now compiles clean, but what about using it?

Unstuck

In order to not break existing code the getAnnotation(Class<T>) reflection method has not been changed – it still returns at most one instance of the annotation type. We can give it our container type (in this case Stickies.class) and process the nested values ourselves, or alternatively we can call the new getAnnotationsByType(Class<T>) method which returns an array instead. Here’s a simple Sticky Field manager (which for brevity will sidestep unpleasantness such as type conversion and using getters and setters):-

public class StickyFields {

    private Map<StickyField, Object> cache =
            new HashMap<StickyField, Object>();

    public void updateStickies(Object form)
            throws IllegalArgumentException, IllegalAccessException {
        Field[] fields = form.getClass().getDeclaredFields();
        for(Field field : fields) {
            Object value = field.get(form);
            Sticky[] stickies = field.getAnnotationsByType(Sticky.class);
            for(Sticky sticky : stickies) {
                if(value == null) {
                    cache.remove(sticky.id());
                } else {
                    cache.put(sticky.id(), value);
                }
            }
        }
    }

    public void applyStickies(Object form)
            throws IllegalArgumentException, IllegalAccessException {
        Field[] fields = form.getClass().getDeclaredFields();
        for(Field field : fields) {
            if(field.get(form) != null) {
                continue;
            }
            Sticky[] stickies = field.getAnnotationsByType(Sticky.class);
            for(Sticky sticky : stickies) {
                if(cache.containsKey(sticky.id())) {
                    field.set(form, cache.get(sticky.id()));
                    break;
                }
            }
        }
    }
}

Finally, and again avoiding unpleasantness like robust test design, here’s some JUnit to exercise it:-

public class StickyFieldsTest {

    @Test
    public void test() throws Exception {
        StickyFields cut = new StickyFields();

        // "Submit" the MyDetailsForm
        MyDetailsForm myDetailsForm = new MyDetailsForm();
        myDetailsForm.setName("Joe Bloggs");
        myDetailsForm.setHomeEmail("joe.bloggs@somedomain.com");
        cut.updateStickies(myDetailsForm);

        // "Render" a LeaveAMessageForm
        LeaveAMessageForm messageForm = new LeaveAMessageForm();
        cut.applyStickies(messageForm);

        // Check the results     
        assertNull(messageForm.getMessage());
        assertNull(messageForm.getSubject());
        assertEquals("joe.bloggs@somedomain.com", 
                messageForm.getYourEmail());
        assertEquals("Joe Bloggs", messageForm.getYourName());
    }
}

If you’re excited by this change to the core Java language my advice is that you need to get out more. However it definitely removes what appears to be a needless restriction from the language which doubtless annoys people from time to time and maybe, just maybe, I’ll find a real use for it sometime.

Leave a Reply

Your email address will not be published. Required fields are marked *