需求:有一组功能模块,每个功能模块负责处理一种具体功能且有一个唯一的标识;这些功能模块随项目迭代会有动态的修改、增加或者删除。
如果是你会如何设计实现这个需求?
我可能会这样做:新建一个功能模块管理类,管理类中预加载所有的功能模块;提供一个方法,可以根据标识获取具体的功能模块;然后就可以调用功能模块的具体方法了。这样做有什么问题?
每增加一个功能模块你可能至少需要改两个地方甚至更多:创建具体的功能模块类;在功能模块管理类中加入新增的功能模块(此处可能要改一个地方以上)。有没有更好的实现方式?
答案当然是有,比如只新建一个功能模块类,其他工作自动完成。下面介绍如果通过编译时注解
的方式解决这个问题。
0x01 从一个例子开始
考虑到原理理解的难易,这里先给出解决方案:创建具体的功能模块类,在这些类上添加自定义的注解,注解上标识这个类可以处理哪些具体的功能。在程序编译期根据这些注解自动生成一个功能模块管理类。使用时直接调用此功能管理类即可(此管理类和我们手动创建的一样)。若有新的功能模块加入(或者移除),我们只需要创建(或者删除)对应功能模块类即可,只改这一个地方。
下面以一个具体的例子说明这个问题。
有一个班级,包含若干个学生,每个学生有姓名和年龄,同时还有一个对应的职责,如班长,语文课代表,数学课代表,体育课代表等。班长负责管理班级,课代表负责收作业等。这个班级可能有同学会退学,也可能有新的同学加入。老师通过一个管理类来管理这个班级的所有学生。
创建项目
创建一个Android项目:DemoAnnotation
,在DemoAnnotation
中创建两个Java Library
: lib_annotation
和 lib_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;
}
}
不用太在意代码长度,只需关注关键点即可。下面对这个类做简要分析:
-
@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) ✗
-
重写几个重要方法
-
init(ProcessingEnvironment processingEnv)
初始化的配置,一般包含
Messager
,Elements
和Filer
。Messager
用于打印Log。注解处理器创建之后此方法只会被调用一次。 -
getSupportedSourceVersion()
配置源码的版本,等同于
@SupportedSourceVersion(SourceVersion.RELEASE_8)
。只在创建之后调用一次。 -
getSupportedAnnotationTypes()
处理的注解类型,这里只有一个
Student
注解。只在创建之后调用一次。 -
process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
具体的处理逻辑,包含注解的处理,最终管理类的生成。每一轮注解处理此方法都会被调用。所以此方法会被调用多次。
-
-
编译项目后会自动生成下面这个类
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.java
在com.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
StudentProcessor
的init(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,我们也可以对相关代码进行远程调试,操作如下:
-
Run/Debug Configuration
>Edit Configurations...
-
+
>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
- Input Configuration Name:
- Open
gradle.properties
, add line in the end:
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
- Add breakpoints in your Processor files and click
Debug
button
- Rebuild your project, start debug
Congratulations!