需求:有一組功能模塊,每個功能模塊負責處理一種具體功能且有一個唯一的標識;這些功能模塊隨項目迭代會有動態(tài)的修改、增加或者刪除。
如果是你會如何設計實現(xiàn)這個需求?
我可能會這樣做:新建一個功能模塊管理類,管理類中預加載所有的功能模塊;提供一個方法,可以根據(jù)標識獲取具體的功能模塊;然后就可以調(diào)用功能模塊的具體方法了。這樣做有什么問題?
每增加一個功能模塊你可能至少需要改兩個地方甚至更多:創(chuàng)建具體的功能模塊類;在功能模塊管理類中加入新增的功能模塊(此處可能要改一個地方以上)。有沒有更好的實現(xiàn)方式?
答案當然是有,比如只新建一個功能模塊類,其他工作自動完成。下面介紹如果通過編譯時注解的方式解決這個問題。
0x01 從一個例子開始
考慮到原理理解的難易,這里先給出解決方案:創(chuàng)建具體的功能模塊類,在這些類上添加自定義的注解,注解上標識這個類可以處理哪些具體的功能。在程序編譯期根據(jù)這些注解自動生成一個功能模塊管理類。使用時直接調(diào)用此功能管理類即可(此管理類和我們手動創(chuàng)建的一樣)。若有新的功能模塊加入(或者移除),我們只需要創(chuàng)建(或者刪除)對應功能模塊類即可,只改這一個地方。
下面以一個具體的例子說明這個問題。
有一個班級,包含若干個學生,每個學生有姓名和年齡,同時還有一個對應的職責,如班長,語文課代表,數(shù)學課代表,體育課代表等。班長負責管理班級,課代表負責收作業(yè)等。這個班級可能有同學會退學,也可能有新的同學加入。老師通過一個管理類來管理這個班級的所有學生。
創(chuàng)建項目
創(chuàng)建一個Android項目:DemoAnnotation,在DemoAnnotation中創(chuàng)建兩個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中創(chuàng)建注解類
這里創(chuàng)建一個叫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中處理注解
創(chuàng)建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這個文件中添加一行,內(nèi)容為當前類的完整路徑。若你有多個注解處理器類,則會每個注解處理器都會在這個文件中占一行。別問為什么要這樣做,問就是javac規(guī)定的。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。注解處理器創(chuàng)建之后此方法只會被調(diào)用一次。 -
getSupportedSourceVersion()配置源碼的版本,等同于
@SupportedSourceVersion(SourceVersion.RELEASE_8)。只在創(chuàng)建之后調(diào)用一次。 -
getSupportedAnnotationTypes()處理的注解類型,這里只有一個
Student注解。只在創(chuàng)建之后調(diào)用一次。 -
process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)具體的處理邏輯,包含注解的處理,最終管理類的生成。每一輪注解處理此方法都會被調(diào)用。所以此方法會被調(diào)用多次。
-
-
編譯項目后會自動生成下面這個類
DemoAnnotation/app/build/generated/ap_generated_sources └── debug └── out └── com └── ttdevs └── demo └── annotation └── StudentManager.java此文件可以在自己的代碼中直接調(diào)用,最終會和其他源碼共同參與編譯。具體如何生成參見后續(xù)介紹。
app中添加數(shù)據(jù)
創(chuàng)建幾個學生數(shù)據(jù):
DemoAnnotation/app/src/main/java
└── com
└── ttdevs
└── demo
└── annotation
├── MainActivity.java
└── model
├── BaseStudent.java
├── David.java
├── Harry.java
├── Jason.java
└── Norris.java
重新編譯項目,下面看一下具體生產(chǎn)的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();
}
}
}
構(gòu)造方法中,我們創(chuàng)建了一個以學生姓名為Key的Map,一個學生職責為Key的Map。我們可以通過職責或者姓名查找到對應的學生,然后執(zhí)行他的方法。也可以對全班同學進行操作,如考試等。
完整的代碼參考這里,根據(jù)這個例子,班級中若有學生加入或者離開,我們只需刪除或者添加對應的學生類重新編譯即可。
0x02 annotationProcessor
APT是什么?javac、apt、android-apt和annotationProcessor這幾個又是什么關系?
APT和javac
-
APT:Annotation Processing Tool
APT是Sun(沒錯,不是Oracle)在JDK1.5版本提供的處理源碼級別注解的工具(注解也是在JDK1.5版本引入的)。作用是根據(jù)源碼中的注解生成新的文件,這里主要還是java文件。不過在JDK1.6就無情的被javac取代了。
-
annotationProcessor和android-apt
二者是相同的東西,android-apt為個人開發(fā)者開發(fā)的,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,此文件記錄了用戶的所有注解處理器,每行一個(用戶在自己的注解處理器中可聲明所處理的具體注解,若你不聲明則不會調(diào)用這個注解處理器的process方法); - 根據(jù)注解處理器聲明的所處理的注解,將相應的注解分配給對應的注解處理類處理;
- 若注解處理類產(chǎn)生了新的源文件,則重復上述動作,直到產(chǎn)生的新文件無注解為止;
- 至此,注解處理流程結(jié)束,javac轉(zhuǎn)去處理其他工作。
更詳細準確的介紹參見這里:https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/Processor.html
annotationProcessor
第一部分的例子已經(jīng)詳細介紹如何使用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();
}
通過注釋,我們可以得知,使用時必須注意下面幾點:
- 必須用在非內(nèi)部,非匿名,具體的類上
- 這個類必須包含一個public無參的構(gòu)造函數(shù)
- 實現(xiàn)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
其內(nèi)容僅有一行,如下:
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這個庫中定義。其內(nèi)容僅僅包含AutoService.java這個注解的定義。大家思考一下為什么就這一個類不和com.google.auto.service:auto-service:1.0定義在一起?請自尋答案。
工作流程
用戶創(chuàng)建自定義注解,同時創(chuàng)建處理這個注解的注解處理器,在注解處理器中使用@AutoService注解,javac檢測到這個注解丟給AutoServiceProcessor處理,AutoServiceProcessor自動幫我們把自定義的注解處理器配置到META-INF/services/javax.annotation.processing.Processor(當然你也可以不用@AutoService注解自己手動配置)。
以上可知,annotationProcessor僅僅告訴javac這個java library內(nèi)有注解需要處理。
0x03 JavaPoet
生成Java文件,待續(xù)。
0x04 Debug
Log
StudentProcessor的init(ProcessingEnvironment processingEnv)被調(diào)用的時候,我們可以獲取一個Messager對象,通過這個對象我們可以向編譯控制臺輸出我們的調(diào)試信息,如下:
@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,我們也可以對相關代碼進行遠程調(diào)試,操作如下:
-
Run/Debug Configuration>Edit Configurations...Debug_01_Configuration.png -
+>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
Debugbutton

- Rebuild your project, start debug
Congratulations!
