概念
APT(Annotation Processing Tool)编译时注解,是javac的一个工具,在java源文件被编译为.class的过程中,注解处理器会去扫描java源文件处理注解,在这个过程中我们可以自定义注解器,根据自定义注解,生成java代码、文件,减少了很多重复代码的编写。很多流行的框架都是用的这个思想,比如Butterknife、Dragger、Room、组件化框架都是用的编译时注解原理,自动生成了代码,简化了使用。
其实,java源文件到加载运行过程中,很多环节都是可以操作利用的,这也是很多框架的思想。
在java加载运行过程中的各个环节都是有自己的玩法,
- 在编译的过程中,可以自定义注解处理器,通过处理注解来自动生成java代码
- 可以利用字节码插桩技术对class文件进行修改,实现数据收集SDK时,为了实现非侵入的,全量的数据采集,采用了AOP这也叫切面编程,在Android里面常用的框架是aspactj,典型的应用场景就是埋点、计算方法的停留时间。
- 动态代理主要是在class文件加载到虚拟机的时候自定义ClassLoader来完成,运用场景为热修复框架、换肤框架、插件化框架,还有retrofit核心逻辑也是动态代理。
- 运行时那就是大家都熟悉的反射。
实现
一般的apt框架都是习惯建立三个module,两个java module,一个Android lib module,这里以模仿butterknife来实现。
- apt-annotation 存放注解,例如@BindView
- apt-processor 存放自定义注解处理器,java代码将在这里组织生成
- apt-api 暴露给用户的api,我们生成的java代码怎么使用,需要提供api
当然这只是一个参考的目录结构。
创建annotation module
new ->module->java module 并创建注解类
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD})
public @interface BindeView {
int value() default -1;
}
@Retention 注解的存在时期,存在三个时期
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
SOURCE、CLASS、RUNTIME 由于是编译时注解,选择SOURCE就行了。
@Target表示被注解的类型,主要有:
- TYPE class上的注解
- FIELD 用来注解变量
- METHOD 作用与方法
- PARAMETER 注解方法的参数
这时候我们就可以使用注解:
public class MainActivity extends AppCompatActivity {
@BindeView(R.id.tv_hello_world)
public TextView tv;
@BindeView(R.id.tv_hello_world2)
public TextView tv2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
XcInject.bind(this);
tv.setText("注解成功");
tv2.setText("注解成功2");
}
}
注解作用于成员变量,值为View的id
创建注解处理器
同上,创建一个java module,建一个类继承自AbstractProcessor
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor{
private Types mTypeUtils;
private Elements mElementUtils;
private Filer mFiler;
private Messager mMessager;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment){
super.init(processingEnvironment);
mTypeUtils = processingEnv.getTypeUtils();
mElementUtils = processingEnv.getElementUtils();
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
}
@Override
public SourceVersion getSupportedSourceVersion() {
//支持的java版本
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
//支持的注解类型
Set<String> annotations = new HashSet<>();
annotations.add(BindView.class.getCanonicalName());
return annotations;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
所有的注解处理器必须继承AbstractProcessor类,复写四个重要方法
- init(ProcessingEnvironment processingEnv) ,当注解处理器被初始化的时候调用,传入ProcessingEnvironment类型,提供注解器运行时处理注解的一些工具类,一般有四个常用的方法
public interface ProcessingEnvironment {
// 用来处理注解器的异常信息
Messager getMessager();
// 用来生成java源文件工具类
Filer getFiler();
// 用来处理被注解的Element,获取Element信息
Elements getElementUtils();
// 用来操作类型数据
Types getTypeUtils();
}
- getSupportedSourceVersion()支持java的版本,一般值为SourceVersion.latestSupported();就可以了
- getSupportedAnnotationTypes(),返回一个当前注解处理器所有支持的注解的集合。当前注解处理器需要处理那种注解就加入那种注解。如果类型符合,就会调用process()方法
当然上面两个方法在java7之后可以用注解代替:
@AutoService(Processor.class)
@SupportedAnnotationTypes("xc.com.apt.annotation.BindeView")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class XAptProcessor extends AbstractProcessor{
...
}
- AutoService
我们自定义的注解器需要被javac识别并且调用那就必须给注解处理器打一个标记,javac工具扫描到了注解处理器类会实例化它并且调用相应的方法。这里可以导入:
implementation 'com.google.auto.service:auto-service:1.0-rc4'
这是
Element
可以说注解处理器中最重要的就是Element这个元素了,所有被注解过的元素经过注解处理器扫描之后被封装为Element元素,Element是个接口,有五个实现类,分别代表了不同类型的类型的元素:
package com.example; // PackageElement
public class Foo // TypeElement
<T> { // TypeParameterElement
private int a; // VariableElement
private Foo other; // VariableElement
public Foo () {} // ExecuteableElement
public void setA ( // ExecuteableElement
int newA // TypeElement
) {}
}
- TypeElement 一个类的元素,如果注解处理器处理的对象是类或者接口,那么这种元素将是TypeElement
- PackageElement 一个包名的元素
- VariableElement 一般为变量、枚举、方法参数元素
- ExecuteableElement 构造函数、方法的元素
- TypeParameterElement 泛型元素
public interface Element extends javax.lang.model.AnnotatedConstruct {
// 获取元素的类型
TypeMirror asType();
// 获取Element的类型
ElementKind getKind();
// 获取修饰符
Set<Modifier> getModifiers();
// 获取名字
Name getSimpleName();
// 获取当前元素被封装的元素,比如:当前Element是VariableElement,获取的就是他的所在的类的Elenment --> TypeElement
Element getEnclosingElement();
<A extends Annotation> A getAnnotation(Class<A> annotationType);
}
TypeElement
public interface TypeElement extends Element, Parameterizable, QualifiedNameable {
List<? extends Element> getEnclosedElements();
NestingKind getNestingKind();
Name getQualifiedName();
Name getSimpleName();
TypeMirror getSuperclass();
List<? extends TypeMirror> getInterfaces();
List<? extends TypeParameterElement> getTypeParameters();
Element getEnclosingElement();
}
- getEnclosedElements: 获取被封装的上级的元素
- getQualifiedName: 获取全限定名
- getSimpleName 获取名字
- getInterfaces 获取接口
- getTypeParameters 获取泛型
VariableElement
代表变量类型,主要有三个方法
- getConstantValue() :获取初始化变量的值
- getEnclosingElement : 获取类相关元素
- getSimpleName :获取变量的名字
编写注解处理器
准备工作做好了那就开始编写注解处理器,主要分为两个步骤,首先收集注解的元素信息,再生成java代码
收集信息:
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindeView.class);
for (Element ele : elements) {
// 判断Element的类型是否是变量类型Element
if(!(ele instanceof VariableElement)) {
return false;
}
// 第二种校验方式
if(ele.getKind() != ElementKind.FIELD) {
return false;
}
// 强转
VariableElement variableElement = (VariableElement)ele;
// 获取封装Element,即变量所属的类的Element
TypeElement enclosingElement = (TypeElement) ele.getEnclosingElement();
// 获取全限定名
String qualifiedName = enclosingElement.getQualifiedName().toString();
// 获取类名
String className = enclosingElement.getSimpleName().toString();
// 获取注解类的包名,将生成后的代码放置一个包名下
String pkgName = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
ClassName hostName = ClassName.bestGuess(qualifiedName);
// 获取变量的名字
String variableName = variableElement.getSimpleName().toString();
// 获取变量的类型
String variableType = variableElement.asType().toString();
// 获取注解的值,也就是变量的id
BindeView annotation = ele.getAnnotation(BindeView.class);
int id = annotation.value();
// 生成代码
// 手动拼接示例代码
// analysisAnnotated(Element element)
// javapoet框架生成代码
MethodSpec method = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addAnnotation(Override.class)
.addParameter(hostName, "host")
.addParameter(Object.class, "object")
.addCode(""
+ "if(object instanceof android.app.Activity){\n"
+ "\thost." + variableName + " = (" + variableType + ")(" + "((android.app.Activity)object).findViewById(" + id + "));\n"
+ "\t}else {\n"
+ "\thost." + variableName + " = (" + variableType + ")(" + "((android.view.View)object).findViewById(" + id + "));\n"
+ "}"
+ ""
).build();
ClassName interfaceName = ClassName.bestGuess("xc.com.apt.api.ViewInjector");
TypeSpec classType = TypeSpec.classBuilder(className + "_ViewInjector")
.addModifiers(Modifier.PUBLIC)
.addMethod(method)
.addSuperinterface(ParameterizedTypeName.get(interfaceName, hostName))
.build();
JavaFile build = JavaFile.builder(pkgName, classType)
.addFileComment("this file don't modify")
.build();
try {
build.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
private void analysisAnnotated(Element element) {
StringBuilder builder = new StringBuilder()
.append("package com.xc.apt;\n\n")
.append("public class APTTest {\n\n")
.append("\tpublic String getMessage() {\n")
.append("\t\treturn \"");
String objectType = element.getSimpleName().toString();
builder.append(objectType).append(" hello world!\\n");
builder.append("\";\n")
.append("\t}\n")
.append("}\n");
try {
JavaFileObject source = processingEnv.getFiler().createSourceFile("com.xc.apt.APTTest");
Writer writer = source.openWriter();
writer.write(builder.toString());
writer.flush();
writer.close();
} catch (IOException e) {
}
}
- roundEnvironment.getElementsAnnotatedWith 获取被BindView注解过的元素集合
- for循环遍历所有被注解过的元素,因为我们注解的是变量类型元素,所以做下类型校验可以用instanceof或者ele.getKind()校验都是可以的,校验完成进行强转
- variableElement.getEnclosingElement()获取变量所在的类的TypeElement
- 获取类的全限定名、类名、包名、变量信息等为生成代码做准备
- 每个Activity 生成一个对应的xxxActivity_ViewInjector的类的代码
生成代码的方式可以自己手动拼接,按照java源文件的语法拼接字符串,这种方式相对麻烦,不够优雅,而且面对相对复杂的场景难以封装。所以早就前人造轮子了,用javapoet 框架来组织代码非常方便,工程引入:
implementation 'com.squareup:javapoet:1.10.0'
具体的用法可以去github看wiki,写的很详细
到此为止,注解处理器的代码基本功能已经完成了,但是现在还不能生成java代码,因为现在它仅仅只是一个普通的java类而已,那么我们怎么样才能是注解处理器在编译时被编译器识别、实例化并且运行呢?我们需要把注解处理器注册给编译器。
1、在 processors 库的 main 目录下新建 resources 资源文件夹;
2、在 resources文件夹下建立 META-INF/services 目录文件夹;
3、在 META-INF/services 目录文件夹下创建 javax.annotation.process.Processors 文件;
4、在 javax.annotation.process.Processors 文件写入注解处理器的全限定名;
这样在编译器扫描源文件的时候回去改文件中查找是否有自定义注解器,如果有就会实例化运行。为了简化步骤google也为我们写了个框架只需一个注解也就是AutoService,在上面源码中也也已看到导入:
implementation 'com.google.auto.service:auto-service:1.0-rc4'
最后主工程还需要依赖processor
annotationProcessor project(':apt-processor')
查看源码就知道AutoService也是一个注解处理器,专门为自定义注解处理器生成META-INF文件的
可以在processor->build下看到:
配置好了之后rebuild工程就可以看到生成好的代码app\build\generated\source\apt:
public class MainActivity_ViewInjector implements ViewInjector<MainActivity> {
@Override
public void inject(MainActivity host, Object object) {
if(object instanceof android.app.Activity){
host.tv = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131165320));
}else {
host.tv = (android.widget.TextView)(((android.view.View)object).findViewById(2131165320));
}}
}
inject方法传入两个参数一个是host,一个是object,在Activity中host和object是同一个对象,这里为了兼容Fragment把host和object分开。
公开API
我们自动生成的代码如果给用户使用呢,不可能说用户还要知道你的代码生成规则,知道类名然后调用,那这个体验就有点差了,这就需要我们暴露公共方法。
public class XcInject {
public static void bind(Object host) {
String name = host.getClass().getName();
try {
Class<?> aClass = Class.forName(name + "_ViewInjector");
Method method = aClass.getMethod("inject", host.getClass(), Object.class);
method.invoke(aClass.newInstance(),host,host);
} 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();
}
}
}
使用:
public class MainActivity extends AppCompatActivity {
@BindeView(R.id.tv_hello_world)
public TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
XcInject.bind(this);
tv.setText("注解成功");
}
}
到这里就基本完成了,但是要做下代码优化,上述注解处理器只能处理单个变量的情况,实际上一个Activity是有多个变量使用注解的,这就需要把一个相同的类的所有注解元素存起来,统一去findViewById。
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindeView.class);
for (Element ele : elements) {
/** 判断Element的类型是否是变量类型Element */
if(!(ele instanceof VariableElement)) {
return false;
}
/** 第二种校验方式*/
if(ele.getKind() != ElementKind.FIELD) {
return false;
}
VariableElement variableElement = (VariableElement)ele;
/** 获取封装Element,即变量所属的类的Element */
TypeElement enclosingElement = (TypeElement) variableElement.getEnclosingElement();
/** 获取全限定名 */
String qualifiedName = enclosingElement.getQualifiedName().toString();
/** 获取类名 */
String className = enclosingElement.getSimpleName().toString();
/** 获取注解类的包名,将生成后的代码放置一个包名下 */
String pkgName = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
// ClassName hostName = ClassName.bestGuess(qualifiedName);
// /** 获取变量的名字 */
// String variableName = variableElement.getSimpleName().toString();
// /** 获取变量的类型 */
// String variableType = variableElement.asType().toString();
/** 获取注解的值,也就是View的id*/
BindeView annotation = ele.getAnnotation(BindeView.class);
int id = annotation.value();
/** 将相同类的Element用map装起来*/
ClassInfo classInfo = classInfoMap.get(qualifiedName);
if(classInfo == null) {
classInfo = new ClassInfo(pkgName,className);
classInfoMap.put(qualifiedName,classInfo);
}
/** 用id作为键值,保证唯一性 */
classInfo.getVariableElementMap().put(id,variableElement);
}
/** 收集完信息,生成代码*/
for (String key:classInfoMap.keySet()) {
ClassInfo classInfo = classInfoMap.get(key);
ClassName hostName = ClassName.bestGuess(key);
MethodSpec method = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addAnnotation(Override.class)
.addParameter(hostName, "host")
.addParameter(Object.class, "object")
.addCode( ""
+ "if(object instanceof android.app.Activity){\n"
+ getMethodCode(classInfo,"android.app.Activity")
+ "}else {\n"
+ getMethodCode(classInfo,"android.view.View")
+ "}"
+ ""
).build();
/** 生成class相关信息*/
ClassName interfaceName = ClassName.bestGuess("xc.com.apt.api.ViewInjector");
TypeSpec classType = TypeSpec.classBuilder(classInfo.getClassName() + "_ViewInjector")
.addModifiers(Modifier.PUBLIC)
.addMethod(method)
.addSuperinterface(ParameterizedTypeName.get(interfaceName, hostName))
.build();
JavaFile build = JavaFile.builder(classInfo.getPkg(), classType)
.addFileComment("this file don't modify")
.build();
try {
build.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
这样就是显示了ButterKnife的基本功能 了。