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

文/毛毛

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

今天讲点技术干货吧!

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容

  • 最近新了一个架构,之前用dagger2时候,每当添加新activity还要修改或者新建component来完成da...
    hello_word阅读 1,498评论 1 0
  • 从那时到现在明勋一直是洪镇洙的好朋友。明勋引导他进入读书的世界,帮助他结识了会一和河英。自洪镇洙开始读书后两人的友...
    休文刀阅读 136评论 0 2
  • 思妍家住新区,沿着河的两片都在陆续开发,河边绿化了一下,风景不错,但人烟稀少。思妍周末会沿着河边跑步,河边种了柳树...
    雅丝颜阅读 411评论 0 0
  • 没有星星的夜里,总是在回忆一个场景:如果我们处于这个世界,除了工作我们的人生还有什么意义。 问了几个朋友,但答案终...
    明轩在雨中阅读 203评论 1 0