Coming Up for Air

Annotation Processing the New Way

Wednesday, July 25, 2012 |

I recently ran into an issue with our dependency injection system: it won’t return a list of interfaces, only implementations. That system, for what it’s worth, is HK2, but CDI has the same "problem". Since the rest of the system worked using these interfaces, I really wanted to solve the discoverability issue rather than redesigning that part of the system. After considering and playing with a Maven plugin, I opted to use the javax.annotation.processing API. Let’s take a quick look.

The first step, of course, is to create the annotation. We’ll use this very simple one:

1
2
3
4
5
6
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
    String parent();
}

Nothing special there. The next step is to create the Processor class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@SupportedAnnotationTypes("com.foo.MyAnnotation")
public class MyAnnotationProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
        Messager messager = processingEnv.getMessager();
        try {
            Map<String, List<String>> classes = new HashMap<String, List<String>>();

            for (TypeElement te : elements) {
                for (Element e : env.getElementsAnnotatedWith(te)) {
                    final String parent = e.getAnnotation(MyAnnotation.class).parent();
                    List<String> list = classes.get(parent);
                    if (list == null) {
                        list = new ArrayList<String>();
                        classes.put(parent, list);
                    }
                    list.add(e.toString());
                }
            }

            if (!classes.isEmpty()) {
                final Filer filer = processingEnv.getFiler();
                FileObject fo = filer.createResource(StandardLocation.CLASS_OUTPUT,
                    "", "META-INF/com.foo.MyAnnotation");

                BufferedWriter bw = new BufferedWriter(fo.openWriter());
                // ...
                bw.close();
            }
        } catch (IOException ex) {
            messager.printMessage(Kind.ERROR, ex.getLocalizedMessage());
        }

        return true;
    }
}

I trimmed as much of the logic as I could to clarify, I hope, the details of the processor. The class extends AbstractProcessor. It is also annotated with @SupportedAnnotationTypes, which is a multi-valued annotation telling the system which annotations we care about. In our case, it’s just one.

In the process() method, we iterate over the elements which is, as best as I can tell, a Set of the annotations we just told the system we care about. Taking that, we ask the system (via env.getElementsAnnotatedWith()) for the elements that have that annotation. From here, we can get the annotation instance and process it (e.getAnnotation(MyAnnotation.class)). You may need to do some type checking (e.g., is this annotation only on a String?). In this example, we’re going to store it in a List, which is then stored in a Map, keyed by the value of parent.

Once we’ve processed all the elements, we’re ready to create our metadata file. To do that, we instruct the Filer, obtained from the ProcessingEnvironment we get from AbstractProcessor, to create a resource. We tell it to use StandardLocation.CLASS_OUTPUT as the output directory (or Location in the parlance of the API), and to name it META-INF/com.foo.MyAnnotation.

Once that’s done, the final step is to add this jar as a compile-time dependency to any project that uses the annotation (and which needs the metadata generated):

1
2
3
4
5
6
<dependency>
    <groupId>com.foo</groupId>
    <artifactId>annotation-processor</artifactId>
    <version>1.0</version>
    <scope>compile</scope>
</dependency>

And that’s it. When your build tool (Maven, Gradle, or…​ shudder Ant :) compiles the classes in the project, it will create the metadata file. If you’re using Maven, you can verify by viewing target/class/META-INF/com.foo.MyAnnotation.

Update: Reading the data

The other side of this reading the data to finish locating the interfaces. Here is the code I’m currently using:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// This Map<List> holds MyAnnotation data keyed by parent().  Since there may be more than one
// MyAnnotation pointing to a given parent, we store the name of the actual MyAnnotation-annotated
// interfaces in a List.
private static final Map<String, List<String>> myAnnotations = new HashMap<String, List<String>>();
private static void loadMyAnnotationMetadata(Class similarClass) {
    try {
        Enumeration<URL> urls = similarClass.getClassLoader().getResources("META-INF/com.foo.MyAnnotation");
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()));
            while (reader.ready()) {
                final String line = reader.readLine();
                if (line.charAt(0) != '#') {
                    if (!line.contains(":")) {
                        Logger.getLogger(MyAnnotationUtil.class.getName()).log(Level.INFO,
                            "Incorrectly formatted entry in {0}: {1}",
                            new String[] {"META-INF/com.foo.MyAnnotation", line}); // TODO: i18n
                    }
                    String[] entry = line.split(":");
                    String base = entry[0];
                    String ext = entry[1];
                    List<String> list = myAnnotations.get(base);
                    if (list == null) {
                        list = new ArrayList<String>();
                        myAnnotations.put(base, list);
                    }
                    list.add(ext);
                }
            }
        }
    } catch (IOException ex) {
        Logger.getLogger(MyAnnotationsUtil.class.getName()).log(Level.SEVERE, null, ex);
    }
}

This code reads any metadata files found in the running system and builds a Map<String,List> to hold the data. Elsewhere in the system, I iterate over these Lists and load the classes (Class.forName()) to integrate the interfaces into the system:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
List<String> list = myAnnotations.get(parent));
if (list != null) {
    for (String className : list) {
        try {
            Class<?> c = Class.forName(className, true, similarClass.getClassLoader());
            exts.add(c);
        } catch (ClassNotFoundException ex) {
            Logger.getLogger(MyAnnoationUtil.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

That should cover it. There’s much, much more that can be done in your processor, which you can read about in the javadocs, but this should get you going.

Search

    Quotes

    Sample quote

    Quote source

    About

    My name is Jason Lee. I am a software developer living in the middle of Oklahoma. I’ve been a professional developer since 1997, using a variety of languages, including Java, Javascript, PHP, Python, Delphi, and even a bit of C#. I currently work for Red Hat on the WildFly/EAP team, where, among other things, I maintain integrations for some MicroProfile specs, OpenTelemetry, Micrometer, Jakarta Faces, and Bean Validation. (Full resume here. LinkedIn profile)

    I am the president of the Oklahoma City JUG, and an occasional speaker at the JUG and a variety of technical conferences.

    On the personal side, I’m active in my church, and enjoy bass guitar, running, fishing, and a variety of martial arts. I’m also married to a beautiful woman, and have two boys, who, thankfully, look like their mother.

    My Links

    Publications