Standing in the Field
Notes from SJS Application Server Field Engineering
I really have to stop taking on big blog articles like this. They always take a lot longer to write than I expect. It's been a month since the last article in this series, but here we go with part two on my annotations preso, converted to blog format. Sorry it's so long, I really should have broken it into smaller posts. But I got on a roll and since I'm a month late I figured I might as well get it done.
Last article was just a basic introduction to the concept of Metadata. We introduced annotations, the reason behind introducing them into Java, the basic syntax for using annotations, and the built-in annotations.
Now that we understand the basics of metadata, we can now take a look at the more interesting topic of how to create our own metadata. The pre-built annotations have a certain amount of value, but creating your own annotations is where metadata gets interesting. (At least until EJB 3.0 and J2EE 1.5, which will have lots of time saving new annotations.)
Why would we want to create a new annotation?
@inject (arg="ObjectPoolSize", field="size")
@todo (owner="David F. Ogren", note="upgrade")
There are several articles on the web about how to add markers to code and how to use reflection to allow code to inspect itself. I'd recommend starting with the annotations section of the language guide. It walks through creating a new marker interface @Preliminary and creating a single valued interface of @Copyright. It also creates a @Test marker interface and then shows how to use reflection to determine if that marker exists for a given class.
I'm not going to take a lot of time detail those simple examples again here. In summary, creating new annotations is a lot like creating new interfaces. The major difference (besides the @ symbol) is that you have to be conscious of the meta annotations such as @Retention and @Target. And there are some new methods and in the java.lang.reflect package such as isAnnotationpresent and getDeclaredAnnotations that allow you to detect and manipulate annotations. Here's a quick example of a single value custom annotation with a default value.
package AnnotationDemo;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Maintainer {
String value() default "unknown";
}
And here is a little command line app to parse for those annotations:
package AnnotationDemo;
//This class accepts a class and returns the value of the @maintainer
//annotation. (If the targeted class has one.)
@Maintainer(value="ogren")
public class SingleValueDemo {
public static String findMaintainer(String className) {
String maintainer;
try {
Maintainer notation = (Maintainer)
Class.forName(className).getAnnotation(Maintainer.class);
if (notation!=null) {
maintainer="Maintainer is " + notation.value();
}
else {
maintainer="Class exists but is not annotated with maintainer.";
}
} catch (ClassNotFoundException e) {
maintainer= "No such class found";
}
return maintainer;
}
public static void main(String[] args) {
if (args[0]!=null) {
System.out.println(findMaintainer(args[0]));
} else
System.out.println("Usage: SingleValueDemo classname");
}
}
But if you want to change the compiler behavior, the most interesting part of annotations in my opinion, you have to venture into the world of apt and annotation factories.
apt is a javac replacement that finds and executes annotation processors. Yes, this means that you have to change your build chain if you want to use these new features. If you use javac directly it will not perform all of your custom metadata enhanced behavior.
Annotation Factories implement a simple interface with three methods: getProcessorFor, supportedAnnotationTypes, and supportedOptions. These three methods allow the apt tool to determine which factories are interested in which annotation types and to obtain a AnnotationProcessor object for a given annotation type.
Here is the bulk of the code from the annotation factory I use in my demo: (for the reasons I explained a while ago, I'm not going post all of the code.)
public class PooledFactoryApf implements AnnotationProcessorFactory {
private static final String FQTAGNAME = "AnnotationDemo.PooledFactory";
//(This commented line is how you would declare the annotations you support.
//So that this demo can watch all
//annotations, however, this factory tells apt we process all ("*") annotations.
// private static final Collection supportedAnnotations =
// Collections.unmodifiableCollection(Arrays.asList("AnnotationDemo.PooledFactory"));
private static final Collection supportedAnnotations =
Collections.unmodifiableCollection(Arrays.asList("*"));
private static final Collection supportedOptions = emptySet();
public Collection supportedAnnotationTypes() {
return supportedAnnotations;
}
public Collection supportedOptions() {
return supportedOptions;
}
public AnnotationProcessorFor(
Set atds, AnnotationProcessorEnvironment env) {
return new PooledFactoryProcessor(env);
}
}
The code is pretty self explanatory. You return collections of Strings defining the annotations and options you support. You can use wildcards in those Strings to support ranges of items. My particular demo accepts all annotations (just for demo purposes) and no options. And when asked for a AnnotationProcessor the factory blindly returns the PooledFactoryProcessor which I define below, regardless of what the annotation is or what is going on in the environment.
The AnnotationProcessor is the place where we we actually place our customized behavior. AnnotationProcessors support a simple interface with only one method: process(). Once the apt tool has obtained the correct AnnotationProcessor from the factory, it will call the process method. Once we receive this call to the process method we can use the environment (which we received during construction) to search for instances the annotation we are looking for and perform our custom behavior.
For my demo application I developed an annotation that would automagically generate the code to implement a factory pattern classes it decorates. In theory, this could be used to automatically make any class a pooled resource, although I built this as a proof of concept and didn't actually add the logic to implement pooling. Here is the declaration for my annotation:
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface PooledFactory {
int poolSize();
}
As you can see it is a single valued annotation that marks types (classes and interfaces) and is discarded after compilation. The annotation is discarded after compilation because its job generating the factory code is complete after compile time. Another thing to note is that legally this annotation could be placed on an interface since the annotation syntax considers both classes and interfaces to be types. My demo does throw an error at compile time if an interface is marked with @PooledFactory, but it will not show up as syntactically invalid in an IDE. The following is a simple illustration of how the @PooledFactory annotation might be used:
@PooledFactory(poolSize=4)
public class ExpensiveClass {
ExpensiveClass() {
System.out.println(
"For some reason, I take a long time to create so I am being pooled.");
}
public void someMethod() {
System.out.println("Some business method is executing");
}
}
Clients would then access an instance via the automatically generated factory class:
ExpensiveClass myClass = ExpensiveClassFactory.getInstance(); myClass.someMethod(); ExpensiveClassFactory.returnInstance(myClass);
To me, this is why annotations are exciting. We have just extended Java with a new piece of declarative functionality. If we are using a profiler and discover a bottleneck creating ExpensiveClass objects we can attempt a fix with three lines of code (one to add the annotation, one to change the allocation, and one to return the object to the pool). We also get the added benefits of code readability and code reuse.
So let's implement the code generation in the AnnotationProcessor. The first step is to receive the process() call from apt and find all of the classes that are marked with our annotation:
private final AnnotationProcessorEnvironment env;
private final AnnotationTypeDeclaration myType;
PooledFactoryProcessor(AnnotationProcessorEnvironment env) {
this.env = env;
this.myType = (AnnotationTypeDeclaration)
env.getTypeDeclaration("AnnotationDemo.PooledFactory");
}
public void process() {
Collection annotatedClasses =
env.getDeclarationsAnnotatedWith(myType);
for (Declaration decl : annotatedClasses) {
if (decl instanceof ClassDeclaration)
//this method creates the new source file
createFactorySource((ClassDeclaration) decl);
else
env.getMessager().printWarning(
"An interface was marked with @PooledFactory");
}
}
In the constructor we save the environment we receive from the factory and save the AnnotationTypeDeclaration we are looking for. Once in the process method all we have to do is use the com.sun.mirror.apt.AnnotationProcessorEnvironment to get a list of types that implement our annotation and then iterate over that Collection (using the new foreach style of for loop). As mentioned before we have to check to make sure that our types are classes and then we can execute our custom method (below) to generate the new java source. Notice how that we can print a compiler warning via the AnnotationProcessorEnvironment object. We could use the same technique to implement annotations that perform syntax checking or analysis.
The createFactorySource method is were we actually generate the new source file for the factory class. This demo uses a very simplistic method for doing so, shown below:
void createFactorySource(ClassDeclaration decl) {
//Determine new class name
String realClass = decl.getSimpleName();
String newClassName = realClass + "Factory";
String newQualName = decl.getQualifiedName() + "Factory";
System.out.println("Attempting to create factory :" + newQualName);
try {
PrintWriter out = env.getFiler().createSourceFile(newQualName);
out.println("//autogenerated by PooledFactoryProcessor");
out.println("package " + decl.getPackage() + ";");
out.println("public class " + newClassName +" { ");
out.println("public static " + realClass +
" getInstance() { return new " + realClass + "(); }");
out.println("public static void returnInstance( " + realClass +
" old) { }");
out.println("}");
} catch (java.io.IOException e) {
System.out.println("Factory already exists or cannot be created");
}
}
This class is a bit of a hack, but it shows the basics of source code generation. We figure out what class name (and qualified class name) to use and then use the AnnotationProcessorEnvironment to get a PrintWriter leading to a new source file. (The new source file ends up getting created in a temp directory that can be specified using the -s apt argument.) We then write out the new code to PrintWriter and apt handles the rest. It will automatically pass the new code through the AnnotationFactory again since we might have annotations in our generated code. After that recursive step completes it will then use javac to compile all of the code, including our generated code. At that point ExpensiveClassFactory exists just as if it had been coded by hand.
Obviously this is just a little demo hack and doesn't do any real pooling. Real world code generation takes more than six println statements. But it illustrates the basics of modifying compile time behavior and its a lot more fun than the little reflection samples I see in most annotation tutorials. The same techniques cold be used to do all kinds of powerful compile time behavior.
In the next an final chapter, I'll share some my closing thoughts about metadata. I'll talk about the pros and cons of apt and the future of annoations, such as how metadata will be used in J2EE 1.5 and EJB 3.0.
(2005-01-06 06:47:40.0) Permalink| « January 2005 » | ||||||
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 7 | 8 | |||
9 | 11 | 12 | 13 | 14 | 15 | |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | ||||||
| Today | ||||||