美文网首页Android开发经验谈Android技术知识Android开发
WMRouter(3)-ServiceLoader动态注入路由节

WMRouter(3)-ServiceLoader动态注入路由节

作者: susion哒哒 | 来源:发表于2018-10-17 17:52 被阅读6次

    文章是作者学习WMRouter的源码的重点纪要。 WMRouter官方文档 : https://mp.weixin.qq.com/s/pKRi5qpZmol7xFIfeBbK_A

    在继续阅读源码前,我们需要先了解一下ServiceLoader,WMRouter中的ServiceLoader类似于java spi, 了解java spi可以看一下下面这篇文章:

    https://www.jianshu.com/p/deeb39ccdc53

    ServiceLoader

    WMRouter中的ServiceLoader是自己实现的,机制大致和java spi相同, 但不同的是对于实现类的读取,它不是从META-INF/services中读取。而是自建了一个实现类的读取机制,我们来看一下它的初始化:

        void doInit() {
           Class.forName(“com.sankuai.waimai.router.generated.ServiceLoaderInit”).getMethod("init").invoke(null);
        }
    

    即初始化的时候反射调用了ServiceLoaderInit.init()方法。我全局搜索了一下这个类并没有发现它的声明,最后发现这个类是使用Gradle Transform API动态生成的。对于Gradle Transform API可以大致看一下下面这个文章了解一下:

    https://www.jianshu.com/p/37df81365edf

    下面来看一下WMRouter的transform插件。

    WMRouterPlugin

    官方是这样描述它的作用的 : 将注解生成器生成的初始化类汇总到ServiceLoader.Init,运行时直接调用ServiceLoader.Init。 所以上面的SerciceLoader初始化反射调用的类就是由这个插件生成的。

    这里我大致描述一下这个插件的工作逻辑:

    1. 扫描编译生成的class文件夹,或者输入的jar包的指定目录 : com/sankuai/waimai/router/generated/service, 收集目录下的类并保存起来 (这个类其实就是ServiceInit_xxx1这种类)
    2. 使用asm生成ServiceLoaderInit类,并调用前面扫描到的类的init方法。

    即最终产生如下代码:

        public class ServiceLoaderInit {
            public static void init() {
                ServiceInit_xxx1.init();
                ServiceInit_xxx2.init();
            }
        }
    

    到这里就有疑问了,从开始分析到现在我们并没有看到ServiceInit_xxx1这种类是如何生成的呢。那它是在哪里生成的呢?

    ServiceInit_xx

    怎么生成的?

    在上一篇文章已经了解到UriAnnotationProcessor在编译时会扫描@RouterUri,并且会生成UriAnnotationInit_xx1这种类,它的代码就是把根据@RouterUri生成的路由UrlHandler注册到UriAnnotationHandler中。但这些代码在运行时的哪个节点会被调用呢?

    其实UriAnnotationProcessor在扫描@RouterUri生成相关类的同时,还会生成一个类,就是ServiceInit_xx:

      public void buildHandlerInitClass(CodeBlock code, String genClassName, String handlerClassName, String interfaceName) {
            .... // 生成 UriAnnotationInit_hasValue 代码
            String fullImplName = Const.GEN_PKG + Const.DOT + genClassName;
            String className = "ServiceInit" + Const.SPLITTER + hash(genClassName);
            new ServiceInitClassBuilder(className)
                    .putDirectly(interfaceName, fullImplName, fullImplName, false)
                    .build();
        }
    

    我们看一下ServiceInitClassBuilderputDirectlybuild方法:

      public class ServiceInitClassBuilder {
            ...
            public ServiceInitClassBuilder putDirectly(String interfaceName, String key, String implementName, boolean singleton) {
                builder.addStatement("$T.put($T.class, $S, $L.class, $L)",
                        serviceLoaderClass, className(interfaceName), key, implementName, singleton);
                return this;
            }
    
            public void build() {
                MethodSpec methodSpec = MethodSpec.methodBuilder(Const.INIT_METHOD)
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        .returns(TypeName.VOID)
                        .addCode(this.builder.build())
                        .build();
    
                TypeSpec typeSpec = TypeSpec.classBuilder(this.className)
                        .addModifiers(Modifier.PUBLIC)
                        .addMethod(methodSpec)
                        .build();
    
                JavaFile.builder(“com.sankuai.waimai.router.generated.service”, typeSpec)
                        .build()
                        .writeTo(filer);
            }
    

    其实就是会把上面代码生成到com/sankuai/waimai/router/generated/service文件夹下,这样WMRouter的transform插件就能扫描到这些类了:

    public class ServiceInit_36ed390bf4b81a8381d45028b37cc645 {
      public static void init() {
           ServiceLoader.put(IUriAnnotationInit.class, "com.xxx.UriAnnotationInit_72565413b8384a4bebb02d352762d60d", com.xx.UriAnnotationInit_72565413b8384a4bebb02d352762d60d.class, false);
      }
    }
    

    有何作用

    我们重新看一下上面生成的代码,它调用了SerciceLoaderput方法:

         /**
         * @param interfaceClass 接口类
         * @param implementClass 实现类
         */
        public static void put(Class interfaceClass, String key, Class implementClass, boolean singleton) {
            ServiceLoader loader = SERVICES.get(interfaceClass);
            if (loader == null) {
                loader = new ServiceLoader(interfaceClass);
                SERVICES.put(interfaceClass, loader);
            }
            loader.putImpl(key, implementClass, singleton);
        }
    

    结合我们了解的 java spi, 这里其实就是把IUriAnnotationInit接口的实现类UriAnnotationInit_xx1放入了SerciceLoader中。而SerciceLoader在初始化的时候就会调用ServiceInit_xxinit方法,SerciceLoader中保存了IUriAnnotationInit接口的实现类UriAnnotationInit_xx1

    经过上面的分析下面我们就再来看一下@RouterUri标记的page,是如何在我们app运行时注册到UriAnnotationHandler?

    我们继续来看一下UriAnnotationHandler的初始化方法:

       protected void initAnnotationConfig() {
            RouterComponents.loadAnnotation(this, IUriAnnotationInit.class);
        }
    

    上面的代码最终会调用到这里:

        List<? extends AnnotationInit<T>> services = ServiceLoader.load(clazz).getAll();
        for (AnnotationInit<T> service : services) {
            service.init(handler);
        }
    

    SerciceLoader会加载IUriAnnotationInit的所有实现类,并传入UriAnnotationHandler,调用init方法。从而实现了在运行时动态注册了@RouterUri标记生成的page的UriHandler

    我们用下面这张图总结一下上面的过程:

    ServiceLoader.png

    ServiceLoader中更强大的功能

    其实上面只是使用了SerciceLoader的一部分功能,WMRouter实现的SerciceLoader还是比较强大的,很有利于代码的解耦。

    @RouterService

    java 中的spi机制需要我们在 META-INF/services规范好接口与实现类的关系,WMRouter中提供@RouterService,来简化了这个操作。我们来看一下这个注解是如何使用的:

    比如在一个项目中有3个库: interface、lib1、lib2

    //定义在interface库中
    public abstract class LibraryModule {
        public abstract String getModuleName();
    }
    
    //定义在lib1中
    @RouterService(interfaces = LibraryModule.class)
    public class LibraryModule1 extends LibraryModule {
    }
    
    //定义在lib2
    @RouterService(interfaces = LibraryModule.class)
    public class LibraryModule2 extends LibraryModule {
    }
    

    WMRouter中有一个ServiceAnnotationProcessor负责处理RouterService注解,它会把标记了这个注解的类,生成ServiceInit_xx, 即

    public class ServiceInit_f3649d9f5ff15a62b844e64ca8434259 {
      public static void init() {
        ServiceLoader.put(IUriAnnotationInit.class, "xxx",xxx.class, false);
      }
    }
    

    这样再由WMRouter的插件转换生成 ServiceLoaderInit.init()中的调用代码。就达到在运行时把LibraryModule的实现注入到SerciceLoader中,从而我们可以获得不同库的实现。

    这个特性非常有用,比如一个业务库的代码需要被另一个业务库复用,这时候,我们就可以使用这个机制,从而使两个业务库不耦合的情况下,调用对方功能 赞!

    关于SerciceLoader的更多特性,大家可以自行了解,这里就不介绍了。

    相关文章

      网友评论

        本文标题:WMRouter(3)-ServiceLoader动态注入路由节

        本文链接:https://www.haomeiwen.com/subject/dkirzftx.html