|
Developer: J2EE & Web Services
Making the Most of Java's Metadata, Part 2: Custom Annotations
by Jason Hunter
Learn how to write your own annotation types and make use of built-in annotations to control their behavior.
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.
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 unfinishedbasic 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:
package 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:
- @DocumentedWhether to put the annotation in Javadocs
- @RetentionWhen the annotation is needed
- @TargetPlaces the annotation can go
- @InheritedWhether 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.
Send us your comments
|