关于我 一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。 Github:https://github.com/hylinux1024 微信公众号:终身开发者(angrycode)
在前文的讲解中对 EventBus
的实现逻辑有了大概的理解之后,我们知道 Java
解析注解可以在运行时解析也可以在编译期间解析。由于运行时解析是通过反射来获取注解标记的类、方法、属性等对象,它的性能要受到反射的影响。因此在一些基础组件中更常见的做法是使用注解解析器技术,像 Dagger
、 butterknife
、 ARouter
以及本文所接触的 EventBus
等框架库都是使用到了注解解析器的技术。接下来我们来实现一个注解解析器。(本文代码有点多)
首先我们需要把项目结构改造一下
# 项目结构省略了部分文件展示
├── annotation # 注解等元数据定义
├── annotationProcessor # 注解解析以及代码生成
├── app # 客户端使用入口
├── easybuslib # 核心接口
├── local.properties
└── settings.gradle
与 app
同级的目录增加了 annotation
、 annotationProcessor
和 easybuslib
。其中创建 annotation
和 annotationProcessor
这两个项目时一定要选择 java library
。前者主要是用于定义注解和封装一些基础数据结构,后者是用于解析注解。注意 annotationProcessor
在项目使用时,并不会打包到 app
中,它只会在编译期间对注解进行解析处理。easybuslib
是 android library
。
它们之间的关系为
# 符号 “->” 表示库依赖
# 符号 “=>” apt 依赖,并不会打包到 app 中
app -> easybuslib -> annotation
app => annotationProcessor
annotationProcessor -> annotation
annotation
是一个纯粹的 java
项目,主要定义了注解 EasySubscribe
、 SubscriberMethod
和 Subscription
这个是 EasyBus
会直接使用到的类,而在 meta
包中定义了注解解析器需要使用到的数据结构。这个包结构分工是很明确的。
# annotation 主要的项目结构
└── src/main/java
└── com.gitlab.annotation
├── EasySubscribe.java
├── SubscriberMethod.java
├── Subscription.java
└── meta
├── SubscriberInfo.java
├── SubscriberInfoIndex.java
└── SubscriberMethodInfo.java
在这个库中实现自定义的注解
# annotationProcessor 主要的项目结构
└── src/main/java
└── com.gitlab.annotationprocessor
└── EasyBusAnnotationProcessor.java
└── resources/META-INF.services
└── javax.annotation.processing.Processor
这个只有一个 java
类和一个配置 Processor
的文件。解析注解生成 java
代码的逻辑就在 EasyBusAnnotationProcessor
里面。
# easybuslib 主要项目结构
└── src
└── main/java
└── com.gitlab.easybuslib
├── EasyBus.java
└── Logger.java
└── res
这里封装了 EasyBus
主要接口,其逻辑在前面已经解释过了。不过今天也会对它进行改造使它支持编译期间解析得到的订阅者的 onEvent
方法(不是必需以 onEvent
开头,本文为了表达方便而使用)。
项目结构改造完成之后,接下来我们自上而下对注解解析器进行解读和实现。
定义注解在前面已经解读过,这里直接贴出代码
EasySubscribe.java
/**
* 自定义注解
* 指定该注解修饰方法
* 由于我们使用编译期间处理注解,所以指定其生命周期为只保留在源码文件中
*/
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface EasySubscribe {
}
修改 EasyBus
中的注册逻辑,添加由注解解析器生成的索引列表,并从索引列表中获取到订阅者被 @EasySubscribe
标记的方法。
SubscriberInfoIndex.java
/**
* 订阅者的索引接口
* 通过Class获取到该Class下定义的被标记的 @EasySubscribe 方法
*/
public interface SubscriberInfoIndex {
SubscriberInfo getSubscriberInfo(Class<?> subscriberClass);
}
这个接口非常重要,我们使用注解解析器生成的类将继承于这个接口,这样我们在 EasyBus
中就依赖于该接口,而接口的实现交给注解解析器。
修改后的 EasyBus
public class EasyBus {
//省略部分代码...
/**
* 编译期间生成订阅者索引,通过订阅者 Class 类获取到 @EasySubscribe 的方法
*/
private List<SubscriberInfoIndex> subscriberInfoIndexList;
//省略部分代码...
/**
* 添加订阅者索引
*
* @param subscriberInfoIndex
*/
public void addIndex(SubscriberInfoIndex subscriberInfoIndex) {
if (subscriberInfoIndexList == null) {
subscriberInfoIndexList = new ArrayList<>();
}
subscriberInfoIndexList.add(subscriberInfoIndex);
}
public void register(Object subscriber) {
Class<?> subscriberClass = subscriber.getClass();
List<SubscriberMethod> subscriberMethods = new ArrayList<>();
//使用反射获取 onEvent 方法
if (subscriberInfoIndexList == null) {
Method[] methods = subscriberClass.getDeclaredMethods();
for (Method method : methods) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length != 1) {
continue;
}
// 这里可以修改成使用反射获取,这样就不需要求方法以 onEvent 开头
if (method.getName().startsWith("onEvent")) {
subscriberMethods.add(new SubscriberMethod(method, parameterTypes[0]));
}
}
} else {
//注意这里!!!
//使用注解解析器获取 onEvent 方法
subscriberMethods = findSubscriberMethods(subscriberClass);
}
synchronized (this) {
for (SubscriberMethod method : subscriberMethods) {
subscribe(subscriber, method);
}
}
}
/**
* 从索引中获取订阅者方法信息
*
* @param subscriberClass
* @return
*/
private List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
List<SubscriberMethod> subscriberMethods = new ArrayList<>();
for (SubscriberInfoIndex subscriberIndex : subscriberInfoIndexList) {
SubscriberInfo subscriberInfo = subscriberIndex.getSubscriberInfo(subscriberClass);
List<SubscriberMethod> methodList = Arrays.asList(subscriberInfo.getSubscriberMethods());
subscriberMethods.addAll(methodList);
}
return subscriberMethods;
}
// 省略部分代码...
}
主要对 register
方法进行了改造,当 subscriberInfoIndexList
不为空时,就从索引列表中查询订阅者信息。findSubscriberMethods()
遍历索引列表并执行 subscriberIndex.getSubscriberInfo(subscriberClass)
方法得到订阅者的信息。那么 subscriberIndex
具体是怎么实现的呢?
打开 app/HomeActivity
看到以下代码
EasyBus.getInstance().addIndex(new MyEventBusIndex());
通过 addIndex()
方法将 MyEventBusIndex
实例添加到索引列表中。接下来我们看看其内部到底有何乾坤。
MyEventBusIndex.java
这个类是由注解解析器生成的
/** This class is generated by EasyBus, do not edit. */
public class MyEventBusIndex implements SubscriberInfoIndex {
private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;
static {
SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();
putIndex(new SubscriberInfo(com.github.easybus.demo.HomeActivity.class, new SubscriberMethodInfo[] {
new SubscriberMethodInfo("onUpdateMessage", com.github.easybus.demo.MessageEvent.class),
new SubscriberMethodInfo("onEventNotify", com.github.easybus.demo.MessageEvent.class),
}));
}
private static void putIndex(SubscriberInfo info) {
SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);
}
@Override
public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {
SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
if (info != null) {
return info;
} else {
return null;
}
}
}
它代码逻辑很简单,首先定义一个静态 Map
变量 SUBSCRIBER_INDEX
,它的 key
是 Class<?>
对象, value
是 SubscriberInfo
对象。然后 在一个静态的代码块中将订阅者的方法名称和参数类型封装成 SubscriberInfo
后添加到这个 Map
中。
SubscriberInfo.java
/**
* 订阅者信息
* 主要是从注解中解析出Class以及通知方法(即被@EasySubscribe标记的方法)
*/
public class SubscriberInfo {
private Class subscriberClass;
private SubscriberMethodInfo[] subscriberMethodInfos;
public SubscriberInfo(Class subscriberClass, SubscriberMethodInfo[] subscriberMethods) {
this.subscriberClass = subscriberClass;
this.subscriberMethodInfos = subscriberMethods;
}
//省略代码...
public synchronized SubscriberMethod[] getSubscriberMethods() {
int length = subscriberMethodInfos.length;
SubscriberMethod[] methods = new SubscriberMethod[length];
for (int i = 0; i < length; i++) {
SubscriberMethodInfo info = subscriberMethodInfos[i];
SubscriberMethod method = createSubscribeMethod(info);
if (method != null) {
methods[i] = method;
}
}
return methods;
}
private SubscriberMethod createSubscribeMethod(SubscriberMethodInfo info) {
try {
Method method = subscriberClass.getDeclaredMethod(info.getMethodName(), info.getEventType());
return new SubscriberMethod(method, info.getEventType());
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return null;
}
}
SubscriberMethodInfo.java
/**
* 用于编译期间生成的订阅者信息
*/
public class SubscriberMethodInfo {
private final String methodName;
private final Class<?> eventType;
public SubscriberMethodInfo(String methodName, Class<?> eventType) {
this.methodName = methodName;
this.eventType = eventType;
}
// 省略代码...
}
SubscriberInfo
与 SubscriberMethodInfo
都是元数据类,主要是由生成的 MyEventBusIndex
类使用
如何生成代码呢?
我们重点看 annotationProcessor
这个项目
首先配置 build.gradle
// annotationProcessor 工程库必须使用 java 工程
// 不要使用 android lib 工程
// 本工程只会生成辅助代码,不会打包到 apk 中
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.squareup:javapoet:1.11.1'
implementation project(':annotation')
}
sourceCompatibility = "7"
targetCompatibility = "7"
添加 javapoet
依赖,这个框架帮助我们生成代码(注意只能生成新代码,而不能修改现有代码哦)
然后继承 AbstractProcessor
// 可以使用注解指定要解析的自定义注解以及Java版本号
// 也可以重写 AbstractProcessor 中的方法达到类似的目的
// @SupportedAnnotationTypes({"com.gitlab.annotation.EasySubscribe"})
// @SupportedSourceVersion(SourceVersion.RELEASE_8)
public class EasyBusAnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
collectSubscribers(set, roundEnvironment, messager);
return true;
}
// 省略代码...
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
// annotations.add("com.gitlab.annotation.EasySubscribe");
//指定要解析的注解
annotations.add(EasySubscribe.class.getCanonicalName());
return annotations;
}
}
需要实现核心的几个方法
init
初始化方法process
处理注解的核心方法getSupportedSourceVersion
指定 Java
版本,一般使用 SourceVersion.latestSupported()
getSupportedAnnotationTypes
指定要解析的注解,有一个或多个注解,将其添加到 set
中,并返回。我们重点关注 process
方法。这里有两个参数,一个是 TypeElement
类型的 set
和 RoundEnvironment
变量。其中 TypeElement
是 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
) {}
}
RoundEnvironment
是一个接口,是对上下文信息的抽象。
我们回到 process()
方法,它在编译时会被执行,此时会将被注解标记的类、方法等信息传递过来
process()
方法会执行 collectSubscribers()
方法(此方法是从 EventBus
里中 copy 过来的)
private void collectSubscribers(Set<? extends TypeElement> annotations, RoundEnvironment env, Messager messager) {
// 遍历要解析的注解
for (TypeElement annotation : annotations) {
messager.printMessage(Diagnostic.Kind.NOTE, "annotation:" + annotation.getSimpleName());
// 获取被注解标记的对象
Set<? extends Element> elements = env.getElementsAnnotatedWith(annotation);
// Element 是接口,是对包括:包名、类、接口、方法、构造方法等的抽象
for (Element element : elements) {
// 自定义注解 EasySubscribe 是作用在方法上的
// 所以检查一下是否是 ExecutableElement 对象
// 它可以表示方法以及构造方法
if (element instanceof ExecutableElement) {
ExecutableElement method = (ExecutableElement) element;
if (checkHasNoErrors(method, messager)) {
// 获取到这个被自定义注解的标记的方法所在类
TypeElement classElement = (TypeElement) method.getEnclosingElement();
List<ExecutableElement> list = methodsByClass.get(classElement);
if (list == null) {
list = new ArrayList<>();
}
list.add(method);
methodsByClass.put(classElement, list);
}
} else {
messager.printMessage(Diagnostic.Kind.ERROR, "@EasySubscribe is only valid for methods", element);
}
}
}
if (!writeDone && !methodsByClass.isEmpty()) {
createInfoIndexFile("com.github.easybus.MyEventBusIndex");
writeDone = true;
} else {
messager.printMessage(Diagnostic.Kind.WARNING, "No @EasySubscribe annotations found");
}
}
Messager
对象可以用于输入打印信息。
annotations
集合是所有待解析的注解,如果你定义了两个注解,并在 getSupportedAnnotationTypes
中返回了,那么这里就是两个需要解析的注解。
遍历注解集合,并使用 RoundEnvironment
获取到被注解标记的 Element
,由于 EasySubscribe
是作用在方法上,所以我们主要关注 ExecutableElement
就可以了。
然后再通过 ExecutableElement.getEnclosingElement()
方法获取方法所在的类对象 Class
信息。
最后将其保存在 key
为代表 Class
的 TypeElement
, value
为代表方法列表的 Map
对象 methodsByClass
中。
这样就将类信息 Class
与被 @EasySubscribe
标记的方法列表对应起来了。这样就为接下来的生成代码逻辑作好了铺垫。
有了 methodsByClass
接下来就是生成代码的逻辑了。
代码生成的逻辑在 createInfoIndexFile()
方法中,它有个参数 index
,用来指定生成文件的包和类名的。(在 EventBus
中这里是在 gradle
中配置的,本文为了展示核心流程省略了)
由于 process()
方法会被执行多次,所以这里使用一个变量 writeDone
来判断是否已经生成过代码了,避免重复执行。
private void createInfoIndexFile(String index) {
BufferedWriter writer = null;
try {
JavaFileObject sourceFile = filer.createSourceFile(index);
int period = index.lastIndexOf('.');
String myPackage = period > 0 ? index.substring(0, period) : null;
String clazz = index.substring(period + 1);
writer = new BufferedWriter(sourceFile.openWriter());
if (myPackage != null) {
writer.write("package " + myPackage + ";\n\n");
}
writer.write("import com.gitlab.annotation.meta.SubscriberInfoIndex;\n");
writer.write("import com.gitlab.annotation.meta.SubscriberInfo;\n");
writer.write("import com.gitlab.annotation.meta.SubscriberMethodInfo;\n");
writer.write("import java.util.HashMap;\n");
writer.write("import java.util.Map;\n\n");
writer.write("/** This class is generated by EasyBus, do not edit. */\n");
writer.write("public class " + clazz + " implements SubscriberInfoIndex {\n");
writer.write(" private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;\n\n");
writer.write(" static {\n");
writer.write(" SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();\n\n");
writeIndexLines(writer, myPackage);
writer.write(" }\n\n");
writer.write(" private static void putIndex(SubscriberInfo info) {\n");
writer.write(" SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);\n");
writer.write(" }\n\n");
writer.write(" @Override\n");
writer.write(" public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {\n");
writer.write(" SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);\n");
writer.write(" if (info != null) {\n");
writer.write(" return info;\n");
writer.write(" } else {\n");
writer.write(" return null;\n");
writer.write(" }\n");
writer.write(" }\n");
writer.write("}\n");
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("Could not write source for " + index, e);
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
//Silent
e.printStackTrace();
}
}
}
}
如果你还对前面的 MyEventBusIndex.java
的内容还有印象的话,这里的逻辑还是比较好理解的,主要是使用 javapoet
中的接口生成代码。具体就不再赘述了,阅读代码还是比较清晰的,接下来看看如何调试。
由于代码是在编译期间执行的,如果你是刚开始接触注解解析器的编码,不能调试将是非常痛苦的过程。
要调试注解解析器需要做以下配置
1、首先在项目的根目录下 gradle.properties
添加以下配置
org.gradle.jvmargs=-Xmx1536m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
2、然后点击 EditConfigurations
配置 remote
填写名称,例如 processorDebug
后保存。
3、选择 processorDebug
4、添加断点后 RebuildProject
现在就可以对注解解析器进行调试了
注解解析器的实现逻辑其实不是很复杂,主要有以下几步:
AbstractProcessor
解析注解javapoet
生成代码面对一个新技术首先要掌握它的使用方法,然后了解其内部实现原理,最后自己动手实践。这样一个流程下来基本上对一个技术的理解是比较深刻的了。注解解析器作为很多基础组件实现的通用技术,掌握它对实现基础框架以及理解很多开源框架是很有帮助的。