文/毛毛

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

今天讲点技术干货吧!

最近在做一个自动生成代码的架构,这两天调研了一下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

(2018.12.28 23:58)