Introduction to Android Annotation Processor

We can add a lot of annotations to the java code, which will eventually be bound to the target element, and we can configure its retention policy for each annotation.

CLASS
Annotations are to be recorded in the class file by the compiler but need not be retained by the VM at run time.
RUNTIME
Annotations are to be recorded in the class file by the compiler and retained by the VM at run time, so they may be read reflectively.
SOURCE
Annotations are to be discarded by the compiler.
  • SOURCE works only in compile-time editing environments
  • CLASS annotation information will be written to the class file, but will not be loaded by VM
  • RUNTIME writes to the class file and is loaded by VM, which is valid at run time.

RUNTIME is well understood. If you want to use annotated data at runtime, you can set it to RUNTIME.

CLASS developers generally do not need to, after all, rarely need to directly process the class file information, I think this is more used in the packaging stage, such as class merged into jar or dex, etc.

SOURCE is more in the source code compilation stage, based on annotations to do some auxiliary tools, such as parsing, automatic code generation, etc.

The annotation processor introduced here, the compiler mechanism provided by android, enables developers to analyze and process specified annotations at compile time and automatically generate corresponding codes.

When we want to implement and use the ability to automatically generate code based on custom annotations, there are three things we need to do

  1. Custom Annotations
  2. Implementing Annotation Processor
  3. Injecting annotation processor into gradle compilation environment

Custom Annotations

@Retention(RetentionPolicy.SOURCE)
@Target(AnnotationTarget.CLASS)
annotation class TestDataSource

Implementing Annotation Processor

The core of annotation processor is to realize a class that can be recognized by compiler environment. When compiler compiles, it creates such object and calls back the current environment and annotation-related data to the class object, and then implements the processing of specified annotations within the class.

  1. Recognition relies on the @AutoService annotation
@AutoService(javax.annotation.processing.Processor.class)
  1. Callbacks are derived
public class TestProcessor extends AbstractProcessor{

The overloaded functions to be implemented are not detailed here.

Using annotation processors (apt and kapt)

There are two ways to introduce annotation processors into compiler module s

annotationProcessor project(':test_annotation')
//or
kapt project(':test_annotation')

Annotation Processor can only recognize annotations contained in java code

kapt is that annotations in java and kotin code can be recognized

If there is kotlin code in the project, then direct kapt

Generate code

Code generated automatically by the compiler is placed in the module's build directory

//apt
generated/source/apt
//kapt
generated/source/kapt

There are two ways to generate code, one is to use code generation directly by annotating information, the other is to use existing code files directly, such as downloading code zip from the Internet to decompress it.

Of course, I believe that for most annotation processors, they are based on annotation information and automatically generate code with the help of javax. There are a lot of specific generated codes on the Internet, which is not introduced here.

This article focuses on the second way, because it is a direct copy of code files, the premise must be to get the current module/build under the target directory, how to obtain? Processing Environment doesn't seem to directly expose API s, so it's just another way. I'm using the following method:

            TypeSpec.Builder mainActivityBuilder = TypeSpec.classBuilder("zipStub")
                    .addModifiers(Modifier.PUBLIC);

            TypeSpec mainActivity = mainActivityBuilder.build();
            JavaFile file = JavaFile.builder("com.harishhu.test.datasource", mainActivity).build();

            try {
                String fileName = file.packageName.isEmpty()
                        ? file.typeSpec.name
                        : file.packageName + "." + file.typeSpec.name;
                List<Element> originatingElements = file.typeSpec.originatingElements;
                JavaFileObject filerSourceFile = processingEnv.getFiler().createSourceFile(fileName,
                        originatingElements.toArray(new Element[originatingElements.size()]));

                String filepath = filerSourceFile.toUri().getPath();
                println("file uri = " + filepath);

                outputdir = filepath.replace("com/harishhu/test/datasource/zipStub.java", "");
                println("outpur dir = " + outputdir);

                try (Writer writer = filerSourceFile.openWriter()) {
                    file.writeTo(writer);
                } catch (Exception e) {
                    try {
                        filerSourceFile.delete();
                    } catch (Exception ignored) {
                    }
                    throw e;
                }

                decompressZip("/Users/harishhu/code.zip", outputdir);
            } catch (IOException e) {
                // e.printStackTrace();
            }

Or use javax to generate a zipstub class, processingEnv. getFiler (). The Filer generated by createSourceFile actually contains path information

String filepath = filerSourceFile.toUri().getPath();

Then replace zipStub's package path to get the output directory

outputdir = filepath.replace("com/harishhu/test/datasource/zipStub.java", "");

Then extract or copy the code file to the output directory

Be accomplished?

I tried it. Kapt is OK, but apt is not. Why not? Because when gradle compiles, it records and tracks the files generated by javax, only the files generated by javax will be packaged, and kapt does not have this level of verification, so apt is relatively safer and more reasonable.

How to solve that? Since only the file path generated by javax can be recorded, let's use javax to create an empty class file with the same name before copying or decompressing a file, and then replace it.

    private void genJavaClassFile(String classpath, String name){
        TypeSpec.Builder classbuilder = TypeSpec.classBuilder(name)
                .addModifiers(Modifier.PUBLIC);

        TypeSpec mainActivity = classbuilder.build();
        JavaFile file = JavaFile.builder(classpath, mainActivity).build();

        try {
            String fileName = file.packageName.isEmpty()
                    ? file.typeSpec.name
                    : file.packageName + "." + file.typeSpec.name;
            List<Element> originatingElements = file.typeSpec.originatingElements;
            JavaFileObject filerSourceFile = processingEnv.getFiler().createSourceFile(fileName,
                    originatingElements.toArray(new Element[originatingElements.size()]));

            try (Writer writer = filerSourceFile.openWriter()) {
                file.writeTo(writer);
            } catch (Exception e) {
                try {
                    filerSourceFile.delete();
                } catch (Exception ignored) {
                }
                throw e;
            }
        } catch (IOException e) {
            // e.printStackTrace();
        }
    }

    /**
     * Unzip the file
     * @param zipPath Target file to unzip
     * @param descDir Specify a decompressed directory
     * @return Decompression results: success, failure
     */
    @SuppressWarnings("rawtypes")
    public boolean decompressZip(String zipPath, String descDir) {
        File zipFile = new File(zipPath);
        boolean flag = false;
        File pathFile = new File(descDir);
        if(!pathFile.exists()){
            pathFile.mkdirs();
        }
        ZipFile zip = null;
        try {
            zip = new ZipFile(zipFile, Charset.forName("utf-8"));//Prevent Chinese catalogue from scrambling
            for(Enumeration entries = zip.entries(); entries.hasMoreElements();){
                ZipEntry entry = (ZipEntry)entries.nextElement();
                String zipEntryName = entry.getName();
                InputStream in = zip.getInputStream(entry);
                //Specify the unzipped folder + the name of the current zip file
                String outPath = (descDir+zipEntryName).replace("/", File.separator);
                //Determine whether a path exists or not, and create a file path if it does not exist
                File file = new File(outPath.substring(0, outPath.lastIndexOf(File.separator)));
                if(!file.exists()){
                    file.mkdirs();
                }
                //Determine whether the full path of the file is a folder or not. If the file has been uploaded, no decompression is required.
                if(new File(outPath).isDirectory()){
                    continue;
                }

                println("current zip The path after decompression is:" + outPath + ", zipEntryName = " + zipEntryName);
                if (zipEntryName.endsWith(".java")){
                    int index = zipEntryName.lastIndexOf("/");
                    String classapth = zipEntryName.substring(0, index).replace("/", ".");
                    String name = zipEntryName.substring(index + 1).replace(".java", "");

                    println("classpath = " + classapth + ", name = " + name);

                    genJavaClassFile(classapth, name);
                }

                //Save file path information (you can use the uniqueness of md5.zip name to determine whether it has been decompressed)
                OutputStream out = new FileOutputStream(outPath);
                byte[] buf1 = new byte[2048];
                int len;
                while((len=in.read(buf1))>0){
                    out.write(buf1,0,len);
                }
                in.close();
                out.close();
            }
            flag = true;
            //Must shut down, or this zip file has been occupied, to delete can not be deleted, renamed can not, mobile can not, more, the system has collapsed.
            zip.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return flag;
    }

Note that in the decompressZip function, genJavaClassFile is called to generate the corresponding empty class file before decompressing the java file.

In this way, the compiler environment is perfectly deceived.

Tags: Java Gradle Android Mobile

Posted on Tue, 27 Aug 2019 00:52:12 -0700 by m3mn0n