从Java SPI到Dubbo SPI

基于Dubbo SPI 的加载机制,让整个框架的接口和具体实现完全解耦,从而奠定了整个框架良好的可扩展性。Dubbo SPI 并没有直接使用 Java SPI,而是在其基础上做了相应的改进,形成了一套自己的规范和特性。下面我们从最基础的 Java SPI 介绍一下 SPI 的思想。

Java SPI

SPI (Service Provider Interface)最初是提供给厂商做插件开发的。Java SPI 使用了策略模式,一个接口会有多个实现。我们只生命接口,具体的实现并不在程序中直接确定,而是由程序之外的配置控制,用于最终的装配。一个简单的实现步骤如下:

1. 定义一个接口及方法

2. 编写该接口的一个实现

3. 在META-INF/services/ 目录下创建一个接口全路径命名的文件,如 com.moguhu.spi.HelloService

4. 文件内容为接口的具体实现类的全路径,每个实现类单独一行

5. 在代码中通过 java.util.ServiceLoader 来加载具体的实现

下面我们看下具体的代码实现

下面我们看下获取实现类的工具方法 SpiServiceHelper 的代码实现:

/**
 * SPI类加载辅助工具
 *
 * @author xuefeihu created on 2021/05/31.
 */
public class SpiServiceHelper {

    private final static Map<String, Object> singletonServices = new HashMap<>();

    public synchronized static <S> S load(Class<S> service) {
        return StreamSupport.stream(ServiceLoader.load(service).spliterator(), false).map(SpiServiceHelper::singletonFilter)
                .findFirst().orElseThrow(ServiceLoadException::new);
    }

    public synchronized static <S> Collection<S> loadAll(Class<S> service) {
        return StreamSupport.stream(ServiceLoader.load(service).spliterator(), false).map(SpiServiceHelper::singletonFilter)
                .collect(Collectors.toList());
    }

    @SuppressWarnings("unchecked")
    private static <S> S singletonFilter(S service) {
        if (service.getClass().isAnnotationPresent(MoSingleton.class)) {
            String className = service.getClass().getCanonicalName();
            Object singletonInstance = singletonServices.putIfAbsent(className, service);
            return singletonInstance == null ? service : (S) singletonInstance;
        } else {
            return service;
        }
    }

}

Dubbo SPI 规范

Dubbo SPI 和 Java SPI 类似,需要在 META-INF/dubbo/ 目录下放置对应的 SPI 配置文件,文件名为接口的全路径名。如下所示:


这里面需要注意的是,除了资源路径与Java SPI 不同之外,文件内容中也多了个 key,如上图中的dubbo、hessian2、fastjson 等。这里与dubbo 最开始 2.0.x 版本中的 SPI 也有所不同(原先@SPI 叫做 @Extension),原先每个SPI 的实现的名称是写在每个实现类上的。接口和注解都需要有 @Extension 注解,实现类的注解上面会写这个SPI 实现的名字。而最新的扩展则是只需要在接口上加上 @SPI,然后配置文件上带有对应 SPI 实现类的名称,从一定程度上做了简化。


扩展点的分类与缓存

Dubbo SPI 是按需加载的,可以分为 Class 缓存、实例缓存。这两种缓存又能根据扩展类的种类分为普通扩展类、包装扩展类(Wrapper)、自适应扩展类(Adaptive)等。

Class 缓存:Dubbo SPI 获取扩展类时,首先会从缓存中获取。如果缓存中不存在,则会加载配置文件,将Class 缓存加载到内存中,类似于懒加载。

实例缓存:ExtensionLoader 中不仅会缓存Class,也会缓存Class实例化后的对象。整体原则就是会从缓存中优先获取,缓存中没有的再使用 Class.forName() 将类加载到内存中。

被缓存的Class 和对象实例可以分为以下几类:

1. 普通扩展类,最基础的,配置在SPI 配置文件中的扩展类实现

2. 包装扩展类,包装类通常不会直接承载具体的实现,它是对ExtensionLoader加载的策略类的包装,通过这个包装可以实现Filter、Listener 等效果。如 Protocol 的包装类 ProtocolFilterWrapper、ProtocolListenerWrapper

3. 自适应扩展类,可以理解为对不同策略类的包装,这个类是由Dubbo 动态生成的,里面的内容大概可以理解为就是一个策略的组装,最终的业务调用还是由各个策略类实现。不同策略的选择通常是由 URL 参数决定的。

4. 其他缓存,如扩展类加载器缓存、扩展名缓存等。


扩展点提供的功能

Dubbo SPI 一共包含 4 中特性,包含:自动包装(Wrapper)、自动装载(Autowire)、自适应(Adaptive)、自动激活(Active),下面分别介绍一下这4类特性。其中 Wrapper、Autowire、Adaptive 这3 个功能,在 Dubbo 开源的第一版(2.0.7)中就已经提供,后续只是简单的加了一个 Active 的功能。

自动包装

在ExtensionLoader 加载扩展时,如果发现这个类有1个参数并且参数为ExtensionLoader<T> 中的T 类型时,就会自动认为是 wrappers(老版本命名叫做 autoproxies),也就是表示对真正的扩展包了一层。

public class ProtocolListenerWrapper implements Protocol {

    public ProtocolListenerWrapper(Protocol protocol) {

        if (protocol == null) {

            throw new IllegalArgumentException("protocol == null");

        }

        this.protocol = protocol;

    }

}

从上面可以看出,ProtocolListenerWrapper 有个1个参数且入参为 Protocol 的构造方法,此时ProtocolListenerWrapper j就是一个包装类,当获取一个Protocol的扩展时,如:DubboProtocol,此时就会为 DubboProtocol 自动作为入参,被包装为ProtocolListenerWrapper 的类型。因为 ProtocolListenerWrapper 也是一个Protocol 类型,此时ExtensionLoader 可以返回此包装类给上游使用。包装类主要是用于增强 扩展类的功能,比如 Protocol 的包装类有 2个:ProtocolListenerWrapper 和 ProtocolFilterWrapper ,分别为 Protocol 增加了过滤器和 监听器,以扩展其功能。

自动装载

自动装载类似于Spring 中的 Autowire,也就是自动装配。当一个扩展类是另外一个扩展类的成员变量时,并且有 setter 方法,此时ExtensionLoader 就会自动new 一个扩展类,自动注入。通常被注入的会是一个接口类型,如 Protocol,则会被注入为 Protocol$Adaptive,也就是一个自适应类型。

自适应

自适应的扩展可以认为是策略模式中,组装策略的那部分代码,而策略本身是由 URL 参数中的参数决定的。下面我们看下一个 Adaptive 的例子。

首先我们提供一个扩展接口 SimpleExt,其中有3个方法,echo()、yell() 允许做自适应,而 bang() 不允许做自适应。

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

经过ExtensionLoader 创建出来的自适应扩展代码,大概如下所示,也就是说具体通过哪个策略,是通过 URL 中对应的参数决定的:

package com.alibaba.dubbo.common.extensionloader.ext1;

import com.alibaba.dubbo.common.extension.ExtensionLoader;
import com.alibaba.dubbo.common.extensionloader.ext1.SimpleExt;

public class SimpleExt$Adaptive implements SimpleExt {

	public java.lang.String echo(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) {
		if (arg0 == null) 
			throw new IllegalArgumentException("url == null");
		com.alibaba.dubbo.common.URL url = arg0;
		String extName = url.getParameter("simple.ext", "impl1");
		if(extName == null) 
			throw new IllegalStateException("Fail to get extension(SimpleExt) name from url(" + url.toString() + ") use keys([simple.ext])");
		SimpleExt extension = (SimpleExt)ExtensionLoader.getExtensionLoader(SimpleExt.class).getExtension(extName);
		return extension.echo(arg0, arg1);
	}
	
	public java.lang.String yell(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) {
		if (arg0 == null) 
			throw new IllegalArgumentException("url == null");
		com.alibaba.dubbo.common.URL url = arg0;
		String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));
		if(extName == null) 
			throw new IllegalStateException("Fail to get extension(SimpleExt) name from url(" + url.toString() + ") use keys([key1, key2])");
		SimpleExt extension = (SimpleExt)ExtensionLoader.getExtensionLoader(SimpleExt.class).getExtension(extName);
		return extension.yell(arg0, arg1);
	}
	
	public java.lang.String bang(com.alibaba.dubbo.common.URL arg0, int arg1) {
		throw new UnsupportedOperationException("method bang is not adaptive method!");
	}
	
}

自动激活

自动激活是ExtensionLoader 后续新增的功能,通过 @Adaptive 注解实现。可以标记对应的扩展点是否可以被激活使用,目前主要用于Filter 链构建前的,可用Filter列表获取。因为是后续新增的功能,笔者对比了Dubbo 2.6.0 和 2.0.7 两个版本里面Filter 列表获取的差异,老的里面是通过配置写死的 Filter 列表顺序,而新的顺序则是通过 @Adaptive 在各个扩展实现类中标注的,此时它可以标注分组(Filter 链可以用在服务端/客户端),也可以标注分组内的顺序。此时依赖 @Active 注解就可以构建出原来写死在代码里的 Filter 顺序列表了。


参考:《深入理解Apache Dubbo 与实战》、Dubbo 2.6.0 源代码、Dubbo 2.0.7 源代码