美文网首页程序员IT类作者联盟
「Android」通过注解自动生成类文件:APT实战(Abstr

「Android」通过注解自动生成类文件:APT实战(Abstr

作者: 毛大姑娘 | 来源:发表于2018-12-28 23:58 被阅读39次

    文/毛毛

    欠了自己好几篇文章还没开始动笔。。。

    今天讲点技术干货吧!

    最近在做一个自动生成代码的架构,这两天调研了一下APT自动生成代码的流程,动手写了个小demo。

    demo 内容:通过获取注解内容来生成新类,再通过调用新类的方法来获取注解的内容,并展示出来。

    本文作为总结文,讲解demo的创建过程以及遇到的问题解决。如有描述不详之处,或是遇到了新的问题,欢迎留言探讨。

    一、新建工程

    创建一个普通的Android工程。

    二、新建AbstractProcessor类的实现类。

    @SupportedAnnotationTypes("com.autotestdemo.maomao.javalib.VInjector")
    @SupportedSourceVersion(SourceVersion.RELEASE_6)
    public class VInjectProcessor extends AbstractProcessor {
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment){
            return false;
        }
    }
    

    坑一:首先你把我这代码拷过去你会发现导不了包!根本找不到AbstractProcessor类。

    原因是AbstractProcessor不在Android SDK里面!

    所以我们要建【java工程】

    但是我们最终要放在app里面运行的,怎么办?

    那我们需要建一个java library的module来做为你主工程的引用工程,专门存放AbstractProcessor实例的相关内容。

    建好library之后,需要在主工程引用它:

    上面的javalib是我新建的java工程,app是我的主工程代码。

    我们要在app里的build.gradle文件里添加对javalib的引用:

    dependencies {
        implementation project(':javalib') // 添加依赖模块
    }
    

    三、添加注解

    要实现通过获取注解内容来生成新类,所以首先要有个注解。

    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.TYPE)
    public @interface VInjector {
        int id();
        String name();
        String text();
    }
    

    @Retention(RetentionPolicy.CLASS)指定了该注解是编译时注解,即程序编译时就能获取到所有该注解的内容。
    若指定的是RetentionPolicy.RUNTIME就表示是运行时注解。

    @Target(ElementType.TYPE)指定了该注解是作用在类上面的,而不是属性上。

    然后指定了一个int类型和两个String类型的接收字段。

    用法:

    
    @VInjector(id=1,name="Maomao",text="这是动态代码生成的")
    public class MainActivity extends AppCompatActivity {
    
    

    四、实现AbstractProcessor实例

    这是最重要的一步:代码实现

    首先看看代码:

    
    @SupportedAnnotationTypes("com.autotestdemo.maomao.javalib.VInjector")
    @SupportedSourceVersion(SourceVersion.RELEASE_6)
    public class VInjectProcessor extends AbstractProcessor {
    
        private Filer mFiler;
        private Messager mMessager;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            //初始化我们需要的基础工具
            mFiler = processingEnv.getFiler();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    
            // 遍历所有注解元素
            for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(VInjector.class)) {
                analysisAnnotated(annotatedElement);
            }
            return false;
        }
    
    
        private static final String SUFFIX = "AutoClass";
    
        /**
         * 生成java文件
         * @param classElement 注解
         */
        private void analysisAnnotated(Element classElement) {
            VInjector annotation = classElement.getAnnotation(VInjector.class);
            int id = annotation.id();
            String name = annotation.name();
            String text = annotation.text();
            String newClassName = name + SUFFIX;
    
            StringBuilder builder = new StringBuilder()
                    .append("package com.autotestdemo.maomao.autotestdemo.auto;\n\n")
                    .append("public class ")
                    .append(newClassName)
                    .append(" {\n\n") // open class
                    .append("\tpublic String getMessage() {\n") // open method
                    .append("\t\treturn \"");
    
            // this is appending to the return statement
            builder.append(id).append(text).append(newClassName).append(" !\\n");
    
    
            builder.append("\";\n") // end returne
                    .append("\t}\n") // close method
                    .append("}\n"); // close class
    
    
            try { // write the file
                JavaFileObject source = mFiler.createSourceFile("com.autotestdemo.maomao.autotestdemo.auto." + newClassName);
                Writer writer = source.openWriter();
                writer.write(builder.toString());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                // Note: calling e.printStackTrace() will print IO errors
                // that occur from the file already existing after its first run, this is normal
            }
    
        }
    
    

    上面代码分为两部分。

    第一部分:获取注解

    首先我们看VInjectProcessor上面的两个注解:

    @SupportedAnnotationTypes("com.autotestdemo.maomao.javalib.VInjector")
    @SupportedSourceVersion(SourceVersion.RELEASE_6)
    public class VInjectProcessor extends AbstractProcessor {
    

    @SupportedAnnotationTypes()是指定哪些注解会由该类处理,里面放注解的全包名路径。
    @SupportedSourceVersion()是指定编译器版本

    我们再来看看process方法:

        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    
            // 遍历所有注解元素
            for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(VInjector.class)) {
                analysisAnnotated(annotatedElement);
            }
            return false;
        }
    

    process方法体是获取注解内容的唯一途径。

    这里面包含了所有符合条件的注解(在@SupportedAnnotationTypes()里指定的),因此我们需要循环取出当个注解实例。

    roundEnvironment.getElementsAnnotatedWith(VInjector.class)是获取所有的VInjector注解集合。

    第二部分:生成java文件

    analysisAnnotated()方法是用于获取到注解内容之后生成与内容相关的java文件。

        private static final String SUFFIX = "AutoClass";
    
        /**
         * 生成java文件
         * @param classElement 注解
         */
        private void analysisAnnotated(Element classElement) {
            VInjector annotation = classElement.getAnnotation(VInjector.class);
            int id = annotation.id();
            String name = annotation.name();
            String text = annotation.text();
            String newClassName = name + SUFFIX;
    
            StringBuilder builder = new StringBuilder()
                    .append("package com.autotestdemo.maomao.autotestdemo.auto;\n\n")
                    .append("public class ")
                    .append(newClassName)
                    .append(" {\n\n") // open class
                    .append("\tpublic String getMessage() {\n") // open method
                    .append("\t\treturn \"");
    
            // this is appending to the return statement
            builder.append(id).append(text).append(newClassName).append(" !\\n");
    
    
            builder.append("\";\n") // end returne
                    .append("\t}\n") // close method
                    .append("}\n"); // close class
    
    
            try { // write the file
                JavaFileObject source = mFiler.createSourceFile("com.autotestdemo.maomao.autotestdemo.auto." + newClassName);
                Writer writer = source.openWriter();
                writer.write(builder.toString());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                // Note: calling e.printStackTrace() will print IO errors
                // that occur from the file already existing after its first run, this is normal
            }
    
        }
    

    代码大致内容:拿到注解里面的所有内容,生成一个输出所有内容的类。

    五、使用Processor

    VInjectProcessor类实现好以后,我们怎么使用它?系统如何知道运行它里面的代码?

    注解处理器类编写完后,还需要创建一个 java META_INF 文件来告诉系统具有注解处理功能。Java 代码在编译的时候,系统编译器会查找所有的 META_INF 中的注册的注解处理器来处理注解。

    在项目中创建如下目录:
    src/main/resources/META_INF/services

    main目录下创建如下目录和文件:

    resources
            - META-INF
                  - services
                        - javax.annotation.processing.Processor
    

    在 services 目录下面创建一个名字为 “javax.annotation.processing.Processor” 的文本文件,Processor内容如下:

    com.autotestdemo.maomao.javalib.VInjectProcessor   # 指定处理器全类名
    

    由于我们的VInjectProcessor是在子工程里面,因此我们的目录也需在子工程里面:

    六、编译

    做完以上步骤,编译工程之后,就可以生出新的类,生成好的类长这样:

    package com.autotestdemo.maomao.autotestdemo.auto;
    
    public class MaomaoAutoClass {
    
        public String getMessage() {
            return "1这是动态代码生成的MaomaoAutoClass !\n";
        }
    }
    

    该类需要在app目录下的build目录里找,路径如下:

    这里有个坑:如果你编译之后,source文件夹下面怎么也找不到apt文件夹,或者报以下错误:

    Annotation processors must be explicitly declared now.  The following dependencies on the compile classpath are found to contain annotation processor.  Please add them to the annotationProcessor configuration.
      - javalib.jar (project :javalib)
    Alternatively, set android.defaultConfig.javaCompileOptions.annotationProcessorOptions.includeCompileClasspath = true to continue with previous behavior.  Note that this option is deprecated and will be removed in the future.
    

    这时候你需要在app下的build.gradle里加入如下引用:

    android {
        defaultConfig {
            //解决多包依赖显示使用注解
            javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
        }
    }
    

    七、调用生成的代码

    编译成功以后,我们就能直接访问生成的类。

    @VInjector(id=1,name="Maomao",text="这是动态代码生成的")
    public class MainActivity extends AppCompatActivity {
    
        private TextView tv;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            tv = findViewById(R.id.text);
            tv.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    MaomaoAutoClass auto = new MaomaoAutoClass();
                    tv.setText(auto.getMessage());
                }
            });
        }
    }
    

    我把类名的前缀指定为Maomao,因此生成的类叫MaomaoAutoClass

    此时我们可以访问MaomaoAutoClass类并调用里面的方法。从方法获取的字符串我给它替换掉TextView原有的字符串。

    至此,该功能讲解全部完毕。

    效果图:

    新建空的项目,点击文字
    点击文字之后替换成新类获取的文字

    【参考链接】
    https://www.jianshu.com/p/003be1b75e28

    https://www.jianshu.com/p/07ef8ba80562

    https://blog.csdn.net/feirose/article/details/68486790

    https://blog.csdn.net/keep_holding_on/article/details/76188657

    相关文章

      网友评论

        本文标题:「Android」通过注解自动生成类文件:APT实战(Abstr

        本文链接:https://www.haomeiwen.com/subject/yjvylqtx.html