Wednesday, December 29, 2004

Using apt

While exploring the new JDK 5 tool apt (annotation processing tool), I figured out how to write new source code that is compiled into my build tree along side my regular Java sources. Here is a trivial example.

First, I need an annotation processing factory (I ignore imports and such throughout):

public class MyAnnotationProcessorFactory
        implements AnnotationProcessorFactory {
    public Collection supportedOptions() {
        return Collections.emptySet();
    }

    public Collection supportedAnnotationTypes() {
        return Collections.singleton(
                getClass().getPackage().getName() + ".*");
    }

    public AnnotationProcessor getProcessorFor(
            final Set atds,
            final AnnotationProcessorEnvironment env) {
        return new MyAnnotationProcessor(atds, env);
    }
}

Second is to have an annotation processor:

public class MyAnnotationProcessor
        implements AnnotationProcessor {
    private final Set atds;
    private final AnnotationProcessorEnvironment env;

    MyAnnotationProcessor(final Set atds,
            final AnnotationProcessorEnvironment env) {
        this.atds = atds;
        this.env = env;
    }

    public void process() {
        for (final AnnotationTypeDeclaration atd : atds) {
            for (final Declaration decl
                    : env.getDeclarationsAnnotatedWith(atd)) {
                final String typeName = decl.getSimpleName() + "Example";
                final String fullTypeName = getPackageName() + "." + typeName;

                try {
                    final PrintWriter writer
                            = env.getFiler().createSourceFile(fullTypeName);

                    writer.println("package " + getPackageName() + ";");
                    writer.println("public class " + typeName + " {");
                    writer.println("}");

                } catch (final IOException e) {
                    throw new RuntimeException(fullTypeName, e);
                }
            }
        }
    }

    private String getPackageName() {
        return getClass().getPackage().getName();
    }
}

Last is to tell apt how to fit it all together (I'm using a Maven-style layout):

apt -cp target/classes
    -s target/gen-java -d target/gen-classes
    -target 1.5 -factorypath target/classes
    -factory MyAnnotationProcessorFactory
    src/java/AnnotatedExample

When I run the apt command, given suitable annotations in AnnotatedExample, it pulls them out, instantiates my annotation processor via my factory, and hands them to process() therein. The key is to use com.sun.mirror.apt.Filer, a class in $JAVA_HOME/lib/tools.jar. There are no online javadocs that I have found yet. Here is what the JDK 1.5.0_01 sources say about the Filer interface:

This interface supports the creation of new files by an annotation processor. Files created in this way will be known to the annotation processing tool implementing this interface, better enabling the tool to manage them. Four kinds of files are distinguished: source files, class files, other text files, and other binary files. The latter two are collectively referred to as auxiliary files.

There are two distinguished locations (subtrees within the file system) where newly created files are placed: one for new source files, and one for new class files. (These might be specified on a tool's command line, for example, using flags such as -s and -d.) Auxiliary files may be created in either location.

During each run of an annotation processing tool, a file with a given pathname may be created only once. If that file already exists before the first attempt to create it, the old contents will be deleted. Any subsequent attempt to create the same file during a run will fail.

My next step is to glue velocity into my processor so I can use templates for writing the new Java sources.

8 comments:

benedict said...

Your post prompted me to play with APT.
Suppose I want to add something to the top/bottom of each method. I need to get access to the source code of the existing method. I can get a MethodDeclaration, and from it a SourcePosition which gives a line/column in the source file. Is there an easy way to use a SourcePosition to get the corresponding text from the file, or do I have to do it by steam?

thanks,
Benedict

Brian Oxley said...

So far, I only see how to do it the hard way. It is a nifty idea you suggest, a way to do aspects using annotations, one I've been toying with myself. For the nonce I want to glue in templates, perhaps with an annotation such as @template("path-to-template"), but aspects are more ambitious given that the source code model doesn't chunk the source by type.

Ideally, the source code model would match the type anotated, so that methods would give the boundaries of the method in the source, fields would give the field boundaries, etc.

A question about that approach, what about:

@someAnnotation int foo, bar;

When I tested this construct, I find that the annotation is transitive and applies to both foo and bar. Too bad the source code marker only marks the beginning boundary for the declaration.

benedict said...

Has anyone discovered a way to get at the body of a method using APT?
Without this, APT seems to be just a cleaner Doclet.
I wanted to modify methods by putting some code at the top/bottom, but I seem to be able to get only the approximate coordinates in the source file, and not the actual method body.

Benedict

benedict said...

I ran some your factory generating code in apt. From a class Thing it produced ThingFactory, ThingFactoryFactory, ThingFactoryFactoryFactory..... up to 16 deep!

What am I doing wrong? I thought it only recursed if the generated java contained further annotations.

Brian Oxley said...

Curious. Would you object to posting your processor coass to me (binkley@alumni.rice.edu)? I'd like to play with it and see where we differ.

Anonymous said...

Nice example about a non-documented API, but I notice something about this code. The package name accessed by getClass().getPackage().getName() is the package name of the processor and not the package name of the class where the annotation is. The file is so generated in the processor package tree and not in the annotated class tree. Was it the goal ?

Do you know if it is possible to access to the context of an annotation : source code associated with an annotation, class or package where the annotation is in ?

Thanks :)

Brian Oxley said...

Oops... a bug! I have to look at this more.

Anonymous said...

And if you're interested in IDE support for apt, check out the work being done to support it in Eclipse's JDT: APT page