注解的本质
编程中经常会使用到注解,比如 Override,
找到 Annotation 这个接口,然后 Control + H
可以发现 Override 继承自 Annotation。
所以注解的本质很简单:注解就是一个继承了 Annotation 的接口。
注解的作用
注解就是用来标记 SourceCode 中的 Class
,Field
,Method
,给某一段代码标注额外的信息,可以理解为程序员偷懒的方式。
比如 findViewById
写起来很麻烦,就用 butterknife 来偷个懒:
@BindView(R.id.user) EditText username;
那么既然标记了就需要在背后进行解析,否则只能当注释看,具体的解析方式后面再讲。
自定义一个注解
自定义一个叫做 Name 的注解,它有一个方法 value ,用来设置一个 String 值:
@Target(value = {ElementType.FIELD, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Name {
String value();
}
我们会发现注解上面也有注解,比如上面的 @Target
,@Retention
,这种用来描述注解的注解被称为元注解。
元注解
-
@Target
:描述注解作用的目标,比如作用于字段还是方法,比如下面几个常用的类型:ElementType.TYPE
:作用于类和接口ElementType.FIELD
:作用于字段ElementType.METHOD
:作用于方法
-
@Retention
:注解的生命周期:RetentionPolicy.SOURCE
:编译期可见,不会写入 class 文件,通常用于 apt 编译时自动生成代码
RetentionPolicy.CLASS
:类加载阶段丢弃,会写入 class 文件,运行时不可见RetentionPolicy.RUNTIME
:永久保存,运行时可以反射获取
其他元注解 @Inherited
, @Documented
很简单,在此不再讨论。
解析自定义注解
根据注解的生命周期大致可以把解析方式分为两类,比如对于 RUNTIME 标识的可以通过代码在运行时通过反射进行解析,对于 SOURCE 标识的可以通过 apt(Annotation Processing Tool)在编译期进行解析。
运行时反射解析注解
- 通过反射获取某个被注解的类,方法,字段
- 获取注解对象上的注解信息
public class Test {
public static void main(String[] args){
testAnnotation();
}
@Name(value = "Mr.test")
public static void testAnnotation(){
try {
Class clazz = Test.class;
// 1. 获取被注解的方法
Method method = clazz.getMethod("testAnnotation",null);
// 2.获取 method 上的注解及其 value
Name name = method.getAnnotation(Name.class);
System.out.println(name.value());
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
编译期解析注解
APT 是 javac 用来处理注解的工具,它可以在编译期扫描所有的注解,获取注解的值以及被注解的 Element,然后自定义一些操作,比如可以检查类是否有有无参构造函数,这个功能可以参考这里;你也可以在编译期根据注解生成一些模板代码,就像 butterknife 所做的事情,下面就以这个功能为例。 在编译期解析注解的步骤如下:
- 自定义注解类
- 实现一个 AbstractProcessor 的子类 BindViewProcessor
- 让编译器识别自定义的 BindViewProcessor
自定义注解类
新建一个 JavaModule,命名为 annotation-lib。 自定义一个 BindView 类,它作用在 Field 上,并且只会在编译期存在,它有一个 value 方法,用来获取被注解 View 的 id:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindView {
int value();
}
自定义一个 AbstractProcessor 的子类
新建一个 JavaModule,命名为 annotation-processor,它的 build.gradle 中需要依赖 annotation-lib:
dependencies {
implementation project(':annotation-lib')
}
自定义 BindViewProcessor 如下:
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
private Elements elementUtil;
private Map<String, ClassCreatorProxy> proxyMap = new HashMap<>();
private ProcessingEnvironment processingEnv;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
System.out.println("-------BindViewProcessor init");
elementUtil = processingEnvironment.getElementUtils();
processingEnv = processingEnvironment;
}
/**
* 指定 java 版本,这个可以用注解 @SupportedSourceVersion(SourceVersion.RELEASE_8) 来代替
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 设置这个处理器是处理什么类型注解的,这个可以用注解 @SupportedAnnotationTypes(com.wangzhen.annotation_complier.StudentProcessor) 来代替
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> annotations = new LinkedHashSet<>();
annotations.add(BindView.class.getCanonicalName());
return annotations;
}
/**
* @param set 请求处理的注解类型
* @param roundEnvironment
* @return true:表示当前注解已经处理;false:可能需要后续的 processor 来处理
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
System.out.println("-------BindViewProcessor process");
// todo 解析注解,生成 java 文件
return true;
}
}
需要实现的 4 个方法代码中已经添加了注释,这里就不在多说了,但是 init 方法有一个 ProcessingEnvironment 参数,process 方法中有一个 RoundEnvironment 参数,这两个参数又是干什么的呢?
ProcessingEnvironment
package javax.annotation.processing;
public interface ProcessingEnvironment {
// 返回用来报告错误、警报和其他通知的 Messager,可以用来打印日志。
Messager getMessager();
// 返回用来创建新源、类或辅助文件的 Filer。
Filer getFiler();
// 返回用来对 Element 进行操作的实用工具方法。
Elements getElementUtils();
// 用来对类型进行操作的实用工具方法。
Types getTypeUtils();
...
}
getFiler
好理解,就是返回一个 Filer
,它支持创建新文件:
public interface Filer {
// 创建一个新的源文件,并返回一个对象以允许写入它。
JavaFileObject createSourceFile(CharSequence var1, Element... var2) throws IOException;
// 创建一个新的类文件,并返回一个对象以允许写入它。
JavaFileObject createClassFile(CharSequence var1, Element... var2) throws IOException;
// 创建一个用于写入操作的新辅助资源文件,并为它返回一个文件对象。
FileObject createResource(Location var1, CharSequence var2, CharSequence var3, Element... var4) throws IOException;
// 返回一个用于读取现有资源的对象。
FileObject getResource(Location var1, CharSequence var2, CharSequence var3) throws IOException;
}
比如我们将使用它的 createSourceFile 创建一个 java 文件。
Elements
用来对 Element
进行操作,那么 Element
又是什么呢?
Element
表示一个程序元素,比如包、类或者方法。
它有几个常见的子类:
Element
|--- PackageElement // 包元素,可以获得包名
|--- TypeElement // 表示一个类或接口
|--- VariableElement //表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。
可以看出 Element
其实就是包含了我们所写代码的一些信息,它常用的方法如下:
package javax.lang.model.element;
public interface Element extends AnnotatedConstruct {
// 返回此元素直接封装(非严格意义上)的元素,比如如果是一个 VariableElement,获得是它所在的类。
List<? extends Element> getEnclosedElements();
// 获取此元素上的注解
<A extends Annotation> A getAnnotation(Class<A> var1);
...
}
Elements
常见的方法如下:
package javax.lang.model.util;
public interface Elements {
// 返回所给元素的包元素
PackageElement getPackageOf(Element var1);
// 返回类型元素的所有成员,不管是继承的还是直接声明的。
List<? extends Element> getAllMembers(TypeElement var1);
...
}
所以我们可以通过 Elements
获取所有包元素,这样就获取到了新建 java 文件的包名,可以通过 Filer
创建一个 java 文件,现在缺少的是获取被 BindView 注释的 Element 了,它可以通过 RoundEnvironment
来获得。
RoundEnvironment
RoundEnvironment
常见的方法如下:
package javax.annotation.processing;
public interface RoundEnvironment {
// 返回使用给定类型注解的元素。
Set<? extends Element> getElementsAnnotatedWith(TypeElement var1);
// 返回使用给定类型注解的元素。
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> var1);
...
}
在 process 方法中拼写字符串然后生成文件
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
System.out.println("-------BindViewProcessor process");
proxyMap.clear();
// 1.获取所有有 BindView 注解的 Element,由于 BindView 是作用在 Field 上的,所以这些 Element 一定是 Field
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
// 2.遍历所有被注解的 Element
for (Element element : elements) {
VariableElement variableElement = (VariableElement) element;
// 3.获得它所在的类
TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
String fullClassName = classElement.getQualifiedName().toString();
ClassCreatorProxy classCreatorProxy = proxyMap.get(fullClassName);
if (classCreatorProxy == null) {
classCreatorProxy = new ClassCreatorProxy(elementUtil, classElement);
proxyMap.put(fullClassName, classCreatorProxy);
}
// 4.获得它注解中的信息
BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
int id = bindAnnotation.value();
classCreatorProxy.putElement(id, variableElement);
// 5.创建一个 java 文件
createJavaFile();
}
return true;
}
private void createJavaFile(){
for(String key:proxyMap.keySet()){
ClassCreatorProxy proxyInfo = proxyMap.get(key);
try {
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(proxyInfo.getProxyClassFullName(),proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
// 6.拼写 java 文件中的代码字符串
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close();
System.out.println("-------BindViewProcessor create java file success");
} catch (IOException e) {
System.out.println("-------BindViewProcessor create java file failed");
e.printStackTrace();
}
}
}
ClassCreatorProxy 只是用来拼写代码字符串:
package com.wangzhen.annotation_processor;
public class ClassCreatorProxy {
private String bindingClassName;
private String packageName;
private TypeElement typeElement;
private Map<Integer, VariableElement> variableElementMap = new HashMap<>();
public ClassCreatorProxy(Elements elements, TypeElement classElement) {
this.typeElement = classElement;
PackageElement packageElement = elements.getPackageOf(typeElement);
packageName = packageElement.getQualifiedName().toString();
bindingClassName = typeElement.getSimpleName() + "_ViewBinding";
}
public void putElement(int id, VariableElement element) {
variableElementMap.put(id, element);
}
public String generateJavaCode() {
StringBuilder builder = new StringBuilder();
// 拼写包名
builder.append("package ").append(packageName).append(";\n\n")
// 拼写依赖
.append("import com.wangzhen.annotation_lib.*;").append("\n\n")
// 拼写类结构
.append("public class ").append(bindingClassName).append(" {\n");
generateMethods(builder);
builder.append('\n');
builder.append("}\n");
return builder.toString();
}
// 拼写方法
private void generateMethods(StringBuilder builder) {
builder.append("public void bind(" + typeElement.getQualifiedName() + " host ) {\n");
for (int id : variableElementMap.keySet()) {
VariableElement element = variableElementMap.get(id);
String name = element.getSimpleName().toString();
String type = element.asType().toString();
builder.append("host." + name).append(" = ");
builder.append("(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));\n");
}
builder.append(" }\n");
}
public String getProxyClassFullName() {
return packageName + "." + bindingClassName;
}
public TypeElement getTypeElement() {
return typeElement;
}
}
让编译器识别自定义的 BindViewProcessor
虽然代码写好了,但是还需要编译器在编译期间执行我们的代码,主要包含两步:
- 向编译器声明我们的 BindViewProcessor 最简单的方式是使用 google 提供的 AutoService:
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
...
}
使用 AutoService 需要在 annotaiton-processor module 下导入依赖:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.auto.service:auto-service:1.0-rc6'
implementation project(':annotation-lib')
}
- 在主 module app 中添加依赖 annotation-processor
dependencies {
...
implementation project(':annotation-lib')
// 需要使用 annotationProcessor
annotationProcessor project(':annotation-processor')
}
使用
在 app 下的 MainActivity 使用 BindView,然后使用反射调用我们生成的 MainActivity_ViewBinding.java:
public class MainActivity extends AppCompatActivity {
@BindView(value = R.id.tv_test)
TextView tvTest;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bind();
tvTest.setText("bind success");
}
private void bind() {
try {
Class bindViewClazz = Class.forName(this.getClass().getName() + "_ViewBinding");
Method method = bindViewClazz.getMethod("bind", this.getClass());
method.invoke(bindViewClazz.newInstance(), this);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
现在我们编译一下项目,编译器会在下面的路径中生成我们所拼写的 java 文件:
app
|- build
|-- generated
|--- source
|---- apt
|----- debug
|------ 你的包名
|------- MainActivity_ViewBinding.java
MainActivity_ViewBinding.java 内容如下:
package com.wangzhen.annotationjavatest;
import com.wangzhen.annotation_lib.*;
public class MainActivity_ViewBinding {
public void bind(com.wangzhen.annotationjavatest.MainActivity host) {
host.tvTest = (android.widget.TextView) (((android.app.Activity) host).findViewById(2131165359));
}
}
通过 javapoet 生成代码
手动拼写字符串易出错且麻烦,可以用 javapoet 像诗人一样书写 java source code:
public TypeSpec generateJavaCodeByPoet(){
TypeSpec bindingClass = TypeSpec.classBuilder(bindingClassName)
.addModifiers(Modifier.PUBLIC)
.addMethod(generateMethodByPoet())
.build();
return bindingClass;
}
private MethodSpec generateMethodByPoet(){
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(ClassName.bestGuess(typeElement.getQualifiedName().toString()),"host");
for (int id : variableElementMap.keySet()) {
VariableElement element = variableElementMap.get(id);
String name = element.getSimpleName().toString();
String type = element.asType().toString();
String codeString = "host." + name + " = " + "(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));";
methodBuilder.addCode(codeString);
}
return methodBuilder.build();
}