利用apt和javapoet生成路由映射文件 - Xiasm/EasyRouter GitHub Wiki

通过上节我们知道在Activity类上加上@Route注解之后,便可通过apt来生成对应的路由表,那么这节我们就来讲述一下如何通过apt来生成路由表。这节我会拿着demo里面的代码来跟大家详细介绍,我们先来了解一下apt吧!

利用apt和javapoet生成映射文件

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进程启动的时候去遍历拿到这些类文件。

⚠️ **GitHub.com Fallback** ⚠️