Annotation processing

We have already discussed annotations in great detail. You may recall that we defined our annotation interfaces using the following annotation:

@Retention(RetentionPolicy.RUNTIME)

This told the Java compiler to keep the annotation and put it into the JVM code so that the code can access it during runtime using reflection. The default value is RetentionPolicy.CLASS, which means that the annotation gets into the byte code, but the JVM does not make it available for the runtime system. If we use RetentionPolicy.SOURCE, the annotation does not even get into the class file. In this case, there is only one possibility to do anything with the annotation: compile time.

How can we write code that runs during compile time? Java supports the notion of annotation processors. If there is a class on the classpath of the compiler that implements the javax.annotation.processing.Processor interface, then the compiler will invoke the implemented methods one or more times, passing information about the source file that the compiler is actually processing. The methods will be able to access the compiled methods, classes, or whatever is annotated, and also the annotation that triggered the processor invocation. It is important, however, that this access is not the same as in runtime. What the annotation processor accesses is neither a compiled nor a loaded class, that is, it is available when the code uses reflection. The source file at this time is under compilation; thus, the data structures that describe the code are actually structures of the compiler, as we will see in our next example.

The annotation processor is called one or more times. The reason it is invoked many times is that the compiler makes it possible for the annotation processors to generate source code based on what it sees in the partially compiled source code. If the annotation processor generates any Java source file, the compiler has to compile the new source code and perhaps compile some of the already compiled files again. This new compilation phase needs annotation processor support until there are no more rounds to execute.

Annotation processors are executed one after the other, and they work on the same set of source files. There is no way to specify the order of the annotation processor executions; thus, two processors working together should perform their tasks, no matter in what order they are invoked. Also, note that these codes run inside the compiler. If an annotation processor throws an exception, then the compilation process will most probably fail. Thus, throwing an exception out of the annotation processor should only be done if there is an error that cannot be recovered and the annotation processor decides that the compilation after that error cannot be complete.

When the compiler gets to the phase to execute the annotation processors, it looks at the classes that implement the javax.annotation.processing.Processor interface and creates instances of these classes. These classes have to have a public no-argument constructor. To streamline the execution of the processors and to invoke a processor only for the annotations that it can handle, the interface contains two methods:

  • getSupportedSourceVersion to return the latest version the annotation processor can support
  • getSupportedAnnotationTypes to return a set of String objects containing the fully qualified class name of the annotations that this processor can handle

If an annotation processor was created for Java 1.8, it may work with Java 9, but it may also not work. If it declares that the latest supported version is 1.8, then the compiler in a Java 9 environment will not invoke it. It is better not to invoke an annotation processor than calling it and messing up the compilation process, which may even create compiled but erroneous code.

The values returned by these methods are fairly constant for an annotation processor. An annotation processor will return the same source version it can handle and will return the same set of annotations. Therefore, it would be clever to have some way to define these values in the source code in a declarative manner.

It can be done when we extend the javax.annotation.processing.AbstractProcessor class instead of directly implementing the Processor interface. This abstract class implements these methods. Both of them get the information from the annotation so that we can decorate the class that extends the abstract class. For example, the getSupportedAnnotationTypes method looks at the SupportedAnnotationTypes annotation and returns an array of annotation type strings that are listed in the annotation.

Now, this is a bit brain twisting and can also be confusing at first. We are executing our annotation processor during compile time. But the compiler itself is a Java application, and in this way, the time is runtime for the code that runs inside the compiler. The code of AbstractProcessor accesses the SupportedAnnotationTypes annotation as a runtime annotation using reflection methods. There is no magic in it. The method in the JDK 9 is as follows:

public Set<String> getSupportedAnnotationTypes() { 
SupportedAnnotationTypes sat = this.getClass().getAnnotation
(SupportedAnnotationTypes.class);
if (sat == null) {
... error message is sent to compiler output ...
return Collections.emptySet();
}
else
return arrayToSet(sat.value());
}

(The code has been edited for brevity.)

To have an example, we will sort of look at the code of a polyglot annotation processor. Our very simple annotation processor will process one simple annotation: com.javax0.scriapt.CompileScript, which can specify a script file. The annotation processor will load the script file and execute it using the scripting interface of Java 9.

This code was developed as a demonstration code by the author of this book a few years ago and is available with the Apache license from GitHub. Thus, the package of the classes is retained.

The annotation processor contains two code files. One of the annotation itself that the processor will work on:

@Retention(RetentionPolicy.SOURCE) 
@Target(ElementType.TYPE)
public @interface CompileScript {
String value();
String engine() default "";
}

As you can see, this annotation will not get into the class file after compilation; thus, there will be no trace during runtime so that any class source may occasionally use this annotation. Target of the annotation is ElementType.TYPE, meaning that this annotation can only be applied to those Java 9 language constructs that are some kind of types: class, interface, and enum.

The annotation has two parameters. The value should specify the name of the script file, and the engine may optionally define the type of the script that is in that file. The implementation we'll create will try to identify the type of the script from the filename extension, but if somebody would like to bury some Groovy code into a file that has the .jy extension (which is usually for Jython), so be it.

The processor extends AbstractProcessor and, in this way, some of the methods are inherited at the expense of some annotations used in the class:

package com.javax0.scriapt; 
import ...
@SupportedAnnotationTypes("com.javax0.scriapt.CompileScript")
@SupportedSourceVersion(SourceVersion.RELEASE_9)
public class Processor extends AbstractProcessor {

There is no need to implement the getSupportedAnnotationTypes and getSupportedSourceVersion methods. These are replaced by the use of the annotations on the class. We support only one annotation in this processor, the one that we defined in the previously listed source file, and we are prepared to manage the source code up to Java version 9. The only method we have to override is process:

@Override 
public boolean process(
final Set<? extends TypeElement> annotations,
final RoundEnvironment roundEnv) {
for (final Element rootElement :
roundEnv.getRootElements()) {
try {
processClass(rootElement);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
return false;
}

This method gets two arguments. The first is the set of annotations that it was invoked for. The second is the round environment. Because the processor can be invoked many times, the different invocations may have different environments. Each invocation is in a round and the RoundEnvironment argument is an object that can be used to get information about the given round. It can be used to get the root elements of the round for which this annotation is invoked. In our case, this will be a set of class elements that have the CompileScript annotation. We iterate over this set, and for each class, we invoke the processClass method (see the next code snippet). The method may throw some checked exception and the method process cannot because it should match the same method of the interface. Thus, we catch any exception that may be thrown and we re-throw these encapsulated in RunTimeException. If any of these exceptions are thrown by the called method, then the compilation could not run the scripts and it should be treated as failed. The compilation should not succeed in such a case:

private void processClass(final Element element) 
throws ScriptException, FileNotFoundException {
for (final AnnotationMirror annotationMirror :
element.getAnnotationMirrors()) {
processAnnotation(annotationMirror);
}
}

The actual annotation is not available during compile time as we already mentioned. Hence, what we have available is only a compile time mirror image of the annotation. It has the AnnotationMirror type, which can be used to get the actual type of the annotation and, also, the values of the annotation. The type of the annotation is available during compile time. The compiler needs it; otherwise, it could not compile the annotation. The values are available from the annotation itself. Our processAnnotation method handles each annotation it gets as an argument:

private void processAnnotation( 
final AnnotationMirror annotationMirror)
throws ScriptException, FileNotFoundException {
final String script =
FromThe.annotation(annotationMirror).
getStringValue();
final String engine =
FromThe.annotation(annotationMirror).
getStringValue("engine");
execute(script, engine);
}

Our @CompileScript annotation defines two parameters. The first value is the script filename and the second one is the scripting engine name. If the second one is not specified, then an empty string is set as the default value. The execute method is called for each and every occasion of the annotation:

private void execute(final String scriptFileName, 
final String engineName)
throws ScriptException, FileNotFoundException {
final ScriptEngineManager factory =
new ScriptEngineManager();
final ScriptEngine engine;
if (engineName != null && engineName.length() > 0) {
engine = factory.getEngineByName(engineName);
}
else {
engine =
factory.getEngineByExtension
(getExtensionFrom(scriptFileName));
}
Reader scriptFileReader = new FileReader
(new File(scriptFileName));
engine.eval(scriptFileReader);
}

The method tries to load the script, based on the filename, and tries to instantiate the script engine, based on the given name. If there is no name given, then the filename extension is used to identify the scripting engine. By default, the JavaScript engine is on the classpath as it is part of the JDK. If any other JVM-based scripting engine is in use, then it has to be made available on the classpath or on the module path.

The last method of the class is a simple script manipulation method, nothing special. It just chops off the filename extension so that the engine can be identified based on the extension string:

private String getExtensionFrom(final String scriptFileName) { 
final int indexOfExtension = scriptFileName.lastIndexOf('.'),
if (indexOfExtension == -1) {
return "";
}
else {
return scriptFileName.substring(indexOfExtension + 1);
}
}

And just for the sake of completeness, we have the closing brace of the class:

}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.73.175