[Android] 注解处理

需求:有一组功能模块,每个功能模块负责处理一种具体功能且有一个唯一的标识;这些功能模块随项目迭代会有动态的修改、增加或者删除。

  • 如果是你会如何设计实现这个需求?
    我可能会这样做:新建一个功能模块管理类,管理类中预加载所有的功能模块;提供一个方法,可以根据标识获取具体的功能模块;然后就可以调用功能模块的具体方法了。

  • 这样做有什么问题?
    每增加一个功能模块你可能至少需要改两个地方甚至更多:创建具体的功能模块类;在功能模块管理类中加入新增的功能模块(此处可能要改一个地方以上)。

  • 有没有更好的实现方式?
    答案当然是有,比如只新建一个功能模块类,其他工作自动完成。下面介绍如果通过编译时注解的方式解决这个问题。

0x01 从一个例子开始

考虑到原理理解的难易,这里先给出解决方案:创建具体的功能模块类,在这些类上添加自定义的注解,注解上标识这个类可以处理哪些具体的功能。在程序编译期根据这些注解自动生成一个功能模块管理类。使用时直接调用此功能管理类即可(此管理类和我们手动创建的一样)。若有新的功能模块加入(或者移除),我们只需要创建(或者删除)对应功能模块类即可,只改这一个地方。

下面以一个具体的例子说明这个问题。

有一个班级,包含若干个学生,每个学生有姓名和年龄,同时还有一个对应的职责,如班长,语文课代表,数学课代表,体育课代表等。班长负责管理班级,课代表负责收作业等。这个班级可能有同学会退学,也可能有新的同学加入。老师通过一个管理类来管理这个班级的所有学生。

创建项目

创建一个Android项目:DemoAnnotation,在DemoAnnotation中创建两个Java Library: lib_annotationlib_compiler,然后分别配置其build.gradle

➜  DemoAnnotation git:(master) ✗ tree -L 1
.
├── app
├── lib_annotation
└── lib_compiler

以下全部使用Java 8

DemoAnnotation

plugins {
    id 'com.android.application'
}
android {
    ...
    defaultConfig {
        applicationId "com.ttdevs.demo.annotation"
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                argument "debug", "true"
                argument "param1", "value1"
            }
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
dependencies {
    ...
    implementation project(path: ':lib_annotation')
    annotationProcessor project(path: ':lib_compiler')
}

引入两个library,注意一个是implementation,另一个是annotationProcessor。

lib_annotation

plugins {
    id 'java-library'
}
java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

lib_compiler

plugins {
    id 'java-library'
}
java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
    implementation project(path: ':lib_annotation')
    implementation 'com.squareup:javapoet:1.13.0'
    implementation 'com.google.auto.service:auto-service-annotations:1.0'
    annotationProcessor 'com.google.auto.service:auto-service:1.0'
}

通过implementation 'com.google.auto.service:auto-service-annotations:1.0'引入@AutoService(Processor.class)

lib_annotation中创建注解类

这里创建一个叫Student的注解,包含姓名,年龄,职责,如下:

package com.ttdevs.demo.lib.annotation;
...
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Student {
    String name();
    int age() default 8;
    /**
     * Duty information, ClassMonitor, Chinese, Math, Sport, Art etc.
     *
     * @return
     */
    String[] duty() default {};
}

lib_compiler中处理注解

创建StudentProcessor类,继承AbstractProcessor,其上添加注解@AutoService(Processor.class),代码如下:

package com.ttdevs.demo.lib.compiler;
...
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedOptions({StudentProcessor.OPTIONS_PARAM_DEBUG})
public class StudentProcessor extends AbstractProcessor {
    protected static final String OPTIONS_PARAM_DEBUG = "debug";

    private Filer mFiler;
    private Elements mElements; // source file
    private Map<String, Element> mClassMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        LogUtils.init(processingEnv.getMessager());
        mFiler = processingEnv.getFiler();
        mElements = processingEnv.getElementUtils();

        LogUtils.d("Init debug: " + processingEnv.getOptions().get(OPTIONS_PARAM_DEBUG));
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(ClassUtils.CLASS_STUDENT.getCanonicalName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        LogUtils.d(String.format("=========Process %s============", !roundEnv.processingOver() ? "start" : "  end"));

        for (Element item : roundEnv.getRootElements()) {
            LogUtils.d("Process Class: " + item.getSimpleName());
        }

        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(ClassUtils.CLASS_STUDENT);
        if (null != elements && !elements.isEmpty()) {
            for (Element element : elements) {
                if (element.getKind() == ElementKind.CLASS) {
                    Student student = element.getAnnotation(ClassUtils.CLASS_STUDENT);
                    mClassMap.put(student.name(), element);
                }
            }
            // Create StudentManager.java
            StudentManagerBuilder.create()
                    .filer(mFiler)
                    .build(mClassMap);
        }
        return true;
    }
}

不用太在意代码长度,只需关注关键点即可。下面对这个类做简要分析:

  1. @AutoService(Processor.class)

    这个注解会在META-INF/services/javax.annotation.processing.Processor这个文件中添加一行,内容为当前类的完整路径。若你有多个注解处理器类,则会每个注解处理器都会在这个文件中占一行。别问为什么要这样做,问就是javac规定的。

    DemoAnnotation/lib_compiler
    └── build
        └── classes
            └── java
                └── main
                    └── META-INF
                        └── services
                            └── javax.annotation.processing.Processor
    
    ➜  DemoAnnotation git:(master) ✗ cat lib_compiler/build/classes/java/main/META-INF/services/javax.annotation.processing.Processor
    com.ttdevs.demo.lib.compiler.StudentProcessor
    ➜  DemoAnnotation git:(master) ✗
    
  2. 重写几个重要方法

    • init(ProcessingEnvironment processingEnv)

      初始化的配置,一般包含MessagerElementsFilerMessager用于打印Log。注解处理器创建之后此方法只会被调用一次。

    • getSupportedSourceVersion()

      配置源码的版本,等同于@SupportedSourceVersion(SourceVersion.RELEASE_8)。只在创建之后调用一次。

    • getSupportedAnnotationTypes()

      处理的注解类型,这里只有一个Student注解。只在创建之后调用一次。

    • process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)

      具体的处理逻辑,包含注解的处理,最终管理类的生成。每一轮注解处理此方法都会被调用。所以此方法会被调用多次。

  3. 编译项目后会自动生成下面这个类

    DemoAnnotation/app/build/generated/ap_generated_sources
        └── debug
            └── out
                └── com
                    └── ttdevs
                        └── demo
                            └── annotation
                                └── StudentManager.java
    

    此文件可以在自己的代码中直接调用,最终会和其他源码共同参与编译。具体如何生成参见后续介绍。

app中添加数据

创建几个学生数据:

DemoAnnotation/app/src/main/java
    └── com
        └── ttdevs
            └── demo
                └── annotation
                    ├── MainActivity.java
                    └── model
                        ├── BaseStudent.java
                        ├── David.java
                        ├── Harry.java
                        ├── Jason.java
                        └── Norris.java

重新编译项目,下面看一下具体生产的StudentManager代码:

package com.ttdevs.demo.annotation;
...
public class StudentManager {
    private static final Map<String, BaseStudent> MAP_STUDENT_NAME = new HashMap<>();
    private static final Map<String, BaseStudent> MAP_STUDENT_DUTY = new HashMap<>();

    public static final StudentManager INSTANCE = new StudentManager();

    private StudentManager() {
        BaseStudent tempNorris = new BaseStudent();
        tempNorris.name = "Norris";
        tempNorris.age = 28;
        MAP_STUDENT_NAME.put("Norris", tempNorris);
        BaseStudent tempHarry = new BaseStudent();
        tempHarry.name = "Harry";
        tempHarry.age = 30;
        tempHarry.duty = new java.lang.String[]{"Math"};
        MAP_STUDENT_NAME.put("Harry", tempHarry);
        BaseStudent tempDavid = new BaseStudent();
        tempDavid.name = "David";
        tempDavid.age = 50;
        tempDavid.duty = new java.lang.String[]{"ClassMonitor"};
        MAP_STUDENT_NAME.put("David", tempDavid);
        BaseStudent tempJason = new BaseStudent();
        tempJason.name = "Jason";
        tempJason.age = 20;
        tempJason.duty = new java.lang.String[]{"Chinese", "Sport"};
        MAP_STUDENT_NAME.put("Jason", tempJason);
        ;
        MAP_STUDENT_DUTY.put("Math", MAP_STUDENT_NAME.get("Harry"));
        MAP_STUDENT_DUTY.put("ClassMonitor", MAP_STUDENT_NAME.get("David"));
        MAP_STUDENT_DUTY.put("Chinese", MAP_STUDENT_NAME.get("Jason"));
        MAP_STUDENT_DUTY.put("Sport", MAP_STUDENT_NAME.get("Jason"));
    }

    /**
     * Get student by duty
     *
     * @param duty
     * @return
     */
    public BaseStudent getStudent(String duty) {
        return MAP_STUDENT_DUTY.get(duty);
    }

    public int exam() {
        int result = 0;
        for (String key : MAP_STUDENT_NAME.keySet()) {
            BaseStudent student = MAP_STUDENT_NAME.get(key);
            result += student.exam();
        }
        return result / MAP_STUDENT_NAME.size();
    }

    public void study() {
        for (String key : MAP_STUDENT_NAME.keySet()) {
            BaseStudent student = MAP_STUDENT_NAME.get(key);
            student.study();
        }
    }

    public void work() {
        for (String key : MAP_STUDENT_NAME.keySet()) {
            BaseStudent student = MAP_STUDENT_NAME.get(key);
            student.work();
        }
    }
}

构造方法中,我们创建了一个以学生姓名为Key的Map,一个学生职责为Key的Map。我们可以通过职责或者姓名查找到对应的学生,然后执行他的方法。也可以对全班同学进行操作,如考试等。

完整的代码参考这里,根据这个例子,班级中若有学生加入或者离开,我们只需删除或者添加对应的学生类重新编译即可。

0x02 annotationProcessor

APT是什么?javac、apt、android-apt和annotationProcessor这几个又是什么关系?

APT和javac

  • APT:Annotation Processing Tool

    APT是Sun(没错,不是Oracle)在JDK1.5版本提供的处理源码级别注解的工具(注解也是在JDK1.5版本引入的)。作用是根据源码中的注解生成新的文件,这里主要还是java文件。不过在JDK1.6就无情的被javac取代了。

  • annotationProcessor和android-apt

    二者是相同的东西,android-apt为个人开发者开发的,gradle2.2之前的版本被广泛使用。gradle2.2版本,google官方出了annotationProcessor,android-apt也随之退出历史舞台。我的理解:annotationProcessor是一个将我们的写的注解相关代码(注解,注解处理器等)打包传给javac处理的工具,最终注解的处理还是由javac来完成。

  • javac

    javac不仅负责java的编译工作,同时还负责处理java源码中的编译期注解。引用一段关于javac的说明:

The javac command provides direct support for annotation processing, superseding the need for the separate annotation processing command, apt.
简单翻译:javac提供了对注解处理的直接支持,从而取代了对单独处理注解命令apt的需求。

javac对注解的处理流程

  • 首先javac扫描所有源文件,确定有哪些类中包含注解;
  • 然后javac查询注解处理器确定他们处理的注解,查找路径为META-INF/services/javax.annotation.processing.Processor,此文件记录了用户的所有注解处理器,每行一个(用户在自己的注解处理器中可声明所处理的具体注解,若你不声明则不会调用这个注解处理器的process方法);
  • 根据注解处理器声明的所处理的注解,将相应的注解分配给对应的注解处理类处理;
  • 若注解处理类产生了新的源文件,则重复上述动作,直到产生的新文件无注解为止;
  • 至此,注解处理流程结束,javac转去处理其他工作。

更详细准确的介绍参见这里:https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/Processor.html

annotationProcessor

第一部分的例子已经详细介绍如何使用annotationProcessor,下面介绍一下原理。

com.google.auto.service:auto-service-annotations:1.0

这个Library仅仅包含了AutoService这个注解,源码如下:

package com.google.auto.service;
@Documented
@Retention(CLASS)
@Target(TYPE)
public @interface AutoService {
  /** Returns the interfaces implemented by this service provider. */
  Class<?>[] value();
}

通过注释,我们可以得知,使用时必须注意下面几点:

  • 必须用在非内部,非匿名,具体的类上
  • 这个类必须包含一个public无参的构造函数
  • 实现values()返回的接口类型

com.google.auto.service:auto-service:1.0

➜  auto-service-1.0-sources tree
.
├── META-INF
│   ├── MANIFEST.MF
│   ├── gradle
│   │   └── incremental.annotation.processors
│   └── services
│       └── javax.annotation.processing.Processor
└── com
    └── google
        └── auto
            └── service
                └── processor
                    ├── AutoServiceProcessor.java
                    ├── ServicesFiles.java
                    └── package-info.java

jar包中主要包含两部分

  • META-INF/services/javax.annotation.processing.Processor

    其内容仅有一行,如下:

    com.google.auto.service.processor.AutoServiceProcessor
    
  • AutoServiceProcessor

    public class AutoServiceProcessor extends AbstractProcessor {
      ...
      @Override
      public ImmutableSet<String> getSupportedAnnotationTypes() {
        return ImmutableSet.of(AutoService.class.getName());
      }
      ...
      @Override
      public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {...}
    }
    

以上信息可以得知,AutoServiceProcessor是处理AutoService的注解处理器。通过源码可以得知其主要功能是帮我们在META-INF/services/javax.annotation.processing.Processor中配置自定义的注解处理器。

另外,AutoService.javacom.google.auto.service:auto-service:1.0这个库中定义。其内容仅仅包含AutoService.java这个注解的定义。大家思考一下为什么就这一个类不和com.google.auto.service:auto-service:1.0定义在一起?请自寻答案。

工作流程

用户创建自定义注解,同时创建处理这个注解的注解处理器,在注解处理器中使用@AutoService注解,javac检测到这个注解丢给AutoServiceProcessor处理,AutoServiceProcessor自动帮我们把自定义的注解处理器配置到META-INF/services/javax.annotation.processing.Processor(当然你也可以不用@AutoService注解自己手动配置)。

以上可知,annotationProcessor仅仅告诉javac这个java library内有注解需要处理。

0x03 JavaPoet

生成Java文件,待续。

0x04 Debug

Log

StudentProcessorinit(ProcessingEnvironment processingEnv)被调用的时候,我们可以获取一个Messager对象,通过这个对象我们可以向编译控制台输出我们的调试信息,如下:

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class StudentProcessor extends AbstractProcessor {
    private Messager messager;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        messager = processingEnv.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE, String.format("=========Process %s============",
                !roundEnv.processingOver() ? "start" : "  end"));

        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(ClassUtils.CLASS_STUDENT);
        if (null == elements || elements.isEmpty()) {
            return true;
        }
        for (Element element : elements) {
            Student student = element.getAnnotation(ClassUtils.CLASS_STUDENT);
            messager.printMessage(Diagnostic.Kind.ERROR, student.name()));
        }
        return true;
    }
}

实际使用我们可以把Messager封装到一个工具类,具体可参见DemoAndroid

Debug

除了打Log,我们也可以对相关代码进行远程调试,操作如下:

  1. Run/Debug Configuration > Edit Configurations...

    Debug_01_Configuration.png
  2. + > Remote:

    • Input Configuration Name: Your Config Name
    • Copy Command line arguments for remote JVM: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
    • Click OK
Debug_02_Add_Configuration.png
  1. Opengradle.properties, add line in the end:
    org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
Debug_03_Edit_Gradle_Properties.png
  1. Add breakpoints in your Processor files and click Debug button
Debug_04_Add_Breakpoints_Debug.png
  1. Rebuild your project, start debug

Congratulations!

0xFF Reference

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

推荐阅读更多精彩内容