Search code examples
javaannotationsmetaprogrammingabstract-syntax-tree

Java static metaprogramming


I'd like to implement annotation processor that will generate new class based on existing "prototype" class.

import java.util.List

@MyAnnotation
class MySuperClassPrototype {
    static MySuperClassPrototype createInstance() {
      return new MySuperClassPrototype();
    }
}

As a result of code below. The following new source file (compilation unit) will be generated:

import java.util.List

class MySuperClass {
    static MySuperClass createInstance() {
      return new MySuperClass();
    }
    public void specialAddedMethod() {
      /*...*/
    }
}

I'd like to copy all top-level import statements and static members and not static members of prototype-class. I've moved pretty far with Compiler Tree API (com.sun.source.tree). I can print out Tree data-type while substituting new class name for old. But there are problems that seems pretty hard.

If I get Tree.Kind.IDENTIFIER in the tree, how can I find what actual class it references. I need to replace all occurrences of MySuperClassPrototype identifier with MySuperClass identifier, and than print out whole tree.

Is it feasible?

Similarly I need to filter out @MyAnnotation annotation, and again it is represented with Tree.Kind.IDENTIFIER or Tree.Kind.MEMBER_SELECT.

How can I find out actual annotation class that is referenced by this identifier?

And another problem is printing out tree. If I use toString method I got decent result, but constructors are printed as methods with "<init>" name instead of methods with the same name as it's class, so I need to manually print every kind of Tree node.

You can see code I've come with here


Solution

  • 8 Years and not yet answered. Because of that, i will try to answer it, to your satisfaction.

    I fill furthermore concentrate on the static part of the question.

    TL;DR:

    You will not find copy and paste code in this answer.

    Is it feasible?

    Yes, absolutely.

    How can I find out actual annotation class that is referenced by this identifier?

    You will have to use the RoundEnvironment within an Annotation Processor to get the TypeElement.

    Static Metaprogramming

    Static metaprogramming (which you asked for) is metaprogramming done at compile time. By Kontrast: Dynamic metaprogramming is metaprogramming done at run time. And metaprogramming it self is the design of programs, that handle other programs as data.

    Pfeh, a lot to take in. If you are interested in this topic, a more or less good source for that is wikipedia.

    Your target would be, to generate a class at compile time. For run time, this would be done with something like cglib. But, since you choose static (and for all the right reasons), i will not explain this.

    The concept you are looking for is the annotation processor. The link is a link to Baeldung, where they do exactly, what you are looking for, only with the builder pattern in mind. You will love to hear, that this scenario is highly encouraged and easy to do with the annotation processor API. It even allows you, to generate code, which again is passed to the same or another annotation processor, without you doing anything.

    Before jumping right in, try to google yourself about "Java Annotation Processing". There are a lot of good sources out there, which will help you. To much, to list here. Just note, that coding in an annotation processor is different than coding normally. Not a huge difference, but the classes you are working on are not yet created. So keep this in mind and don't get discouraged!

    Using the Annotation Processor

    Your basic annotation processor would look something like this:

    @SupportedAnnotationTypes("package.of.MyAnnotation")
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    @AutoService(Processor.class)
    public class BuilderProcessor extends AbstractProcessor {
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, 
                               RoundEnvironment roundEnv) {
            // First let's find all annotated elements
            Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(MyAnnotation.class);
            // Handle all the annotated classes
            return false;
        }
    }
    

    The AutoService Annotation is used, to dynamically register your annotation processor. It comes from an external source, just so you don't wonder, why this code won't compile.

    In the handle all annotated classes part, you have the annotated Elements (which are the annotated classes). You now would have to verify, that they are classes and not interfaces or other annotations. This is because @Target(ElementType.Type) aims at any type, which includes interfaces and annotations. Furthermore, you would want to verify, that anything you require is present, or print an error to the compiler using the Messager.

    If you print an error here (for example), you will stop compiling and the error will be seen in most modern IDEs. It can be reached by calling roundEnv.getMessager()

    Afterwards you can generate a new class and write it to the input of the compiler, as a .java file. This can be done by using the Filer.

    An answer in StackOverflow really does no justice to this topic. I highly recommend looking at the Baeldung example and trying to uncover things from there. This API is as old as Java 6, but still not that greatly used. I encourage you, the reader, to try it out for yourself :)