• No results found

Writing Your Own Annotation Processor

In document Advanced Java (Page 115-118)

We are going to develop several kinds of annotation processors, starting from the simplest one, immutability checker. Let us define a simple annotation Immutable which we are going to use in order to annotate the class to ensure it does not allow to modify its state.

@Target( ElementType.TYPE )

@Retention( RetentionPolicy.CLASS ) public @interface Immutable { }

Following the retention policy, the annotation is going to be retained by Java compiler in the class file during the compilation phase however it will not be (and should not be) available at runtime.

As we already know from part 3 of the tutorial,How to design Classes and Interfaces, immutability is really hard in Java. To keep things simple, our annotation processor is going to verify that all fields of the class are declared as final. Luckily, the Java standard library provides an abstract annotation processor, javax.annotation.processing.AbstractProcessor, which is designed to be a convenient superclass for most concrete annotation processors. Let us take a look on SimpleAnnotationProcessor annotation processor implementation.

@SupportedAnnotationTypes( "com.javacodegeeks.advanced.processor.Immutable" )

@SupportedSourceVersion( SourceVersion.RELEASE_7 )

public class SimpleAnnotationProcessor extends AbstractProcessor {

@Override

public boolean process(final Set< ? extends TypeElement > annotations, final RoundEnvironment roundEnv) {

for( final Element element: roundEnv.getElementsAnnotatedWith( Immutable.class ) ) { if( element instanceof TypeElement ) {

final TypeElement typeElement = ( TypeElement )element;

for( final Element eclosedElement: typeElement.getEnclosedElements() ) { if( eclosedElement instanceof VariableElement ) {

final VariableElement variableElement = ( VariableElement )eclosedElement;

if( !variableElement.getModifiers().contains( Modifier.FINAL ) ) { processingEnv.getMessager().printMessage( Diagnostic.Kind.ERROR,

String.format( "Class ’%s’ is annotated as @Immutable, but field ’%s’ is not declared as final",

typeElement.getSimpleName(), variableElement.getSimpleName()

// Claiming that annotations have been processed by this processor return true;

} }

The SupportedAnnotationTypes annotation is probably the most important detail which defines what kind of annotations this annotation processor is interested in. It is possible to use * here to handle all available annotations.

Because of the provided scaffolding, our SimpleAnnotationProcessor has to implement only a single method, proc ess. The implementation itself is pretty straightforward and basically just verifies if class being processed has any field declared without final modifier. Let us take a look on an example of the class which violates this naïve immutability contract.

@Immutable

public class MutableClass { private String name;

public MutableClass( final String name ) { this.name = name;

Running the SimpleAnnotationProcessor against this class is going to output the following error on the console:

Class ’MutableClass’ is annotated as @Immutable, but field ’name’ is not declared as final

Thus confirming that the annotation processor successfully detected the misuse of Immutable annotation on a mutable class.

By and large, performing some introspection (and code generation) is the area where annotation processors are being used most of the time. Let us complicate the task a little bit and apply some knowledge of Java Compiler API from the part 13 of the tutorial, Java Compiler API. The annotation processor we are going to write this time is going to mutate (or modify) the generated bytecode by adding the final modifier directly to the class field declaration to make sure this field will not be reassigned anywhere else.

@SupportedAnnotationTypes( "com.javacodegeeks.advanced.processor.Immutable" )

@SupportedSourceVersion( SourceVersion.RELEASE_7 )

public class MutatingAnnotationProcessor extends AbstractProcessor { private Trees trees;

@Override

public void init (ProcessingEnvironment processingEnv) { super.init( processingEnv );

trees = Trees.instance( processingEnv );

}

@Override

public boolean process( final Set< ? extends TypeElement > annotations, final RoundEnvironment roundEnv) {

final TreePathScanner< Object, CompilationUnitTree > scanner = new TreePathScanner< Object, CompilationUnitTree >() {

@Override

public Trees visitClass(final ClassTree classTree, final CompilationUnitTree unitTree) {

if (unitTree instanceof JCCompilationUnit) {

final JCCompilationUnit compilationUnit = ( JCCompilationUnit )unitTree;

// Only process on files which have been compiled from source

if (compilationUnit.sourcefile.getKind() == JavaFileObject.Kind.SOURCE) { compilationUnit.accept(new TreeTranslator() {

public void visitVarDef( final JCVariableDecl tree ) { super.visitVarDef( tree );

for( final Element element: roundEnv.getElementsAnnotatedWith( Immutable.class ) ) { final TreePath path = trees.getPath( element );

scanner.scan( path, path.getCompilationUnit() );

}

// Claiming that annotations have been processed by this processor return true;

} }

The implementation became more complex, however many classes (like TreePathScanner, TreePath) should be already familiar. Running the annotation processor against the same MutableClass class will generate following byte code (which could be verified by executing javap -p MutableClass.class command):

public class com.javacodegeeks.advanced.processor.examples.MutableClass { private final java.lang.String name;

public com.javacodegeeks.advanced.processor.examples.MutableClass(java.lang.String);

public java.lang.String getName();

}

Indeed, the name field has final modifier present nonetheless it was omitted in the original Java source file. Our last example is going to show off the code generation capabilities of annotation processors (and conclude the discussion). Continuing in the same vein, let us implement an annotation processor which will generate new source file (and new class respectively) by appending Immutable suffix to class name annotated with Immutable annotation.

@SupportedAnnotationTypes( "com.javacodegeeks.advanced.processor.Immutable" )

@SupportedSourceVersion( SourceVersion.RELEASE_7 )

public class GeneratingAnnotationProcessor extends AbstractProcessor {

@Override

public boolean process(final Set< ? extends TypeElement > annotations, final RoundEnvironment roundEnv) {

for( final Element element: roundEnv.getElementsAnnotatedWith( Immutable.class ) ) { if( element instanceof TypeElement ) {

final TypeElement typeElement = ( TypeElement )element;

final PackageElement packageElement =

( PackageElement )typeElement.getEnclosingElement();

try {

final String className = typeElement.getSimpleName() + "Immutable";

final JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(

packageElement.getQualifiedName() + "." + className);

try( Writer writter = fileObject.openWriter() ) {

writter.append( "package " + packageElement.getQualifiedName() + ";" );

writter.append( "\\n\\n");

writter.append( "public class " + className + " {" );

writter.append( "\\n");

writter.append( "}");

}

} catch( final IOException ex ) {

processingEnv.getMessager().printMessage(Kind.ERROR, ex.getMessage());

} } }

// Claiming that annotations have been processed by this processor return true;

} }

As the result of injecting this annotation processor into compilation process of the MutableClass class, the following file will be generated:

package com.javacodegeeks.advanced.processor.examples;

public class MutableClassImmutable { }

Nevertheless the source file and its class have been generated using primitive string concatenations (and it fact, this class is really very useless) the goal was to demonstrate how the code generation performed by annotation processors works so more sophisticated generation techniques may be applied.

In document Advanced Java (Page 115-118)

Related documents