一、前言
随着 Android 开发的发展,越来越多场景需要用到编译插桩了。日常开发中我们肯定也都接触过编译插桩,只是没有去深入挖掘它的实现原理,比如 ButterKnife、无痕埋点等,所以学习编译插桩不仅可以提升我们的竞争力,还可以让我们在开发中开拓视野、更好地解决问题。
现在让我们一起学习一些编译插桩的基础来入门。
二、编译插桩基础知识
顾名思义,所谓的编译插桩就是在代码编译期间修改已有的代码或者生成新代码。它主要有以下几种应用场景:
-
代码生成。除了 Dagger、ButterKnife 这些常用的注解生成框架,Protocol Buffers、数据库 ORM 框架也都会在编译过程生成代码。代码生成隔离了复杂的内部实现,让开发更加简单高效,而且也减少了手工重复的劳动量,降低了出错的可能性。
-
代码监控。除了网络监控和耗电监控,我们可以利用编译插桩技术实现各种各样的性能监控。为什么不直接在源码中实现监控功能呢?首先我们不一定有第三方 SDK 的源码,其次某些调用点可能会非常分散,例如想监控代码中所有 new Thread() 调用,通过源码的方式并不那么容易实现。
-
代码修改。我们在这个场景拥有无限的发挥空间,例如某些第三方 SDK 库没有源码,我们可以给它内部的一个崩溃函数增加 try catch,或者说替换它的图片库等。我们也可以通过代码修改实现无痕埋点,就像网易的HubbleData、51 信用卡的埋点实践。
-
代码分析。例如检查代码中的 new Thread() 调用、检查代码中的一些敏感权限使用等。事实上,Findbugs 这些第三方的代码检查工具也同样使用的是编译插桩技术实现。
编译插桩技术非常有趣,同样也很有价值,掌握它之后,可以完成一些其他技术很难实现或无法完成的任务。学会这项技术以后,我们就可以随心所欲地操控代码,满足不同场景的需求。
我们先来看看 java 的编译流程:
编译插桩是从代码编译的哪个流程介入的呢?我们可以把它分为两类:
- Java 文件。类似 APT、AndroidAnnotation 这些代码生成的场景,它们生成的都是 Java 文件,是在编译的最开始介入。
- 字节码。对于代码监控、代码修改以及代码分析这三个场景,一般采用操作字节码的方式。可以操作“.class”的 Java 字节码,也可以操作“.dex”的 Dalvik 字节码,这取决于我们使用的插桩方法。常用的字节码插桩方法有 AspectJ、ASM。
根据下图可以看出它们介入的流程:
需要注意的是,AspectJ 是 Java 中流行的 AOP(aspect-oriented programming)编程扩展框架,网上很多文章说它处理的是 Java 文件,其实并不正确,它内部也是通过字节码处理技术实现的代码注入。
相对于 Java 文件方式,字节码操作方式功能更加强大,应用场景也更广,但是它的使用复杂度更高。
三、APK 编译打包流程
我们来看一下详细的编译打包流程图:
从上面的流程图,我们可以看出apk打包流程可以分为以下七步
- 通过 AAPT 打包 res 资源文件,生成 R.java、resources.arsc 和 res 文件(二进制 & 非二进制如 res/raw 和 pic 保持原样)
- 通过 aidl 工具处理 .aidl 文件,生成对应的 Java 接口文件
- 通过 Java Compiler 编译 R.java、Java 接口文件、Java 源文件,生成 .class 文件
- 通过 dex 命令,将 .class 文件和第三方库中的 .class 文件处理生成 classes.dex
- 通过 apkbuilder 工具,将 aapt 生成的 resources.arsc 和 res 文件、assets 文件和 classes.dex 一起打包生成 apk
- 通过 Jarsigner 工具,对上面的 apk 进行 debug 或 release 签名
- 通过 zipalign 工具,将签名后的 apk 进行对齐处理。
APT 之注解知识
一、前言
不管是在 Java 后端开发还是 Android 开发,Java 注解都有很广泛的运用。在 Android 中要想看懂很多开源库如 ARouter、dagger、ButterKnife 等都不得不弄懂注解知识,想更好地提升开发效率和代码质量注解可以帮上很大的忙。下面我们一起来学习注解知识。
二、注解定义
注解(java.lang.annotation,接口 Annotation,在 JDK 5.0 及以后版本引入)是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用 Annotation,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证、处理或者进行部署。
注解不能运行,它只有成员变量,没有方法,跟 public、final 等修饰符的地位一样,都是程序元素的一部分,不能作为一个程序元素使用。其实大家都是用过注解的,只是可能没有过深入了解它的原理和作用,比如肯定见过 @Override 、@Deprecated 等。注解可以将一些本来重复性的工作变成程序自动完成,简化和自动化该过程。比如用于生成 Java doc、编译时进行格式检查、自动生成代码等,用于提升软件的质量和提高软件的生产效率。
三、元注解
平时我们使用的注解有来自 JDK 里包含的,也有 Android SDK 里包含的,也可以自定义。这些注解是怎么创建的呢?Java 提供了四种元注解来专门负责新注解的创建工作,即注解其他注解。
3.1、@Target
定义了 Annotation 所修饰的对象范围,取值:
- ElementType.CONSTRUCTOR:用于描述构造器
- ElementType.FIELD:用于描述域
- ElementType.LOCAL_VARIABLE:用于描述局部变量
- ElementType.METHOD:用于描述方法
- ElementType.PACKAGE:用于描述包
- ElementType.PARAMETER:用于描述参数
- ElementType.TYPE:用于描述类、接口(包括注解类型)或 enum 声明
3.2、@Retention
定义了该 Annotation 被保留的时间长短,取值:
- RetentionPoicy.SOURCE:注解只保留在源文件,当 Java 文件编译成 class 文件的时候注解被遗弃。可以用于做一些检查性的操作,比如 @Override 和 @SuppressWarnings
- RetentionPoicy.CLASS:注解被保留到 class 文件,但 jvm 加载 class 文件时被遗弃,这是默认的生命周期。可以用于在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife)
- RetentionPoicy.RUNTIME:注解不仅被保存到 class 文件中,jvm 加载 class 文件之后仍然存在。可以用于在运行时去动态获取(反射)注解信息。
3.3、@Documented
标记注解,用于描述其它类型的注解应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc 类的工具文档化,不用赋值。
3.4、@Inherited
标记注解,允许子类继承父类的注解。 这里一开始有点理解不了,需要断句一下,允许子类继承父类的注解。示例:
@Target(value = ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Sample {
public String name() default "";
}
@Sample
class Test{
}
class Test2 extents Test{
}
这样类 Test2 其实也有注解 @sample 。
另外在写法上,如果成员名称是 value,则在赋值过程中可以简写。如果成员类型为数组但是只赋值一个元素,则也可以简写。示例以下三个写法都是等价的。
正常写法:
@Documented
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.ANNOTATION_TYPE})
public @interface Target {
ElementType[] value();
}
省略 value 的写法(只有成员名称是 value 时才能省略):
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Target {
ElementType[] value();
}
成员类型是数组,只赋值一个元素的简写:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
四、其他注解
4.1、JDK 内置的其他注解
@Override、@Deprecated、@SuppressWarnings、@SafeVarargs、@FunctionalInterface、@Resources等。
4.2、Android SDK 内置的注解
Android SDK 内置的注解都在 com.android.support:support-annotations 包里,下面以 'com.android.support:support-annotations:25.2.0' 为例
-
资源引用限制类:用于限制参数必须为对应的资源类型
@AnimRes、@AnyRes、@ArrayRes、@AttrRes、@BoolRes、@ColorRes等。
-
线程执行限制类:用于限制方法或者类必须在指定的线程执行
@AnyThread、@BinderThread、@MainThread、@UiThread、@WorkerThread。
-
参数为空性限制类:用于限制参数是否可以为空
@NonNull、@Nullable。
- 类型范围限制类:用于限制标注值的值范围
@FloatRang、@IntRange。
- 类型定义类:用于限制定义的注解的取值集合
@IntDef、@StringDef。
- 其他的功能性注解:
@CallSuper、@CheckResult、@ColorInt、@Dimension、@Keep、@Px、@RequiresApi、@RequiresPermission、@RestrictTo、@Size、@VisibleForTesting。
五、自定义注解
使用收益最大的,还是需要根据自身需求自定义注解,下面依次介绍三种类型的注解自定义示例:
5.1、RetentionPolicy.SOURCE
一般函数的参数值有限定的情况,比如 View.setVisibility 的参数就有限定,可以看到 View.class 源码里除了 @IntDef,还有 @StringDef。
@IntDef({VISIBLE, INVISIBLE, GONE})
@Retention(RetentionPolicy.SOURCE)
public @interface Visibility {}
public static final int VISIBLE = 0x00000000;
public static final int INVISIBLE = 0x00000004;
public static final int GONE = 0x00000008;
public void setVisibility(@Visibility int visibility) {
setFlags(visibility, VISIBILITY_MASK);
}
5.2、RetentionPolicy.RUNTIME
运行时注解的定义如下:
// 适用类、接口(包括注解类型)或枚举
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ClassInfo {
String value();
}
// 适用field属性,也包括enum常量
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FieldInfo {
int[] value();
}
// 适用方法
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodInfo {
String name() default "long";
int age() default 27;
}
定义一个测试类来使用这些注解:
@ClassInfo("Test Class")
public class TestRuntimeAnnotation {
@FieldInfo(value = {1, 2})
public String fieldInfo = "FiledInfo";
@MethodInfo(name = "BlueBird")
public static String getMethodInfo() {
return return fieldInfo;
}
}
使用注解:
private void _testRuntimeAnnotation() {
StringBuffer sb = new StringBuffer();
Class<?> cls = TestRuntimeAnnotation.class;
Constructor<?>[] constructors = cls.getConstructors();
// 获取指定类型的注解
sb.append("Class注解:").append("\n");
ClassInfo classInfo = cls.getAnnotation(ClassInfo.class);
if (classInfo != null) {
sb.append(cls.getSimpleName()).append("\n");
sb.append("注解值: ").append(classInfo.value()).append("\n\n");
}
sb.append("Field注解:").append("\n");
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
FieldInfo fieldInfo = field.getAnnotation(FieldInfo.class);
if (fieldInfo != null) {
sb.append(field.getName()).append("\n");
sb.append("注解值: ").append(Arrays.toString(fieldInfo.value())).append("\n\n");
}
}
sb.append("Method注解:").append("\n");
Method[] methods = cls.getDeclaredMethods();
for (Method method : methods) {
MethodInfo methodInfo = method.getAnnotation(MethodInfo.class);
if (methodInfo != null) {
sb.append(Modifier.toString(method.getModifiers())).append(" ")
.append(method.getName()).append("\n");
sb.append("注解值: ").append("\n");
sb.append("name: ").append(methodInfo.name()).append("\n");
sb.append("age: ").append(methodInfo.age()).append("\n");
}
}
System.out.print(sb.toString());
}
所做的操作都是通过反射获取对应元素,再获取元素上面的注解,最后得到注解的属性值。因为涉及到反射,所以运行时注解的效率多少会受到影响,现在很多的开源项目使用的是编译时注解。
5.3、RetentionPolicy.CLASS
步骤如下:
5.3.1 添加依赖(根据情况)
如果 Gradle 插件版本是 2.2 以上的话,不需要添加以下android-apt依赖。
classpath 'com.android.tools.build:gradle:2.2.1'
在整个工程的 build.gradle 中添加 android-apt 的依赖:
buildscript { repositories { jcenter() mavenCentral() // add } dependencies { classpath 'com.android.tools.build:gradle:2.1.2' //2.2以上无需添加apt依赖 classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' // add } }
android-apt 是一个 Gradle 插件,协助 Android Studio 处理 annotation processors,它有两个目的:
- 允许配置只在编译时作为注解处理器的依赖,而不添加到最后的 APK 或 library
- 设置源路径,使注解处理器生成的代码能被 Android Studio 正确的引用
伴随着 Android Gradle 插件 2.2 版本的发布,android-apt 作者在官网发表声明证实了后续将不会继续维护 android-apt,并推荐大家使用 Android 官方插件提供的相同能力。也就是说,大约五年前推出的 android-apt 即将告别开发者,退出历史舞台,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt。
5.3.2 定义要使用的注解
建一个 Java 库来专门放注解,示例库名为 annotations。定义注解:
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
String value();
}
5.3.3 定义注解处理器
另外建一个 Java 库工程,示例库名为 processors,这里必须为 Java 库,不然会找不到 javax 包下的相关资源。processors.build.gradle 要依赖以下内容:
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile 'com.squareup:javapoet:1.9.0'
compile(project(':annotations'))
其中:
- auto-service 用于自动在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
- javapoet 用于产生 .java 源文件的辅助库,它可以很方便地帮助我们生成需要的.java 源文件。(关于 javapoet 的请看我的 Android javapoet 使用解析)
示例代码如下:
package com.example;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
// Filer是个接口,支持通过注解处理器创建新文件
filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement element : annotations) {
//新建文件
if (element.getQualifiedName().toString().equals(MyAnnotation.class.getCanonicalName())) {
// 创建main方法
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
.build();
// 创建HelloWorld类
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
try {
// 生成 com.example.HelloWorld.java
JavaFile javaFile = JavaFile.builder("com.example", helloWorld)
.addFileComment(" This codes are generated automatically. Do not modify!")
.build();
// 生成文件
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
//在Gradle console 打印日志
for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
System.out.println("------------------------------");
// 判断元素的类型为Class
if (element.getKind() == ElementKind.CLASS) {
// 显示转换元素类型
TypeElement typeElement = (TypeElement) element;
// 输出元素名称
System.out.println(typeElement.getSimpleName());
// 输出注解属性值System.out.println(typeElement.getAnnotation(MyAnnotation.class).value());
}
System.out.println("------------------------------");
}
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotataions = new LinkedHashSet<String>();
annotataions.add(MyAnnotation.class.getCanonicalName());
return annotataions;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
5.3.4 在代码中使用定义的注解
需要依赖上面的两个 java 库 annotations 和 processors。
import com.example.MyAnnotation;
@MyAnnotation("test")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
编译后就会生成指定包名的指定文件,如下图:
关于注解处理器的辅助接口我们看下面的代码:
public class MyProcessor extends AbstractProcessor {
private Types typeUtils;
private Elements elementUtils;
private Filer filer;
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}
}
如上所示,在自定义注解处理器的初始化接口,可以获取到以下 4 个辅助接口:
- Types: Types 是一个用来处理 TypeMirror 的工具
- Elements: Elements 是一个用来处理 Element 的工具
- Filer: 一般我们会用它配合 JavaPoet 来生成我们需要的 .java 文件
- Messager:Messager 提供给注解处理器一个报告错误、警告以及提示信息的途径
有关注解处理器我另起了一篇 Android 注解处理器解析 来详细讲解。
5.3.5 带有注解的库提供给第三方
以下例子默认用gradle插件 2.2 以上,不再使用 apt。一般使用编译时注解的库,都会有三个 module:
- xxxx-annotations:java库,定义注解的 module;
- xxxx-compiler: java库,实现注解器的 module;
- xxxx-api:android库,提供对外接口的 module。
其中 module xxxx-api 的依赖这么写:
dependencies {
annotationProcessor 'xxxx-compiler:1.0.0'
compile ' xxxx-annotations:1.0.0'
//....others
}
然后第三方使用时,可以像如下这样依赖:
dependencies {
...
compile 'com.google.dagger:dagger:2.9'
annotationProcessor 'com.google.dagger:dagger-compiler:2.9'
}
APT 之注解处理器
一、前言
Java 中的注解(Annotation)是一个很神奇的东西,特别现在有很多 Android 库都是使用注解的方式来实现的。我们并不讨论那些在运行时通过反射机制运行处理的注解,而是讨论在编译时处理的注解,下面便入手学习下 Java 注解处理器。
二、注解处理器解析
注解处理器是一个在 javac 中的,用来编译时扫描和处理的注解的工具。你可以为特定的注解注册你自己的注解处理器。
注解处理器可以生成 Java 代码,这些生成的 Java 代码会组成 .java 文件,但不能修改已经存在的Java类(即不能向已有的类中添加方法)。而这些生成的 Java 文件会同时与其他普通的手写 Java 源代码一起被 javac 编译。
我们每一个注解处理器都要继承于 AbstractProcessor,如下所示:
public class MyProcessor extends AbstractProcessor
{
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment)
{
super.init(processingEnvironment);
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
{
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes()
{
return super.getSupportedAnnotationTypes();
}
@Override
public SourceVersion getSupportedSourceVersion()
{
return super.getSupportedSourceVersion();
}
}
init():每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的 init() 方法,它会被注解处理工具调用,并输入 ProcessingEnviroment 参数。ProcessingEnviroment 提供很多有用的工具类 Elements、Types 和 Filer。后面我们将看到详细的内容。
process():这相当于每个处理器的主函数 main()。你可以在这里写你的扫描、评估和处理注解的代码,以及生成对应的 Java 文件。输入参数 RoundEnviroment 可以让你查询出包含特定注解的被注解元素,后面我们将看到详细的内容。
getSupportedAnnotationTypes(): 这里你必须指定这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,在这里定义你的注解处理器注册到哪些注解上。
getSupportedSourceVersion(): 用来指定你使用的 Java 版本。通常这里返回 SourceVersion.latestSupported()。然而如果你有足够的理由只支持 Java 7 的话,你也可以返回 SourceVersion.RELEASE_7。推荐使用前者。
简单实践一下,首先创建一个 @TestAnnotation 注解,后面将用这个注解来自动生成一个 java 文件。先来看下 @TestAnnotation 注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface TestAnnotation
{
String name() default "undefined";
String text() default "";
}
接下来可以在需要注解的类上增加我们的注解了,代码如下:
@TestAnnotation(
name = "test",
text = "Hello !!! Welcome "
)
public class MainActivity extends AppCompatActivity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
注解完了,那我们该怎么运行呢?请看下面。这里我们通过代码加注释的方式来一步步构建我们想要的处理器。代码如下:
@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 LinkedHashSet<>();
annotations.add(TestAnnotation.class.getCanonicalName());
return annotations;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
{
//这里开始处理我们的注解解析了,以及生成Java文件
return false;
}
}
第一行 @AutoService(Processor.class) 是 Google 开发的一个注解处理器,用来生成 META-INF/services/javax.annotation.processing.Processor 文件。需要通过 compile 'com.google.auto.service:auto-service:1.0-rc2' 来引入。
在 getSupportedAnnotationTypes() 中,我们指定本处理器将处理 @TestAnnotation 注解。
在 init() 中我们获得如下引用:
- Elements:一个用来处理 Element 的工具类,源代码的每一个部分都是一个特定类型的 Element,例如:
package com.example; // PackageElement
public class Foo { // TypeElement
private int a; // VariableElement
private Foo other; // VariableElement
public Foo () {} // ExecuteableElement
public void setA ( // ExecuteableElement
int newA // TypeElement
) {}
}
举例来说,假如你有一个代表 Foo 类的 TypeElement 元素,你可以遍历它的孩子,如下:
TypeElement fooClass = ... ;
for (Element e : fooClass.getEnclosedElements()){ // iterate over children
Element parent = e.getEnclosingElement(); // parent == fooClass
}
TypeElement 并不包含类本身的信息。你可以从 TypeElement 中获取类的名字,但是你获取不到类的信息,例如它的父类。这种信息需要通过 TypeMirror 获取。你可以通过调用 elements.asType() 获取元素的TypeMirror。
- Types:一个用来处理 TypeMirror 的工具类;(后面会使用到,再进行讲解)
- Filer:正如这个名字所示,使用 Filer 你可以创建文件。
搜索 @TestAnnotation 注解:
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
{
//这里开始处理我们的注解解析了,以及生成Java文件
for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(TestAnnotation.class)) {
//遍历所有被注解了@TestAnnotation的元素
...
}
return false;
}
roundEnvironment.getElementsAnnotatedWith(TestAnnotation.class) 返回所有被注解了 @TestAnnotation 的元素的列表。你可能已经注意到我们并没有说“所有被注解了 @TestAnnotation 的类的列表”,因为它真的是返回 Element 的列表。请记住 Element 可以是类、方法、变量等。所以接下来我们必须检查这些 Element 是否是一个类:
// 遍历所有被注解了@TestAnnotation的元素
for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(TestAnnotation.class)) {
// 检查被注解为@TestAnnotation的元素是否是一个类
if (annotatedElement.getKind() != ElementKind.CLASS) {
error(annotatedElement, "Only classes can be annotated with @%s",TestAnnotation.class.getSimpleName());
return true; // 退出处理
}
//解析,并生成代码
analysisAnnotated(annotatedElement);
}
这里就配合了 TypeMirror 使用 EmentKind 或者 TypeKind 进行元素类型判断。error() 日志和错误信息打印:
private void error(Element e, String msg, Object... args) {
mMessager.printMessage(Diagnostic.Kind.ERROR,String.format(msg, args),e);
}
除了 Diagnostic.Kind.ERROR 日志等级,还有其它,如:
public static enum Kind {
ERROR,
WARNING,
MANDATORY_WARNING,
NOTE,
OTHER;
private Kind() {
}
}
代码生成,编写解析和生成的代码格式:
private static final String SUFFIX = "$$Test";
private void analysisAnnotated(Element classElement)
{
TestAnnotation annotation = classElement.getAnnotation(TestAnnotation.class);
String name = annotation.name();
String text = annotation.text();
String newClassName = name + SUFFIX;
StringBuilder builder = new StringBuilder()
.append("package com.zyao89.demoprocessor.auto;\n\n")
.append("public class ")
.append(newClassName)
.append(" {\n\n") // open class
.append("\tpublic String getMessage() {\n") // open method
.append("\t\treturn \"");
builder.append(text).append(name).append(" !\\n");
builder.append("\";\n") // end return
.append("\t}\n") // close method
.append("}\n"); // close class
try { // write the file
JavaFileObject source = mFiler.createSourceFile("com.zyao89.demoprocessor.auto."+newClassName);
Writer writer = source.openWriter();
writer.write(builder.toString());
writer.flush();
writer.close();
} catch (IOException e) {
// Note: calling e.printStackTrace() will print IO errors
// that occur from the file already existing after its first run, this is normal
}
info(">>> analysisAnnotated is finish... <<<");
}
以上的连接字符串工作,可以使用 JavaPoet 开源库进行编写,提升效率。可以看我的 Android javapoat 使用解析。
最后 build 一下工程,生成最终的 java 文件。生成的文件路径:/app/build/generated/source/apt/debug/com 下。