Code replacement with an annotation processor
Asked Answered
M

3

24

I'm trying to write an annotation processor to insert methods and fields on a class... and the documentation is so sparse. I'm not getting far and I don't know if I'm approaching it correctly.

The processing environment provides a Filer object which has handy methods for creating new source and class files. Those work fine but then I tried to figure out how read the existing source files, and all it provides is "getResource". So in my Processor implementation I've done this:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    try {
        for (TypeElement te : annotations) {
            for (Element element : roundEnv.getElementsAnnotatedWith(te)) {
                FileObject in_file = processingEnv.getFiler().getResource(
                    StandardLocation.SOURCE_PATH, "",
                    element.asType().toString().replace(".", "/") + ".java");

                FileObject out_file = processingEnv.getFiler().getResource(
                    StandardLocation.SOURCE_OUTPUT, "",
                    element.asType().toString().replace(".", "/") + ".java");

                //if (out_file.getLastModified() >= in_file.getLastModified()) continue;

                CharSequence data = in_file.getCharContent(false);

                data = transform(data); // run the macro processor

                JavaFileObject out_file2 = processingEnv.getFiler().createSourceFile(
                    element.asType().toString(), element);
                Writer w = out_file2.openWriter();
                w.append(data);
                w.close();
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage());
    }
    return true;
}

My first quandary is I can't help feeling that element.asType().toString().replace(".", "/") + ".java" (to get the qualified type name and convert it into a package and source file path) is not a nice way to approach the problem. The rest of the API is so over-engineered but there doesn't seem to be a handy method for retrieving the original source code.

The real problem is that then the compiler gets spontaneously upset by the second source file in the output directory ("error: duplicate class") and now I'm stuck.

I've already written the rest of this -- a macro lexer and parser and whatnot for calculating some data and inserting the field values and methods -- but it operates as a initial step outside the compiler. Except for the fact that the original files cannot have a .java extension (to prevent the compiler seeing them), this works nicely. Then I heard that annotations can do code generation, which I assume will be more proper and convenient, but I can't find much guidance on it.

Monofilament answered 3/12, 2012 at 19:28 Comment(1)
See: techbitsfromsridhar.blogspot.ca/2013/02/…Unexperienced
C
21

The intention behind the annotation processor is to allow a developer to add new classes, not replace existing classes. That being said, there is a bug that allows you to add code to existing classes. Project Lombok has leveraged this to add getter and setter (among other things) to your compiled java classes.

The approach I have taken to 'replace' methods/fields is either extend from or delegate to the input class. This allows you to override/divert calls to the target class.

So if this is your input class:

InputImpl.java:

public class InputImpl implements Input{
    public void foo(){
        System.out.println("foo");
    }
    public void bar(){
        System.out.println("bar");
    }
}

You could generate the following to "replace" it:

InputReplacementImpl.java:

public class InputReplacementImpl implements Input{

    private Input delegate;

    //setup delegate....

    public void foo(){
        System.out.println("foo replacement");
    }
    public void bar(){
        delegate.bar();
    }
}

This begs the question, how do you reference InputReplacementImpl instead of InputImpl. You can either generate some more code to perform the wrapping or simply call the constructor of the code expected to be generated.

I'm not really sure what your question is, but I hope this sheds some light on your issues.

Caporetto answered 20/12, 2012 at 20:38 Comment(3)
Ah ha! I think I saw Lombok before, and that was partly why I had no reason to think this was impossible. This answer explains the compiler error and gives me a few choices. I've no desire to get mired in AST hacks when, currently, simple regexps do code find & replace for me. Delegating to the class is not really doable in my case because the original class is not complete and compilable. As far as I see, this silly little limitation of the annotations API means it's not useful to me, despite its complexity. But now I know what to do: not use annotations! Thank you very much for your help.Monofilament
@JohnEricksen I'm curious how to refer to the generated class. To simply use the constructor, the annotation class should be compiled first. If not, IDE will report no class def error. To wrapping, how to do that?Danettedaney
@yk42b, the generated class should be referencable in the IDE, as long as the classpath is set up properly and the annotation processor is running.Caporetto
S
1

A bit late :), but one solution could be to use Byte Buddy's transformer as part of the build process. See eg https://github.com/raphw/byte-buddy/tree/master/byte-buddy-maven-plugin.

Shaylynn answered 8/1, 2020 at 8:30 Comment(0)
H
0

I don't know if this hint matches your needs, but your idea will work in conjunction with dependency-injection e.g. Spring.

  1. Create a custom annnotation for types and retention "SOURCE" which will be ignored by Spring.
  2. Do the desired change to the code in your annotation-processor and add Spring's @Component Annotation to you generated Java-Type and save it as a subtype of the original type with a class-name different form the origifinal one.
  3. When Spring creates the context, it will load the class based on your processing-result instead of the original one.

In contrary, Lombok is manipulating the abstract source tree directly, which is much more complex then generating source-code. Problem here is, the Java-Compiler-Module is exposed (Jigsaw).

Another alternative could be to use some bytecode-api like ByteBuddy, to proxy the original class at runtime. This will also require dependency-injection if you do not want to write code for instantiatig the class. Here the magic is an autowired field of type collection inside a Configuration class. Let me know, if this is interesting to you. Then I will add more details, here.

Hypervitaminosis answered 9/10, 2021 at 12:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.