写点什么

详解 Apache Dubbo 的 SPI 实现机制

发布于: 3 小时前

一、SPI

SPI 全称为 Service Provider Interface,对应中文为服务发现机制。SPI 类似一种可插拔机制,首先需要定义一个接口或一个约定,然后不同的场景可以对其进行实现,调用方在使用的时候无需过多关注具体的实现细节。在 Java 中,SPI 体现了面向接口编程的思想,满足开闭设计原则。


1.1 JDK 自带 SPI 实现


从 JDK1.6 开始引入 SPI 机制后,可以看到很多使用 SPI 的案例,比如最常见的数据库驱动实现,在 JDK 中只定义了 java.sql.Driver 的接口,具体实现由各数据库厂商来提供。下面一个简单的例子来快速了解下 Java SPI 的使用方式:


1)定义一个接口

package com.vivo.study public interface Car {    void getPrice();}
复制代码

2)接口实现

package com.vivo.study.impl /** * 实现一 * */public class AudiCar implements Car {    @Override    public void getPrice() {        System.out.println("Audi A6L's price is  500000 RMB.");    }} package com.vivo.study.impl/** * 实现二 * */public class BydCar implements Car {    @Override    public void getPrice() {        System.out.println("BYD han's price is 220000 RMB.");    }}
复制代码


3)挂载扩展类信息

在 META-INF/services 目录下以接口全名为文件名的文本文件,对应此处即在 META-INF/services 目录下创建一个文件名为 com.vivo.study.Car 的文件,文件内容如下:

com.vivo.study.impl.AudiCarcom.vivo.study.impl.BydCar
复制代码

4)使用

public class SpiDemo {      public static void main(String[] args) {        ServiceLoader<Car> load = ServiceLoader.load(Car.class);          Iterator<Car> iterator = load.iterator();          while (iterator.hasNext()) {            Car car = iterator.next();            car.getPrice();        }    }}
复制代码

上面的例子简单的介绍了 JDK SPI 机制的使用方式,其中最关键的类为 ServiceLoader,通过 ServiceLoader 类来加载接口的实现类,ServiceLoader 是 Iterable 接口的实现类,对于 ServiceLoader 加载的详细过程此处不展开。


JDK 对 SPI 的加载实现存在一个较为突出的小缺点,无法按需加载实现类,通过 ServiceLoader.load 加载时会将文件中的所有实现都进行实例化,如果想要获取具体某个具体的实现类需要进行遍历判断。


1.2 Dubbo SPI


SPI 扩展是 Dubbo 的最大的优点之一,支持协议扩展、调用拦截扩展、引用监听扩展等等。在 Dubbo 中,根据不同的扩展定位,扩展文件分别被放置在 META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/这三个路径下。


Dubbo 中有直接使用 JDK SPI 实现的方式,比 org.apache.dubbo.common.extension.LoadingStrategy 放在 META-INF/services/路径下,但大多情况下都是使用其自身对 JDK SPI 的实现的一种优化方式,可称为 Dubbo SPI,也就是本文要讲解的点。


相比于 JDK 的 SPI 的实现,Dubbo SPI 具有以下特点:

配置形式更灵活:支持以 key:value 的形式在文件里配置类似 name:xxx.xxx.xxx.xx,后续可以通过 name 来进行扩展类按需精准获取。


缓存的使用:使用缓存提升性能,保证一个扩展实现类至多会加载一次。


对扩展类细分扩展:支持扩展点自动包装(Wrapper)、扩展点自动装配、扩展点自适应(@Adaptive)、扩展点自动激活(@Activate)。


Dubbo 对扩展点的加载主要由 ExtensionLoader 这个类展开。


二、加载-ExtensionLoader


ExtensionLoader 在 Dubbo 里的角色类似 ServiceLoader 在 JDK 中的存在,用于加载扩展类。在 Dubbo 源码里,随处都可以见到 ExtensionLoader 的身影,比如在服务暴露里的关键类 ServiceConfig 中等,弄清楚 ExtensionLoader 的实现细节对阅读 Dubbo 源码有很大的帮助。


2.1 获取 ExtensionLoader 的实例


ExtensionLoader 没有提供共有的构造函数,只能通过 ExtensionLoader.getExtensionLoader(Class<T> type)来获取 ExtensionLoader 实例。

public class ExtensionLoader<T> {     // ConcurrentHashMap缓存,key -> Class value -> ExtensionLoader实例    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);     private ExtensionLoader(Class<?> type) {        this.type = type;        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());    }     public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {        if (type == null) {            throw new IllegalArgumentException("Extension type == null");        }        // 检查是否是接口,如果不是则抛出异常        if (!type.isInterface()) {            throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");        }         // 检查接口是否是被@SPI注解修饰,如果没有则抛出异常        if (!withExtensionAnnotation(type)) {            throw new IllegalArgumentException("Extension type (" + type +                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");        }         // 从缓存里取,没有则初始化一个并放入缓存EXTENSION_LOADERS中        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);        if (loader == null) {            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);        }        return loader;    }}
复制代码

上面的代码展示了获取 ExtensionLoader 实例的过程,可以看出,每一个被 @SPI 修饰的接口都会对应同一个 ExtensionLoader 实例,且对应 ExtensionLoader 只会被初始化一次,并缓存在 ConcurresntHashMap 中。

2.2 加载扩展类


加载扩展类入口,当使用 ExtensionLoader 时,getExtensionName、getActivateExtension 或是 getDefaultExtension 都要经过 getExtensionClasses 方法来加载扩展类,如下图;


getExtensionClasses 方法调用的路径如下图,getExtensionClasses 是加载扩展类的一个起点,会首先从缓存中获取,如果缓存中没有则通过 loadExtensionClasses 方法来加载扩展类,所以说实际上的加载逻辑入口在 loadExtensionClasses。


getExtensionClasses	|->loadExtensionClasses		|->cacheDefaultExtensionName		|->loadDirectory			|->loadResource				|->loadClass
复制代码

2.2.1 loadExtensionClasses 加载扩展类


由于整个加载过程设计的源码较多,因此用一个流程图来进行描述,具体细节可以结合源码进行查看。


loadExtensionClasses 主要做了以下这几件事:


默认扩展名:

抽取默认扩展实现名并缓存在 ExtensionLoader 里的 cachedDefaultName,默认扩展名配置通过 @SPI 注解在接口上配置,如配置 @SPI("defaultName")则默认扩展名为 defaultName。


加载扩展类信息:

从 META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/这三个路径下寻找以类的全路径名命名的文件,并逐行读取文件里的内容。


加载 class 并缓存:

对扩展类分为自适应扩展实现类(被 @Adaptive 修饰的实现类)、包装类(拥有一个只有一个为这个接口类型的参数的构造方法)、普通扩展类,其中普通扩展类中又包含自动激活扩展类(被 @Activate 修饰的类)和真普通的类,对自适应扩展实现类、包装类、自动激活扩展类这三种类型的类分别加载并分别缓存到 cachedAdaptiveClass、cachedActivates、cachedWrapperClasses。


返回结果 Map<String, Class<?>>:

结果返回 Map,其中 key 对应扩展文件里配置的 name,value 对应扩展的类 class,最后在 getExtensionClasses 方法里会将此结果放入缓存 cachedClasses 中。此结果 Map 中包含除了自适应扩展实现类和包装实现类的其他所用的扩展类名与对应类的映射关系。


通过 loadExtensionClasses 方法把扩展类(Class 对象)都加载到相应的缓存中,是为了方便后面实例化扩展类对象,通过 newInstance()等方法来实例化。

2.2.2 扩展包装类


什么是扩展包装类?是不是类名结尾包含了 Wrapper 的类就是扩展包装类?


在 Dubbo SPI 接口的实现扩展类中,如果此类包含一个此接口作为参数的构造方法,则为扩展包装类。扩展包装类的作用是持有具体的扩展实现类,可以一层一层的被包装,作用类似 AOP。



包装扩展类的作用是类似 AOP,方便扩展增强。具体实现代码如下:



从代码中可以得出,可以通过 boolean wrap 选择是否使用包装类,默认情况下为 true;如果有扩展包装类,实际的实现类会被包装类按一定的顺序一层一层包起来。


如 Protocol 的实现类 ProtocolFilterWrapper、ProtocolListenerWrapper 都是扩展包装类。

2.2.3 自适应扩展实现类

2.2.3.1 @Adaptive


@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE, ElementType.METHOD})public @interface Adaptive {    String[] value() default {};}@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE, ElementType.METHOD})public @interface Adaptive {String[] value() default {};}
复制代码

从源码以及源码注释中可以得出以下几点:


Adaptive 是一个注解,可以修饰类(接口,枚举)和方法。


此注解的作用是为 ExtensionLoader 注入扩展实例提供有用的信息。


从注释中理解 value 的作用:


value 可以决定选择使用具体的扩展类。


通过 value 配置的值 key,在修饰的方法的入参 org.apache.dubbo.common.URL 中通过 key 获取到对应的值 value,根据 value 作为 extensionName 去决定使用对应的扩展类。


如果通过 2 没有找到对应的扩展,会选择默认的扩展类,通过 @SPI 配置默认扩展类。


2.2.3.2 @Adaptive 简单例子


由于 @Adaptive 修饰类时比较好理解,这里举一个 @Adaptive 修饰方法的例子,使用 @Adaptive 修饰方法的这种情况在 Dubbo 也是随处可见。


/*** Dubbo SPI 接口*/@SPI("impl1")public interface SimpleExt {    @Adaptive({"key1", "key2"})    String yell(URL url, String s);}
复制代码


如果调用 ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension().yell(url, s)方法,最终调用哪一个扩展类的实例去执行 yell 方法的流程大致为:先获取扩展类的名称 extName(对应上面说的 name:class 中的 name),然后通过 extName 来获取对应的类 Class,再实例化进行调用。所以关键的步骤在怎么得到 extName,上面的这个例子得到 extName 的流程为:


通过 url.getParameters.get("key1")获取,


没有获取到则用 url.getParameters.get("key2"),如果还是没有获取到则使用 impl1 对应的实现类,


最后还是没有获取到则抛异常 IllegalStateException。


可以看出,@Adaptive 的好处就是可以通过方法入参决定具体调用哪一个实现类。下面会对 @Adaptive 的具体实现进行详细分析。

2.2.3.3 @Adaptive 加载流程



流程关键点说明:

1)黄色标记的,cachedAdaptiveClass 是在 ExtensionLoader#loadClass 方法中加载 Extension 类时缓存的。


2)绿色标记的,如果 Extension 类中存在被 @Adaptive 修饰的类时会使用该类来初始化实例。


3)红色标记的,如果 Extension 类中不存在被 @Adaptive 修饰的类时,则需要动态生成代码,通过 javassist(默认)来编译生成 Xxxx$Adaptive 类来实例化。


4)实例化后通过 injectExtension 来将 Adaptive 实例的 Extension 注入(属性注入)。


后续围绕上述的关键点 3 详细展开,关键点 4 此处不展开。


动态生成 Xxx$Adaptive 类:下面的代码为动态生成 Adaptive 类的相关代码,具体生成代码的细节在 AdaptiveClassCodeGenerator#generate 中


public class ExtensionLoader<T> {    // ...     private Class<?> getAdaptiveExtensionClass() {        // 根据对应的SPI文件加载扩展类并缓存,细节此处不展开        getExtensionClasses();        // 如果存在被@Adaptive修饰的类则直接返回此类        if (cachedAdaptiveClass != null) {            return cachedAdaptiveClass;        }        // 动态生成Xxxx$Adaptive类        return cachedAdaptiveClass = createAdaptiveExtensionClass();    }     private Class<?> createAdaptiveExtensionClass() {        // 生成Xxxx$Adaptive类代码,可自行加日志或断点查看生成的代码        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();        ClassLoader classLoader = findClassLoader();        // 获取动态编译器,默认为javassist        org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();        return compiler.compile(code, classLoader);    }}public class ExtensionLoader<T> {// ...private Class<?> getAdaptiveExtensionClass() {// 根据对应的SPI文件加载扩展类并缓存,细节此处不展开        getExtensionClasses();// 如果存在被@Adaptive修饰的类则直接返回此类if (cachedAdaptiveClass != null) {return cachedAdaptiveClass;        }// 动态生成Xxxx$Adaptive类return cachedAdaptiveClass = createAdaptiveExtensionClass();    }private Class<?> createAdaptiveExtensionClass() {// 生成Xxxx$Adaptive类代码,可自行加日志或断点查看生成的代码        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();        ClassLoader classLoader = findClassLoader();// 获取动态编译器,默认为javassist        org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();return compiler.compile(code, classLoader);    }}
复制代码


AdaptiveClassCodeGenerator#generate 生成 code 的方式是通过字符串拼接,大量使用 String.format,整个代码过程比较繁琐,可通过 debug 去了解细节。


最关键的的部分是生成被 @Adaptive 修饰的方法的内容,也就是最终调用实例的 @Adaptive 方法时,可通过参数来动态选择具体使用哪个扩展实例。下面对此部分进行分析:


public class AdaptiveClassCodeGenerator {    // ...    /**     * generate method content     */    private String generateMethodContent(Method method) {        // 获取方法上的@Adaptive注解        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);        StringBuilder code = new StringBuilder(512);        if (adaptiveAnnotation == null) {            // 方法时没有@Adaptive注解,生成不支持的代码            return generateUnsupported(method);        } else {            // 方法参数里URL是第几个参数,不存在则为-1            int urlTypeIndex = getUrlTypeIndex(method);             // found parameter in URL type            if (urlTypeIndex != -1) {                // Null Point check                code.append(generateUrlNullCheck(urlTypeIndex));            } else {                // did not find parameter in URL type                code.append(generateUrlAssignmentIndirectly(method));            }            // 获取方法上@Adaptive配置的value            // 比如 @Adaptive({"key1","key2"}),则会返回String数组{"key1","key2"}            // 如果@Adaptive没有配置value,则会根据简写接口名按驼峰用.分割,比如SimpleExt对应simple.ext            String[] value = getMethodAdaptiveValue(adaptiveAnnotation);             // 参数里是否存在org.apache.dubbo.rpc.Invocation            boolean hasInvocation = hasInvocationArgument(method);             code.append(generateInvocationArgumentNullCheck(method));            // 生成String extName = xxx;的代码 ,extName用于获取具体的Extension实例            code.append(generateExtNameAssignment(value, hasInvocation));            // check extName == null?            code.append(generateExtNameNullCheck(value));             code.append(generateExtensionAssignment());             // return statement            code.append(generateReturnAndInvocation(method));        }         return code.toString();    } }
复制代码


上述生成 Adaptive 类的方法内容中最关键的步骤在生成 extName 的部分,也就是

generateExtNameAssignment(value,hasInvocation),此方法 if 太多了(有点眼花缭乱)。


以下列举几个例子对此方法的实现流程进行简单展示:假设方法中的参数不包含 org.apache.dubbo.rpc.Invocation,包含 org.apache.dubbo.rpc.Invocation 的情况会更加复杂。


1)方法被 @Adaptive 修饰,没有配置 value,且在接口 @SPI 上配置了默认的实现

@SPI("impl1")public interface SimpleExt {    @Adaptive    String echo(URL url, String s);}
复制代码


对应生成 extName 的代码为:

String extName = url.getParameter("simple.ext", "impl1")
复制代码


2)方法被 @Adaptive 修饰,没有配置 value,且在接口 @SPI 上没有配置默认的实现

@SPIpublic interface SimpleExt {    @Adaptive    String echo(URL url, String s);}
复制代码


对应生成 extName 的代码为:

String extName = url.getParameter( "simple.ext")
复制代码


3)方法被 @Adaptive 修饰,配置了 value(假设两个,依次类推),且在接口 @SPI 上配置了默认的实现

@SPI("impl1")public interface SimpleExt {    @Adaptive({"key1", "key2"})    String yell(URL url, String s);}
复制代码


对应生成 extName 的代码为:

String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));
复制代码


4)方法被 @Adaptive 修饰,配置了 value(假设两个,依次类推),且在接口 @SPI 没有配置默认的实现

@SPIpublic interface SimpleExt {    @Adaptive({"key1", "key2"})    String yell(URL url, String s);}
复制代码


对应生成 extName 的代码为:

String extName = url.getParameter("key1", url.getParameter("key2"));
复制代码

完整的生成类可参见附录。

2.2.4 自动激活扩展类


如果你有扩展实现过 Dubbo 的 Filter,那么一定会对 @Activate 很熟悉。@Activate 注解的作用是可以通过给定的条件来自动激活扩展实现类,通过 ExtensionLoader#getActivateExtension(URL,String, String)方法可以找到指定条件下需要激活的扩展类列表。


下面以一个例子来对 @Activate 的作用进行说明,在 Consumer 调用 Dubbo 接口时,会经过消费方的过滤器链以及提供方的过滤器链,在 Provider 暴露服务的过程中会拼接需要使用哪些 Filter。


对应源码中的位置在 ProtocolFilterWrapper#buildInvokerChain(invoker, key, group)方法中。


// export:key-> service.filter ; group-> providerprivate static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {    // 在Provider暴露服务服务export时,会根据获取Url中的service.filter对应的值和group=provider来获取激活对应的Filter    List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);}
复制代码


ExtensionLoader#getActivateExtension(URL, String, String)是怎么根据条件来自动激活对应的扩展类列表的可以自行查看该方法的代码,此处不展开。

三、总结 


本文主要对 Dubbo SPI 机制的扩展类加载过程通过 ExtensionLoader 类源码来进行总结,可以概况为以下几点:


1.Dubbo SPI 结合了 JDK SPI 的实现,并在此基础上进行优化,如精准按需加载扩展类、缓存提升性能。


2.分析 ExtensionLoader 加载扩展类的过程,加载 META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/这三个路径下的文件,并分类缓存在 ExtensionLoader 实例。


3.介绍扩展包装类及其实现过程,扩展包装类实现了类似 AOP 的功能。


4.自适应扩展类,分析 @Adptive 修饰方法时动态生成 Xxx$Adaptive 类的过程,以及通过参数自适应选择扩展实现类完成方法调用的案例介绍。


简单介绍自动激活扩展类及 @Activate 的作用。


四、附录

4.1 Xxx$Adaptive 完整案例


@SPI 接口定义

@SPI("impl1")public interface SimpleExt {    // @Adaptive example, do not specify a explicit key.    @Adaptive    String echo(URL url, String s);     @Adaptive({"key1", "key2"})    String yell(URL url, String s);     // no @Adaptive    String bang(URL url, int i);}
复制代码


生成的 Adaptive 类代码

package org.apache.dubbo.common.extension.ext1;  import org.apache.dubbo.common.extension.ExtensionLoader;  public class SimpleExt$Adaptive implements org.apache.dubbo.common.extension.ext1.SimpleExt {      public java.lang.String yell(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {        if (arg0 == null) throw new IllegalArgumentException("url == null");        org.apache.dubbo.common.URL url = arg0;        String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));        if (extName == null)            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([key1, key2])");        org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt)ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);        return extension.yell(arg0, arg1);    }      public java.lang.String echo(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {        if (arg0 == null) throw new IllegalArgumentException("url == null");        org.apache.dubbo.common.URL url = arg0;        String extName = url.getParameter("simple.ext", "impl1");        if (extName == null)            throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([simple.ext])");        org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt) ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);        return extension.echo(arg0, arg1);    }      public java.lang.String bang(org.apache.dubbo.common.URL arg0, int arg1) {        throw new UnsupportedOperationException("The method public abstract java.lang.String org.apache.dubbo.common.extension.ext1.SimpleExt.bang(org.apache.dubbo.common.URL,int) of interface org.apache.dubbo.common.extension.ext1.SimpleExt is not adaptive method!");    }  }
复制代码


作者:vivo 互联网服务器团队-Ning Peng

发布于: 3 小时前阅读数: 4
用户头像

官方公众号:vivo互联网技术,ID:vivoVMIC 2020.07.10 加入

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

评论

发布
暂无评论
详解Apache Dubbo的SPI实现机制