7

I'm new to annotation processing and code generation. I want to find out how can I perform such operation like appending new method to existing class. Here is an example of what I want to do:

Assume that we have a class with with custom annotations like this one:

class SourceClass {
    @CustomAnnotation
    fun annotatedFun1(vararg argument: Any) {
        //Do something
    }

    @CustomAnnotation
    fun annotatedFun2(vararg argument: Any) {
        //Do something
    }

    fun someOtherFun() {
        //Do something
    }
}

And the result I want to get - extended copy of that class:

class ResultClass {
    fun hasFunWithName(name: String): Boolean {
        return (name in arrayOf("annotatedFun1", "annotatedFun2"))
    }

    fun callFunByName(name: String, vararg arguments: Any) {
        when (name) {
            "annotatedFun1" -> annotatedFun1(*arguments)
            "annotatedFun2" -> annotatedFun2(*arguments)
        }
    }

    fun annotatedFun1(vararg argument: Any) {
        //Do something
    }

    fun annotatedFun2(vararg argument: Any) {
        //Do something
    }

    fun someOtherFun() {
        //Do something
    }
}

I've already found out how to create annotation processor. I'm looking for a method to save all existing fields, properties and methods in source class and to append a few more methods to it.

If it is possible to modify class without creating new one - it would be perfect, but in all tutorials only new classes are created and I didn't find any example where all contents of source class are being copied to another one.

Please, do not advise to use reflection. I need this for android and so reflection is not the option cause of resources cost. I'm looking for compile-time solution.

It is required for custom script language implemented in app and should be used to simplify wrapper classes structure. When this job is done directly in code - it looks awful when such method count exceeds 20 per class.

Sasha Shpota
  • 7,441
  • 6
  • 49
  • 100
Andrei Vinogradov
  • 1,637
  • 10
  • 26

4 Answers4

4

Here is a good example of Java Annotation Processing I recently worked with. It's an implementation of @Immutable annotation.

Check out ByteBuddy or Kotlin Poet to understand how additional code generation works.

For Kotlin you do almost the same, check this manual for Kotlin-specific steps.

Sergei Rybalkin
  • 2,890
  • 11
  • 24
  • 1
    Thank you (+1). Could you please elaborate more on how this can be achieved? As I understand, Kotlin Poet helps generate new code, while I need to **change a syntax tree** of an already **existing class**. I didn't find in the source code that ByteBuddy uses annotation processing. It is clear to me how to create an annotation processor, what is not clear is how to modify an existing file so that it is compiled to an enhanced byte code. – Sasha Shpota Dec 26 '20 at 14:26
  • To add a new method you need to extend a class definition stage or implement a `process` method in your own `AnnotationProcessor`. Then you can `getElementsAnnotatedWith` something and apply code generation for these elements. Check Section 5 here: https://www.baeldung.com/byte-buddy . It's also possible that you'll need to redefine the entire class, check Section 6 for more details – Sergei Rybalkin Dec 26 '20 at 17:53
  • Thank you, Sergei. Am I understanding it correctly, the changes which I introduce with ByteBuddy inside the annotation processing stage will stay in the bytecode of the generated class files? – Sasha Shpota Dec 26 '20 at 21:18
  • You will get the declared methods only in bytecode (.class files) – Sergei Rybalkin Dec 26 '20 at 21:43
  • Maybe I am missing something, but on the annotation processing stage, I am not able to access the `Class` object of the class which I need to modify (it is not compiled yet). At the same time, I need it in for ByteBuddy to work. How can I handle this? – Sasha Shpota Dec 28 '20 at 17:19
  • I was trying to find something without getting too deep. There is a good process illustration here https://www.adrianbartnik.de/blog/annotation-processing/ Depending on your situation there are two(maybe more) ways: 1. Generate a code, that will generate and add methods in runtime. 2. Generate your methods (and pass back to javac) – Sergei Rybalkin Dec 29 '20 at 10:55
1

With Kotlin, you can use extension functions and that is the recommended way of adding new functionality to existing classes that you don't control. https://kotlinlang.org/docs/reference/extensions.html

Chirdeep Tomar
  • 3,513
  • 5
  • 29
  • 60
1

You may be abel to follow the pattern used by Project Lombok. See How does lombok work? or the source code for details.

Another option would be to write a new class that extends your source class:

class ResultClass : SourceClass {
    fun hasFunWithName(name: String): Boolean {
        return (name in arrayOf("annotatedFun1", "annotatedFun2"))
    }

    fun callFunByName(name: String, vararg arguments: Any) {
        when (name) {
            "annotatedFun1" -> annotatedFun1(*arguments)
            "annotatedFun2" -> annotatedFun2(*arguments)
        }
    }
}

Or perhaps use composition instead and implemnent cover methods for all the public methods in SourceClass.

If you are not tied to doing this using annotation processing, you could use a separate piece of custom code to process the source code files before compiling. Maybe use a regular expression like /@CustomAnnotation\s+.*fun (\w+)\s*\(([^)]*)\)/gm (Test on Regex101) to find the annotated methods.

Rangi Keen
  • 823
  • 9
  • 27
  • Thank you (+1). Could you please elaborate a bit more on how Lombok does it? It is mentioned in the linked SO question that in Lombok, they cast `javax.lang.model.element.Element` to the underlying internal API in order to modify the existing sources, but I couldn't find it in the code base. Probably I need to learn the code base well, but if you know how/where it is done please let me know. – Sasha Shpota Dec 25 '20 at 11:13
  • Sorry, I don’t know how Project Lombok works well enough to explain. – Rangi Keen Dec 25 '20 at 12:31
1

If I understood the requirement correctly, the goal is to implement something like described below.

You have a source file C.java that defines the class C like this:

public final class C
{
    @Getter
    @Setter
    private int m_IntValue;

    @Getter
    @Constructor
    private final String m_Text;
}

And now you want to know how to write an annotation processor that jumps in during compilation and modifies the source from C.java that the compiler sees to something like this:

public final class C
{
    private int m_IntValue;
    public final int getIntValue() { return m_IntValue; }
    public final void setIntValue( final int intValue ) { m_IntValue = intValue; }

    private final String m_Text;
    public final String getText() { return m_Text; }

    public C( final String text ) { m_Text = text; }
}

The bad news is, that this is not possible … not with an annotation processor, not for Java 15.

For Java 8 there was a way, using some internal classes with reflection to convince the AP to manipulate the already loaded source code in some way and let the compiler compile it a second time. Unfortunately, it failed more often than it worked …

Currently, an annotation processor can only create a new (in the sense of additional) source file. So one solution could be to extend the class (of course, that would not work for the sample class C above, because the class itself is final and all the attributes are private …

So writing a pre-processor would be another solution; you do not have a file C.java on your hard drive, but one named C.myjava that will be used by that preprocessor to generate C.java, and that in turn is used by the compiler. But that is not done by an annotation processor, but it may be possible to abuse it in that way.

You can also play around with the byte code that was generated by the compiler and add the missing (or additional) functionality there. But that would be really far away from annotation processing …


As a summary: today (as of Java 15), an annotation processor does not allow the manipulation of existing source code (you cannot even exclude some source from being compiled); you can only generate additional source files with an annotation processor.

tquadrat
  • 1,653
  • 12
  • 16
  • Thank you (+1). I guess [Lombok](https://github.com/rzwitserloot/lombok) overcomes this issue with Java 15 as well. – Sasha Shpota Dec 29 '20 at 10:35