Android 自定义注解之编译时注解(RetenttionPolicy.CLASS)-阿里云开发者社区

开发者社区> 开发与运维> 正文

Android 自定义注解之编译时注解(RetenttionPolicy.CLASS)

简介: 注解处理器(Annotation Processor) 注解处理器是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以自定义注解,并注册到相应的注解处理器,由注解处理器来处理你的注解。

注解处理器(Annotation Processor)

注解处理器是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以自定义注解,并注册到相应的注解处理器,由注解处理器来处理你的注解。一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这些生成的Java代码是在生成的.java文件中,所以你不能修改已经存在的Java类,例如向已有的类中添加方法。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译。

RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;

1. 新建两个Module

File->New->New Module...->Java Library,填写好Library name和Java class name后点击完成。
注意,这里必须为Java库,不然会找不到javax包下的相关资源。


img_e74f5f4cc63e5899e03c5ce81143cb1f.png
图1.png

其中annations是存放注解的,processors是存放注解处理器的。

2. 新建编译时注解

/**
 * 编译时注解
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface CustomAnnation {
    String value();
}

3. 定义注解处理器

定义一个注解处理器 CustomProcessor ,每一个处理器都是继承于AbstractProcessor,并要求必须复写 process() 方法,通常我们使用复写以下4个方法:

/**
 * 每一个注解处理器类都必须有一个空的构造函数,默认不写就行
 */
public class CustomProcessor extends AbstractProcessor{
    /**
     * init()方法会被注解处理器工具调用,并输入ProcessingEnvironment参数。
     * ProcessingEnvironment 提供很多有用的工具类Elements,Types和Filter
     * @param processingEnvironment 提供给process用来访问工具框架的环境
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    /**
     * 相当于每个处理器的主函数main(),在这里写扫描、评估和处理注解的代码,以及生成java文件。
     * 输入参数RoundEnvironment可以查询出包含特定注解的被注解元素
     * @param set 请求处理注解类型
     * @param roundEnvironment 有关当前和以前的信息环境
     * @return 返回true,则这些注解已声明并且不要求后续Processor处理他们;
     *          返回false,则这些注解未声明并且可能要求后续Processor处理他们;
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }

    /**
     * 这里必须指定,这个注解处理器是注册给那个注解的。
     * 注意:它的返回值是一个字符串的集合,包含本处理器想要处理注解的注解类型的合法全程。
     * @return 注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合。
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(CustomAnnation.class.getCanonicalName());
        return annotations;
    }

    /**
     * 指定Java版本,通常这里使用SourceVersion.latestSupported(),
     * 默认返回SourceVersion.RELEASE_6
     * @return 使用的Java版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

也可以使用注解的方式来指定Java版本和注解类型


img_d4dee54ae1c80c80d646e48268921535.png
图2.png

4. 添加注解的处理

    /**
     * 相当于每个处理器的主函数main(),在这里写扫描、评估和处理注解的代码,以及生成java文件。
     * 输入参数RoundEnvironment可以查询出包含特定注解的被注解元素
     * @param set 请求处理注解类型
     * @param roundEnvironment 有关当前和以前的信息环境
     * @return 返回true,则这些注解已声明并且不要求后续Processor处理他们;
     *          返回false,则这些注解未声明并且可能要求后续Processor处理他们;
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // roundEnvironment.getElementsAnnotatedWith(CustomAnnotation.class)返回使用给定的注解类型的元素
        for (Element element : roundEnvironment.getElementsAnnotatedWith(CustomAnnotation.class)) {
            System.out.println("----------------------------------");
            // 判断元素的类型为Class
            if (element.getKind() == ElementKind.CLASS) {
                // 显示转换元素类型
                TypeElement typeElement = (TypeElement) element;
                // 输出元素名称
                System.out.println(typeElement.getSimpleName());
                // 输出注解属性值
                System.out.println(typeElement.getAnnotation(CustomAnnotation.class).value());
            }
            System.out.println("----------------------------------");
        }
        return false;
    }

5. 注解处理器配置

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

项目结构:


img_427483ca3597de3197e343d4e9234eab.png
图3.png

6. 使用

主Module依赖annotations和processors两个moudle,然后编译,如果未打印出结果,则Build->ReBuild或者Build->Clean Project->Make Project.

@CustomAnnotation("Hello World")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

7. 打印结果

img_535fd96ae09b94ca21e3fde6423a3c53.png
图4.png

8. 存在的问题

我们的主项目中引用了 processors 库,但注解处理器只在编译处理期间需要用到,编译处理完后就没有实际作用了,而主项目添加了这个库会引入很多不必要的文件,为了处理这个问题我们需要引入个插件android-apt,它能很好地处理这个问题。

9. AutoService

AutoService注解处理器是Google开发的,用来生成 META-INF/services/javax.annotation.processing.Processor 文件的,你只需要在你定义的注解处理器上添加 @AutoService(Processor.class) 就可以了,简直不能再方便了。

  • 依赖AutoService库,在processors的build.gradle文件中添加一下依赖,并同步工程。
    compile 'com.google.auto.service:auto-service:1.0-rc2'
  • 使用


    img_b26d533fd3af0fca7b6d0fea55499daa.png
    图5.png

10. 问题

这时重新Make下工程也能看到同样的输出信息了。但是如果你编译生成APK时,可能会出现文件重复的问题。解决办法是在主项目的 build.gradle 加上这么一段:

  packagingOptions {  
     exclude 'META-INF/services/javax.annotation.processing.Processor'  
  }  

这样就不会报错了,这是其中的一个解决方法,还有个更好的解决方法就是用上面提到的android-apt

11. Android-apt

官网有这么一段描述:

The android-apt plugin assists in working with annotation processors in combination with Android Studio. It has two purposes:
1、Allow to configure a compile time only annotation processor as a dependency, not including the artifact in the final APK or library
2、Set up the source paths so that code that is generated from the annotation processor is correctly picked up by Android Studio

大体来讲它有两个作用:
能在编译时期去依赖注解处理器并进行工作,但在生成 APK 时不会包含任何遗留的东西
能够辅助 Android Studio 在项目的对应目录中存放注解处理器在编译期间生成的文件

12. 使用

// 主工程的build.gradle
buildscript {
    repositories {
      mavenCentral()
    }
    dependencies {
        // replace with the current version of the Android plugin
        classpath 'com.android.tools.build:gradle:1.3.0'
        // the latest version of the android-apt plugin
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}
// 主Module的build.gradle
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'


dependencies {
// ...
    compile project(':annotations')
    apt project(':processors')
//    compile project(':processors')
//    compile project(':processors1')
}

13. JavaPoet

在使用编译时注解时,需要在编译期间对注解进行处理,在这里我们没办法影响程序的运行逻辑,但我们可以进行一些需处理,比如生成一些功能性代码来辅助程序的开发,最常见的是生成.java 源文件,并在程序中可以调用到生成的文件。这样我们就可以用注解来帮助我们处理一些固定逻辑的重复性代码(如butterknife),提高开发的效率。

通过注解处理器来生成 .java 源文件基本上都会使用javapoet 这个库,JavaPoet一个是用于产生 .java 源文件的辅助库,它可以很方便地帮助我们生成需要的.java 源文件,下面来看下具体使用方法。
使用如下代码进行依赖:

    compile "com.squareup:javapoet:1.9.0"

14. 注解生成器

/**
 * 每一个注解处理器类都必须有一个空的构造函数,默认不写就行
 */
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.mazaiting.CustomAnnotation")
public class CustomProcessor extends AbstractProcessor{

    private Filer mFiler;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        // Filter是个接口,至此吃通过注解处理器创建新文件
        mFiler = processingEnvironment.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        for (Element element : roundEnvironment.getElementsAnnotatedWith(CustomAnnotation.class)) {
            // 判断元素的类型为Class
            if (element.getKind() == ElementKind.CLASS) {
                // 显示转换元素类型
                TypeElement typeElement = (TypeElement) element;
                // 输出元素名称
//                System.out.println(typeElement.getSimpleName());
//                // 输出注解属性值
//                System.out.println(typeElement.getAnnotation(CustomAnnotation.class).value());
                // 创建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, "Hello, JavaPoet!")
                                .build();
                String value = element.getAnnotation(CustomAnnotation.class).value();
                String first = value.substring(0, 1);
                String className = first.toUpperCase() + value.substring(1, value.length());
                TypeSpec valueClass =
                        TypeSpec.classBuilder(className)
                                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                                .addMethod(main)
                                .build();
                // 生成文件
                try {
                    // 生成com.mazaiting.xxx.java
                    JavaFile javaFile =
                            JavaFile.builder("com.mazaiting.example", valueClass)
                                    .addFileComment("This codes are generated automatically. Do not modify!")
                                    .build();
                    javaFile.writeTo(mFiler);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return false;
    }
}

15. MainActivity使用

@CustomAnnotation("HelloWorld")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

16. 生成Java类

生成的Java类结构


img_472ca3e07cb9e8d29237cdc35cd7b7cd.png
图6.png

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章