AbstractProcessor注解处理器

欢迎大家加入QQ群一起讨论: 489873144(android格调小窝)
我的github地址:https://github.com/jeasonlzy

1. 注解

一般我们定义一个注解的代码如下:

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Json {
  String value();
}

在注解上面的注解叫做元注解,元注解共有四种:

  • @Retention: 保留的范围,默认值为CLASS. 可选值有三种
    SOURCE,只在源码中可用
    CLASS,在源码和字节码中可用
    RUNTIME,在源码,字节码,运行时均可用
  • @Target,可以用来修饰哪些程序元素,有以下几种,未标注则表示可修饰所有:
    TYPE,定义在类上
    METHOD,方法上
    CONSTRUCTOR,构造方法上
    FIELD,成员变量上
    PARAMETER,方法参数上
  • @Inherited,是否可以被继承,默认为false
  • @Documented,是否会保存到 Javadoc 文档中

其中, @Retention定义的保留策略, 直接决定了我们用何种方式解析,SOUCE级别的注解是用来标记的, 比如Override, SuppressWarnings. 我们真正使用的类型是CLASS(编译时)和RUNTIME(运行时)。运行时一般采用反射获取,不在本文的范围,后面主要讲解编译时注解。

注解的属性定义方法如下:

类型 参数名() default 默认值;

其中默认值是可选的,可以定义, 也可以不定义。

2. AbstractProcessor

注意:

我们在新建一个编译注解的demo时,我们需要用到javax包下的内容,而android的普通model和library工程师没有的,所以我们不能新建这两个工程,而是需要新建java工程

我们需要创建两个java工程,如下:

为什么需要两个?因为Processor这个类是帮助我们生成需要的java源文件的,我们最后需要的是他生成的文件,最后打包进apk也是他生成的文件,Processor本身是不需要打包的,所以我们把需要打包的放进processor-lib中,不需要打包的放进processor中。

我们在项目的ap model中需要依赖这两个项目,依赖方法如下app/build.gradle,Processor相关的使用annotationProcessor去依赖,这样不会被打包进apk,需要打包的使用compile依赖。

dependencies {
    compile project(':processor-lib')
    annotationProcessor project(':processor')
}

对应注解的处理器需要继承AbstractProcessor类,需要复写以下4个方法:

  1. init方法,会被注解处理工具调用

  2. process方法,这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。

  3. getSupportedSourceVersion方法,指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6
  4. getSupportedAnnotationTypes方法,这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称,即注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合

现在对AbstractProcessor有了一定的了解后,知道了编译时注解主要使用在process方法中,在process中我们通过得到传递过来的数据,写入代码,这里先采用打印的方式,简单输出信息,后续会详细讲解如何生成java代码,我们编写一个类如下:

public class JsonProcessor extends AbstractProcessor {

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> set = new HashSet<>();
        set.add(Json.class.getCanonicalName());
        return set;
    }
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        // 准备在gradle的控制台打印信息
        Messager messager = processingEnv.getMessager();
        messager.printMessage(Diagnostic.Kind.NOTE, "start: --------------");

        // 打印注解
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Json.class);
        for (Element element : elements) {
            String name = element.getSimpleName().toString();
            String value = element.getAnnotation(Json.class).value();
            messager.printMessage(Diagnostic.Kind.NOTE, name + " --> " + value);
        }
        //该方法返回ture表示该注解已经被处理, 后续不会再有其他处理器处理; 返回false表示仍可被其他处理器处理.
        return true;
    }
}

这里特别说明一下,对于getSupportedAnnotationTypesgetSupportedSourceVersion这两个方法,也可以使用注解标识,如下所示,如果两个地方都写,以该方法的结果为主

到这里Json注解对应的注解处理器已经编写完成,准备工作完成以后,我们需要触发调用,在App Model中创建一个类如下:

@Json("TestModel")
public class TestModel {

    @Json("name") public String name;
    @Json("age") public String age;
    @Json("city") public String city;

    public String address;
}

我们就很简单的给类和几个字段加上了注解,这时如果直接编译或者运行工程的话,是看不到任何输出信息的,这里还要做的一步操作是指定注解处理器的所在,需要做如下操作:

  1. 在 processors 库的 main 目录下新建 resources 资源文件夹;
  2. 在 resources文件夹下建立 META-INF/services 目录文件夹;
  3. 在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
  4. 在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;

现在我们再次编译运行,点击这个小锤子按钮,就可以在gradle控制台可以看到如下输出的log,证明我们的注解处理器成功运行了,并且我们能验证上面的结论,有几个注解,process方法就会执行几次。

现在我们发现要配置上面的4步还是很麻烦的,我们可以使用一个开源的框架,在processor/build.gradle中添加以下依赖,同时删除上面4步的操作,我们同样可以得到相同的结果。

compile 'com.google.auto.service:auto-service:1.0-rc2'

然后在我们自己的processor上增加一个注解如下:

然后重写Processor的以下两个方法,就可以了。

3. Filer与JavaPoet

现在我们得到了需要的数据,下一步当然是将数据写入到java文件中,如何才能写入,这里需要借助Filer类。首先在AbstractProcessorinit方法中初始Filer,如下:

private Filer filer;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    filer = processingEnv.getFiler();
}

我们都知道java文件是具有一定格式的代码组成的,我们完全靠自己去组织这样的代码是比较麻烦的,所以这推荐一个开源框架,当然是square家的javapoet,这是一个非常强大的工具,开源地址点击这里:square/javapoet,我们看看他的文档介绍:

JavaPoet is a Java API for generating .java source files.
Source file generation can be useful when doing things such as annotation processing or interacting with metadata files (e.g., database schemas, protocol formats). By generating code, you eliminate the need to write boilerplate while also keeping a single source of truth for the metadata.

意思是JavaPoet是一个java API,用来帮助你生成java源文件,特别是在用注解处理和数据库处理,schema协议处理等方面意义重大。比如我们常使用的的以下几个框架:

都是使用这个框架帮助生成java源文件。

我们依然继续在processor/build.gradle中添加依赖:

compile 'com.squareup:javapoet:1.8.0'

以下是一个生成HelloWorld的例子,JavaPoet使用的代码如下:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    for (Element element : roundEnv.getElementsAnnotatedWith(Hello.class)) {

        if (!(element.getKind() == ElementKind.CLASS)) return false;

        // 在gradle的控制台打印信息
        messager.printMessage(Diagnostic.Kind.NOTE, "className: " + element.getSimpleName().toString());

        // 创建main方法
        MethodSpec main = MethodSpec.methodBuilder("main")//
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)//
                .returns(void.class)//
                .addParameter(String[].class, "args")//
                .addStatement("$T.out.println($S)", System.class, "自动创建的")//
                .build();

        // 创建HelloWorld类
        TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")//
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)//
                .addMethod(main)//
                .build();

        String packageName = processingEnv.getElementUtils().getPackageOf(element).getQualifiedName().toString();
        try {
            JavaFile javaFile = JavaFile.builder(packageName, helloWorld)//
                    .addFileComment(" This codes are generated automatically. Do not modify!")//
                    .build();
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return true;
}

编译后,我们会在如图所示的位置发现自动生成的代码文件,更多详细的用法参看文档介绍。

4. AnnotationProcessor与android-apt

到这里,一个最简单的编译时注解就搞定了,但是编译时注解的自动写入也会导致代码混乱,可能在多次build编译过程中出现文件冲突的情况,这就需要一个能在编译时期去依赖注解处理器并进行工作,但在生成 APK 时不会包含任何遗留无用的文件,辅助 Android Studio项目在对应目录中存放注解处理器在编译期间生成的文件。

那么这个东西是什么呢?

在Android Gradle 插件 2.2 版本的发布前,使用的是 android-apt 插件,但是发布后,该 作者在官网发表声明证实了后续将不会继续维护 android-apt,并推荐大家使用 Android 官方插件提供的相同能力。也就是说,大约三年前推出的 android-apt 即将告别开发者,退出历史舞台,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt。

我们还是先说说以前android-apt的依赖方式,使用方法如下:

  1. 在项目根目录的build.gradle中添加依赖
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  1. 在各个modelbuild.gradle中添加依赖
apply plugin: 'com.neenbedankt.android-apt'
  1. dependencies节点下添加以下依赖
apt project(':processor')

谷歌官方推荐的使用方式
以上1,2步全部不需要,直接将第三步的apt改为annotationProcessor即可,就是我们再第二节讲的那个依赖,再发一遍:

dependencies {
    compile project(':processor-lib')
    annotationProcessor project(':processor')
}

这样就完成了整个注解框架的配置和生成。

5. debug调试

我们做了这么多,知道这里代码的生成是在编译时期执行的,那么一般的debug调试断点肯定是不会进入AbstractProcessor中的,那我们需要如何调试呢?

其实很简单,只需要以下几步:

  1. 在项目根目录下的gradle.properties中添加如下两行配置,注意红色箭头的配置要注释起来,address表示端口,默认5005,可以根据自己需要修改。
org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
  1. 我们打开运行配置,添加一个远程调试如下, 其中name可以任意取,port端口号就是上面一步指定的端口号,对应好就行
  2. 切换到第二步刚刚创建的processor,然后点击debug按钮
  3. 最后,我们在我们需要调试的地方打上断点,然后再次点击编译按钮(小锤子按钮),即可进入断点,如下所示:

总结

到此简单的编译时注解就搞定了,运用掌握的编译时注解可以替换掉反射提高效率,可以动态生成我们需要的代码,使用注解解耦各个模块,使用熟悉后还是很方便的。

如果你觉得好,对你有过帮助,请给我一点打赏鼓励吧,一分也是爱呀!

  • 7
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值