Learn techniques and mechanisms for processing annotations and changing program behavior at runtime—and even compile-time.
by Jason Hunter
Learn techniques and mechanisms for processing annotations and changing program behavior at runtime—and even compile-time.
In the first article in this series of four, I introduced Java's new metadata facility and described the built-in annotation types @Override,@Deprecated,
and @SuppressWarning
. In the second article I showed how to write custom annotation types and use the meta-annotations from java.lang.annotation to control annotation behavior. In this third article, I'd like to demonstrate the techniques and mechanisms to process annotations and change program behavior at runtime and even compile-time.
The ability to interact with annotations at runtime can provide some terrific value. Imagine a next generation test harness built to take advantage of annotations. Such a harness can run methods marked as @Test
—with no method name mangling required to differentiate test methods from support methods. Using parameters to the @Test
annotation, each test can be logically grouped, can control what tests it depends on, and can accept various test case parameters. (You don't have to just imagine such a test harness, you'll actually find one at http://testng.org/doc/index.html.)
The possibilities continue in J2EE 5.0 environments where annotation-driven "resource injection" looks to become standard operating procedure. With resource injection, a container can "injects" values into the specially annotated variables of its managed objects. For example, if a servlet needs a data source, the model in J2SE 1.4 was to pull the resource from JNDI:
public javax.sql.DataSource getCatalogDS() {
try {
javax.naming.IntialContext initCtx = new InitialContext();
catalogDS = (javax.sql.DataSource)
initCtx.lookup("java:comp/env/jdbc/catalogDS");
}
catch (javax.naming.NamingException ex) {
// Handle failure
}
}
public Products[] getProducts() {
javax.sql.DataSource catalogDS = getCatalogDS();
Connection con = catalogDS.getConnection();
// ...
}
Not only was the code fairly complicated, for the resource to be available for the JNDI lookup, the servlet had to declare a <resource-ref>
entry in its separate web.xml deployment descriptor:
<resource-ref>
<description>Catalog DataSource</description>
<res-ref-name>jdbc/catalogDS</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
<res-sharing-scope>Shareable</res-sharing-scope>
</resource-ref>
Looking forward, under J2SE 5.0 with resource injection, it should be possible for the servlet to simply annotate its requirement within the code and have the data source reference "injected" before execution:
@Resource javax.sql.DataSource catalogDS;
public Products[] getProducts() {
Connection con = catalogDS.getConnection();
// ...
}
Especially exciting is that the annotation itself acts as the deployment descriptor, eliminating the need for the separate entry in the web.xml deployment descriptior. The @Resource
annotation can accept parameters to dictate the resource name, type, authentication, and scope values. Without parameters, those are conveniently inferred.
Now that JSR-181, Web Services Metadata for Java, has completed we'll see annotations used extensively to guide containers in deploying Wweb services. Annotations are becoming the raw materials for defining the contract between the container and managed object. I'll cover the JSR-181 Web services annotations in the fourth and final article in this series.
Java uses reflection to expose annotations and enable programs to alter their behavior at runtime. Java introduced basic reflection in J2SE 1.2, and to support annotations has added in J2SE 5.0 the AnnotatedElement
and Annotation interfaces. These two interfaces let you locate annotations and, once you've got a handle to an annotation, make calls to retrieve its parameters.
The new AnnotatedElement
interface is in the java.lang.reflect package and implemented by the classes Class, Constructor,Field, Method, and Package:
public interface AnnotatedElement {
Annotation[] getAnnotations();
Annotation[] getDeclaredAnnotations();
<T extends Annotation>
T getAnnotation(Class<T>);
boolean isAnnotationPresent(Class<? extends Annotation>);
}
The getAnnotations()
method returns all the annotations attached to the given element. The getDeclaredAnnotations()
method is similar but returns only those specifically declared at this location, not those that were @Inherited.
The getAnnotation()
method takes a class type and returns the annotation of that type. The method uses generics so the returned value is implicitly cast for you as appropriate. Whatever type of class is passed to the method, that's the type it returns. TheisAnnotationPresent()
method lets you peek if an annotation is present without retrieving it. It too uses generics to enforce that theClass type it accepts must be a class that implements Annotation.
Every annotation type automatically implements the java.lang.annotation.Annotation
interface. This happens in the background when you use the @interface
keyword to declare a new annotation type. Using the methods on AnnotatedElement
you can fetch anyAnnotation type. For example, the following code pulls the @Unfinished
annotation off the UnfinishedDemo
from the previous article and requests its priority:
Unfinished u =
UnfinishedDemo.class.getAnnotation(Unfinished.class);
u.priority();
Notice how generics make the interface declarations a bit more messy but make the functioning code quite elegant! It's also interesting to see how the stubbed-out methods used to declare annotation parameters end up being the methods called to retrieve their value.
The example below demonstrates how to do a complete "dump" of all the unfinished parts of the UnfinishedDemo
class.
import com.servlets.*;
import java.lang.reflect.*;
import java.util.*;
public class UnfinishedDump {
public static void main(String[] args) {
Class c = UnfinishedDemo.class;
System.out.println("Package:");
dump(c.getPackage());
System.out.println("Class:");
dump(c);
System.out.println("Constructor:");
dump(c.getConstructors());
System.out.println("Methods:");
dump(c.getMethods());
}
public static void dump(AnnotatedElement[] elts) {
for(AnnotatedElement e : elts) { dump(e); }
}
// Written specifically for Unfinished annotation type
public static void dump(AnnotatedElement e) {
if (e == null ||
!e.isAnnotationPresent(Unfinished.class)) {
return;
}
Unfinished u = e.getAnnotation(Unfinished.class);
String desc = u.value();
Unfinished.Priority prio = u.priority();
String[] owner = u.owner();
System.out.println(" " + desc + "; prio: " + prio +
"; owner: " + Arrays.asList(owner));
}
}
Let's walk through the code. The main()
method requests a dump of information about the UnfinishedDemo
class, its package, its constructors, and each of its methods. The dump()
method comes in two overloaded varieties. The first accepts an array and, using Java's new foreach loop, calls dump()
on each item in the array. The second accepts a single item and performs the bulk of the work.
The workhorse dump()
method first checks if the Unfinished
annotation is present.
If not, there's nothing to display so it returns. If so, it fetches the annotation using getAnnotation(),
gets its value, priority, and owner list, and then prints the values to the console. It wraps the array with a List
as a simple way to pretty print an array.
The output looks something like this:
Package:
Package scope; prio: MEDIUM; owner: []
Class:
Class scope; prio: LOW; owner: []
Constructor:
Constructor; prio: MEDIUM; owner: []
Methods:
Method; prio: MEDIUM; owner: [Jason]
A Basic @Test
Harness
I mentioned earlier the possibility of a test harness driven by an @Test
annotation.
The annotation itself we'll keep simple. It accepts no parameters, persists until runtime, and can only be applied to methods:
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test { }
We'll dictate the convention that any method marked @Test
will pass if it executes without throwing an exception, and will fail if an exception propagates to the caller. Thus the following simple example has one success, one failure, and one non-test method:
public class Foo {
@Test public static void m1() { }
public static void m2() { }
@Test public static void m3() {
throw new RuntimeException("boom");
}
}
The test harness can be implemented as simply as this:
import java.lang.reflect.*;
import static java.lang.System.out;
public class RunTests {
public static void main(String[] args) throws Exception {
int passed = 0, failed = 0;
for (Method m : Class.forName(args[0]).getMethods()) {
if (m.isAnnotationPresent(Test.class)) {
try {
m.invoke(null);
passed++;
} catch (Throwable ex) {
out.printf(
"Test %s failed: %s %n", m, ex.getCause());
failed++;
}
}
}
out.printf("Passed: %d, Failed: %d%n", passed, failed);
}
}
The main()
method loads the class specified as args[0]
and iterates over each of its methods. On each method it checks if the @Testannotation
is present, and if so invokes the method. Successful return increments the passed count. Any exception triggers the catch, an error report, and a failure increment. When the testable methods have all run their course, the method prints a final summary. A more advanced version would inspect the Annotation
to determine whether and how the method should be called.
Those with a sharp eye will notice the out.printf()
calls. The printf()
method is new in J2SE 5.0 and operates quite a lot like our old favorite from the C language. The %d
in the format string gets substituted by a decimal value while %n resolves to the platform-specific newline characters. We're able to use "out" instead of "System.out" because of the static import statement at the top, yet another J2SE 5.0 feature.
Annotation processing at compile-time gets a bit more involved than processing at runtime but can be significantly more empowering. To provide the compile-time hooks Java 5 introduces a new Annotation Processing Tool known as "apt". It's a wrapper for javac that includes a com.sun.mirror.* Mirror API for programmatic access to the code being processed through the tool. Using the apt tool you emit notes, warnings, or even errors during the compile phase. You can also generate new text, binary, or source files. That's where things get interesting.
Imagine you need an immutable subclass, a class that behaves like its superclass but where any attempt to change its value results in an exception. You see this implemented in the Java Collections library and it's a common convention in other programs. Unfortunately, it's fairly difficult to write in plain Java because of the challenge of keeping the subclass in sync with the super and not letting a new setter method slip by.
It's a problem that can be solved using annotations. First, we write a basic annotation type @Immutable
:
@Documented
@Target(ElementType.TYPE)
public @interface Immutable {
String value();
}
Then we can add the annotation to each immutable subclass:
public class Project {
// Content here
}
@Immutable
public class ImmutableProject { }
Even though we leave ImmutableProject
empty, the apt tool can generate during the compile a fleshed out ImmutableProject.java. You control the apt tool by providing it with an AnnotationProcessorFactory
that returns custom AnnotationProcessor
instances. Each AnnotationProcessor
can use the Mirror classes to review the classes going through the tool and output notes, warnings, errors, support files, or new source files (as we'll need for @Immutable
). After the apt tool finishes it invokes javac.
Below is a basic AnnotationProcessorFactory
implementation that supports only @Immutable
and returns anImmutableAnnotationProcessor:
import java.util.*;
import java.io.*;
import com.sun.mirror.apt.*;
import com.sun.mirror.declaration.*;
import com.sun.mirror.util.*;
public class ImmutableAnnotationProcessorFactory
implements AnnotationProcessorFactory {
public AnnotationProcessor
getProcessorFor(Set<AnnotationTypeDeclaration> atds,
AnnotationProcessorEnvironment env) {
if (!atds.isEmpty()) {
return new ImmutableAnnotationProcessor(env);
}
else {
return AnnotationProcessors.NO_OP;
}
}
public Collection<String> supportedAnnotationTypes()
{
return Collections.singletonList("Immutable");
}
public Collection<String> supportedOptions()
{
return Collections.emptyList();
}
}
Factories are fairly simple. The meat lies in the ImmutableAnnotationProcessor
class, shown below:
import java.util.*;
import java.io.*;
import com.sun.mirror.apt.*;
import com.sun.mirror.declaration.*;
import com.sun.mirror.util.*;
public class ImmutableAnnotationProcessor implements AnnotationProcessor {
private final AnnotationProcessorEnvironment env;
ImmutableAnnotationProcessor(AnnotationProcessorEnvironment env) {
this.env = env;
}
public void process() {
DeclarationVisitor visitor =
DeclarationVisitors.getSourceOrderDeclarationScanner (
new ClassVisitor(),
DeclarationVisitors.NO_OP);
for (TypeDeclaration type : env.getSpecifiedTypeDeclarations()) {
type.accept(visitor);
}
}
private class ClassVisitor extends SimpleDeclarationVisitor {
public void visitClassDeclaration(ClassDeclaration c) {
Collection<AnnotationMirror> annotations = c.getAnnotationMirrors();
TypeDeclaration immutable = env.getTypeDeclaration("Immutable");
for (AnnotationMirror mirror : annotations) {
if (mirror.getAnnotationType().getDeclaration().equals(immutable)) {
ClassDeclaration superClass = c.getSuperclass().getDeclaration();
// Check that we found a super class other than Object
if (superClass.getSimpleName().equals("Object")) {
env.getMessager().printError(
"@Immutable annotations can only be placed on subclasses");
return;
}
String errorMessage = null;
Map<AnnotationTypeElementDeclaration,AnnotationValue> values =
mirror.getElementValues();
for (Map.Entry<AnnotationTypeElementDeclaration, AnnotationValue>
entry : values.entrySet()) {
AnnotationValue value = entry.getValue();
errorMessage = value.toString();
}
String newline = System.getProperty("line.separator");
String packageString = c.getPackage().getQualifiedName();
String newClass = c.getSimpleName();
try {
StringBuffer sourceString = new StringBuffer();
sourceString.append("package " + packageString + ";" + newline);
sourceString.append("public class " + newClass + " extends " +
superClass.getSimpleName() + " { " + newline);
Collection<MethodDeclaration> methods = superClass.getMethods();
for (MethodDeclaration m : methods) {
if (m.getSimpleName().startsWith("set")) {
Collection<Modifier> modifiers = m.getModifiers();
for(Modifier mod : modifiers) {
sourceString.append(mod + " ");
}
sourceString.append(m.getReturnType() + " ");
sourceString.append(m.getSimpleName() + "(");
Collection<ParameterDeclaration> params = m.getParameters();
int count = 0;
for (ParameterDeclaration p : params) {
sourceString.append(p.getType() + " " + p.getSimpleName());
count++;
if (count != params.size()) {
sourceString.append(", ");
}
}
sourceString.append(") {" + newline);
sourceString.append("throw new RuntimeException(" +
errorMessage + ");" + newline);
sourceString.append("}" + newline);
}
}
sourceString.append("}" + newline);
System.out.println("------- GENERATED SOURCE FILE --------");
System.out.println(sourceString.toString());
System.out.println("--------------------------------------");
PrintWriter writer = env.getFiler().
createSourceFile(packageString + "." + newClass);
writer.append(sourceString); }
catch(IOException e) {
env.getMessager().printError("Failed to create " + newClass +
": " + e.getMessage());
}
}
}
}
}
}
This processor visits declarations looking for classes marked @Immutable
and, when it finds one, looks to the methods of the superclass that start with "set" and copies them into a new source file. Permissions, return values, and parameters all get copied over—except the body is hard coded to throw a RuntimeException
. The processor logic generates a compile error if a class marked @Immutable
lacks a non-Object superclass. Once the processor completes and the alternative source has been generated, control passes to javac.
The apt
tool gets installed next to javac and shares similar command line options, with the addition of sm -factory to direct apt toward thesm AnnotationProcessorFactory instance and a sm -factorypath dictating where to look for the factory's class files.
apt -factorypath . -factory ImmutableAnnotationProcessorFactory *.java
As of this writing, there's no <apt> Ant task, but you can invoke apt using <exec>
:
<exec executable="apt">
<env key="PATH" path="${java.home}/bin"/>
<arg line="-d ${classes}"/>
<arg line="-s ${temp}"/>
<arg line="-cp ${classpath}"/>
<arg line="-factorypath ${build}"/>
<arg line="-factory org.qnot.ImmutableAnnotationProcessorFactory"/>
<arg line="${temp}/org/qnot/Project.java"/>
<arg line="${temp}/org/qnot/ImmutableProject.java"/>
<arg line="${temp}/org/qnot/Immutable.java"/>
<arg line="-nocompile"/>
</exec>
Let me leave you with a final thought. This basic @Immutable
implementation has an Achilles heel: the assumption that every setter method begins with "set". Real world classes have delete()
and remove()
methods and others that would need to be overridden. What mechanism pops to mind to solve this? If you said you'd add an annotation to every setter method, then kudos to you! Writing such an annotation and adjusting the processor to respect it can be your first live experiment with compile-time annotation processing.
In the next and last article in this series, I'll show how authoring and deploying Web services has gotten much easier thanks to the metadata annotations introduced by JSR-181.
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.