Android的注解处理器介绍

我们可以在java代码中添加很多的注解, 注解最终会绑定到target element上,每个注解我们都可以配置其retention policy

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 只在编译期的编辑环境内有效
  • CLASS 注解信息会写入class文件,但是不会被VM加载
  • RUNTIME 写入class文件,并且会被VM加载,运行时有效

RUNTIME很好理解,运行时如果要使用注解数据的话,就设置成RUNTIME即可

CLASS开发人员一般用不到,毕竟很少需要直接去处理class文件的信息,我觉得这个更多是在打包阶段用的,比如class合并成jar或者dex等

SOURCE则更多的是在源码编译阶段,基于注解做一些辅助工具的,比如语法分析,代码自动生成等

这里要介绍的注解处理器,主要android提供的编译器机制,能够让开发人员有能力在编译期去分析处理指定注解并自动生成对应代码

当我们想要实现并使用基于自定义注解自动生成代码的功能,要做三件事情

  1. 自定义注解
  2. 实现注解处理器
  3. 将注解处理器注入gradle编译环境

自定义注解

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

实现注解处理器

注解处理器的核心,就是要实现一个能被编译环境识别的类,编译环境在编译时会创建该类对象并将当前环境和注解相关数据回调给类对象,类内部再去实现对指定注解的处理;

  1. 识别靠的是@AutoService注解
@AutoService(javax.annotation.processing.Processor.class)
  1. 回调则用派生
public class TestProcessor extends AbstractProcessor{

具体要实现的重载函数这里就不细说了

使用注解处理器(apt和kapt)

在编译module引入注解处理器,有两种方式

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

annotationProcessor只能识别java代码中包含的注解

kapt则是java和kotin代码中的注解都能识别

如果工程中有kotlin代码,那就直接kapt

生成代码

在编译器自动生成的代码,都会被放置于module的build目录下

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

生成代码的方式有两种,一种是通过注解信息,直接使用代码生成,另外一种是直接使用现有的代码文件,比如从网上下载代码zip解压

当然,我相信对于大多数注解处理器来说,都是基于注解信息并且借助javax自动生成代码的,具体生成代码网上很多,这里就不介绍了

扫描二维码关注公众号,回复: 9099319 查看本文章

这里重点介绍下第二种方式,由于是直接拷贝代码文件,那前提必须要拿到当前module/build下面的目标目录,要如何获取?ProcessingEnvironment好像没有直接暴露相关API,那只能另辟蹊径了,我使用的方法:

            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();
            }

还是使用javax生成一个zipstub类,processingEnv.getFiler().createSourceFile生成的Filer其实是包含路径信息的

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

接着替换掉zipStub的包路径,获取输出目录

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

然后将代码文件解压或者拷贝到输出目录下

大功告成?

我试了一下,kapt可以,但是apt不行,为什么不行?因为gradle编译时,会对javax生成的文件做记录跟踪,只有javax生成的文件,才会被打包,kapt则没有这层校验,所以apt相对来说,更安全也更合理一点

那怎么解决?既然只有javax生成的文件路径才会被记录,那咱们在拷贝或者解压某一文件前,先使用javax创建一个同名空类文件,然后再替换就好了

    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();
        }
    }

    /**
     * 解压文件
     * @param zipPath 要解压的目标文件
     * @param descDir 指定解压目录
     * @return 解压结果:成功,失败
     */
    @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"));//防止中文目录,乱码
            for(Enumeration entries = zip.entries(); entries.hasMoreElements();){
                ZipEntry entry = (ZipEntry)entries.nextElement();
                String zipEntryName = entry.getName();
                InputStream in = zip.getInputStream(entry);
                //指定解压后的文件夹+当前zip文件的名称
                String outPath = (descDir+zipEntryName).replace("/", File.separator);
                //判断路径是否存在,不存在则创建文件路径
                File file = new File(outPath.substring(0, outPath.lastIndexOf(File.separator)));
                if(!file.exists()){
                    file.mkdirs();
                }
                //判断文件全路径是否为文件夹,如果是上面已经上传,不需要解压
                if(new File(outPath).isDirectory()){
                    continue;
                }

                println("当前zip解压之后的路径为:" + 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);
                }

                //保存文件路径信息(可利用md5.zip名称的唯一性,来判断是否已经解压)
                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;
            //必须关闭,要不然这个zip文件一直被占用着,要删删不掉,改名也不可以,移动也不行,整多了,系统还崩了。
            zip.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return flag;
    }

注意decompressZip函数里,在解压java文件前,都会调用genJavaClassFile生成对应的空类文件

就这样,完美的骗过了编译环境

发布了46 篇原创文章 · 获赞 25 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/zhejiang9/article/details/100100909