利用apt和javapoet生成路由映射文件 - Xiasm/EasyRouter GitHub Wiki
通过上节我们知道在Activity类上加上@Route注解之后,便可通过apt来生成对应的路由表,那么这节我们就来讲述一下如何通过apt来生成路由表。这节我会拿着demo里面的代码来跟大家详细介绍,我们先来了解一下apt吧!
APT是Annotation Processing Tool的简称,即注解处理工具。它是在编译期对代码中指定的注解进行解析,然后做一些其他处理(如通过javapoet生成新的Java文件)。我们常用的ButterKnife,其原理就是通过注解处理器在编译期扫描代码中加入的@BindView、@OnClick等注解进行扫描处理,然后生成XXX_ViewBinding类,实现了view的绑定。
关于apt和javapoet的文章,网上一大堆,下面我通过apt在路由框架中的应用来一步步解释它们的用法。
第一步:定义注解处理器,用来在编译期扫描加入@Route注解的类,然后做处理。
这也是apt最核心的一步,新建RouterProcessor 继承自 AbstractProcessor,然后实现process方法。在项目编译期会执行RouterProcessor的process()方法,我们便可以在这个方法里处理Route注解了。此时我们需要为RouterProcessor指明它需要处理什么注解,这里引入一个google开源的自动注册工具AutoService,如下依赖(也可以手动进行注册,不过略微麻烦):
implementation 'com.google.auto.service:auto-service:1.0-rc2'
这个工具可以通过添加注解来为RouterProcessor指定它需要的配置(当然也可以自己手动去配置,不过会有点麻烦),如下所示
@AutoService(Processor.class)
public class RouterProcessor extends AbstractProcessor {
//...
}
完整的RouterProcessor注解处理器配置如下:
@AutoService(Processor.class)
/**
处理器接收的参数 替代 {@link AbstractProcessor#getSupportedOptions()} 函数
*/
@SupportedOptions(Constant.ARGUMENTS_NAME)
/**
* 指定使用的Java版本 替代 {@link AbstractProcessor#getSupportedSourceVersion()} 函数
*/
@SupportedSourceVersion(SourceVersion.RELEASE_7)
/**
* 注册给哪些注解的 替代 {@link AbstractProcessor#getSupportedAnnotationTypes()} 函数
*/
@SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)
public class RouterProcessor extends AbstractProcessor {
/**
* key:组名 value:类名
*/
private Map<String, String> rootMap = new TreeMap<>();
/**
* 分组 key:组名 value:对应组的路由信息
*/
private Map<String, List<RouteMeta>> groupMap = new HashMap<>();
/**
* 节点工具类 (类、函数、属性都是节点)
*/
private Elements elementUtils;
/**
* type(类信息)工具类
*/
private Types typeUtils;
/**
* 文件生成器 类/资源
*/
private Filer filerUtils;
private String moduleName;
private Log log;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//获得apt的日志输出
log = Log.newLog(processingEnvironment.getMessager());
elementUtils = processingEnvironment.getElementUtils();
typeUtils = processingEnvironment.getTypeUtils();
filerUtils = processingEnvironment.getFiler();
//参数是模块名 为了防止多模块/组件化开发的时候 生成相同的 xx$$ROOT$$文件
Map<String, String> options = processingEnvironment.getOptions();
if (!Utils.isEmpty(options)) {
moduleName = options.get(Constant.ARGUMENTS_NAME);
}
if (Utils.isEmpty(moduleName)) {
throw new RuntimeException("Not set processor moudleName option !");
}
log.i("init RouterProcessor " + moduleName + " success !");
}
/**
*
* @param set 使用了支持处理注解的节点集合
* @param roundEnvironment 表示当前或是之前的运行环境,可以通过该对象查找找到的注解。
* @return true 表示后续处理器不会再处理(已经处理)
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (!Utils.isEmpty(set)) {
//被Route注解的节点集合
Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(Route.class);
if (!Utils.isEmpty(rootElements)) {
processorRoute(rootElements);
}
return true;
}
return false;
}
//...
}
我们通过@SupportedOptions(Constant.ARGUMENTS_NAME)拿到每个module的名字,用来生成对应module下存放路由信息的类文件名。在这之前,我们需要在module的gradle下配置如下
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
Constant.ARGUMENTS_NAME便是每个module的名字。
@SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)指定了需要处理的注解的路径地址,在此就是Route.class的路径地址。
RouterProcessor中我们实现了init方法,拿到log apt日志输出工具用以输出apt日志信息,并通过以下代码得到上面提到的每个module配置的moduleName
//参数是模块名 为了防止多模块/组件化开发的时候 生成相同的 xx$$ROOT$$文件
Map<String, String> options = processingEnvironment.getOptions();
if (!Utils.isEmpty(options)) {
moduleName = options.get(Constant.ARGUMENTS_NAME);
}
if (Utils.isEmpty(moduleName)) {
throw new RuntimeException("Not set processor moudleName option !");
}
第二步,在process()方法里开始生成EaseRouter_Route_moduleName类文件和EaseRouter_Group_moduleName文件。这里在process()里生成文件用javapoet,这是squareup公司开源的一个库,通过调用它的api,可以很方便的生成java文件,在含有注解处理器(demo中apt相关的代码实现都在easy-compiler module中)的module中引入依赖如下:
implementation 'com.squareup:javapoet:1.7.0'
好了,我们终于可以生成文件了,在process()方法里有如下代码,
if (!Utils.isEmpty(set)) {
//被Route注解的节点集合
Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(Route.class);
if (!Utils.isEmpty(rootElements)) {
processorRoute(rootElements);
}
return true;
}
return false;
set就是扫描得到的支持处理注解的节点集合,然后得到rootElements,即被@Route注解的节点集合,此时就可以调用 processorRoute(rootElements)方法去生成文件了。processorRoute(rootElements)方法实现如下:
private void processorRoute(Set<? extends Element> rootElements) {
//获得Activity这个类的节点信息
TypeElement activity = elementUtils.getTypeElement(Constant.ACTIVITY);
TypeElement service = elementUtils.getTypeElement(Constant.ISERVICE);
for (Element element : rootElements) {
RouteMeta routeMeta;
//类信息
TypeMirror typeMirror = element.asType();
log.i("Route class:" + typeMirror.toString());
Route route = element.getAnnotation(Route.class);
if (typeUtils.isSubtype(typeMirror, activity.asType())) {
routeMeta = new RouteMeta(RouteMeta.Type.ACTIVITY, route, element);
} else if (typeUtils.isSubtype(typeMirror, service.asType())) {
routeMeta = new RouteMeta(RouteMeta.Type.ISERVICE, route, element);
} else {
throw new RuntimeException("Just support Activity or IService Route: " + element);
}
categories(routeMeta);
}
TypeElement iRouteGroup = elementUtils.getTypeElement(Constant.IROUTE_GROUP);
TypeElement iRouteRoot = elementUtils.getTypeElement(Constant.IROUTE_ROOT);
//生成Group记录分组表
generatedGroup(iRouteGroup);
//生成Root类 作用:记录<分组,对应的Group类>
generatedRoot(iRouteRoot, iRouteGroup);
}
上节中提到过生成的root文件和group文件分别实现了IRouteRoot和IRouteGroup接口,就是通过下面这两行文件代码拿到IRootGroup和IRootRoot的字节码信息,然后传入generatedGroup(iRouteGroup)和generatedRoot(iRouteRoot, iRouteGroup)方法,这两个方法内部会通过javapoet api生成java文件,并实现这两个接口。
TypeElement iRouteGroup = elementUtils.getTypeElement(Constant.IROUTE_GROUP);
TypeElement iRouteRoot = elementUtils.getTypeElement(Constant.IROUTE_ROOT);
generatedGroup(iRouteGroup)和generatedRoot(iRouteRoot, iRouteGroup)就是生成上面提到的EaseRouter_Root_app和EaseRouter_Group_main等文件的具体实现,代码太多,我粘出一个实现供大家参考,其实生成java文件的思路都是一样的,我们只需要熟悉javapoet的api如何使用即可。大家可以后续在demo里详细分析,这里我只是讲解核心的实现。
/**
* 生成Root类 作用:记录<分组,对应的Group类>
* @param iRouteRoot
* @param iRouteGroup
*/
private void generatedRoot(TypeElement iRouteRoot, TypeElement iRouteGroup) {
//创建参数类型 Map<String,Class<? extends IRouteGroup>> routes>
//Wildcard 通配符
ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(
ClassName.get(Map.class),
ClassName.get(String.class),
ParameterizedTypeName.get(
ClassName.get(Class.class),
WildcardTypeName.subtypeOf(ClassName.get(iRouteGroup))
));
//参数 Map<String,Class<? extends IRouteGroup>> routes> routes
ParameterSpec parameter = ParameterSpec.builder(parameterizedTypeName, "routes").build();
//函数 public void loadInfo(Map<String,Class<? extends IRouteGroup>> routes> routes)
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constant.METHOD_LOAD_INTO)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(parameter);
//函数体
for (Map.Entry<String, String> entry : rootMap.entrySet()) {
methodBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(Constant.PACKAGE_OF_GENERATE_FILE, entry.getValue()));
}
//生成$Root$类
String className = Constant.NAME_OF_ROOT + moduleName;
TypeSpec typeSpec = TypeSpec.classBuilder(className)
.addSuperinterface(ClassName.get(iRouteRoot))
.addModifiers(Modifier.PUBLIC)
.addMethod(methodBuilder.build())
.build();
try {
JavaFile.builder(Constant.PACKAGE_OF_GENERATE_FILE, typeSpec).build().writeTo(filerUtils);
log.i("Generated RouteRoot:" + Constant.PACKAGE_OF_GENERATE_FILE + "." + className);
} catch (IOException e) {
e.printStackTrace();
}
}
可以看到,ParameterizedTypeName是创建参数类型的api,ParameterSpec是创建参数的实现,MethodSpec是函数的生成实现等等。最后,当参数、方法、类信息都准备好了之后,调用JavaFileapi生成类文件。JavaFile的builder ()方法传入了PACKAGE_OF_GENERATE_FILE变量,这个就是指定生成的类文件的目录,方便我们在app进程启动的时候去遍历拿到这些类文件。