Android中處理編譯時注解使用AnnotationProcessor
盯腌,下面我們來看下如何使用AnnotationProcessor
:
創(chuàng)建module
新建一個Java Library Module(必須為java library)弓摘。
如果出現(xiàn)
Plugin with id 'java-library' not found.
這樣的錯誤圣蝎,則在build.gradle中將apply plugin: 'java-library'
改成apply plugin: 'java'
刃宵,或者將gradle版本升級到4或以上。java-library
插件是gradle4才引入的徘公,在這之前合租java
插件牲证。
配置gradle
在上面創(chuàng)建的module的build.gradle中加入如下依賴:
// 下面兩個庫版本是jdk 1.7最后一個版本,再沒改成1.8之前关面,不要升級
compile 'com.squareup:javapoet:1.9.0'
compile 'com.google.auto.service:auto-service:1.0-rc3'
-
javapoet
是創(chuàng)建和修改java文件的工具坦袍,不是必須的,但建議使用等太,Java原生的API實在是難用捂齐,沒有必要花時間去學習不好的東西。 -
autoservice
也不是必需的缩抡,但建議使用奠宜,它可以處理好編譯時注解需要的配置,如果不使用它則需要手動配置瞻想,后面講到autoservice
的使用時會同步說明下手動配置的相關操作压真。
在調用方的build.gradle中加入如下依賴:
// annotationProcessor專用依賴方式,此依賴下的庫不會打包到apk中
annotationProcessor(':sgetter')
// 如果庫中僅有processor類蘑险,那么不需要compile滴肿;如果注解也放在庫里,或者還提供了其他類可供調用佃迄,那么需要compile
compile project(':sgetter')
用例實踐
本文所使用的例子是動態(tài)生成一個類泼差,該類的職責是為其它對象的字段賦值,我們先來看下使用注解的類以及自動生成的類長啥樣:
// 使用注解的類
public class TestEntity {
@Sgetter
long id;
@Sgetter
String name;
String remark;
}
// 自動生成的類和屎,兩個類位于同一個包下
public class TestEntitySgetter {
private TestEntity target;
public TestEntitySgetter(TestEntity target) {
this.target = target;
}
public void setId(long id) {
target.id = id;
}
public long getId() {
return target.id;
}
public void setName(String name) {
target.name = name;
}
public String getName() {
return target.name;
}
}
接下去開始講解具體的編碼過程拴驮。
創(chuàng)建注解
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface Sgetter {
}
創(chuàng)建Processor
創(chuàng)建一個Processor
類繼承自AbstractProcessor
:
@AutoService(Processor.class)
public class SgetterProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
觸發(fā)下編譯,可以發(fā)現(xiàn)自動生成了如下圖所示的Processor文件
這是由
autoservice
自動生成的柴信,是編譯時注解生效的必要條件套啤,如果沒有使用autoservice
,那么需要手動創(chuàng)建Processor文件(路徑和文件全名參照上圖)随常,然后將自定義的Processor
類的全名寫入該文件中潜沦,多個Processor
以換行隔開。
接著绪氛,我們來看下如何通過SgetterProcessor
實現(xiàn)自動生成setter唆鸡、getter方法:
@AutoService(Processor.class)
public class SgetterProcessor extends AbstractProcessor {
private Elements mElementUtils;
private Messager mMessager;
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mElementUtils = processingEnvironment.getElementUtils(); // 元素操作輔助工具
mMessager = processingEnvironment.getMessager(); // 日志輔助工具
mFiler = processingEnvironment.getFiler(); // 文件操作輔助工具
log("init");
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
/*
1. set:攜帶getSupportedAnnotationTypes()中的注解類型,一般不需要用到枣察。
2. roundEnvironment:processor將掃描到的信息存儲到roundEnvironment中争占,從這里取出所有使用Sgetter注解的字段燃逻。
*/
Set<? extends Element> sgetterElements = roundEnvironment.getElementsAnnotatedWith(Sgetter.class);
Map<String, ClassInfo> classes = new HashMap<>();
if (!sgetterElements.isEmpty()) {
log("----------------------------------");
}
for (Element element : sgetterElements) {
log("process element [" + element.getSimpleName().toString() + "]");
// 獲取注解目標所在的包,在本例中臂痕,即使用Sgetter注解的字段所在的類所在的包
PackageElement packageElement = mElementUtils.getPackageOf(element);
String pkgName = packageElement.getQualifiedName().toString();
log("pkg=" + pkgName);
// 獲取包裝類類型伯襟,在本例中,即使用Sgetter注解的字段所在的類
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
String enclosingName = enclosingElement.getQualifiedName().toString(); // enclosingName為完整類名
String simpleName = enclosingElement.getSimpleName().toString();
log("class=" + enclosingName);
// 獲取字段信息握童,因為Sgetter只作用于字段姆怪,因此這里可以直接強轉
VariableElement variableElement = (VariableElement) element;
String fieldname = variableElement.getSimpleName().toString(); // 獲取字段名
String fieldtype = variableElement.asType().toString(); // 獲取字段類型
log("field name=" + fieldname + ", type=" + fieldtype);
ClassInfo classInfo = classes.get(enclosingName);
if (classInfo == null) {
classInfo = new ClassInfo();
classInfo.pkgName = pkgName;
classInfo.classname = enclosingName;
classInfo.fields = new LinkedList<>();
classes.put(enclosingName, classInfo);
}
FieldInfo fieldInfo = new FieldInfo();
fieldInfo.name = fieldname;
fieldInfo.type = fieldtype;
classInfo.fields.add(fieldInfo);
log("----------------------------------");
}
for (ClassInfo classInfo : classes.values()) {
generateJavaFile(classInfo);
}
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new HashSet<>();
types.add(Sgetter.class.getCanonicalName());
log("types=" + types);
return types;
}
@Override
public SourceVersion getSupportedSourceVersion() {
SourceVersion version = SourceVersion.RELEASE_7;
// SourceVersion version = SourceVersion.latestSupported();
log("version=" + version);
return version;
}
private void log(String msg) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "SgetterProcessor xxx " + msg);
System.out.println("SgetterProcessor " + msg);
}
/**
* 使用javapoet生成java文件
* @param classInfo
*/
private void generateJavaFile(ClassInfo classInfo) {
try {
TypeSpec.Builder builder = TypeSpec.classBuilder(splitClassName(classInfo.classname)[1] + "Sgetter")
.addModifiers(Modifier.PUBLIC);
// 生成target字段
TypeName targetClass = getClassName(classInfo.classname);
FieldSpec targetField = FieldSpec.builder(targetClass, "target")
.addModifiers(Modifier.PRIVATE)
.addStatement("this.target = target")
.build();
builder.addField(targetField);
// 生成構造函數(shù)
MethodSpec constructor = MethodSpec.constructorBuilder()
.addParameter(ParameterSpec.builder(targetClass, "target").build())
.addModifiers(Modifier.PUBLIC)
.build();
builder.addMethod(constructor);
for(FieldInfo fieldInfo : classInfo.fields) {
// 生成set方法
StringBuilder sb = new StringBuilder();
sb.append("set").append(fieldInfo.name.substring(0, 1).toUpperCase())
.append(fieldInfo.name.substring(1, fieldInfo.name.length()));
MethodSpec setMethod = MethodSpec.methodBuilder(sb.toString())
.addParameter(ParameterSpec.builder(getClassName(fieldInfo.type), fieldInfo.name).build())
.addModifiers(Modifier.PUBLIC)
.addStatement("target.$L = $L", fieldInfo.name, fieldInfo.name)
.build();
builder.addMethod(setMethod);
// 生成get方法
sb.delete(0, sb.length());
sb.append("get").append(fieldInfo.name.substring(0, 1).toUpperCase())
.append(fieldInfo.name.substring(1, fieldInfo.name.length()));
MethodSpec getMethod = MethodSpec.methodBuilder(sb.toString())
.addModifiers(Modifier.PUBLIC)
.addStatement("return target.$L", fieldInfo.name)
.returns(getClassName(fieldInfo.type))
.build();
builder.addMethod(getMethod);
}
JavaFile javaFile = JavaFile.builder(classInfo.pkgName, builder.build()).build();
javaFile.writeTo(mFiler);
} catch (Exception e) {
e.printStackTrace();
}
}
private class FieldInfo {
String name;
String type;
}
private class ClassInfo {
String pkgName;
String classname;
List<FieldInfo> fields;
}
private TypeName getClassName(String classname) {
switch (classname) {
case "void":
return TypeName.VOID;
case "boolean":
return TypeName.BOOLEAN;
case "byte":
return TypeName.BYTE;
case "short":
return TypeName.SHORT;
case "int":
return TypeName.INT;
case "long":
return TypeName.LONG;
case "char":
return TypeName.CHAR;
case "float":
return TypeName.FLOAT;
case "double":
return TypeName.DOUBLE;
default:
String[] fields = splitClassName(classname);
return ClassName.get(fields[0], fields[1]);
}
}
private String[] splitClassName(String classname) {
int pos = classname.lastIndexOf('.');
if(pos == -1) { // int等基礎類型
return null;
}
return new String[]{classname.substring(0, pos), classname.substring(pos + 1)};
}
}
我們來看下幾個核心對象和方法的作用(代碼細節(jié)這里不再詳述,很容易看懂澡绩,而且代碼中也有注釋):
-
Elements
:元素操作工具稽揭,如果使用javapoet
的話該原生工具基本用不到,不需要過多關注肥卡。 -
Messager
:日志輸出工具溪掀,Messager
輸出的日志本應顯示在Messages窗口中,不過AS3.0之后已經(jīng)找不到這個窗口了步鉴,好在Messager
輸出的日志會顯示在終端中(和使用System.out已經(jīng)區(qū)別不大了)膨桥,前提是在終端中使用命令編譯工程。 -
Filer
:文件操作工具唠叛,通過Filer
生成的文件位于app/build/generated/source/apt
中。 -
getSupportedSourceVersion
:返回jdk的版本(也可以通過@SupportedSourceVersion
注解到Processor
類)沮稚,默認1.6艺沼。 -
init
:初始化方法,可以在這里進行一些初始化操作以及獲取環(huán)境信息(環(huán)境信息已經(jīng)保存processingEnv
成員中蕴掏,子類可以直接使用)障般。Processor
類必須保留默認構造函數(shù)(編譯時反射),并且由于初始化方法的存在盛杰,因此一般沒有必要編寫構造函數(shù)挽荡。 -
getSupportedAnnotationTypes
:返回當前Processor
支持的注解(也可以通過@SupportedAnnotationTypes
注解到Processor
類),為了保持代碼的整潔及可維護即供,一般一個Processor
只處理一個注解定拟。 -
process
:最核心的方法,用來處理注解逗嫡,該方法是由基類定義的抽象方法青自,子類必須實現(xiàn)。這個方法千萬不能出現(xiàn)異常驱证,否則編譯時會出現(xiàn)各種莫名其妙的問題延窜。
常見問題整理
Processor不執(zhí)行
如果Processor
已經(jīng)執(zhí)行過則再次build便不會再執(zhí)行,可以build之前clean抹锄,或者直接rebuild逆瑞。
process方法不執(zhí)行
如果在Processor
中注冊的注解沒有使用荠藤,那么process
方法就不用執(zhí)行。需要特別注意的是获高,如果注解的使用者和Processor
位于同一個module哈肖,那么該使用者會被忽略(筆者在這里吃了大虧,花了很長時間才找出問題)谋减。