[TOC]
zero bind library是一個仿ButterKnife的編譯期注解框架的練習,旨在熟悉編譯期注解和注解處理器的工作原理以及相關的API。當前基本都使用Android Studio進行android開發,因此這個練習也基于AS開發環境(AS3.0, gradle-4.1-all, com.android.tools.build:gradle:3.0.0)。練習中大量參考了ButterKnife的源碼,這些代碼基本都源于ButterKnife,甚至目錄結構和gradle的一些配置和編寫風格,注釋未及之處參考JakeWharton/butterknife 。筆者水平有限,錯誤在所難免,歡迎批評指正。
關于Processor
為了能更好的了解注解處理器在處理注解時進行了那些操作,代碼調試的功能似乎是必不可少的,然而注解處理器是在javac之前執行,所以直接在處理器中打斷點然后運行是調試不到注解處理器的。可以搜索相關的文章了解,比如這個如何調試編譯時注解處理器AnnotationProcessor ,鑒于調試的麻煩,剛開始了解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);
這里傳入的參數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()
將使得注解處理器在調用處打印錯誤信息,并導致最終編譯失敗:
...MainActivity.java:9: 錯誤: bind with layout id = 0x7f09001b
public class MainActivity extends AppCompatActivity {
^
2 個錯誤
:sample:compileDebugJavaWithJavac FAILED
FAILURE: Build failed with an exception.
有了這兩個日志方法,就可以在適當的時候在控制臺打印想要了解的信息。
第一個注解@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)
之后調用注解處理器生成的java代碼Activity.setContentView(id)
,注意不是使用反射來調用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);
處理注解的方法,待處理的注解通過參數
annotations
傳遞,返回值表示注解是否已被處理,roundEnv
表示當前和之前的處理環境。
上面的代碼簡單的遍歷了使用@ContentView
的類,并將其中的布局文件id打印在控制臺(驗證System.out.println
是否生效)。我們循序漸進旨在能在探索中了解Processor
。為了在AS上使用該處理器,需要進行一些配置,這些配置相比eclipse相對簡單。
//1.結構
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內容
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
的類,并且實現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);
}
}
當使用Zero.bind(this)
時,反射創建MainActivity$$ZeroBind
對象,調用IContent.setContentView
來為MainActivity
設置布局。因此下面的小目標就是通過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();
}
}
通過上面的處理器將產生MainActivity$$ZeroBind.java
文件在:
sample/
├── build
└── generated
└── source
└── apt
└── debug
└── com
└── example
└── annotationtest
└── MainActivity$$ZeroBind.java
//注解處理器生成的源代碼都在 build/generated/source/apt目錄下
這個源碼與MainActivity
在同一個包中,因此可以訪問到MainActivity
中的包級成員。
為了說明上面的代碼以及理解,需要一些準備知識。
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
繼承關系 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
代表程序中的元素,如包、類或方法。每個元素表示一個靜態的、語言級的構造(不是運行時虛擬機構造的)。
元素的比較應該使用
equals(Object)
方法. 不能保證任何特定元素總是由同一對象表示。實現基于一個
Element
對象的類的操作, 使用 visitor 或者getKind()
方法. 由于一個實現類可以選擇多個Element
的子接口,使用instanceof
來決定在這種繼承關系中的一個對象的實際類型未必是可靠的。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() 返回元素定義的類型
一個泛型元素定義一族類型,而不是一個。泛型元素返回其原始類型. 這是元素在類型變量相應于其形式類型參數上的調用. 例如, 對于泛型元素
C<N extends Number>
, 返回參數化類型C<N>
.Types
實用接口有更通用的方法來獲取元素定義的所有類型的范圍。ElementKind getKind() 返回元素的類型
List<? extends AnnotationMirror> getAnnotationMirrors() 返回直接呈現在元素上的注解
使用getAllAnnotationMirrors可以獲得繼承來的注解
<A extends Annotation> A getAnnotation(Class<A> annotationType)
返回呈現在元素上的指定注解實例,不存在返回
null
。注解可以直接直接呈現或者繼承。Set<Modifier> getModifiers() 返回元素的修飾符
Name getSimpleName() 返回元素的簡單名字
泛型類的名字不帶任何形式類型參數,比如
java.util.Set<E>
的SimpleName是"Set"
. 未命名的包返回空名字, 構造器返回"<init>
",靜態代碼快返回 "<clinit>
" , 匿名內部類或者構造代碼快返回空名字.Element getEnclosingElement()
返回元素所在的最里層元素, 簡言之就是閉包.
- 如果該元素在邏輯上直接被另一個元素包裹,返回該包裹的元素
- 如果是一個頂級類, 返回包元素(PackageElement)
- 如果是包元素返回null
- 如果是類型參數或泛型元素,返回類型參數(TypeParametrElement)
List<? extends Element> getEnclosedElements()
返回當前元素直接包裹的元素集合。類和接口視為包裹字段、方法、構造器和成員類型。 這包括了任何隱式的默認構造方法,枚舉中的
values
和valueOf
方法。包元素包裹在其中的頂級類和接口,但不認為包裹了子包。其他類型的元素當前默認不包裹任何元素,但可能 跟隨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中的一個類型. 類型包含基本類型、聲明類型 (類和接口)、數組、類型變量和null 類型. 也表示通配符類型參數(方法簽名和返回值中的), 以及對應包和關鍵字
void
的偽類型.類型的比較應該使用
Types
. 不能保證任何特定類型總是由同一對象表示。實現基于一個
TypeMirror
對象的類的操作, 使用 visitor 或者getKind()
方法. 由于一個實現類可以選擇多個TypeMirror
的子接口,使用instanceof
來決定在這種繼承關系中的一個對象的實際類型未必是可靠的。
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代碼續
現在我們詳細注釋下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:和創建的文件相關的類元素或包元素,可省略或為null
JavaFileObject sourceFile = filer.createSourceFile(classFullName, element);
Writer writer = sourceFile.openWriter();
//關于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代碼已完成,最后一道工序需要將代碼調用起來即可。
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();
}
}
}
現在可以向ButterKnife一樣使用Zero.bind
。這里根據我們定義的規則使用了少量的運行時反射手段用于動態調用適當的代碼,另外發布時需要將相應的類不做混淆處理即可。
本文著重介紹注解處理器相關api及其應用,至于代碼的封裝可以參考筆者添加 @BindView
和 @OnClick
后的代碼(zero-bind-library)或者ButterKnife 。