[TOC]
zero bind library是一個仿ButterKnife的編譯期注解框架的練習(xí),旨在熟悉編譯期注解和注解處理器的工作原理以及相關(guān)的API鹦付。當(dāng)前基本都使用Android Studio進行android開發(fā)变骡,因此這個練習(xí)也基于AS開發(fā)環(huán)境(AS3.0, gradle-4.1-all, com.android.tools.build:gradle:3.0.0)。練習(xí)中大量參考了ButterKnife的源碼,這些代碼基本都源于ButterKnife倍啥,甚至目錄結(jié)構(gòu)和gradle的一些配置和編寫風(fēng)格,注釋未及之處參考JakeWharton/butterknife 澎埠。筆者水平有限虽缕,錯誤在所難免,歡迎批評指正蒲稳。
關(guān)于Processor
為了能更好的了解注解處理器在處理注解時進行了那些操作氮趋,代碼調(diào)試的功能似乎是必不可少的,然而注解處理器是在javac之前執(zhí)行江耀,所以直接在處理器中打斷點然后運行是調(diào)試不到注解處理器的剩胁。可以搜索相關(guān)的文章了解祥国,比如這個如何調(diào)試編譯時注解處理器AnnotationProcessor 昵观,鑒于調(diào)試的麻煩,剛開始了解Processor可以使用類似于打印日志的方式舌稀,這里需要注意的是System.out.println()
無法在控制臺打印日志啊犬,因此首先搭建一個具有日志輸出功能的Processor。以下給出一個LoggerProcessor
:
package zero.annotation.processor;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.tools.Diagnostic;
public abstract class LoggerProcessor extends AbstractProcessor {
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
}
protected void error(Element element, String message, Object... args) {
printMessage(Diagnostic.Kind.ERROR, element, message, args);
}
protected void note(Element element, String message, Object... args) {
printMessage(Diagnostic.Kind.NOTE, element, message, args);
}
private void printMessage(Diagnostic.Kind kind, Element element, String message, Object[] args) {
if (args.length > 0) {
message = String.format(message, args);
}
messager.printMessage(kind, message, element);
}
}
Processor#init
顧名思義對注解處理器進行一些配置壁查,如這里獲取Message
對象觉至。注解處理器框架涉及到大量的接口,這些接口用于幫助我們對注解進行處理睡腿,比如Processor
康谆、Messager
、Element
等等都是接口嫉到。
Messager#printMessage(Diagnostic.Kind, CharSequence, Element)
/**
* Prints a message of the specified kind at the location of the
* element.
*
* @param kind the kind of message
* @param msg the message, or an empty string if none
* @param e the element to use as a position hint
*/
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e);
這里傳入的參數(shù)Element
用于源碼的定位沃暗,比如處理注解時警告或者錯誤信息。上面的note()
方法使用后note(element, "bind with layout id = %#x", id)
的效果如:
/home/jmu/AndroidStudioProjects/zero/sample/src/main/java/com/example/annotationtest/MainActivity.java:9: 注: bind with layout id = 0x7f09001b
public class MainActivity extends AppCompatActivity {
^
error()
將使得注解處理器在調(diào)用處打印錯誤信息何恶,并導(dǎo)致最終編譯失斈踝丁:
...MainActivity.java:9: 錯誤: bind with layout id = 0x7f09001b
public class MainActivity extends AppCompatActivity {
^
2 個錯誤
:sample:compileDebugJavaWithJavac FAILED
FAILURE: Build failed with an exception.
有了這兩個日志方法,就可以在適當(dāng)?shù)臅r候在控制臺打印想要了解的信息细层。
第一個注解@ContentView
ContentView.java
package zero.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface ContentView {
int value();
}
這個注解使用在Activity類上惜辑,為Activity指定布局。類似于ButterKnife(ButterKnife不提供類似的注解)疫赎,@ContentView
的作用使得我們將來要在
package com.example.annotationtest;
@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Zero.bind(this);
}
}
Zero.bind(this)
之后調(diào)用注解處理器生成的java代碼Activity.setContentView(id)
盛撑,注意不是使用反射來調(diào)用Activity.setContentView
。
ContentViewProcessor
package zero.annotation.processor;
@SupportedAnnotationTypes({"zero.annotation.ContentView"})
public class ContentViewProcessor extends LoggerProcessor {
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
Set<? extends Element> elements = env.getElementsAnnotatedWith(ContentView.class);
for (Element element : elements) {
Element enclosingElement = element.getEnclosingElement();
System.out.println(enclosingElement.getClass());
int id = element.getAnnotation(ContentView.class).value();
note(element, "bind with layout id = %#x", id);
}
return true;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
再議Processor(詳見api)
Set<String> getSupportedAnnotationTypes();
指定該注解處理器可以處理那些注解捧搞,重寫該方法返回一個
Set<String>
或者在處理器上使用注解@SupportedAnnotationTypes
SourceVersion getSupportedSourceVersion();
支持的java編譯器版本抵卫,重寫或者使用
@SupportedSourceVersion
注解void init(ProcessingEnvironment processingEnv)狮荔;
Initializes the processor with the processing environment.
boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);
處理注解的方法,待處理的注解通過參數(shù)
annotations
傳遞介粘,返回值表示注解是否已被處理殖氏,roundEnv
表示當(dāng)前和之前的處理環(huán)境。
上面的代碼簡單的遍歷了使用@ContentView
的類姻采,并將其中的布局文件id打印在控制臺(驗證System.out.println
是否生效)雅采。我們循序漸進旨在能在探索中了解Processor
。為了在AS上使用該處理器慨亲,需要進行一些配置婚瓜,這些配置相比eclipse相對簡單。
//1.結(jié)構(gòu)
sample
├── build.gradle
├── proguard-rules.pro
└── src
└── main
├── AndroidManifest.xml
└── java/android/com/example/annotationtest
└── MainActivity.java
zero-annotation
├── build.gradle
└── src/main/java/zero/annotation
└── ContentView.java
zero-annotation-processor/
├── build.gradle
└── src/main
├── java/zero/annotation/processor
│ ├── ContentViewProcessor.java
│ └── LoggerProcessor.java
└── resources/META-INF/services
└── javax.annotation.processing.Processor
//2.1 javax.annotation.processing.Processor內(nèi)容
zero.annotation.processor.ContentViewProcessor
//2.2 sample/build.gradle依賴部分
dependencies {
//其他依賴...
annotationProcessor project(path: ':zero-annotation-processor')
api project(path: ':zero-annotation')
}
對比eclipse下的配置刑棵,as中只需要上面的2.1,2.2即可使用自定義的注解處理器闰渔。
Processor生成java代碼
建立Android library :zero, 依賴
zero
├── build.gradle
├── proguard-rules.pro
└── src/main
├── AndroidManifest.xml
└── java/zero
├── IContent.java
└── Zero.java
//IContent.java
public interface IContent {
void setContentView(Activity activity);
}
//build.gradle.dependencies
dependencies {
...
annotationProcessor project(path: ':zero-annotation-processor')
compile project(path: ':zero-annotation-processor')
}
提供IContent
接口,希望使用了@ContentView
后的Activity
可以在同目錄下生成一個形如Activity$$ZeroBind
的類铐望,并且實現(xiàn)IContent
接口冈涧,如MainActivity$$ZeroBind
:
// Generated code from Zero library. Do not modify!
package com.example.annotationtest;
public class MainActivity$$ZeroBind implements zero.IContent {
@Override
public void setContentView(android.app.Activity activity) {
activity.setContentView(2131296283);
}
}
當(dāng)使用Zero.bind(this)
時,反射創(chuàng)建MainActivity$$ZeroBind
對象正蛙,調(diào)用IContent.setContentView
來為MainActivity
設(shè)置布局督弓。因此下面的小目標(biāo)就是通過Processor
生成MainActivity$$ZeroBind.java
文件:
@SupportedAnnotationTypes({"zero.annotation.ContentView"})
public class ContentViewProcessor extends LoggerProcessor {
public static final String SUFFIX = "$$ZeroBind";
private Filer filer;
private Elements elementUtils;
private Types typeUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
elementUtils = processingEnv.getElementUtils();
typeUtils = processingEnv.getTypeUtils();
}
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
Set<? extends Element> elements = env.getElementsAnnotatedWith(ContentView.class);
for (Element element : elements) {
// Element enclosingElement = element.getEnclosingElement();
// note(enclosingElement, "%s", enclosingElement.getClass().getSuperclass());
int id = element.getAnnotation(ContentView.class).value();
// note(element, "bind with layout id = %#x", id);
TypeMirror typeMirror = element.asType();
// note(element, "%s\n%s", typeMirror.toString(), typeMirror.getKind());
try {
String classFullName = typeMirror.toString() + SUFFIX;
JavaFileObject sourceFile = filer.createSourceFile(classFullName, element);
Writer writer = sourceFile.openWriter();
TypeElement typeElement = elementUtils.getTypeElement(typeMirror.toString());
PackageElement packageOf = elementUtils.getPackageOf(element);
writer.append("http:// Generated code from Zero library. Do not modify!\n")
.append("package ").append(packageOf.getQualifiedName()).append(";\n\n")
.append("public class ").append(typeElement.getSimpleName()).append(SUFFIX).append(" implements zero.IContent {\n\n")
.append(" @Override\n")
.append(" public void setContentView(android.app.Activity activity) {\n")
.append(" activity.setContentView(").append(String.valueOf(id)).append(");\n")
.append(" }\n")
.append("}")
.flush();
writer.close();
} catch (IOException e) {
error(element, "不能寫入java文件!");
}
}
return true;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
通過上面的處理器將產(chǎn)生MainActivity$$ZeroBind.java
文件在:
sample/
├── build
└── generated
└── source
└── apt
└── debug
└── com
└── example
└── annotationtest
└── MainActivity$$ZeroBind.java
//注解處理器生成的源代碼都在 build/generated/source/apt目錄下
這個源碼與MainActivity
在同一個包中乒验,因此可以訪問到MainActivity
中的包級成員愚隧。
為了說明上面的代碼以及理解,需要一些準(zhǔn)備知識锻全。
javax.lang.model包
包 | 描述 |
---|---|
javax.lang.model | Classes and hierarchies of packages used to model the Java programming language. |
javax.lang.model.element | Interfaces used to model elements of the Java programming language. |
javax.lang.model.type | Interfaces used to model Java programming language types. |
javax.lang.model.util | Utilities to assist in the processing of program elements and types. |
主要介紹:Element
和TypeMirror
Element
參看https://docs.oracle.com/javase/7/docs/api/javax/lang/model/element/Element.html
All Known Subinterfaces:
ExecutableElement, PackageElement, Parameterizable, QualifiedNameable, TypeElement, TypeParameterElement, VariableElement
繼承關(guān)系 Element PackageElement (javax.lang.model.element) ExecutableElement (javax.lang.model.element) VariableElement (javax.lang.model.element) TypeElement (javax.lang.model.element) QualifiedNameable (javax.lang.model.element) PackageElement (javax.lang.model.element) TypeElement (javax.lang.model.element) Parameterizable (javax.lang.model.element) ExecutableElement (javax.lang.model.element) TypeElement (javax.lang.model.element) TypeParameterElement (javax.lang.model.element)
public interface Element
代表程序中的元素狂塘,如包、類或方法鳄厌。每個元素表示一個靜態(tài)的荞胡、語言級的構(gòu)造(不是運行時虛擬機構(gòu)造的)。
元素的比較應(yīng)該使用
equals(Object)
方法. 不能保證任何特定元素總是由同一對象表示了嚎。實現(xiàn)基于一個
Element
對象的類的操作, 使用 visitor 或者getKind()
方法. 由于一個實現(xiàn)類可以選擇多個Element
的子接口泪漂,使用instanceof
來決定在這種繼承關(guān)系中的一個對象的實際類型未必是可靠的。package com.example.demo;//[PackageElement, ElementKind.PACKAGE] public class Main {//[TypeElement,ElementKind.CLASS] int a;//[VariableElement, ElementKind.FIELD] static {//[ExecutableElement, ElementKind.STATIC_INIT] System.loadLibrary("c"); } {//[ExecutableElement, ElementKind.INSTANCE_INIT] a = 100; } public Main(){//[ExecutableElement,ElementKind.CONSTRUCTOR] int b = 10;//[VariableElement, ElementKind.LOCAL_VARIABLE] } public String toString(){//[ExecutableElement, ElementKind.METHOD] return super.toString(); } } public @interface OnClick{//[TypeElement, ElementKind.ANNOTATION_TYPE] } public interface Stack<T>{//[TypeElement,ElementKind.INTERFACE] T top;//[VariableElement, ElementKind.FIELD, TypeKind.TYPEVAR] TypeNotExists wtf;//[VariableElement, ElementKind.FIELD, TypeKind.ERROR] }
Method Detail
TypeMirror asType() 返回元素定義的類型
一個泛型元素定義一族類型歪泳,而不是一個萝勤。泛型元素返回其原始類型. 這是元素在類型變量相應(yīng)于其形式類型參數(shù)上的調(diào)用. 例如, 對于泛型元素
C<N extends Number>
, 返回參數(shù)化類型C<N>
.Types
實用接口有更通用的方法來獲取元素定義的所有類型的范圍。ElementKind getKind() 返回元素的類型
List<? extends AnnotationMirror> getAnnotationMirrors() 返回直接呈現(xiàn)在元素上的注解
使用getAllAnnotationMirrors可以獲得繼承來的注解
<A extends Annotation> A getAnnotation(Class<A> annotationType)
返回呈現(xiàn)在元素上的指定注解實例呐伞,不存在返回
null
敌卓。注解可以直接直接呈現(xiàn)或者繼承。Set<Modifier> getModifiers() 返回元素的修飾符
Name getSimpleName() 返回元素的簡單名字
泛型類的名字不帶任何形式類型參數(shù)伶氢,比如
java.util.Set<E>
的SimpleName是"Set"
. 未命名的包返回空名字趟径, 構(gòu)造器返回"<init>
"瘪吏,靜態(tài)代碼快返回 "<clinit>
" , 匿名內(nèi)部類或者構(gòu)造代碼快返回空名字.Element getEnclosingElement()
返回元素所在的最里層元素, 簡言之就是閉包.
- 如果該元素在邏輯上直接被另一個元素包裹舵抹,返回該包裹的元素
- 如果是一個頂級類, 返回包元素(PackageElement)
- 如果是包元素返回null
- 如果是類型參數(shù)或泛型元素,返回類型參數(shù)(TypeParametrElement)
List<? extends Element> getEnclosedElements()
返回當(dāng)前元素直接包裹的元素集合劣砍。類和接口視為包裹字段惧蛹、方法、構(gòu)造器和成員類型刑枝。 這包括了任何隱式的默認(rèn)構(gòu)造方法香嗓,枚舉中的
values
和valueOf
方法。包元素包裹在其中的頂級類和接口装畅,但不認(rèn)為包裹了子包靠娱。其他類型的元素當(dāng)前默認(rèn)不包裹任何元素,但可能 跟隨API和編程語言而變更掠兄。注意某些類型的元素可以通過
ElementFilter
中的方法分離出來.
TypeMirror
參考https://docs.oracle.com/javase/7/docs/api/javax/lang/model/type/TypeMirror.html
All Known Subinterfaces:
ArrayType, DeclaredType, ErrorType, ExecutableType, NoType, NullType, PrimitiveType, ReferenceType, TypeVariable, UnionType, WildcardType
public interface TypeMirror
表示java中的一個類型. 類型包含基本類型像云、聲明類型 (類和接口)、數(shù)組蚂夕、類型變量和null 類型. 也表示通配符類型參數(shù)(方法簽名和返回值中的), 以及對應(yīng)包和關(guān)鍵字
void
的偽類型.類型的比較應(yīng)該使用
Types
. 不能保證任何特定類型總是由同一對象表示迅诬。實現(xiàn)基于一個
TypeMirror
對象的類的操作, 使用 visitor 或者getKind()
方法. 由于一個實現(xiàn)類可以選擇多個TypeMirror
的子接口,使用instanceof
來決定在這種繼承關(guān)系中的一個對象的實際類型未必是可靠的婿牍。
Utility
javax.lang.model.util下的接口(主要指Elements侈贷,Types)擁有一些實用的方法。
PackageElement Elements.getPackageOf(Element type)
Returns the package of an element. The package of a package is itself.
TypeElement Elements.getTypeElement(CharSequence name)
Returns a type element given its canonical name.
boolean Types.isAssignable(TypeMirror t1, TypeMirror t2)
Tests whether
t1
is assignable tot2
.boolean Types.isSameType(TypeMirror t1, TypeMirror t2)
Tests whether two
TypeMirror
objects represent the same type. Returntrue
if and only if the two types are the sameboolean Types.isSubtype(TypeMirror t1, TypeMirror t2)
Return
true
if and only if thet1
is a subtype oft2
Process生成java代碼續(xù)
現(xiàn)在我們詳細注釋下ContentViewProcessor#process
等脂,代碼有少許不同
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
Set<? extends Element> elements = env.getElementsAnnotatedWith(ContentView.class);
for (Element element : elements) {
//ContentView定義時指定作用范圍是類俏蛮,所以只能作用于類上,Element一定是類元素
if(element.getKind() != ElementKind.CLASS){
error(element, "ContentView注解必須作用在類上!");
throw new RuntimeException();
}
TypeElement typeElement = (TypeElement) element;
//獲取包元素上遥,主要為了方便獲取Element的包名
//element是類元素搏屑,因此還可以使用:
//PackageElement packageOf = (PackageElement) element.getEnclosingElement();
PackageElement packageOf = elementUtils.getPackageOf(element);
int id = element.getAnnotation(ContentView.class).value();
try {
//仿照ButterKnife粉楚,使用自己的后綴
String classFullName = typeElement.getQualifiedName() + SUFFIX;
//JavaFileObject createSourceFile(CharSequence name, Element... originatingElements)
//name:完整類名
//originatingElements:和創(chuàng)建的文件相關(guān)的類元素或包元素睬棚,可省略或為null
JavaFileObject sourceFile = filer.createSourceFile(classFullName, element);
Writer writer = sourceFile.openWriter();
//關(guān)于ContentView注解的java 文件模板
String tmp =
"http:// Generated code from Zero library. Do not modify!\n" +
"package %s;\n\n" +
"public class %s implements zero.IContent {\n\n" +
" @Override\n" +
" public void setContentView(android.app.Activity activity) {\n" +
" activity.setContentView(%d);\n" +
" }\n" +
"}";
//填充包名,類名解幼,布局文件id
writer.write(String.format(tmp, packageOf.getQualifiedName(), typeElement.getSimpleName()+SUFFIX, id));
writer.close();
} catch (IOException e) {
error(element, "不能寫入java文件抑党!");
}
}
return true;//ContentView被我處理了
}
Zero.bind
基于注解處理器生成的java代碼已完成,最后一道工序需要將代碼調(diào)用起來即可撵摆。
public class Zero {
public static void bind(Activity activity){
try {
String fullName = activity.getClass().getCanonicalName()+ ContentViewProcessor.SUFFIX;
Class<?> zeroBind = Class.forName(fullName);
IContent content = (IContent) zeroBind.getConstructor().newInstance();
content.setContentView(activity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
現(xiàn)在可以向ButterKnife一樣使用Zero.bind
底靠。這里根據(jù)我們定義的規(guī)則使用了少量的運行時反射手段用于動態(tài)調(diào)用適當(dāng)?shù)拇a,另外發(fā)布時需要將相應(yīng)的類不做混淆處理即可特铝。
本文著重介紹注解處理器相關(guān)api及其應(yīng)用暑中,至于代碼的封裝可以參考筆者添加 @BindView
和 @OnClick
后的代碼(zero-bind-library)或者ButterKnife 壹瘟。