Gordon
Assistant Engineer
Assistant Engineer
  • UID622
  • Fans3
  • Follows0
  • Posts52
Reads:7357Replies:0

Implementation of a simple Java compile annotation processing tool

Created#
More Posted time:Sep 7, 2016 15:36 PM
Introduction
Java annotation is a special syntactic metadata supported to be added into source code starting from Java 5.0.
Classes, methods, variables, parameters and packages in Java can be labeled. Unlike Javadoc, Java annotation has reflexivity. Java annotation can be embedded into byte code when the compiler generates a class file, and the label is obtained when executed by Java virtual machine.
According to different values the meta annotation @Retention specified,annotations can be divided into three types: SOURCE, CLASS and RUNTIME. When declared as SOURCE, annotations are retained only at source code level and dropped at compile; whendeclared as CLASS, annotations are recorded in the class file by the compiler, but they are ignored during runtime, the default Retention level is Class; whendeclared as RUNTIME, annotations will be retained until runtime and be obtained by reflection during runtime.
We will introduce methods of processing CLASS-level annotations below.
APT
Annotation Processing Tool is a tool built in javac for scanning and processing annotation information at compilation. Apt exposed available API since JDK 6. A specific processing tool receives Java source code or a compiled byte code as input, and then outputs some files (.java files in general). This means you can use apt to dynamically generate code logic. It needs to be noted that apt can only generate new Java classes, and cannot modify existing Java classes. All generated Java classes are compiled along with other source code by javac.
Define and use annotation
For example, we are here defining an annotation Meta for labeling Field, including two parameters, repeat and id. During the compilation, we assign values to the labeled Field (e.g. repeat is 2, id is Aa) by processing the annotation, the labeled Field is assigned “AaAa”.
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Meta {
    int repeat() default 0;
    String id() default "";
}


Using annotation on Field
@Meta(repeat = 3, id = "^_^")
public String test;


Process annotation
We are writing a processing tool for processing the Meta annotation defined above based on Android Studio.
Create Module
We are developing an annotation parser as a module in an Android Project here, create a Module, and select Java Library in types.
Create processing tool
Annotation needs to be processed by an annotation processing tool. All annotation processing tools implement the Processor interface. Generally we choose to inherit AbstractProcessor to create a custom annotation processing tool.
Inherit AbstractProcessor, and implement public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) method. In method parameters, annotations contain the annotations that the processing tool declares to support and has used in source code, while roundEnv contains the context of the annotation processing. When the method returns true, it indicates the annotation has been processed; when it returns false, the annotation will be delivered to other processing tools for further processing.
Declare supported annotation types and source code versions
Override getSupportedSourceVersion method,return source code version supported by the processing tool. In general, return SourceVersion.latestSupported()directly.
Override getSupportedAnnotationTypes method,return annotation types that the processing tool intended to process, returning a set comprising all annotation fully qualified names is required here.
In Java 7 or later,class annotations @SupportedAnnotationTypes and @SupportedSourceVersion can be used to declare instead of methods above.
Declare annotation processing tool
Annotation processing tool needs to be registered to JVM before use. Create a services directory under the META-INF directory, and create a file named javax.annotation.processing.Processor, declare the annotation processing tool in the file line by line. Likewise, what needs to be declared here is the fully qualified name of processing tool class.
Another simple method is to use the auto-services library provided by Google to introduce com.google.auto.service:auto-service:1.0-rc2 in build.gradle,and add annotation @AutoService(Processor.class) on the processing tool class. Auto-services is also an annotation processing tool and will generate a declaration file for the module at compilation.
Parse annotation
First we define an interface to regulate the generated class:
public interface Actor {
    void action();
}


And define another class structure to describe the generated class:
public class TargetGen<T extends Target> implements Actor{
    protected T target;

    public TargetGen(T obj) {
        this.target = obj;
    }

    @Override
    public void action() {
        //Value assignment
    }
}


If we have class A, wherein Field f comprises Meta annotation, we generate an AGen class for it and finish the value assignment in the action method.
Finish parse for annotation and code generation in the process method.
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    /*roundEnv.getRootElements() can return all classes in the project.
    In practical application, classes are required to be filtered to improve efficiency and to avoid scanning content in every class */
    for (Element e : roundEnv.getRootElements()) {
        List<String> statements = new ArrayList<>();
        /*Traverse all of the elements within the Class*/
        for (Element el : e.getEnclosedElements()) {
            /*Only Fields comprising annotation and modified as public are processed*/
            if (el.getKind().isField() && el.getAnnotation(Meta.class) != null && el.getModifiers().contains(Modifier.PUBLIC)) {
                /*Obtain annotation information, generate code snippet*/
                Meta meta = el.getAnnotation(Meta.class);
                int repeat = meta.repeat();
                String seed = meta.id();
                String result = "";
                for (int i = 0; i < repeat; i++) {
                    result += seed;
                }
                statements.add("\t\ttarget." + el.getSimpleName() + " = \"" + result + "\";");
            }
        }
        if (statements.size() == 0) {
            return true;
        }

        String enclosingName;
        if (e instanceof PackageElement) {
            enclosingName = ((PackageElement) e).getQualifiedName().toString();
        } else {
            enclosingName = ((TypeElement) e).getQualifiedName().toString();
        }
        /*Obtain class name of the generated class and package*/
        String pkgName = enclosingName.substring(0, enclosingName.lastIndexOf('.'));
        String clsName = e.getSimpleName() + "Gen";
        log(pkgName + "," + clsName);
        /*Create a file, write code content*/
        try {
            JavaFileObject f = processingEnv.getFiler().createSourceFile(clsName);
            log(f.toUri().toString());
            Writer writer = f.openWriter();
            PrintWriter printWriter = new PrintWriter(writer);
            printWriter.println("//Auto generated code, do not modify it!");
            printWriter.println("package " + pkgName + ";");
            printWriter.println("\nimport com.moxun.Actor;\n");
            printWriter.println("public class " + clsName + "<T extends " + e.getSimpleName() + "> implements Actor{");
            printWriter.println("\tprotected T target;");
            printWriter.println("\n\tpublic " + clsName + "(T obj) {");
            printWriter.println("\t\tthis.target = obj;");
            printWriter.println("\t}\n");
            printWriter.println("\t@Override");
            printWriter.println("\tpublic void action() {");
            for (String statement : statements) {
                printWriter.println(statement);
            }
            printWriter.println("\t}");
            printWriter.println("}");
            printWriter.flush();
            printWriter.close();
            writer.close();
        } catch (IOException e1) {
            e1.printStackTrace();
        }
    }
    return true;
}


Add dependency of processing tool module to dependencies of the object module, clean and rebuild the project, then the source code is processed by the custom annotation processing tool and the generated class is added to build/intermediates/classes directory. Due to an issue with Android Gradle plug-in, the generated class will be under the directory until plug-in version 2.2.0-alpha4. Source files under the intermediates directory are not retrieved by IDE, which results in inconvenience in debugging of the generated code but won't affect subsequent compilation. The issue may be fixed in a future version, and the results should be output to a right place, that is, under build/generated/source/apt directory.
Use the generated classes during runtime
Reflection may be used to access the generated classes during runtime. A simple helper class is defined here to instantiate the generated class and to assign value for the object Fields:
public class MetaLoader {
    public static void load(Object obj) {
        String fullName = obj.getClass().getCanonicalName();
        String pkgName = fullName.substring(0, fullName.lastIndexOf('.'));
        String clsName = pkgName + "." + obj.getClass().getSimpleName() + "Gen";

        try {
            Class<Actor> clazz = (Class<Actor>) Class.forName(clsName);
            Constructor<Actor> constructor = clazz.getConstructor(obj.getClass());
            Actor actor = constructor.newInstance(obj);
            actor.action();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


When the object classes are initializing, call MetaLoader.load with the instances of the object classes passed in, and Field value assignment is finished.
Eliminate processing tool during package
Due to the introduction of auto-service library,Duplicate files copied in APK META-INF/services/javax.annotation.processing.Processor error occurs during apk packing in the final stage. But since the file is not needed during runtime,the file can be excluded in packagingOptions to avoid the error:
packagingOptions {
    exclude 'META-INF/services/javax.annotation.processing.Processor'
}


However, it is not a thorough solution. As described above, the annotation processing tool is completely useless during runtime. Is it possible for an annotation processing tool to exist only at compilation without being packed into the final product? The answer is yes.
Add plug-ins within build.gradle to the project:
dependencies {
     classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
     //etc……
}


Apply the plug-ins within build.gradle of the module:
apply plugin: 'com.neenbedankt.android-apt'

Dependencies will create a new dependency method apt when the plug-ins are applied, and the declaration of dependency is modified to:
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    apt project(':processor')
    //etc……
}


Classes within processing tool module will not be packed into the final product upon such declaration, which helps to decrease the size of the product.
Debug annotation processing tool
Add new Run/Debug Configurations in Android Studio, select Remote in types,
and add in the gradle.properties of the project.
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

Select Configuration defined above, click the Debug button and wait for the object process to attach; set breakpoints within the annotation processing tool logic, select Rebuild Project, andbreakpoint debugging is implemented upon triggering annotation processing tool process logic.
Guest