Making the Most of Java's Metadata, Part 2: Custom Annotations

Learn how to write your own annotation types and make use of built-in annotations to control their behavior.
by Jason Hunter

Developer: J2EE & Web Services

In my previous article in this series, I introduced Java's new metadata facility and described the built-in annotation types @Override,@Deprecated, and @SuppressWarnings. In this article I'll show you how to write your own annotation types and make use of the built-in annotations from the java.lang.annotation package to control your annotation's behavior.

Downloads for this article:  

When thinking about custom annotation several ideas come to mind. Picture a @ThreadSafe annotation that declares to any caller the class or method's thread safe design, or a @NotThreadSafe to act as a warning. Picture a @Copyright("Jason Hunter") annotation to dictate within the bytecode the copyright of a class file, with perhaps a corresponding @License(License.APACHE20) annotation to dictate the terms under which the code has been shared. Unlike copyright and license statements placed in comments, these could persist through the compile and be charted programmatically. Perhaps you have your own ideas. There's a lot of experimenting to be done! This article will get you started.

An @Unfinished Annotation Type

For our example let's imagine and then create an @Unfinished annotation type. The idea for this example comes from the common situation where you sketch out a block of code and leave some classes or methods temporarily unfinished—basic scaffolding on which proper code can and will be added later. Perhaps like me, you've traditionally marked these areas with a special "XXX" or "TODO" comment (something easy to search for). Using Java Metadata and a custom annotation type you can instead use an annotation to mark these code construct as @Unfinished.

This approach opens up a few interesting possibilities. You can, for example, at runtime have any unit tests that call into unfinished code produce a special "unfinished" result, something not to be confused with a regular failure. Or you could at compile time output warnings whenever fully completed code has a dependency on unfinished code, marking it as "effectively unfinished" as well. Plus, while comments tend to be free-form, an annotation type can be written to accept a description string, optional owner list, and a priority (with an assumed default).

You write annotations using a special @interface syntax:

public @interface Unfinished { } // Unfinished.java

The @interface looks a lot like an annotation but isn't exactly. Consider it a pseudo-annotation, something that just marks a class as an annotation. As the comment above explains, you place annotations in regular .java source files. Annotations compile down to regular .class files. 

Our annotation is missing a lot of features, so for the moment let's self-referentially mark it unfinished:

@Unfinished
public @interface Unfinished { }

Annotation Parameters 

To provide your @Unfinished annotation type with its description, owner, and priority you'll need to add parameters to the annotation declaration. Annotation parameters follow certain strict rules:

  • Parameters may only be typed as a primitive, String, Class, enum, annotation, or an array of any of these.
  • Parameter values may never be null!
  • Each parameter may declare a default value.
  • A single parameter named "value" can be set in a shorthand style.
  • Parameters are written as simple methods (no arguments, no throws clauses, etc).

Here then is an improved version of @Unfinished, something a bit closer to being finished:

@Unfinished("Just articleware")
public @interface Unfinished {
  public enum Priority { LOW, MEDIUM, HIGH }

  String value();
  String[] owners() default "";
  Priority priority() default Priority.MEDIUM;
}

There are several interesting things to note about this short example. First, we've added the parameters using a syntax that superficially looks like method declarations: value(), owners(), and priority(). As you'll see in my next article, these methods act as getters than can be called at runtime. 

The "value" parameter, because of its special name, is assumed when the attribute is passed just one parameter. You can see this in the "Just articleware" annotation. We had to add that description there because our "value" parameter declares no default value, so any use of the annotation without a description generates a compile error:

Unfinished.java:4: annotation Unfinished is missing value
@Unfinished
 ^
1 error

We could have allowed the no-argument use by providing a default value for the value() parameter, as we did for the owners() parameter. Notice how the owners() parameter takes a simple string as a default value while the type of the parameter is *array* of Strings. That's possible thanks to varargs, another J2SE 5.0 feature. (For more information about varargs and also the new enum facility used to define the Priority type, see my previous article.)

You might find the use of "default" in the declaration to be slightly odd, but the "default" keyword isn't new; Java used it previously in switch statements. You can tell a language is mature when each keyword has multiple uses.

Below is an example that demonstrates the @Unfinished parameter feature:

@Unfinished(
  value="Class scope",
  priority=Unfinished.Priority.LOW
)
public class UnfinishedDemo {

  @Unfinished("Constructor scope")
  public UnfinishedDemo() { }

  @Unfinished(owner="Jason", value="Method scope")
  public void foo() { }
}

The first use tags the full class as unfinished with a low priority and no specified owner. The second use tags the constructor as unfinished and gives just the required description, no specified owner or priority. The last use tags the foo() method as unfinished and provides an owner and a description, moving the description to the second argument rather than the first to demonstrate that order of named parameters doesn't matter.

What about a package level annotation? Because there's no natural place on which to put these, you place them within a specially named package-info.java file. It looks like this:

// package-info.java
@Unfinished("Package scope")
package com.servlets;

You need to include the package statement to indicate on which package the annotation is being placed. You can't assume the location on disk will be reliable.

For the above package-info.java example to compile, the @Unfinished annotation can't reside in the default package anymore. Prior to J2SE 1.4 you could import classes from the default package using a syntax like this:

import Unfinished;

That's no longer allowed. So to access a default package class from within a packaged class requires moving the default package class into a package of its own. So from now on, move @Unfinished into the com.servlets package:

ackage com.servlets;

@Unfinished("Just articleware")
public @interface Unfinished { ...

Now, there are a few extra rules we'd like @Unfinished to follow: It shouldn't be attachable to fields, parameters, or local variables (where it makes no sense). It should appear in the Javadocs. And it should persist into the runtime phase. Rules like these are specified as, you guessed it, annotations. 

Annotations on Annotations

J2SE 5.0 provides four annotations in the java.lang.annotation package that are used only when writing annotations:

  • @Documented—Whether to put the annotation in Javadocs
  • @Retention—When the annotation is needed
  • @Target—Places the annotation can go
  • @Inherited—Whether subclasses get the annotatio

We'll look at each in turn. 

@Documented

Annotations on a class or method don't by default appear in the Javadocs for that class or method. The @Documented annotation changes this. It's a simple marker annotation and accepts no parameters. With @Unfinished we want people to know which classes and methods have work remaining, so we will mark @Unfinished with this meta-annotation:

package com.servlets;
import java.lang.annotation.*;

@Unfinished("Just articleware")
@Documented
public @interface Unfinished { ...

Note the new import line as well as the multiple annotations placed at the same level. You can place as many annotations as you'd like on an element, just not two of the same type. After this change, in the Javadocs for the earlier UnfinishedDemo example you'll see this:

@Retention 

How long do you want to keep your annotation? There are three options as listed in the RetentionPolicy enumeration:

Option Comments Example
RetentionPolicy.SOURCE Discard during the compile. These annotations don't make any sense after the compile has completed, so they aren't written to the bytecode. @Override,@SuppressWarnings
RetentionPolicy.CLASS Discard during class load. Useful when doing bytecode-level post-processing. Somewhat surprisingly, this is the default. -
RetentionPolicy.RUNTIME Do not discard. The annotation should be available for reflection at runtime. @Deprecated

The @Retention annotation lets you specify the desired RetentionPolicy for a custom annotation type; it accepts a single "value" parameter of type RetentionPolicy. For our @Unfinished example RetentionPolicy.RUNTIME makes the most sense, so you'd make the change below.

package com.servlets;
import java.lang.annotation.*;

@Unfinished("Just articleware")
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Unfinished { ...

In my next article I'll explain how you can read the annotation at runtime.

@Target 

Now where do you want your annotation to be placed? You have eight options listed in the ElementType enumeration:

  • ElementType.TYPE (class, interface, enum)
  • ElementType.FIELD (instance variable)
  • ElementType.METHOD
  • ElementType.PARAMETER
  • ElementType.CONSTRUCTOR
  • ElementType.LOCAL_VARIABLE
  • ElementType.ANNOTATION_TYPE (on another annotation)
  • ElementType.PACKAGE (remember package-info.java)

When you've come up with the list of allowed locations, you specify it using the @Target annotation that accepts an array of ElementType values. It's an inclusive list only, meaning you can't exclude just one location, rather you must list the seven that are allowed. The default when no @Target is present is to allow at any location. The @Unfinished example makes sense in five locations, so we can dictate that like this:

package com.servlets;
import java.lang.annotation.*;

@Unfinished("Just articleware")
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD,
 ElementType.CONSTRUCTOR,ElementType.ANNOTATION_TYPE,
 ElementType.PACKAGE})
public @interface Unfinished { ...

@Inherited 

Finally, @Inherited controls if an annotation should affect subclasses. Forexample, does an @Unfinished superclass imply an unfinished subclass? Probably so, so here's the final form of @Unfinished:

package com.servlets;
import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD,
 ElementType.CONSTRUCTOR,ElementType.ANNOTATION_TYPE,
 ElementType.PACKAGE})
@Inherited
public @interface Unfinished {
  public enum Priority { LOW, MEDIUM, HIGH }

  String value();
  String[] owners() default "";
  Priority priority() default Priority.MEDIUM;
}

Futures 

In the next article in this series, I'll show how Java's reflection capabilities have been enhanced to help you discover annotations at runtime and how the Annotation Processing Tool "apt" lets you use annotations at build-time.

Jason Hunter is author of Java Servlet Programming and co-author of Java Enterprise Best Practices (both O'Reilly). He's an Apache Member and as Apache's representative to the Java Community Process Executive Committee he established a landmark agreement for open source Java. He's publisher of Servlets.com and XQuery.com , an original contributer to Apache Tomcat, the creator of the com.oreilly.servlet library, and a member of the expert groups responsible for Servlet, JSP, JAXP, and XQJ API development. He co-created the open source JDOM library to enable optimized Java and XML integration. In 2003, he received the  Oracle Magazine Author of the Year award.