乐趣区

关于dubbo:详解Apache-Dubbo的SPI实现机制

一、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.AudiCar
com.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
 
    // 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 {};
}

从源码以及源码正文中能够得出以下几点:

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);
    }
}

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 上没有配置默认的实现

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

对应生成 extName 的代码为:

String extName = url.getParameter("simple.ext")

3)办法被 @Adaptive 润饰,配置了 value(假如两个,顺次类推),且在接口 @SPI 上配置了默认的实现

@SPI
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 没有配置默认的实现

@SPI
public 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-> provider
private 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

退出移动版