We now discuss how Spoon deals with the processing of annotations.
Java annotations enable developers to embed metadata in their programs.
Although by themselves annotations have no explicit semantics, they can
be used by frameworks as markers for altering the behavior of the programs
that they annotate. This interpretation of annotations can result, for
example, on the configuration of services provided by a middleware
platform or on the alteration of the program source code.
Annotation processing is the process by which a pre-processor modifies an
annotated program as directed by its annotations during a pre-compilation phase.
The Java compiler offers the possibility of compile-time processing of annotations
via the API provided under the javax.annotation.processing
package. Classes
implementing the javax.annotation.processing.Process
interface are used by the
Java compiler to process annotations present in a client program.
The client code is modeled by the classes of the javax.lang.model
package
(although Java 8 has introduced finer-grained annotations, but not on any
arbitrary code elements). It is partially modeled: only types, methods, fields and
parameter declarations can carry annotations. Furthermore, the model does not allow
the developer to modify the client code, it only allows adding new classes.
The Spoon annotation processor overcomes those two limitations: it can handle
annotations on any arbitrary code elements (including within method bodies), and it
supports the modification of the existing code.
Annotation Processing Example
Spoon provides developers with a way to specify the analyses and transformations
associated with annotations. Annotations are metadata on code that start with @
in Java.
For example, let us consider the example of a design-by-contract annotation.
The annotation @NotNull
, when placed on arguments of a method, will ensure that the argument
is not null when the method is executed. The code below shows both the definition of the
NotNull
annotation type, and an example of its use.
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.SOURCE)
public @interface NotNull{}
class Person{
public void marry(@NotNull Person so){
if(!so.isMarried())
// Marrying logic...
}
}
The NotNull
annotation type definition carries two meta-annotations (annotations on annotation
definitions) stating which source code elements can be annotated (line 1), and that the annotation
is intended for compile-time processing (line 2). The NotNull
annotation is used on the argument
of the marry
method of the class Person
. Without annotation processing, if the method marry
is invoked with a null
reference, a NullPointerException
would be thrown by the Java virtual
machine when invoking the method isMarried
in line 7.
The implementation of such an annotation would not be straightforward using Java’s processing API
since it would not allow us to just insert the NULL check in the body of the annotated method.
The Annotation Processor Interface
In Spoon, the full code model can be used for compile-time annotation processing. To this end,
Spoon provides a special kind of processor called AnnotationProcessor
whose interface is:
public interface AnnotationProcessor<A extends Annotation, E extends CtElement> extends Processor<E> {
void process(A annotation, E element);
boolean inferConsumedAnnotationType();
Set<Class<? extends A>> getProcessedAnnotationTypes();
Set<Class<? extends A>> getConsumedAnnotationTypes();
boolean shoudBeConsumed(CtAnnotation<? extends Annotation> annotation);
}
Annotation processors extend normal processors by stating the annotation type those elements must
carry (type parameter A
), in addition of stating the kind of source code element they process
(type parameter E
). The process
method (line 4) receives as arguments both the CtElement
and the
annotation it carries. The remaining four methods (getProcessedAnnotationTypes
, getConsumedAnnotationTypes
,
inferConsumedAnnotationTypes
and shoudBeConsumed
) configure the visiting of the AST during annotation
processing. The Spoon annotation processing runtime is able to infer the type of annotation a processor
handles from its type parameter A
. This restricts each processor to handle a single annotation. To avoid this
restriction, a developer can override the inferConsumedAnnotationType()
method to return false
. When doing
this, Spoon queries the getProcessedAnnotationTypes()
method to find out which annotations are handled by the
processor. Finally, the getConsumedAnnotationTypes()
returns the set of processed annotations that are to be
consumed by the annotation processor. Consumed annotations are not available in later processing rounds.
Similar to standard processors, Spoon provides a default abstract implementation for annotation processors:
AbstractAnnotationProcessor
. It provides facilities for maintaining the list of consumed and processed annotation
types, allowing the developer to concentrate on the implementation of the annotation processing logic.
Going back to our @NotNull
example, we implement a Spoon annotation processor that processes and consumes
NotNull
annotated method parameters, and modifies the source code of the method by inserting an assert
statement
that checks that the argument is not null.
class NotNullProcessor extends AbstractAnnotationProcessor<NotNull, CtParameter> {
@Override
public void process(NotNull anno, CtParameter param){
CtMethod<?> method = param.getParent(CtMethod.class);
CtBlock<?> body = method.getBlock();
CtAssert<?> assertion = constructAssertion(param.getSimpleName());
body.insertBegin(assertion);
}
}
The NotNullProcessor
leverages the default implementation provided by the AbstractAnnotationProcessor
and binds the
type variables representing the annotation to be processed and the annotated code elements to NotNull
and CtParameter
respectively. The actual processing of the annotation is implemented in the process(NotNull,CtParameter)
method
(lines 10-13). Annotated code is transformed by navigating the AST up from the annotated parameter to the owner method,
and then down to the method’s body code block (lines 10 and 12). The construction of the assert
statement is delegated
to a helper method constructAssertion(String)
, taking as argument the name of the parameter to check. This helper method
constructs an instance of CtAssert
by programmatically constructing the desired boolean expression. Having obtained
the desired assert statement, it is injected at the beginning of the body of the method.
More complex annotation processing scenarios can be tackled with Spoon. For example, when using the NotNull
annotation,
the developer is still responsible for manually inspecting which method parameters to place the annotation on.
A common processing pattern is then to use regular Spoon processors to auto-annotate
the application’s source code.
Such a processor, in our running example, can traverse the body of a method, looking for expressions that send messages
to a parameter. Each of these expressions has as hypothesis that the parameter’s value is not null, and thus should result
in the parameter being annotated with NotNull
.
With this processing pattern, the programmer can use an annotation processor in two ways: either by explicitly and manually
annotating the base program, or by using a processor that analyzes and annotates the program for triggering annotation processors
in an automatic and implicit way. This design decouples the program analysis from the program transformation logics, and leaves
room for manual configuration.