Java
Annotations and apt
Metadata is officially
part of the language
J.BENTON
Java
developers have long been able to add metadata to code using
comment-eating tools such as Javadoc and XDoclet. However, Java 5.0
introduce a language facility called "annotations" that integrates
a metadata technology directly into the language, providing a
standard for giving attributes to declarations (see JSR-175,
(http://www.jcp.org/en/jsr/detail?id=175).
To support annotations, Sun Microsystems released apt an
"Annotation Processing Tool that lets you write your own annotation
processors (http://java.sun.com/j2se/1.5.0/docs/guide/apt/GettingStarted.html).
This makes it possible for you to build programs that act upon
annotations for analyzing Java source files and automatically
generating boilerplate code. It does this by providing the mirror
API, which provides functionality similar to the reflection API
built into the Standard Java libraries. In this article, I examine
annotations and show how you use apt to analyze Java source files
and generate boilerplate code.
You use annotations just as you would any modifying
keyword (final, static, transient, and the like), except you put
"@" in front of it. Each annotation has a set of intended
declaration targets. That is, an annotation is intended to be
placed before a package, type (including class, enum, and
interface), field (including an enum constant), constructor,
method, parameter, local variable declaration, or any combination
of these. Three types of annotations exist within Java, each with
slightly different syntax. Table 1 describes each type.
java.lang Annotations
Syntax always gets clearer as you see examples of real usage.
Happily, J2SE 5.0 supplies annotations built into the API to
explore. Three are intended to be used by Java compilers and
defined in java.lang, so no import is required to use them .
• @Deprecated. As a standard practice with Java, any declaration
that has become obsolete or should not be used for some reason,
whether replaced or not, becomes deprecated. Before this
annotation, the only way to mark a deprecated declaration was to
use Javadoc. This annotation helps compilers and IDEs tell you when
you're using a deprecated declaration. It's a marker annotation (no
values required). You can use this to annotate any item; see
Example 1.
• @SuppressWarnings. Compilers warn you when your code is
syntactically legal but potentially problematic. Sun's javac
compiler gives you the ability to turn on warnings using the -Xlint
option, which is very much connected with the @SuppressWarnings
annotation. However, each compiler can define its own warnings
(although there's a request from Sun that compiler writers work to
make the warning values compatible).
To compile Example 2, you'd enter:
javac -Xlint:unchecked
UncheckedExample.java
This returns a warning. When you add
@SuppressWarnings(value={"unchecked"}) as in Example 3, you get no
warning.
You can suppress more than one warning by putting
more into the value array; for instance, @SuppressWarnings(
value={"unchecked", 'finally"}). Table 2 lists the possible options
that can be given to -Xlint (given to javac in the form of
-Xlint:<option>). If you give no options to -Xlint, it
enables all warnings. You can also disable warnings through -Xlint
by doing -Xlint:-<option>. Again, including the extra minus
disables a warning.
• @Override. If you think you're overriding a method, use the
@Override annotation, which lets the compiler catch the common
problem of thinking you're overriding something when you aren't.
This is especially useful to keep track of class hierarchy. For
instance, if I try to implement java.util.Collection, then try to
override the method Object get(int index), I get a compiler error.
If I neglected to use the annotation, the compiler would just think
I'm creating a new method. Example 4 fails to compile because the
List interface - not the Collection interface - defines get(). List
extends Collection, which is occasionally a source of confusion for
those who are used to exclusively using Lists.
Meta Annotations
An annotation type defines annotations, just as an interface
defines an object. In fact, annotation types exist as interfaces
and act like them in some respects. They automatically extend the
java.lang.annotation. Annotation interface and have methods with
return types. Their methods can only return primitive types,
strings, enums, another annotations, and arrays of those
things.
In addition to the three previous annotation
classes in the java.lang package, four are defined in
java.lang.annotation. All the annotations in that package are meta
annotations- annotations that annotate annotation types.
• @Retention. Annotations can have one of three retention
policies in Java 5.0. Annotations having "source" retention
(RetentionPolicy.SOURCE) exist only in source code and don't get
compiled into bytecode. Annotations with "class" retention
(RetentionPolicy.CLASS) are intended to be read at the bytecode
level, after compilation and before runtime. Finally, runtime
retention (RetentionPolicy. RUNTIME) indicates that an annotation
can be accessed through reflection at runtime. I focus on
annotations with "source" retention because apt is a source-level
processing tool.
• @Target. I previously described the type of targets. You can
specify a single target using a single value or multiple ones using
an array. Also, giving an empty array to an annotation of this type
holds special value. It forces you to use annotation types only as
a return type within other annotation types. The apt examples show
why this is sometimes necessary when you want multiple annotations
of the same type to be given on a single item.
• @Documented. The @Documented meta annotation is intended for
use by document-generation tools like javadoc. Javadoc generates
documentation for an annotation type if, and only if, this meta
annotation is applied to it.
• @Inherited. As meta annotations go, @Inherited probably ranks
as the most interesting (although it is also excluded from the apt
examples for simplicity). It lets annotations be inherited by
subclasses of the annotated class. If an annotation type uses this
meta annotation, any type (including classes, interfaces, and
enums) that it annotates lets this annotation be inherited to its
subtypes. This is an "if and only if" condition - if you don't want
subtypes to inherit this annotation, then don't use it.
(Annotations are inherited on the annotation types themselves.
Overridden methods will not inherit annotations.)
Boilerplate Code and apt
In their short history, Java annotations have become widely
known to be useful for generating boilerplate code. I present an
example of this by showing an annotation processor using apt. The
apt annotation processor generates new exception classes based upon
an annotation given to a type. The exception is slightly different
depending upon the values given to the annotation. Listing One
contains one of two annotation types required for this example.
Annotations give the "default" keyword a second
meaning (the first being the "default case" in the switch
statement). You use it in annotation type definitions to specify
the default value that a method in an annotation returns if the
value is not specified.
In Listing One, ©Target contains an empty array.
The Java compiler won't let you use the same annotation type more
than once on the same item. To get around this, you have to create
an annotation that takes an array of ApplicationException
annotations as its value. The only way you can use an annotation
without a target is within another annotation, so defining an
annotation in this way ensures proper semantics for the annotation
processor. Listing Two shows ApplicationExceptions, the annotation
type containing an array of ApplicationException.
ApplicationExceptions targets types.
The real power of apt comes from the way it
processes annotations in Java source files. The tool uses
annotations to:
• Zero in on the flles it needs to process.
• Analyze the files appropriately and produce boilerplate code
or external configuration files.
When apt generates a source file, it processes and
compiles it in turn. ApplicationExceptionsApf. java (available
electronically; http://www.ddj.com) shows the annotation
processor factory and class for creating the boilerplate code based
upon the ApplicationExceptions annotation. Notice the structure of
the factory class: It extends
com.sun.mirror.apt.AnnotationProcessorFactory and overrides
"AnnotationProcessor getProcessorFor (Set,
AnnotationProcessorEnvironment)". You give a list of annotations
that the factory in ApplicationExceptionsApf.java has processors to
handle with the variable supportedAnnotations. The provided list
can sometimes look like Java's import facility, but don't be fooled
- it acts differently. Unlike package imports,
com.ddj.annotations.* means all annotations defined in the package
com.ddj.annotations and all of its subpackages. Also, you can
specify "*" to mean all annotations can be supported by this
factory (this has sort of a double meaning, as it denotes this
annotation processor can process any file - even ones containing no
annotations). If you want to define more than one package, include
them in the array.
You can also define your own, userdefined options
for annotation processors. To do this, you let the method
supportedOptions() return a collection of options beginning with
-A. For instance, say you want the processor to have the ability to
override the additional information given in the example, so you
add -AlgnoreAddedlnfo as a supported option. The list only adds the
convenience of checking the "supported options" against the options
provided to apt. All of apt's own options and those starting with
an -A, whether supported or not, get passed to the processor
through environment.
In the example, the method getFactoryFor() works in
a straightforward manner. It checks that the given supported
options match those given to apt. After that, it goes through the
annotations that apt has found that the annotation processor also
claims to support (given through the atds set). Once it finds
com.ddj.annotationsApplicationExceptions, it returns the proper
annotation processor. If it fails to find the suitable annotation,
it returns a NO_OP annotation. Use NO_OP whenever you want to
return an annotation processor that does nothing.
Also in ApplicationExceptionsApf.java is the
annotation processor itself. It goes into the processor factory
class as a static inner class. If you're familiar with Java
reflection, you recognize the methods used to analyze the code.
Sun's mirror API works much like reflection, except it refers to
source code rather than loaded classes and, therefore, works on
types and declarations. Table 3 shows each package in the mirror
API and a short description of each.
The processor is simple. It first looks at each
type declaration given to the apt environment. It then gets the
ApplicationExceptions annotation and processes each
ApplicationException value within, generating a .java file
containing the appropriate exception definition.
Next, you'll need to use this annotation processor.
apt provides two ways of doing this: You can either use apt's
built-in annotation processor factory discovery facility, or
specify the processor on a command line. You'd use the discovery
procedure when you have more than one annotation processor factory.
Because only one annotation processor factory exists here, I stick
to the command line. You can use the discovery procedure by putting
a file called
"META-INF/services/com.sun.mirror.apt.AnnotationProcessorFactory"
in a .jar file that lists each annotation processor factory that
you created. The .jar file also needs to contain all factories
listed and their processors. You then can put the .jar me in your
class path and apt tries to choose the proper factory for each
.java file you're processing, based upon supported annotations you
list within each factory.
Listing Three is an example of using the
ApplicationExceptions class. Again, I use a command-line option to
generate and execute the annotation processor. Before executing the
following command, make sure the tools.jar file is in your
classpath and that you've compiled the annotation processor
factory:
apt - factory com.ddj.apt.processors.
ApplicationExceptionsApf corn\ddj\ * .java
This should have generated two files:
TestException.java contains an extra integer and AppException.java
exists as a plain extension to Exception.
Conclusion
In this article, I introduced the annotations built into J2SE
5.0 and looked at how you can use apt to generate boilerplate
exception code. I encourage you to expand upon the code. It's
important to note, however, that apt's days may be numbered. The
recently approved JSR-269, the Pluggable Annotation Processing API,
expressly states that it is intended to supersede Sun's mirror API
and apt. If it stays on schedule, it will likely ship with Java 6.0
(codenamed "Mustang").
/** Deprecating a method using the annotation. @author J. Benton
*/
public class DeprecateExample {
//if you use this method, you'll get a warning that it
is deprecated.
@Deprecated
public void ohNoIAmDeprecated() {
System.out.printIn("I am deprecated!");
}
}
Example 1: Deprecating a method using
annotation.
import java.util.ArrayList;
/** Suppressing warnings. @author J. Benton */
public class UncheckedExample {
public void uncheckedGenerics() {
ArrayList blahblah = new
ArrayList();
// warning, warning, warning!! unchecked
call below!
blahblah.add(2);
}
}
Example 2: Generating a warning.
import java.util.ArrayList;
/** Suppressing warnings. @author J. Benton */
// You can use so-called single-valued
annotations
// just like regular annotations if you want
(note
// how I am specifying the method name here,
though
// it is unnecessary) .
@SuppressWarnings(value={"unchecked"})
public class UncheckedExample {
public void uncheckedGenerics() {
ArrayList blahblah = new
ArrayList();
// no warning! I suppressed it!
blahblah.add(2);
}
}
Example 3: Using the @Suppress Warnings
annotation.
Annotation
Description
Example Syntax
Marker
Takes no
values
@MarkerAnnotation
Single valued Takes one
value
@ SingleValuedAnnotation("VALUE")
Regular
Takes more than one value @
RegularAnnotation(name="John Q.", bugValue=1)
Table 1: Annotations get addressed differently
depending upon the number of values they take.
Options
Description
none
Give no warnings.
unchecked Give details on unchecked
conversions of types.
path Check
for a nonexistent path in environment paths (such as
classpath).
serial
Check that a serialVersionUID is given in serializable
classes.
finally
Check that finally classes can be completed normally.
fallthrough Make sure you break
after each case statement that would otherwise fall through to the
next case statement in a switch
depreciation Check for use of depreciated items.
Table 2: Options that can be given the -XIint
in javac.
com.sun.mirror.apt
Provides classes that interact directly with the tool and its
data.
com.sun.mirror.declaration Defines interfaces to
declarations.
com.sun.mirror.type
Defines interfaces to types defined within the source code.
com.sun.mirror.util
Contains utilities for processing types and declarations.
Table 3: Descriptions of the apt mirror
API.
package com.ddj;
import java.util.*;
/** Using the ®Override annotation. @author J.
Benton */
public class OverrideExample implements Collection
{
// ...
/** This won't work! I'm not overriding anything
here!
* This is a new method--not one being
overridden.
* @param index The index of the thing to
get.
* @see java.util.List#get(int)
*/
@Override
public Object get(int index) {
Object value = null;
// code to get stuff.
return value;
}
// ...
}
Example 4: Using the @Override annotation.
Listing One
/* ApplicationException.java * Created on Jan 2,
2ØØ5 */
package com.ddj.annotations;
import java .lang. annotation.*:
/** This annotation is used by the annotation
processing tool (apt)
* to generate an exception. * @author J. Benton
*/
@Retention(RetentionPolicy, SOURCE)
// I give an empty array to "@Target"" to say that
this annotation
// type should be used only as an element of other
annotations
// (you cannot use this to annotate anything
directly).
@Target ( { } )
public @interface ApplicationException {
/** What I want to call this exception
(sans the word "Exception".*/
String exceptionName();
/** I must add extra information to this
exception to make it useful. */
String addedinformationType() default "";
/** I should call this extra information
something that is recongizable if
* you try to read the generated
exception class. */
String addedInformationVariableName() default
"";
}
Listing Two
/* ApplicationExceptions.java * Created on Jan 2,
2ØØ5 */
package com.ddj.annotations;
import java.lang.annotation.*;
/** This was made so I can generate a set of
exceptions for an application
* on the main application class. * @author J.
Benton */
@Target (ElementType.TYPE)
@Retention (RetentionPolicy.SOURCE)
@Documented
public @interface ApplicationExceptions {
/** An array of ApplicationExceptions. I do things this
way because we can 1t
* have more than one instance of the same annotation type on
any item. */
ApplicationException[] applicationExceptions();
}
Listing Three
/* ExceptionAnnotationTest.java * Created on Jan 3,
2ØØ5 */
package com.ddj;
import com.ddj.annotations.*;
/** Use this to test the annotations. * @author J.
Benton */
@ApplicationExceptions {
applicationExceptions={@ApplicationException (
exceptionName="Test",
addedinformationType="int";
addedinformationVariableName="status"),
@ApplicationException
(exceptionName="App")})
public class ExceptionAnnotationTest{
}
"An
annotation type defines annotations, just as an interface defines
an object"
J. Benton is a professional Java programmer and computer science
graduate student with research interest in AI planning. He can be
reached at [email protected].
Dr Dobb's Journal, #374 July 2005, Pg 31-37