如何构建编译时注解解析框架
前言
在前面的文章中,咱们学习了Java类加载、Java反射、Java注解,那现在咱们就可以利用所学搞点事情了,所谓学以致用,方为正途。
如果想直接阅读源码,请点这里Github
铺垫
在开始搞事情前,咱们还需要了解以下几个物件:
- Annotation Processor: 注解处理器
- JavaPoet:Java源码文件生成者
- javax.lang.model.element:用于解析程序中的元素,例如:包、类、方法、变量
Annotation Processor
注解处理器是在编译时用来扫描和处理注解的工具。你可以注册自己感兴趣的注解,程式编译时会将添加注解的元素,交由注册它的注解处理器来处理。
那咱们如何实现一个自己的注解处理器?
- 继承AbstractProcessor
- 覆盖getSupportedAnnotationTypes()
- 覆盖getSupportedSourceVersion()
- 覆盖process()
AbstractProcessor:抽象注释处理器,为大多数自定义注释处理器的超类。
getSupportedAnnotationTypes():这里注册你感兴趣的注解。它的返回一个字符串的Set,包含注解类型的合法全称。
getSupportedSourceVersion():指定使用的Java版本。通常这里返回SourceVersion.latestSupported()。
process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment):注解处理器的核心方法,在这里进行注解扫描、评估和处理,以及生成Java文件。
生成Java文件,就交由JavaPoet来完成
JavaPoet
JavaPoet是一个用来生成 .java源文件的工具(由Square提供)。
咱们来讲一下JavaPoet里面常用的几个类:
- TypeSpec:表示一个类、接口或者枚举声明
- MethodSpec:表示一个构造函数或方法声明
- FieldSpec:表示一个成员变量、字段声明
- JavaFile:生成java文件
下面通过一个实例来说明具体使用方式:
private void generateHelloWorld() throws IOException {
MethodSpec mainMethod = MethodSpec.methodBuilder("main")
.addModifiers(new Modifier[]{Modifier.PUBLIC, Modifier.STATIC})
.addParameter(String[].class, "args")
.addStatement("System.out.println(\"Hello World\")")
.build();
FieldSpec androidVersion = FieldSpec.builder(String.class, "androidVer")
.addModifiers(new Modifier[]{Modifier.PRIVATE})
.initializer("$S", "Lollipop")
.build();
TypeSpec typeSpec = TypeSpec.classBuilder("HelloWorld")
.addModifiers(new Modifier[]{ Modifier.FINAL, Modifier.PUBLIC})
.addMethod(mainMethod)
.addField(androidVersion)
.build();
JavaFile javaFile = JavaFile.builder("com.hys.test", typeSpec).build();
javaFile.writeTo(System.out);
}
执行函数,结果如下:
package com.hys.test;
import java.lang.String;
public class HelloWorld {
private String androidVer = "Lollipop";
public static void main(String[] args) {
System.out.println("Hello World");
}
}
这里$S占位符,JavaPoet占位符如下:
- $S:字符串类型占位符
- $T:类型占位符
- $N:名称占位符(方法名或者变量名等)
- $L:字面常量
这里只是投石问路,关于JavaPoet更多API使用,请参见其文档
javax.lang.model.element
Element
用于 Java 的模型元素的接口。
- ExecutableElement:表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素
- PackageElement:表示一个包程序元素
- TypeElement:表示一个类或接口程序元素
- TypeParameterElement:表示类、接口、方法或构造方法元素的形式类型参数
- VariableElement:表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数
通过Element的getModifiers()获得元素的修饰符
Modifier
表示程序元素(如类、方法或字段)上的修饰符。
以下是常用修饰符:
- ABSTRACT:修饰符 abstract
- FINAL:修饰符 final
- NATIVE:修饰符 native
- PRIVATE:修饰符 private
- PROTECTED:修饰符 protected
- PUBLIC:修饰符 public
- STATIC:修饰符 static
- SYNCHRONIZED:修饰符 synchronized
通过Element的asType()获得元素的类型
TypeMirror
表示 Java 编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和 null 类型。
通过TypeMirror的getKind()类型的种类
TypeKind
表示类型的种类。
以下是常用的类型:
- ARRAY:数组类型
- BOOLEAN:基本类型 boolean
- BYTE:基本类型 byte
- CHAR:基本类型 char
- DECLARED:类或接口类型
- DOUBLE:基本类型 double
- ERROR:无法解析的类或接口类型。
- EXECUTABLE:方法、构造方法或初始化程序
- FLOAT:基本类型 float
- INT:基本类型 int
- LONG:基本类型 long
- NONE:在实际类型不适合的地方使用的伪类型
- NULL:null 类型
- PACKAGE:对应于包元素的伪类型
- SHORT:基本类型 short
- TYPEVAR:类型变量
- VOID:对应于关键字 void 的伪类型
获取元素的父元素
通过Element的getEnclosingElement返回元素的父元素。
获取元素上的注解
通过Element的getAnnotation(Class<A> annotationType)获得元素上的注解。
了解了上述内容,下面咱们开始搞事情
创建注解处理器
1.Android Studio的File->New->New module,如下图:
2.在弹出的Create New Module对话框中选择Java Library,命名为MockButterknife-complier,如下图:
3.创建注解处理器类,继承AbstractProcessor,覆盖getSupportedAnnotationTypes()、getSupportedSourceVersion()、process()三个方法,如下图:
4.注册注解处理器,在项目下创建resources->META-INF->Services目录,在Services目录下创建javax.annotation.processing.Processor文件,如下图:
5.编辑javax.annotation.processing.Processor文件,添加注解处理器类,如下图:
6.配置注解处理器,添加JavaPoet,如下:
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.squareup:javapoet:1.8.0'
}
7.创建自定义注解,咱们在这里创建两个注解:
- BindView注解
package com.hys.mockbutterknife.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
- OnClick注解
package com.hys.mockbutterknife.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnClick {
int[] value();
8.注册自定义注解到注解处理器,在AnnotationProcessor添加如下代码:
private Set<Class<? extends Annotation>> getSupportedAnnotations(){
Set<Class<? extends Annotation>> supportedAnnotations = new LinkedHashSet<>();
supportedAnnotations.add(BindView.class);
supportedAnnotations.add(OnClick.class);
return supportedAnnotations;
}
在getSupportedAnnotationTypes()方法中调用getSupportedAnnotations(),即将自定义注解注册到注解处理器,代码如下:
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> supportedAnnotationTypes = new LinkedHashSet<>();
Iterator ite = getSupportedAnnotations().iterator();
while (ite.hasNext()){
Class annotation = (Class<? extends Annotation>)ite.next();
supportedAnnotationTypes.add(annotation.getCanonicalName());
}
return supportedAnnotationTypes;
}
9.上面咱们已经注册了自定义注解,接下来应该处理这些注解(啰嗦,不处理,注册它们做啥?!)
后面以BindView为例
查找添加注解的元素
Iterator ite = env.getElementsAnnotatedWith(BindView.class).iterator();
验证元素合法性
- 验证元素是否可以访问
private boolean isInaccessible(Element element, String targetThing, Class<? extends Annotation> annotationClass) {
TypeElement enclosingElement = (TypeElement)element.getEnclosingElement();
//检查元素的访问修饰符
Set<Modifier> modifiers = element.getModifiers();
if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.STATIC)) {
this.error(element, "@%s %s must not be private or static. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName());
return true;
}
//检查元素的父元素
if (enclosingElement.getKind() != ElementKind.CLASS) {
this.error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName());
return true;
}
//检查父元素的访问修饰符
if (enclosingElement.getModifiers().contains(Modifier.PRIVATE)) {
this.error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)", annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(), element.getSimpleName());
return true;
}
return false;
}
- 验证元素所在包的合法性
private boolean isInWrongPackage(Element element, Class<? extends Annotation> annotationClass) {
TypeElement enclosingElement = (TypeElement)element.getEnclosingElement();
String qualifiedName = enclosingElement.getQualifiedName().toString();
//元素的父元素(即元素所在的类)不能在android的系统包中
if (qualifiedName.startsWith("android.")) {
this.error(element, "@%s-annotated class incorrectly in Android framework package. (%s)", annotationClass.getSimpleName(), qualifiedName);
return true;
}
////元素的父元素不能在java的资源包中
else if (qualifiedName.startsWith("java.")) {
this.error(element, "@%s-annotated class incorrectly in Java framework package. (%s)", annotationClass.getSimpleName(), qualifiedName);
return true;
}
return false;
}
- 验证元素类型的合法性
/*
* 递归验证
* 以TextView为例:isSubtypeOfType(typeMirror, "android.view.View")
*/
public static boolean isSubtypeOfType(TypeMirror typeMirror, String otherType) {
// 类型相同
if (isTypeEqual(typeMirror, otherType))
return true;
if (typeMirror.getKind() != TypeKind.DECLARED)
return false;
DeclaredType declaredType = (DeclaredType)typeMirror;
List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
if (typeArguments.size() > 0) {
StringBuilder typeString = new StringBuilder(declaredType.asElement().toString());
typeString.append('<');
for(int i = 0; i < typeArguments.size(); ++i) {
if (i > 0) {
typeString.append(',');
}
typeString.append('?');
}
typeString.append('>');
if (typeString.toString().equals(otherType)) {
return true;
}
}
Element element = declaredType.asElement();
if (!(element instanceof TypeElement)) {
return false;
} else {
TypeElement typeElement = (TypeElement)element;
// 获取元素的父类
TypeMirror superType = typeElement.getSuperclass();
// 检查父类的类型
if (isSubtypeOfType(superType, otherType)) {
return true;
} else {
Iterator var7 = typeElement.getInterfaces().iterator();
TypeMirror interfaceType;
do {
if (!var7.hasNext()) {
return false;
}
interfaceType = (TypeMirror)var7.next();
} while(!isSubtypeOfType(interfaceType, otherType));
return true;
}
}
}
生成Java源文件
- 生成类
private TypeSpec createTypeSpec(){
// 生成新类名,原类名+ _ViewBinding
String className = this.encloseingElement.getSimpleName().toString() + "_ViewBinding";
// 获取父元素的类型全称
TypeName targetTypeName = TypeName.get(this.encloseingElement.asType());
// 创建类构建器
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
.addModifiers(new Modifier[]{Modifier.PUBLIC}) // 添加public修饰符
.addField(targetTypeName, "target", new Modifier[]{Modifier.PRIVATE}); // 添加成员变量target
classBuilder.addFields(createFieldForListener());
if(isActivity()){
classBuilder.addMethod(createConstructorForActivity());
} else if(isView()){
classBuilder.addMethod(createConstructorForView());
} else if(isDialog()){
classBuilder.addMethod(createConstructorForDialog());
}
// 默认类构造器
classBuilder.addMethod(createBindConstructor());
// 生成类
return classBuilder.build();
}
- 生成JavaFile对象
public JavaFile brewJava() {
String packageName = MoreElements.getPackage(this.encloseingElement).getQualifiedName().toString();
return JavaFile.builder(packageName, createTypeSpec()).build();
}
- 生成Java源文件
...
JavaFile javaFile = bindSet.brewJava();
try{
javaFile.writeTo(this.processingEnv.getFiler());
}catch (IOException ex){
this.error(typeElement, "Unable to write binding for type %s: %s", typeElement, ex.getMessage());
}
...
创建API
注解处理器搞好了,还需要给用户提供API,用户才能使用。
咱们创建一个新的Module,Android Studio的File->New->New module,选择Android Library,命名为Mockbutterknife-source。
这个Module主要使用反射技术,动态的创建并调用上文中生成的类(下文中称为绑定类)。
- 编写API接口(其中之一)
@UiThread
public static void bind(Activity target) {
View sourceView = target.getWindow().getDecorView();
createBinding(target, sourceView);
}
- 动态创建绑定类,调用其构造器方法
private static void createBinding(Object target, View source) {
Class<?> targetClass = target.getClass();
// 查找targetClass名称+_ViewBinding的class文件,加载并返回构造器
Constructor constructor = findBindConstructorForClass(targetClass);
if (constructor == null) {
return ;
}
try {
constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException("Unable to create binding instance.", cause);
}
}
在APP中使用
- 配置APP,在build.gradle中添加如下内容:
dependencies {
...
annotationProcessor project(':MockButterknife-complier')
implementation project(path: ':MockButterknife-complier')
implementation project(path: ':Mockbutterknife-source')
- 为Activity添加自定义注解
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_click)
TextView tvClick;
@BindView(R.id.tv_dont_click)
TextView tvDontClcik;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MockButterKnife.bind(this);
initData();
}
...
@OnClick(value = {R.id.tv_click, R.id.tv_dont_click})
public void onClick(View view){
if(view.getId() == R.id.tv_click)
new AboutDialog().show(this.getSupportFragmentManager());
else if(view.getId() == R.id.tv_dont_click)
Toast.makeText(this, getString(R.string.main_toast), Toast.LENGTH_SHORT).show();
}
}
- 生成的class文件
package com.hys.annotationprocessortest;
import android.support.annotation.UiThread;
import android.view.View;
import android.widget.TextView;
public class MainActivity_ViewBinding {
private MainActivity target;
private View view2131165309;
private View view2131165310;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
public MainActivity_ViewBinding(final MainActivity target, View source) {
this.target = target;
this.target.tvClick = (TextView)source.findViewById(2131165309);
this.target.tvDontClcik = (TextView)source.findViewById(2131165310);
this.view2131165309 = source.findViewById(2131165309);
this.view2131165309.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
target.onClick(v);
}
});
this.view2131165310 = source.findViewById(2131165310);
this.view2131165310.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
target.onClick(v);
}
});
}
}
好了,关于如何构建编译时注解解析框架,就先讲到这,上述项目的具体代码在Github,感谢你耐心的阅读。
我是青岚之峰,如果读完后觉的有所收获,欢迎点赞加关注