从上一篇 Java SPI 机制解析 可以知道 Java SPI 的一些劣势。Dubbo 的扩展点加载从 Java SPI 扩展点发现机制加强而来。
Dubbo 改进了 Java SPI 的以下问题:
- JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
- 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
- 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。
本文从以下几个方面,深入解析 Dubbo SPI 机制:
- Dubbo SPI 特性
- Dubbo SPI 的一些定义
- Dubbo SPI 源码解析
Dubbo SPI 特性
扩展点自动包装
自动包装扩展点的 Wrapper 类。ExtensionLoader 在加载扩展点时,如果加载到的扩展点有拷贝构造函数,则判定为扩展点 Wrapper 类。Wrapper类内容:
package com.alibaba.xxx;
import com.alibaba.dubbo.rpc.Protocol;
public class XxxProtocolWrapper implements Protocol {
Protocol impl;
public XxxProtocolWrapper(Protocol protocol) { impl = protocol; }
// 接口方法做一个操作后,再调用extension的方法
public void refer() {
//... 一些操作
impl.refer();
// ... 一些操作
}
}
Wrapper 类同样实现了扩展点接口,但是 Wrapper 不是扩展点的真正实现。它的用途主要是用于从 ExtensionLoader 返回扩展点时,包装在真正的扩展点实现外。即从 ExtensionLoader 中返回的实际上是 Wrapper 类的实例,Wrapper 持有了实际的扩展点实现类。
扩展点的 Wrapper 类可以有多个,也可以根据需要新增。通过 Wrapper 类可以把所有扩展点公共逻辑移至 Wrapper 中。新加的 Wrapper 在所有的扩展点上添加了逻辑,有些类似 AOP,即 Wrapper 代理了扩展点。
扩展点自动装配
加载扩展点时,自动注入依赖的扩展点。加载扩展点时,扩展点实现类的成员如果为其它扩展点类型,ExtensionLoader 在会自动注入依赖的扩展点。ExtensionLoader 通过扫描扩展点实现类的所有 setter 方法来判定其成员。即 ExtensionLoader 会执行扩展点的拼装操作。
示例:有两个为扩展点 CarMaker(造车者)、WheelMaker (造轮者)
public interface CarMaker {
Car makeCar();
}
public interface WheelMaker {
Wheel makeWheel();
}
CarMaker 的一个实现类:
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的扩展点实现RaceCar时,setWheelMaker方法的 WheelMaker也是扩展点则会注入WheelMaker的实现。
这里带来另一个问题,ExtensionLoader要注入依赖扩展点时,如何决定要注入依赖扩展点的哪个实现。在这个示例中,即是在多个WheelMaker的实现中要注入哪个。这个问题在下面一点扩展点自适应 中说明。
扩展点自适应
ExtensionLoader 注入的依赖扩展点是一个 Adaptive 实例,直到扩展点方法执行时才决定调用是一个扩展点实现。
Dubbo 使用 URL 对象(包含了Key-Value)传递配置信息。扩展点方法调用会有URL参数(或是参数有URL成员)。这样依赖的扩展点也可以从URL拿到配置信息,所有的扩展点自己定好配置的Key后,配置信息从URL上从最外层传入。URL在配置传递上即是一条总线。
当上面执行:
// ...
Wheel wheel = wheelMaker.makeWheel(url);
// ...
注入的 Adaptive 实例可以提取约定 Key 来决定使用哪个 WheelMaker 实现来调用对应实现的真正的 makeWheel 方法。如提取 wheel.type, key 即 url.get("wheel.type") 来决定 WheelMake 实现。Adaptive 实例的逻辑是固定,指定提取的 URL 的 Key,即可以代理真正的实现类上,可以动态生成。
在 Dubbo 的 ExtensionLoader 的扩展点类对应的 Adaptive 实现是在加载扩展点里动态生成。指定提取的 URL 的 Key 通过 @Adaptive 注解在接口方法上提供。
Dubbo SPI 的一些定义
@SPI注解,被此注解标记的接口,就表示是一个可扩展的接口。
@Adaptive注解,有两种注解方式:一种是注解在类上,一种是注解在方法上。
1、注解在类上,而且是注解在实现类上,目前dubbo只有AdaptiveCompiler和AdaptiveExtensionFactory类上标注了此注解,这是些特殊的类,ExtensionLoader需要依赖他们工作,所以得使用此方式。
2、注解在方法上,注解在接口的方法上,除了上面两个类之外,所有的都是注解在方法上。ExtensionLoader根据接口定义动态的生成适配器代码,并实例化这个生成的动态类。被Adaptive注解的方法会生成具体的方法实现。没有注解的方法生成的实现都是抛不支持的操作异常UnsupportedOperationException。被注解的方法在生成的动态类中,会根据url里的参数信息,来决定实际调用哪个扩展。
ExtensionLoader,是dubbo的SPI机制的查找服务实现的工具类,类似与Java的ServiceLoader,可做类比。dubbo约定扩展点配置文件放在classpath下的/META-INF/dubbo,/META-INF/dubbo/internal,/META-INF/services目录下,配置文件名为接口的全限定名,配置文件内容为配置名=扩展实现类的全限定名。
比如说这段代码:
private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
当上面代码执行的时候,我们其实还不知道要真正使用的Protocol是什么,可能是具体的实现DubboProtocol,也可能是其他的具体实现的Protocol,那么这时候protocol到底是什么呢?protocol其实是在调用getAdaptiveExtension()方法时候,自动生成的一个类,代码如下:
package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adpative implements Protocol {
public Invoker refer(Class arg0, URL arg1) throws Class {
if (arg1 == null) throw new IllegalArgumentException("url == null");
URL url = arg1;
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null) throw new IllegalStateException("Fail to get extension(Protocol) name from url(" + url.toString() + ") use keys([protocol])");
Protocol extension = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName);
return extension.refer(arg0, arg1);
}
public Exporter export(Invoker arg0) throws Invoker {
if (arg0 == null) throw new IllegalArgumentException("Invoker argument == null");
if (arg0.getUrl() == null) throw new IllegalArgumentException("Invoker argument getUrl() == null");URL url = arg0.getUrl();
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null) throw new IllegalStateException("Fail to get extension(Protocol) name from url(" + url.toString() + ") use keys([protocol])");
Protocol extension = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName);
return extension.export(arg0);
}
public void destroy() {
throw new UnsupportedOperationException("method public abstract void Protocol.destroy() of interface Protocol is not adaptive method!");
}
public int getDefaultPort() {
throw new UnsupportedOperationException("method public abstract int Protocol.getDefaultPort() of interface Protocol is not adaptive method!");
}
}
可以看到被@Adaptive注解的方法都生成了具体的实现,并且实现逻辑都相同。而没有被注解的方法直接抛出不支持操作的异常。
当我们使用protocol调用方法的时候,其实是调用生成的类Protocol$Adpative中的方法,这里面的方法根据url中的参数配置来找到具体的实现类,找具体实现类的方式还是通过dubbo的扩展机制。比如url中可能会有protocol=dubbo,此时就可以根据这个dubbo来确定我们要找的类是DubboProtocol。可以查看下生成的代码中getExtension(extName)这里是根据具体的名字去查找实现类。
Dubbo SPI 源码解析
了解源码结构,建立一个全局认识。结果图如下:
下面以Protocol分析扩展点的加载
//先获取ExtensionLoader实例,然后加载自适应的Protocol扩展点
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
//发布服务
protocol.export(Invoker<T> invoker);
获取ExtensionLoader实例
getExtensionLoader(Protocol.class),根据要加载的接口Protocol,创建出一个ExtensionLoader实例,加载完的实例会被缓存起来,下次再加载Protocol的ExtensionLoader的时候,会使用已经缓存的这个,不会再新建一个实例:
@SuppressWarnings("unchecked")
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 interface!");
}
//只有注解了@SPI的才会解析
if(!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}
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的实例之后,开始加载自适应实现,加载是在调用getAdaptiveExtension()方法中进行的:
第一步,从cache中获取自适应扩展点。可以关注一下,这里用了双重校验锁,Dubbo源码很多地方都用了这种方式。
@SuppressWarnings("unchecked")
public T getAdaptiveExtension() {
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
if(createAdaptiveInstanceError == null) {
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
}
}
}
}
}
第二步,缓存中不存在自适应扩展的实例,则调用createAdaptiveExtension()方法创建自适应扩展点。大家一定了解过适配器设计模式,而这个自适应扩展点实际上就是一个适配器。
private T createAdaptiveExtension() {
try {
//先通过getAdaptiveExtensionClass获取AdaptiveExtensionClass
//然后获取其实例
//最后进行注入处理
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {}
}
第三步,getAdaptiveExtensionClass()获取自适应扩展点
private Class<?> getAdaptiveExtensionClass() {
//加载当前Extension的所有实现(这里举例是Protocol,只会加载Protocol的所有实现类),如果有@Adaptive类型的实现类,会赋值给cachedAdaptiveClass
//目前只有AdaptiveExtensionFactory和AdaptiveCompiler两个实现类是被注解了@Adaptive
//除了ExtensionFactory和Compiler类型的扩展之外,其他类型的扩展都是下面动态创建的的实现
getExtensionClasses();
//加载完所有的实现之后,发现有cachedAdaptiveClass不为空
//也就是说当前获取的自适应实现类是AdaptiveExtensionFactory或者是AdaptiveCompiler,就直接返回,这两个类是特殊用处的,不用代码生成,而是现成的代码
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
//没有找到Adaptive类型的实现,动态创建一个
//比如Protocol的实现类,没有任何一个实现是用@Adaptive来注解的,只有Protocol接口的方法是有注解的
//这时候就需要来动态的生成了,也就是生成Protocol$Adaptive
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
第四步,getExtensionClasses()加载所有的扩展类(注意:这里加载的是类,不是初始化类的实例):
private Map<String, Class<?>> getExtensionClasses() {
//从缓存中获取,cachedClasses也是一个Holder,Holder这里持有的是一个Map,key是扩展点实现名,value是扩展点实现类
//这里会存放当前扩展点类型的所有的扩展点的实现类
//这里以Protocol为例,就是会存放Protocol的所有实现类
//比如key为dubbo,value为com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
//cachedClasses扩展点实现名称对应的实现类
Map<String, Class<?>> classes = cachedClasses.get();
//如果为null,说明没有被加载过,就会进行加载,而且加载就只会进行这一次
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
//如果没有加载过Extension的实现,进行扫描加载,完成后缓存起来
//每个扩展点,其实现的加载只会这执行一次
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
loadExtensionClasses()方法,该函数的作用就是扫描classpath底下的配置文件,加载该interface对应的所有的扩展点,并将扩展点进行分类(Adaptive,Activate),以及生成包装类等。在扫描的过程中,如果发现该扩展类为Adaptive类型,则将该class缓存到cachedAdaptiveClass中。如果所有的扩展类均不是Adaptive类型,则调用createAdaptiveExtensionClass生成一个Adaptive类型的扩展类。
private Map<String, Class<?>> loadExtensionClasses() {
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if(defaultAnnotation != null) {
//当前Extension的默认实现名字
//比如说Protocol接口,注解是@SPI("dubbo")
//这里dubbo就是默认的值
String value = defaultAnnotation.value();
//只能有一个默认的名字,如果多了,谁也不知道该用哪一个实现了。
if(value != null && (value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
if(names.length > 1) {
throw new IllegalStateException();
}
//默认的名字保存起来
if(names.length == 1) cachedDefaultName = names[0];
}
}
//下面就开始从配置文件中加载扩展实现类
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
//从META-INF/dubbo/internal目录下加载
loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
//从META-INF/dubbo/目录下加载
loadFile(extensionClasses, DUBBO_DIRECTORY);
//从META-INF/services/下加载
loadFile(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}
从各个位置的配置文件中加载实现类,对于Protocol来说加载的文件是以com.alibaba.dubbo.rpc.Protocol为名称的文件,文件的内容是:
registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
memcached=memcom.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
loadFile()这个函数非常长,它会加载Class类。根据不同的类型加载到不同的缓存,大概的处理流程是对配置文件中的各个扩展点进行如下操作 :
- 判断该扩展点是否是要加载的interface的子类,如果不是则忽略
- 如果该class带有Adaptive的注解,则缓存到cachedAdaptiveClass中
- 如果该class具有拷贝构造函数,则缓存到cachedWrapperClasses中
- 如果该class带有Activate注解,则缓存到cachedActivates中
- 将所有的扩展点缓存到cachedClasses中
第五步,createAdaptiveExtensionClass() ,动态创建自适应扩展点Protocol$Adaptive。
//创建一个适配器扩展点。(创建一个动态的字节码文件)
private Class<?> createAdaptiveExtensionClass() {
//生成字节码代码
String code = createAdaptiveExtensionClassCode();
//获得类加载器
ClassLoader classLoader = findClassLoader();
com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
//动态编译字节码(默认情况下使用的是javassist)
return compiler.compile(code, classLoader);
}
第六步,创建完Protocol$Adaptive后,injectExtension()自动注入到容器。
Protocol$Adaptive的主要功能 :
1、 从url或扩展接口获取扩展接口实现类的名称
2、根据名称,获取实现类ExtensionLoader.getExtensionLoader(扩展接口类).getExtension(扩展接口实现类名称),然后调用实现类的方法。
需要明白一点dubbo的内部传参基本上都是基于Url来实现的,也就是说Dubbo是基于URL驱动的技术。所以,适配器类的目的是在运行期获取扩展的真正实现来调用,解耦接口和实现。