Skip to main content

Validate Java EE Annotations with Annotation Processors

June 29, 2006

{cs.r.title}






Java 5 introduced a set of new features, including annotations (JSR-175), generics, autoboxing, varargs, an enhanced for loop, and enumerated types. Among these, the annotation facility is probably the most significant. Annotations allow developers to annotate program elements, such as classes, methods, and fields, to control application behavior and deployment through tools or containers. They are extensively used in Java EE 5, including EJB 3.0, JAXB 2.0, JSR-181 (Web Services Metadata for the Java Platform), and JAX-WS 2.0, and in other frameworks, such as Spring and SEAM.

The JSR-175 annotations have some weaknesses, however. The biggest current limitation is lack of a validation facility. As an example, we can specify both @Stateless and @Stateful for the same EJB session bean class without compilation errors. Unlike XML-based configuration files, where we can use XML schema and DTD to effectively constraint the constituent parts, annotations have only the @Target meta-annotation to specify what kind of program elements can be applied.

The JDK 5 Annotation Processing Tool (apt) is a little-known but powerful tool that can be used to process program annotations at build time. Even better, Java SE 6 (Mustang) incorporates a standardized pluggable annotation processing API (JSR-269) directly into javac. In this article, we will create a simple annotation validator after discussing apt and mirror-based reflection. Following that, we will have a quick overview of JSR-269 and then show how to write annotation processors using the standard API.

Introduction to Apt

apt is a JDK 5 command-line utility for processing annotations in Java source files. Similar to Xdoclet, apt is designed to generate source and other files, including XML files. The apt tool runs annotation processors identified for the annotations present in the source files in a recursive, multiple round fashion:

  • First round: Process original source files.
  • Second round: Process files generated by first round.
  • Third round: process files generated by second round.
  • Etc.

Depending on the annotations in the generated files, each round may use different processors. When there are no new source files are generated, apt uses javac to compile original source files and all the generated files. Note that, in addition to Java source files, the apt tool can process class files as well. For further details, you can refer to the Apt starting guide that comes with the JDK 5 documentation.

While apt is intended to process annotations, we can use the reflective API to do other build-time, pre-processing-like tasks, such as validating program element annotations and their values. Before moving on to create an apt annotation processor, let's see a quick introduction of mirror-based reflection.

Mirror-Based Reflection

The apt tool has a mirror-based reflective API to provide a build-time, source-based, read-only view of program structure. In traditional run-time reflection, as shown below, reflective operations, such as Class.getDeclaredMethods(), co-exist with application-level operations, for example, OrderService.processOrder().

    ...
    OrderService orderService =...
    orderService.processOrder();
    Method[] orderMethods =
        orderService.getClass().getDeclaredMethods();
    ...

In contrast to the traditional run-time reflection, the mirror-based design encapsulates the reflective capabilities into separate intermediary objects, called mirrors. Mirrors are interface-based. In this way, the reflective operations are separated from application-level operations and the reflection subsystem can be plugged or unplugged, even dynamically. Theoretically, without modifications or re-compiling, a mirror-based reflective application designed for the desktop can be deployed on a mobile device, which may have only a small footprint implementation of the reflection facility.

In the apt reflective API, there are two mirror interface hierarchies: type declarations and types, which correspond to the program element declarations and Java programming language's type system. For example, InterfaceType and InterfaceDeclaration represent an interface type and the declaration of an interface, respectively. Why do we need both type declarations and types? Because when generics are used, there exists a one-to-many, not one-to-one, relationship between the declarations and types. For example, the declaration of java.util.Collection could define java.util.Collection<String>, java.util.Collection<java.util.Date>, and so on.

Remember the apt declaration is not a run-time construct of the virtual machine; instead, it is a static, Java programming-language-level construct. The declaration corresponds one-to-one with a high-level source code fragment. With a declaration we can get, among other things, annotations that are directly present on this declaration. Let's see how to use the mirror-based reflection API to validate the EJB session bean type annotations mentioned in the previous section.

Creating an Apt Annotation Processor

To use apt, we need create annotation processor factories and annotation processors. The apt tool first scans annotations present in the source code and then queries the annotation processor factories. If there are annotations that a factory supports, the annotations will be claimed and the apt tool will request an annotation processor from the factory for the annotations. When all annotations are claimed, apt runs the annotation processors in turn.

As we know, the @Stateless and @Stateful annotations should be mutually exclusive for a session bean. Consider the following code:

package javax.ejb;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME)
@Target(TYPE)
public @interface Stateless {
    String name( ) default "";
}

The retention policy for both the @Stateful and @Stateless annotations is RUNTIME, so that they can be read reflectively at run time, but as far as the build-time annotation processing is concerned, any one of the three retention policies (SOURCE, CLASS, and RUNTIME) works. Let's start with an annotation processor factory and a simple annotation processor to check the mutually-exclusive constraint violation.

Here is the BeanTypeAnnotationProcessorFactory class:

package com.lizjason.annotation;

import static com.lizjason.annotation.BeanTypeNames.STATEFUL;
import static com.lizjason.annotation.BeanTypeNames.STATELESS;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableCollection;

import java.util.Arrays;
import java.util.Collection;
import java.util.Set;

import com.sun.mirror.apt.AnnotationProcessor;
import com.sun.mirror.apt.AnnotationProcessorEnvironment;
import com.sun.mirror.apt.AnnotationProcessorFactory;
import com.sun.mirror.declaration.AnnotationTypeDeclaration;

/**
* A simple annotation processor factory that processes
* EJB session bean type annotations:
* javax.ejb.Stateless and javax.ejb.Stateful
*
* @see com.lizjason.annotation.BeanTypeAnnotationProcessor
*
*/
public class BeanTypeAnnotationProcessorFactory
    implements AnnotationProcessorFactory{

    /**
     * Uses <tt>BeanTypeAnnotationProcessor</tt>
     * to process the supported annotations.
     */
    public AnnotationProcessor getProcessorFor(
        Set<AnnotationTypeDeclaration> typeDeclaration,
        AnnotationProcessorEnvironment processorEnvironment) {
            return new BeanTypeAnnotationProcessor
                (processorEnvironment);
    }

    /**
     * This factory supports javax.ejb.Stateless and
     * javax.ejb.Stateful.
     */
    public Collection<String> supportedAnnotationTypes() {
        String[] supportedTypes = {STATEFUL, STATELESS};

        return unmodifiableCollection(
                Arrays.asList(supportedTypes));
    }

    /**
     * No options recognized by this factory or
     * <tt>BeanTypeAnnotationProcessor</tt>
     */
    public Collection<String> supportedOptions() {
        return emptySet();
    }
}

Note that this factory can process two annotation types: @Stateless and @Stateful, and that BeanTypeAnnotationProcessor, as shown below, will be returned when this factory is queried by apt. The apt tool provides the AnnotationProcessorEnvironment, which is used by the processor to get the information it needs. The processor implementation is as follows:

package com.lizjason.annotation;

import static com.lizjason.annotation.BeanTypeNames.STATEFUL;
import static com.lizjason.annotation.BeanTypeNames.STATELESS;

import java.util.Collection;

import com.sun.mirror.apt.AnnotationProcessor;
import com.sun.mirror.apt.AnnotationProcessorEnvironment;
import com.sun.mirror.declaration.AnnotationMirror;
import com.sun.mirror.declaration.TypeDeclaration;

/**
* A simple annotation processor to process @Stateful and @Stateless
* for a session bean. The implementation is based on
* the apt API.
*
* @see com.lizjason.annotation.BeanTypeAnnotationProcessorFactory
*
*/
public class BeanTypeAnnotationProcessor implements AnnotationProcessor {
    private AnnotationProcessorEnvironment processorEnvironment;

    public BeanTypeAnnotationProcessor(
        AnnotationProcessorEnvironment processorEnvironment) {
        this.processorEnvironment = processorEnvironment;
    }

    /**
     * Check if both @Stateful and @Stateless are present
     * in an session bean. If so, emits a warning message.
     */
    public void process() {

        Collection<TypeDeclaration> typeDeclarations =
            this.processorEnvironment.getSpecifiedTypeDeclarations();

        for (TypeDeclaration typeDeclaration : typeDeclarations) {

            System.out.println("Processing " + typeDeclaration);

            Collection<AnnotationMirror> annotationMirrors =
                typeDeclaration.getAnnotationMirrors();

            System.out.println("Annotations: " + annotationMirrors);

            boolean isStateful = false;
            boolean isStateless = false;

            for (AnnotationMirror annotation : annotationMirrors) {
                String type = annotation.getAnnotationType().toString();
                if (STATELESS.equals(type)) {
                    isStateless = true;
                } else if (STATEFUL.equals(type)) {
                    isStateful = true;
                }
            }

            if (isStateful && isStateless) {
                // both @Stateful and @Stateless are present
                System.out.println(
                    "Warning: Both @Stateful and @Stateless are present. "
                    + "They should be mutually exclusive");
            }
        }
    }
}

The AnnotationProcessor has one method, process(), which is invoked by apt. It simply gets the type declarations from the process environment and then retrieves all of the annotations for the type. If both @Stateful and @Stateless are present, the processor simply emits a warning message. In real world, you may throw a run-time exception to stop the source files from compiling.

What if the session bean is annotated with a given annotation (say, @Statelss) twice? The compiler can catch that as an error because the same annotation cannot be used to a program element more than once.

Note that the implementation does not check the case that neither annotation is present. It is unnecessary here, because in that case, the processor will not be invoked at all. For a complete implementation, it is certain we should report an error when a session bean is not annotated by @Stateless or @Stateful. To make that happen, we only need to change the supportedAnnotationTypes() method in the processor factory to process all annotations using the wild card (*). As a matter of fact, this is a very useful feature because using the wild card causes the processor factory to process an empty collection of annotations as well. As a result, we can take advantage of the build-time, mirror-based reflection to do general purpose source code processing. Remember, besides annotations, we can get doc comments, source position, modifiers, fields, methods, and more from a type declaration!

Annotation Processor in Action

To see our annotation processor in action, we need a source file. Assuming we have a session bean, ProcessPayrollBean.java, as listed below:

//ProcessPayrollBean.java.
package com.lizjason.annotation;

import java.util.Collection;


import javax.ejb.Stateful;
import javax.ejb.Stateless;

@Stateful
@Stateless
public class ProcessPayrollBean implements
    ProcessPayroll {
    public void process(Collection<Employee> employees){
        //do something...
    }
}

Note that both @Stateful and @Stateless are applied here for demonstration purposes. If we run apt with the above file, the output will be:

Processing com.lizjason.annotation.ProcessPayrollBean
Annotations: [@javax.ejb.Stateful, @javax.ejb.Stateless]
Warning: Both @Stateful and @Stateless are present.
They should be mutually exclusive.

Using the same approach, we can write build-time annotation validators to check annotation constraint violations for other Java EE and application-specific annotations.

As you can see, the implementation is straightforward. If you use the Eclipse IDE, it has an apt plugin (jdt-apt). You can use the plugin to debug an annotation processor. In addition to the functionality provided by the apt command-line tool, jdt-apt provides nice features such annotation auto-completion and "quick fix."

You may notice that the apt API is in the com.sun.mirror package. That implies there are potential backward compatibility issues because they may be changed in the future. The good news is that JSR-269 defines a standard pluggable annotation processing API to supersede the apt API.

Pluggable Annotation Processing API (JSR-269)

JSR-269 introduces a standardized Pluggable Annotation Processing API. The spec is not final yet, but the reference implementation is integrated into the Mustang builds. The JSR-269 API is very similar to the apt API. The API consists of two parts:

  • javax.lang.model.*: APIs for Java programming language modeling.
  • javax.annotation.processing.*: APIs for creating annotation processors and communicating with the annotation processing environment.

Like type declarations and types in the apt API, the modeling API has two pieces: element, which models elements of the Java programming language, and type, which models Java programming language types. The annotation processing functionality is part of javac, which runs the annotation processors first and then compiles the source files. Let's create a JSR-269-based annotation processor to perform the same validation as we did with the apt API.

Creating a JSR-269 Annotation Processor

The simplest way to create an annotation processor is to subclass the javax.annotation.processing.AbstractProcessor, which implements the javax.annotation.processing.Processor interface. This interface defines similar operations that we have seen in the apt com.sun.mirror.apt.AnnotationProcessorFactory and com.sun.mirror.apt.AnnotationProcessor. All annotation processors must implement this interface so that Javac can automatically identify them as annotation processors. The source code for the new annotation processor is as follows:

package com.lizjason.annotation;

import static com.lizjason.annotation.BeanTypeNames.STATEFUL;
import static com.lizjason.annotation.BeanTypeNames.STATELESS;
import static javax.lang.model.SourceVersion.RELEASE_6;

import java.util.List;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.TypeElement;

/**
* A simple session bean type annotation processor. The implementation
* is based on the standard annotation processing API in Java 6.
*/
@SupportedAnnotationTypes({STATEFUL, STATELESS})
@SupportedSourceVersion(RELEASE_6)
public class NewBeanTypeAnnotationProcessor extends AbstractProcessor {

    /**
     * Check if both @Stateful and @Stateless are present in an
     * session bean. If so, emits a warning message.
     */
    @Override
    public boolean process(Set<? extends TypeElement> typeElements,
        RoundEnvironment roundEnv) {

        Set<? extends TypeElement> elements =
            roundEnv.getSpecifiedTypeElements();

        for (TypeElement element : elements) {

            System.out.println("Processing " + element);

            List<? extends AnnotationMirror> annotationMirrors =
                element.getAnnotationMirrors();

            System.out.println("Annotations: " + annotationMirrors);

            boolean isStateful = false;
            boolean isStateless = false;

            for (AnnotationMirror annotation : annotationMirrors) {
                String type = annotation.getAnnotationType().toString();
                if (STATELESS.equals(type)) {
                    isStateless = true;
                } else if (STATEFUL.equals(type)) {
                    isStateful = true;
                }
            }

            if (isStateful && isStateless) {
                // both @Stateful and @Stateless are present
                System.out.println(
                    "Warning: Both @Stateful and @Stateless are present. "
                    + "They should be mutually exclusive.");
            }
        }
        return true;//claim the annotations
    }
}

The implementation is almost the same as the implementation using the apt API. The TypeElement is the counterpart of apt's TypeDeclaration, and the SupportedAnnotationTypes annotation corresponds to the apt AnnotationProcessorFactory's supportedAnnotationTypes() method. The SupportedSourceVersion annotation specifies the latest source version the annotation processor supports. To run this processor and compile the source code, we can use:

javac -cp . -processor com.lizjason.annotation.NewBeanTypeAnnotationProcessor
    ProcessPayrollBean.java

The output is identical to that of the apt-based implementation. Note that, in javac, annotation processing is enabled by default. We can use the -proc option to disable the annotation processing or run the annotation processing without compiling.

The examples shown in this article are simplified for clarity, but they show the idea of build-time annotation validation. Instead of embedding the annotation semantic rules in the source code, ideally (like the XML schema for XML files), the annotation semantic constraints should be clearly expressed externally, or as meta-annotations, in a regular expression or AspectJ pointcut-like annotation constraint language.

Summary

The annotation facility is widely used in Java EE 5 and other frameworks, but it needs a validation facility to check the semantic constraints. apt is a great tool for creating annotation processors to validate EE annotations at build time. The apt-based, build-time annotation validation is complementary to run-time reflection-based validation. It is especially useful when the annotations are not available at run time. The upcoming JSR-269 defines a standardized annotation process API to supersede the apt API. The new annotation processing API is easy to use and preferred, even though apt remains supported in the next release of Java.

Resources

width="1" height="1" border="0" alt=" " />
Jason Zhicheng Li is a senior software engineer with Object Computing, Inc. in St. Louis, MO.
Related Topics >> EJB   |