Dubbo(三) 认识SPI

332 阅读8分钟

SPI

SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现。核心思想为解耦

java SPI

先提供一个java spi的demo出来

在META-INF/services 目录下,创建一个文件,文件名为接口的全路径。 例如 com.test.spi.PrintService 文件的内容为实现类的全路径 如 : com.test.spi. PrintServiceImpl 。如果存在多个实现类,用换行进行区分,案例如下

  ServiceLoader<Printservice> serviceLoader = ServiceLoader.load(Printservice.class);

  for (Printservice printservice : serviceLoader) {
  // 具体的方法
      printservice.println();
  }

serviceLoader中,加载了所有的接口的服务。同时在ServiceLoader实现了Iterable接口,所以他拥有迭代器的功能,但是从ServiceLoader所提供的api来看,没有单独获取一个的api,故每次获取指定的实现类都需要遍历。

在ServiceLoader中拥有属性PREFIX = "META-INF/services/"; ,回答了为什么我们的spi配置要放在这个目录下面。

同时,在多个并发多线程使用 ServiceLoader 类的实例是不安全的。

Dubbo SPI

Dubbo框架比较突出的是,因为它拥有非常丰富的扩展点可供开发者自己去扩展。那么必然会针对java的spi做出一些改进。下面是官网的截图:

使用目录有3个 META-INF/services/、META-INF/dubbo/、META-INF/dubbo/internal ,同时文件名称,全路径名称 内容: key = value 换行分割 ,key在@SPI注解中使用 例如文件com.alibaba.dubbo.demo.provider.Printservice,内容为

p=com.alibaba.dubbo.demo.provider.PrintserviceImpl
Printservice printservice = ExtensionLoader.getExtensionLoader(Printservice.class).getDefaultExtension();
printservice.println();

同时提供了@SPI @Adaptive @Active 等注解来完善整个spi的生态

比较

引用官方的网站的介绍:

Dubbo 改进了 JDK 标准的 SPI 的以下问题:

  • DK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因
  • 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点

Dubbo SPI

缓存

Dubbo SPI在性能上面的优势,主要是Dubbo SPI 运用了缓存,采用两种缓存

  • class缓存 根据配置把Class缓存到内存中,并不会直接全部初始化
  • 实例缓存 基于性能的考虑,实例化的对象也会缓存起来,并且是按需实例化。

大多数的框架在内存中的缓存都采用了ConcurrentMap,dubbo具体的缓存信息如下

// 下面2个大缓存是static 保存了全局的缓存
// 扩展类与对应的扩展类加载器缓存
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
//扩展类与类初始化后的实例
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();

// 下面几个缓存是非static 保存了当个ExtensionLoader中的缓存
// Wrapper类缓存
Set<Class<?» cachedWrapperClasses 
//自适应扩展类缓存
Class<?> cachedAdaptiveClass  
//扩展名与扩展对象缓存
ConcurrentMap<String, Holder<Object» cachedlnstances 
// 实例化后的自适应(Adaptive) 扩展对象,只 能同时存在一个
Holder<Obj ect> cachedAdaptivelnstance
//扩展类与扩展名缓存
ConcurrentMap<Class<?>, String> cachedNames
// 扩展名的缓存
Map<String, Activate> cachedActivates
// 普通扩展类缓存,不包括自适应拓展类和 Wrapper 类
Holder<Map<String, Class<?»> cachedClasses

从缓存的结构中,可以看到大致把SPI的类型分为3种

  • Wrapper类包装扩展类
  • 自适应扩展类 直接由@Adaptive 注解修饰类
  • 普通扩展类 ,除了上面2类,都是普通包装类

SPI注解

在Dubbo 中运用了3种注解来玩转SPI,@SPI @Adaptive @Activate, 在源码中的示例代码如下

// 负载均衡的扩展点
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {

   
	@Adaptive("loadbalance")
    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;

}

@SPI修饰的接口,代表是一个扩展点,@Adaptive是自适应,其中的value代表的是url中的一个参数的key,在执行自适应的方法是,会先更具url中对应的value,来选择匹配的扩展类。其中value是一个数组,代表当前一个key不存在时,会按顺序往下寻找,例如org.apache.dubbo.remoting.Transporter#bind

  @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
  Server bind(URL url, ChannelHandler handler) throws RemotingException;

除了可以获取单个的自适应类外,dubbo中还提供了同时启用多个扩展类。@Activate注解 @Activate可以标记在类、接口、枚举类和方法上。大多数场景使用在过滤器中,组成一条过滤链

	//URL中的分组如果匹配则激活,则可以设置多个
	String[] group() default {};
	//查找URL中如果含有该key值,则会激活
	String[] value() default {};
	//填写扩展点列表,表示哪些扩展点要在本扩展点之前
	String[] before() default {};
	//同上,表示哪些需要在本扩展点之后
  	String[] after() default {};
	//整型,直接的排序信息
	int order() default 0;

例如源码中的

//该过滤器主要设置RpcContext中的attachments,提供了隐式参数
//功能,解决了dubbo服务在发布后url不会改变的问题,通过隐式参数
//可以动态的修改url,这样的修改不具备“持久化”,仅生效于当前的会话
@Activate(group = Constants.CONSUMER, order = -10000)
public class ConsumerContextFilter implements Filter {

dubbo SPI特性

扩展类一共包含四种特性:自动包装、自动加载、自适应 和自动激活,由上面提到的3种SPI类来完成。

自动包装

由Wrapper类包装扩展类来完成,例如下

public class ProtocolFilterWrapper implements Protocol {

    private final Protocol protocol;
	// 实现Protocol,但是构造函数中又注入了一个Protocol,框架会自动注入
    public ProtocolFilterWrapper(Protocol protocol) {
        if (protocol == null) {
            throw new IllegalArgumentException("protocol == null");
        }
        this.protocol = protocol;
  	}
	....
}

Wrapper 类同样实现了扩展点接口,但是 Wrapper 不是扩展点的真正实现。它的用途主要是用于从 ExtensionLoader 返回扩展点时,包装在真正的扩展点实现外。即从 ExtensionLoader 中返回的实际上是 Wrapper 类的实例,Wrapper 持有了实际的扩展点实现类。

通过 Wrapper 类可以把所有扩展点公共逻辑移至 Wrapper 中。新加的 Wrapper 在所有的扩展点上添加了逻辑,有些类似 AOP,即 Wrapper 代理了扩展点

自动装配

除了通过构造方法注入,还经常使用setter方法设置属性值。下面是官网的例子 有两个为扩展点 CarMaker(造车者)、WheelMaker (造轮者)

public interface CarMaker {
    Car makeCar();
}
 
public interface WheelMaker {
    Wheel makeWheel();
}
public class RaceCarMaker implements CarMaker {
    WheelMaker wheelMaker;
 
    public setWheelMaker(WheelMaker wheelMaker) {
        this.wheelMaker = wheelMaker;
    }
 
    public Car makeCar() {
        // ...
        Wheel wheel = wheelMaker.makeWheel();
        // ...
        return new RaceCar(wheel, ...);
    }
}

ExtensionLoader 加载 CarMaker 的扩展点实现 RaceCarMaker 时,setWheelMaker 方法的 WheelMaker 也是扩展点则会注入 WheelMaker 的实现。

这里带来另一个问题,ExtensionLoader 要注入依赖扩展点时,如何决定要注入依赖扩展点的哪个实现。在这个示例中,即是在多个WheelMaker 的实现中要注入哪个

自适应

也就是@Adaptive 的使用,在上面中介绍了@Adaptive修饰方法的时候,常用的做法为可以根据url中的key,来选择自适应的实现类。
@Adaptive修饰类时,整个类作为默认实现,所以在扩展点的多个实现类中,只有一个类可以加@Adaptive,如果多个 实现类都有该注解,则会抛出异常:More than 1 adaptive class found

当@Adaptive的key都比对完毕时还找不到自适应的类,那就会采用“驼峰规则”匹配,Dubbo 会自动把接口名称根据驼峰大小写分开,并用符号连接起来,以此来作为默认实现类的名 称,如 org.apache.dubbo.xxx.YyylnvokerWpappep 中的 YyylnvokerWrapper 会被转换为 yyy.invoker.wrapper,以此来比对

扩展点自动激活

就是@Activate 的使用,在上面已经看过

ExtensionLoader

ExtensionLoader是整个扩展机制的主要逻辑类。 主要分为3大入口

  • getExtension 返回ExtensionLoader<T>
  • getActivateExtension 返回List<T>
  • getAdaptiveExtension 返回<T>

流程图如下:源自《深入理解Apache Dubbo与实战》

getAdaptiveExtension会通过代码字符串生成一个自适应的类,一般类名为xxxx$Adaptive。 源码解析就不写了,感觉过于冗余,而且比较拖沓。读者可以自行通过3大入口去翻阅。这里贴一下我的demo中生成的自适应类 接口案例

@SPI("p")
public interface Printservice {
    @Adaptive({Constants.SERVER_KEY, "c"})
    void  println(URL url);

}
public class Printservice$Adaptive implements com.alibaba.dubbo.demo.provider.Printservice {
        public void println(com.alibaba.dubbo.common.URL arg0) {
            if (arg0 == null) throw new IllegalArgumentException("url == null");
            com.alibaba.dubbo.common.URL url = arg0;
           /**
          *注意:如果@Adaptive注解没有传入key参数,则默认会把类名转化为key
          * 如:AaaaBbbb会转化为 aaaa.bbbb *根据key获取对应的扩展点实现名称,第一个参数是key,第二个是获取不到时的默认值 * URL 中没有"aaaa.bbbb"这个key
          *
          */
            url.getParameter("key1", "impll");
            String extName = url.getParameter("server", url.getParameter("c", "p"));
            if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.demo.provider.Printservice) name from url(" + url.toString() + ") use keys([server, c])");
            com.alibaba.dubbo.demo.provider.Printservice extension = (com.alibaba.dubbo.demo.provider.Printservice)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.demo.provider.Printservice.class).getExtension(extName);extension.println(arg0);
        }
    }

上面是自适应类,可以更具url的key来选择对应的实现类 那么假如@SPI和@Adaptive都有value,从代码中可知,当url中包含了key时,会同过url去查找自适应类,当不包含时,采用@SPI中的默认值。如果@SPI注解中没有默认值,则把类名转化 为key,再去查找,也就是“驼峰规则”匹配