Java注解處理器實(shí)戰(zhàn)(附詳細(xì)示例代碼)
注解處理器
注解強(qiáng)大的地方在于: 我們可以在運(yùn)行時或者編譯時處理注解. 在編譯時或者運(yùn)行時處理注解的機(jī)制都可以稱為一個注解處理器.
注解處理器的類型
注解處理器的類型分為兩大類:
- 運(yùn)行時處理器: 這種機(jī)制是在程序運(yùn)行時利用反射機(jī)制去處理注解.
- 編譯時處理器: 這種機(jī)制是在程序編譯時利用javac提供的一個apt工具來處理注解.
運(yùn)行時處理器的特點(diǎn): 該機(jī)制基于反射機(jī)制, 因此靈活性很大, 但是卻相對耗性能. 編譯時處理器的特點(diǎn):這種方法是在編譯時處理注解, 因此不存在性能的問題, 但是靈活性也相對比較低.
在已有的開源庫實(shí)現(xiàn)中, 普遍是結(jié)合編譯時處理器和反射機(jī)制這兩種方法. 舉個例子: ButterKnife. 它是一個View注入的庫. 內(nèi)部實(shí)現(xiàn)的原理: 編譯時利用apt生成findViewById等一些列模板代碼, 然后在運(yùn)行時利用反射去實(shí)例生成的模板代碼類, 這樣完成了一次注入. 從這里可以看出, ButterKnife并沒有完全基于編譯時注解處理器. 而是加了反射.
這樣做的好處: 開發(fā)者不需要手動實(shí)例apt生成的類. 換句話說: 開發(fā)者不需要了解生成類的命令規(guī)則. 也許你可能會覺得利用反射會耗性能. 但是仔細(xì)想想, 如果沒有利用反射的話, 開發(fā)者都需要手動編譯, 然后再實(shí)例化生成的類. 這個過程也是挺繁瑣的. 而且, 庫內(nèi)部是有做相應(yīng)的緩存的, 所以耗的性能還是相對比較低的.
對于反射的利用, 應(yīng)該適當(dāng)使用, 而不是避而不用或者濫用.
接下來, 講講APT的一些知識, 最后再仿寫一個簡單的ButterKnife作為實(shí)戰(zhàn).
AbstractProcessor
開發(fā)注解處理器的第一個步驟就是繼承AbstractProcessor. 然后重寫其四個方法:
- public synchronized void init(ProcessingEnvironment processingEnvironment): 這個方法一般是做一些初始化的工作.
- public SourceVersion getSupportedSourceVersion(): 該處理器所支持的JDK版本, 一般是支持到最新: SourceVersion.latestSupported().
- public Set<String> getSupportedAnnotationTypes(): 你所要處理的注解的類型.
- public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment): 處理注解的方法. 這個是我們主要實(shí)現(xiàn)的方法. 返回值表示當(dāng)前這個注解類型是否需要被隨后的注解處理器處理. true表示不需要, false表示后面的注解處理器可能會對本次處理的注解.
認(rèn)識這4個方法后, 我們還需要熟悉一下編寫處理器涉及到的一些概念和API.
ProcessingEnvironment
ProcessingEnvironment是在init方法傳入的. 它表示APT框架的一個處理時上下文. 這個類主要是提供一些工具類. 詳細(xì)看下面代碼注釋:
public interface ProcessingEnvironment {
//獲取外部配置的參數(shù)
Map<String,String> getOptions();
//獲取Messager對象, 用于打印一些日志
Messager getMessager();
//獲取Filer對象, 該對象用于創(chuàng)建文件
Filer getFiler();
//獲取Elements, 它包含處理Element的一些工具方法
Elements getElementUtils();
//獲取Types, 它包含處理TypeMirror的一些工具方法
Types getTypeUtils();
SourceVersion getSourceVersion();
Locale getLocale();
}
RoundEnvironment
當(dāng)APT啟動時, 它將會掃描源文件, 然后進(jìn)入process方法處理, 如果這個過程生成了新的文件的話, 新文件會被APT再次作為輸入, 接著process會被apt再次調(diào)用, 如此循環(huán)下去, 直到?jīng)]有新的文件產(chǎn)生. RoundEnvironment為處理輪次的上下文環(huán)境. 下面是RoundEnvironment的方法簡單介紹:
public interface RoundEnvironment {
//用于判斷是否處理完了, 只有當(dāng)沒有新文件生成時, 證明已經(jīng)處理完了, 才會返回true
boolean processingOver();
//上一次處理是否存在錯誤
boolean errorRaised();
//獲取前一次處理的根元素集合
Set<? extends Element> getRootElements();
//獲取被指定的類型元素標(biāo)注的元素集
Set<? extends Element> getElementsAnnotatedWith(TypeElement a);
//獲取被指定的注解標(biāo)注的元素集
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a);
}
Element
Element代表Java靜態(tài)語言結(jié)構(gòu)的元素. 這樣聽起來可能有點(diǎn)難以理解, 不過看下面的例子就會很好理解了:
package com.desperado.processors; //PackageElement
public class Test { //TypeElement: 代表類或接口元素
private int xixi; //VariableElement:
public Test() { //ExecutableElement
}
public void haha(String arg) { //haha方法為TExecutableElement
int v; //VariableElement
}
}
- TypeElement: 代表類或接口元素
- VariableElement: 代表字段, 枚舉常量, 方法或構(gòu)造函數(shù)的參數(shù), 局部變量, 資源變量和異常參數(shù).
- PackageElement: 代表包元素
- ExecutableElement: 代表的方法, 構(gòu)造方法,和接口或者類的初始化塊.
DeclaredType, TypeElement和TypeMirror
TypeElement表示類或接口元素, 可以從中獲取類名, 但是不能獲得類本身的信息, 比如父類.
TypeMirror: 表示Java語言中的類型, 其中包括: 基本類型, 聲明類型(類類型和接口類型), 數(shù)組類型, 類型變量和空類型. 也代表通配類型參數(shù),可執(zhí)行文件的簽名和返回類型.
DeclaredType: 代表聲明的類型, 類類型還是接口類型,當(dāng)然也包括參數(shù)化類型,比如Set,也包括原始類型.
開發(fā)一個注解處理器
掌握了上面的概念后, 我們開始來開發(fā)一個仿ButterKnife的注解處理器.
步驟
- 繼承AbstractProcessor并重寫其中的四個方法.
- 注冊注解處理器.
繼承AbstractProcessor
@AutoService(Processor.class)
public class ViewBindingProcessor extends AbstractProcessor {
private Types types;
private Filer filer;
private Messager messager;
private Elements elements;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
types = processingEnvironment.getTypeUtils();
filer = processingEnvironment.getFiler();
messager = processingEnvironment.getMessager();
elements = processingEnvironment.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//process
return true;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(BindView.class.getCanonicalName());
return annotations;
}
}
在getSupportedAnnotationTypes()中, 我們直接返回了需要處理的注解的全限定名稱集合.
注冊處理器
寫好的處理器還需要處理告訴Javac, 讓它編譯時運(yùn)行我們的處理器. 注冊的步驟為: 在項(xiàng)目新建一個resources目錄, 然后在resources內(nèi)新建這樣的目錄結(jié)構(gòu): /META-INF/services/. 最后在services中新建文件: javax.annotation.processing.Processor. 并且在文件上填寫你的處理器的全路徑名稱. 例如: com.desperado.processors.ViewBindingProcessor.
其實(shí)還有一種更為簡單的方法. 添加com.google.auto.service:auto-service:1.0-rc2這個庫(Google開源的). 然后在你的處理器上添加注解: @AutoService(Processor.class). 這樣, 編譯時就會自動幫我們生成注冊文件.
如何組織處理器結(jié)構(gòu)
為了不將注解處理器的代碼打包進(jìn)APK. 我們需要將注解和注解處理器分開. 具體的組織如下:
- annotations: annotations是一個java lib. 是我們自定義的注解
- processors: processors也是一個java lib. 是我們的注解處理器
- viewbinding: 為提供為app的api.
annotations
首先我們在annotation模塊定義一個注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindView {
int value() default -1;
}
processors
在講處理器模塊前, 我們先看看期望生成的代碼模板:
public class MainActivity$ViewBinding {
public MainActivity$ViewBinding(MainActivity activity, View view) {
if (activity == null) {
return;
}
activity.mTvTextView = view.findViewById(2131165249);
}
}
生成的代碼的類名為: XXXX$ViewBinding
然后在構(gòu)造函數(shù)中對傳入的activity中的View進(jìn)行賦值.
定義處理規(guī)則.
- BindView只能標(biāo)注字段.
- BindView不能標(biāo)注接口中的字段, 只能標(biāo)注類中的字段.
- BindView不能標(biāo)注抽象類中的字段.
- BindView不能標(biāo)注被private, static或者final修飾的字段
- BindView標(biāo)注字段所屬的類必須是Activity的子類.
- BindView標(biāo)注的字段必須是View的子類
定義好這些規(guī)則后, 我們就可以來看處理器的代碼了.
處理過程
@AutoService(Processor.class)
public class ViewBindingProcessor extends AbstractProcessor {
private Types types;
private Filer filer;
private Messager messager;
private Elements elements;
private Map<String, ViewClass> map = new HashMap<>();
private static final String TYPE_ACTIVITY = "android.app.Activity";
private static final String TYPE_VIEW = "android.view.View";
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
types = processingEnvironment.getTypeUtils();
filer = processingEnvironment.getFiler();
messager = processingEnvironment.getMessager();
elements = processingEnvironment.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
map.clear(); //該方法會被調(diào)用多次, 所以每次進(jìn)入時, 首先清空之前的臟數(shù)據(jù)
logNote("start process");
for (Element e : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
if (!isValid(e)) { //首先判斷被標(biāo)注的元素是否符合我們上面定的規(guī)則
return true; //出現(xiàn)錯誤, 停止編譯
}
logNote("start parse annotations");
performParseAnnotations(e); //開始解析注解
}
logNote("start generate code");
generateCode(); //生成代碼
return true;
}
private boolean isValid(Element element) {
if (!(element instanceof VariableElement)) {
logError("BindView只能標(biāo)注字段");
return false;
}
VariableElement variableElement = (VariableElement) element;
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
if (typeElement.getKind() != ElementKind.CLASS) {
logError("只能標(biāo)注類中的字段");
return false;
}
if (typeElement.getModifiers().contains(Modifier.ABSTRACT)) {
logError("不能標(biāo)注抽象類中的字段");
return false;
}
for (Modifier modifier : element.getModifiers()) {
if (modifier == Modifier.PRIVATE || modifier == Modifier.STATIC ||
modifier == Modifier.FINAL) {
logError("BindView不能標(biāo)注被static," +
"private或者final的字段");
return false;
}
}
if (!isSubtype(typeElement.asType(), TYPE_ACTIVITY)) { //判斷被標(biāo)注的字段的類是不是Activity的子類
logError(typeElement.getSimpleName() + "必須是 Activity的子類");
return false;
}
if (!isSubtype(variableElement.asType(), TYPE_VIEW)) { //判斷被標(biāo)注的字段是不是View的子類
logError("BindView只能標(biāo)注View的子類");
return false;
}
return true;
}
private boolean isSubtype(TypeMirror tm, String type) {
boolean isSubType = false;
while (tm != null) { //循環(huán)獲取父類信息
if (type.equals(tm.toString())) { //通過全路徑是否相等
isSubType = true;
break;
}
TypeElement superTypeElem = (TypeElement) types.asElement(tm);
if (superTypeElem != null) {
tm = superTypeElem.getSuperclass();
} else { //如果為空, 說明沒了父類, 所以直接退出
break;
}
}
return isSubType;
}
private void performParseAnnotations(Element element) {
VariableElement variableElement = (VariableElement) element;
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
String className = typeElement.getSimpleName().toString();
ViewClass viewClass = map.get(className);
ViewField field = new ViewField(variableElement);
if (viewClass == null) {
viewClass = new ViewClass(elements.getPackageOf(variableElement).getQualifiedName().toString(),
className, typeElement);
map.put(className, viewClass);
}
viewClass.addField(field);
}
private void generateCode() {
for (ViewClass vc : map.values()) {
try {
vc.generateCode().writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
logError("error in parse");
}
}
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(BindView.class.getCanonicalName());
return annotations;
}
private void logError(String msg) {
log(Diagnostic.Kind.ERROR, msg);
}
private void logNote(String msg) {
log(Diagnostic.Kind.NOTE, msg);
}
private void log(Diagnostic.Kind kind, String msg) {
messager.printMessage(kind, msg);
}
}
為了使代碼結(jié)構(gòu)更為清晰, 這里將注解標(biāo)注的字段的信息抽象為ViewField類, 字段所屬的類的信息抽象為ViewClass.
public class ViewField {
private String fieldName;
private int id;
public ViewField(VariableElement variableElement) {
id = variableElement.getAnnotation(BindView.class).value();
fieldName = variableElement.getSimpleName().toString();
}
public String getFieldName() {
return fieldName;
}
public int getId() {
return id;
}
}
public class ViewClass {
private String packName;
private String className;
private List<ViewField> fields = new ArrayList<>();
private TypeElement typeElement;
public ViewClass(String packName, String className, TypeElement typeElement) {
this.packName = packName;
this.className = className;
this.typeElement = typeElement;
}
public void addField(ViewField viewField) {
fields.add(viewField);
}
public JavaFile generateCode() { //為了方便, 這里生成代碼用了JavaPoet庫來生成代碼
MethodSpec.Builder con = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeName.get(typeElement.asType()), "activity")
.addParameter(ClassName.get("android.view", "View"), "view")
.beginControlFlow("if (activity == null)")
.addStatement("return")
.endControlFlow();
for (ViewField f : fields) {
con.addStatement("activity.$N = view.findViewById($L)", f.getFieldName(), f.getId());
}
FieldSpec.Builder fid = FieldSpec.builder(TypeName.get(typeElement.asType()), "activity");
TypeSpec typeSpec = TypeSpec.classBuilder(className + "$ViewBinding")
.addModifiers(Modifier.PUBLIC)
.addField(fid.build())
.addMethod(con.build())
.build();
return JavaFile.builder(packName, typeSpec).build();
}
}
處理過程其實(shí)并不復(fù)雜, 只是需要我們熟悉API而已, 因此, 接下來總結(jié)一下獲取一些常用信息的方法:
常用的api
public class ViewBindingProcessor extends AbstractProcessor {
private Types types;
private Filer filer;
private Messager messager;
private Elements elements;
private static final String TYPE_ACTIVITY = "android.app.Activity";
private static final String TYPE_VIEW = "android.view.View";
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
types = processingEnvironment.getTypeUtils();
filer = processingEnvironment.getFiler();
messager = processingEnvironment.getMessager();
elements = processingEnvironment.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
for (Element e : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
String packName = elements.getPackageOf(e).getQualifiedName().toString(); //獲取包名
//2: 獲取類名.
//如果標(biāo)注的是類的話
TypeElement t = (TypeElement)e;
String className = t.getSimpleName().toString();
//如果被標(biāo)注的是字段或者是方法
TypeElement t = (TypeElement)e.getEnclosingElement();
String className = t.getSimpleName().toString();
//3. 獲取方法名字, 或者字段名字
String name = e.getSimpleName().toString();
//4: 獲取標(biāo)注的注解
BindView annotation = e.getAnnotation(BindView.class);
int id = annotation.value();
//5: 判斷被標(biāo)注的元素類型, 以字段類型為例子
ElementKind kind = e.getKind();
if (kind == ElementKind.FIELD) {
}
//6: 獲取元素的修飾符. 我們以被修飾的字段為例子.
for (Modifier modifier : e.getModifiers()) {
if (modifier == Modifier.PRIVATE || modifier == Modifier.STATIC ||
modifier == Modifier.FINAL) {
logError("BindView不能標(biāo)注被static," +
"private或者final的字段");
}
}
//7: 判讀被標(biāo)注的元素的是不是某個類的子類, 以View類為例子, 判讀被修飾的字段是否是View的子類
TypeMirror tm = e.asType();
boolean isSubType = false;
while (tm != null) {
if ("android.view.View".equals(tm.toString())) {
isSubType = true;
break;
}
TypeElement t = (TypeElement)types.asElement(tm);
if (t != null) {
tm = t.getSuperclass(); //獲取父類信息
} else {
//如果為空, 說明沒了父類, 所以直接退出
break;
}
}
}
return true;
}
}
viewbinding
模板代碼已經(jīng)生成了, 但是我們還需要實(shí)例化這些類. 這個工作交由viewbinding. 它提供一些綁定的API給app. 這樣我們就不需要手動調(diào)用了.
實(shí)現(xiàn)的思路: 運(yùn)行時利用反射去實(shí)例化對應(yīng)的模板類. 為了降低反射的消耗, 內(nèi)部會做響應(yīng)的緩存操作.
public class ViewBinding {
private static String LATTER_NAME = "$ViewBinding";
private static Map<Class<?>, Constructor<?>> CACHE = new HashMap<>();
public static <T extends Activity> void bind(T target) {
View sourceView = target.getWindow().getDecorView();
bind(target, sourceView);
}
public static <T> void bind(T target, View sourceView) {
Class<? extends Activity> cl = target.getClass();
try {
Class<?> bindClass = findBindClass(cl);
Constructor<?> constructor = findBindConstructor(bindClass, cl);
constructor.newInstance(target, sourceView);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
private static Class<?> findBindClass(Class<?> targetClass) throws ClassNotFoundException {
return targetClass.getClassLoader().loadClass(targetClass.getName() + LATTER_NAME);
}
private static Constructor<?> findBindConstructor(Class<?> bindClass, Class<? extends Activity> targetClass) throws NoSuchMethodException {
Constructor<?> constructor = CACHE.get(bindClass);
if (constructor == null) {
constructor = bindClass.getConstructor(targetClass, View.class);
CACHE.put(bindClass, constructor);
}
return constructor;
}
}
使用
注解處理器現(xiàn)在算是開發(fā)好了. 最后的步驟就是在app中添加依賴, 然后實(shí)現(xiàn)綁定.
在app的build.gradle添加下面的依賴:
implementation project(':viewbinding')
implementation project(':annotations')
annotationProcessor project(':processors')
然后在MainActivity中實(shí)現(xiàn)注入:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.main_tv_hello)
TextView mTvTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewBinding.bind(this);
mTvTextView.setText("xuixi");
}
}
完…
參考資料
- http://jinchim.com/2017/08/23/JBind/
- http://blog.csdn.net/dd864140130/article/details/53875814
到此這篇關(guān)于Java注解處理器實(shí)戰(zhàn)的文章就介紹到這了,更多相關(guān)Java注解處理器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Java實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)之并查集
并查集這種數(shù)據(jù)結(jié)構(gòu),可能出現(xiàn)的頻率不是那么高,但是還會經(jīng)常性的見到,其理解學(xué)習(xí)起來非常容易,通過本文,一定能夠輕輕松松搞定并查集2021-06-06
SpringBoot整合GitLab-CI實(shí)現(xiàn)持續(xù)集成的過程
這篇文章主要介紹了SpringBoot整合GitLab-CI實(shí)現(xiàn)持續(xù)集成,本文詳細(xì)講述了 GitLab-CI 持續(xù)集成的安裝、部署、以及配置,需要的朋友可以參考下2022-12-12
SpringBoot整合redis實(shí)現(xiàn)輸入密碼錯誤限制登錄功能
遇到這樣的需求需要實(shí)現(xiàn)一個登錄功能,并且2分鐘之內(nèi)只能輸入5次錯誤密碼,若輸入五次之后還沒有輸入正確密碼,系統(tǒng)將會將該賬號鎖定1小時,這篇文章主要介紹了SpringBoot整合redis并實(shí)現(xiàn)輸入密碼錯誤限制登錄功能,需要的朋友可以參考下2024-02-02
SPRING FRAMEWORK BEAN作用域和生命周期原理解析
這篇文章主要介紹了SPRING FRAMEWORK BEAN作用域和生命周期原理解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01
IntelliJ IDEA中使用mybatis-generator的示例
這篇文章主要介紹了IntelliJ IDEA中使用mybatis-generator,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-04-04
徹底解決java.lang.ClassNotFoundException: com.mysql.jdbc.Dr
這篇文章給大家介紹了如如何徹底解決java.lang.ClassNotFoundException: com.mysql.jdbc.Driver問題,文中有詳細(xì)的解決思路以及解決方法,需要的朋友可以參考下2023-11-11
multi-catch和try-catch異常處理知識點(diǎn)詳解
在本篇文章里我們給大家分享了一篇關(guān)于multi-catch和try-catch異常處理知識點(diǎn)內(nèi)容,有需要的朋友們可以參考學(xué)習(xí)下。2019-11-11

