dubbo系列之dubbo SPI
Dubbo
版本2.7.0
为什么先讲 SPI
? 因为 Dubbo
的拓展实现就是采用这一种机制。
SPI
是一种服务发现机制,全称为 “Service Provider Interface”。SPI
的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。Dubbo
则利用此特性为程序提供拓展功能,不过,Dubbo
并未使用 Java 原生的 SPI
机制,而是对其进行了增强,使其能够更好的满足需求。
所以,基于 SPI
机制,我们能够很好的对 Dubbo
进行拓展。Dubbo SPI
源码位于 org.apache.dubbo.common.extension
包下,后续会先介绍如何使用,再结合源码详细分析。
Dubbo
在考虑拓展点时,有一个设计概念叫做 “ 平等对待第三方 ”,也就是说框架作者能做到的功能,拓展者也一定能做到。微核心+插件式,则是比较能达到开闭原则的思路,详细介绍参考官方文档的开发者指南下的设计原则中的拓展点重构介绍。
以下示例参考自
dubbo
官方文档。
Java SPI
首先,定义一个接口,名称为 Robot。
public interface Robot {
void sayHello();
}
接下来,定义来个实现类:Bumblebee
和 OptimusPrime
。
public class Bumblebee implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}
public class OptimusPrime implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Optimus Prime.");
}
}
在不考虑 SPI
机制的情况下,要使用上述两个实现类,则只能采用硬编码的方式:通过构造函数创建对象,调用 sayHello
方法。如果现在有外部也想提供该接口的实现类供内部使用,这种就很难满足了。
现在继续考虑 SPI
,在 META-INF/services
文件夹下创建一个与接口全限定名称相同的文件,即 com.duofei.spi.Robot
。文件内容则为接口实现类的全限定名,如下:
com.duofei.spi.provider.Bumblebee
com.duofei.spi.provider.OptimusPrime
现在,编写测试代码:
public void javaSPI(){
ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
serviceLoader.forEach(Robot::sayHello);
}
最终结果会成功打印两条输出语句。现在如果,我们想在外部添加实现类供内部使用,那么只需要在上述的文件中,新增内容为接口实现类的全限定名称即可。
尽管这种实现方式满足了上述需求,但仍然会带来一些问题,比如它会实例化所有的实现类,这样就不太利于资源的利用了,当然,并不仅仅是由于以上原因,Dubbo
就实现了自己的一套 SPI
,毕竟量身定做,使用起来也会方便许多。
Dubbo SPI
Dubbo SPI
要求拓展点接口必须添加 @SPI
注解,即为上述的 Robot
接口添加该注解。
关于拓展点接口实现类的描述文件,放在了 META-INF/dubbo
目录下,并且内容改为了键值对的形式,上述用例的文件内容为:
bumblebee=com.duofei.spi.provider.Bumblebee
optimusPrime=com.duofei.spi.provider.OptimusPrime
文件名称仍然使用接口的全限定名,下面是测试代码:
public void dubboSPI(){
ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("bumblebee");
bumblebee.sayHello();
}
首先,按需加载可以从代码中体现出来,但不仅仅如此,为了让拓展机制更加灵活好用,Dubbo
还加入了其它的一些特性,如注入拓展,自适应拓展机制,自动**策略。
源码分析:
Dubbo SPI
的整个加载机制差不多都在 ExtensionLoader
中了,那么如何去阅读源码呢?除了上速的 getExtension
方法作为入口之外,我一般喜欢按以下步骤去做分析:
- 查看类的所在的包结构,从一个更高的层次去看,反而,能体会到更多的东西;
- 查看类的继承结构,从实现的接口和继承的类,能够感知类在整个框架中的角色;
- 查看类的构造函数,了解实例化该类,还需要哪些条件,从而牵引出一个链式的实现关系;
- 查看类的成员变量,俗话说: “巧妇难为无米之炊”,你有了怎样的数据,能够在一定程度上表明你要做怎样的事了。
- 查看类的结构图,看类的私有、公有方法等,这种太笼统了,所以还是从类在使用所调用的方法去入手,就像上面所讲的
getExtension
。
这种方式是个人的一些总结,当然,还有看文档这是肯定的了。
该类并没有实现任何接口或者继承任何类,说明 SPI
的使用直接使用该类即可。该类的构造函数是私有的:
private ExtensionLoader(Class<?> type) {
this.type = type;
objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
这里的 type
指的是拓展接口的 Class 对象。而 objectFactory
暂时看不明白,可以先搁置一边。既然构造函数是私有的,那么就一定有一个公共的静态方法来获取本身的一个对象实例,自然就找到了用例中使用的 getExtensionLoader
方法:
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
}
// type 必须为接口
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
}
// type必须添加 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
时,会先从缓存中去读取,在不存在的情况,才重新情况下。EXTENSION_LOADERS
作为成员变量,提供了缓存已经加载的 ExtensionLoader
实例,所以它会是一个静态的,被所有实例所共享。
接着查看用例中的 getExtension(String)
方法:
public T getExtension(String name) {
if (name == null || name.length() == 0) {
throw new IllegalArgumentException("Extension name == null");
}
// 从这里可以看出,拓展点描述文件的应该避免设置为true
if ("true".equals(name)) {
return getDefaultExtension();
}
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 根据key尝试创建实例,...接下来会展开介绍》
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
上述实例的获取也会尝试先从缓存加载,缓存不存在的情况下,通过双重检测锁才去真正地实例化拓展点实现类。
双重检测外一层是为了解决效率问题,避免大量线程竞争锁,内一层才是为了真正解决并发所带来的问题。
展开介绍 createExtension(name)
方法:
private T createExtension(String name) {
// 根据key获取value的 class 对象,...接下来展开介绍》
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 依赖注入,这将放在后续具体介绍
injectExtension(instance);
// 将拓展对象包裹在相应的 Wrapper对象中
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
type + ") could not be instantiated: " + t.getMessage(), t);
}
}
getExtensionClasses()
方法将负责将指定的拓展点描述文件 key-value 键值对转换为 Map<String, Class<?>>
,存储在实例的成员变量 cachedClasses
中,并且在该方法的调用链中还初始化了 cachedWrapperClasses
成员变量。该成员变量为集合,用于将指定对象包裹在相应的 Wrapper
对象中,这在后续的 注入拓展 中会详细描述。
将拓展对象包裹在相应的 Wrapper对象中,并将 Wrapper 对象返回,使用的场景是当前有拓展点 A,其接口实现中有 B、C、D,其中 B、C 的构造函数含有参数 A 类型,那么 B、C 将作为 Wrapper 对象,在获取 D 时,最终返回的会是 C 的实现类,但 C 包装了 B (即通过构造函数传入),B 包装了 D。 需要注意的是带有构造函数含有参数 A 类型的 B、C 没法在单独创建,即调用 createExtension(name)
会抛出异常。
@SPI
该注解只有一个 value 属性值,该值代表默认拓展名,该拓展名用于ExtensionLoader
实例对象的 getDefaultExtension
方法。
@Adaptive
该注解是自适应拓展机制,它的实现比较复杂,这里只介绍其使用方式。
在 Robot 接口新增如下内容:
@Adaptive("key")
void showColor(URL url);
Bumblebee
和 OptimusPrime
分别实现该方法如下:
@Override
public void showColor(URL url) {
System.out.println("Hello, I an yellow.");
}
@Override
public void showColor(URL url) {
System.out.println("Hello, I am blue and white.");
}
测试代码如下:
ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
Robot robot = extensionLoader.getAdaptiveExtension();
robot.showColor(URL.valueOf("dubbo://127.0.0.1:9092?key=bumblebee"));
robot.showColor(URL.valueOf("dubbo://127.0.0.1:9092?key=optimusPrime"));
采用这种方式,能够通过参数去决定使用哪个拓展点的实现类。
@Activate
该注释对于根据给定的条件自动**某些扩展非常有用。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Activate {
/**
* **当前的 extension ,当 group 数组中的某个值得到匹配时
*/
String[] group() default {};
/**
* **当前的 extension,当 URL 参数中,出现了 value 中声明的 key 时
*/
String[] value() default {};
/**
* 排序
*/
int order() default 0;
}
用例:为 OptimusPrime
实现类添加 @Activate(value = "key", group = "group")
注解,测试代码如下:
ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
List<Robot> activateExtension =
extensionLoader.getActivateExtension(URL.valueOf("dubbo://127.0.0.1:9092?key=optimusPrime"), "key", "group");
activateExtension.forEach(Robot::sayHello);
尽管 getActivateExtension
提供了几个重载方法,但最终实现都在 getActivateExtension(URL url, String[] values, String group)
中:
public List<T> getActivateExtension(URL url, String[] values, String group) {
List<T> exts = new ArrayList<T>();
List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
// keys 是否需要剔除默认**的,即 values包含了 "-default"
if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
getExtensionClasses();
for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Object activate = entry.getValue();
String[] activateGroup, activateValue;
if (activate instanceof Activate) {
activateGroup = ((Activate) activate).group();
activateValue = ((Activate) activate).value();
} else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
} else {
continue;
}
// 匹配组
if (isMatchGroup(group, activateGroup)) {
T ext = getExtension(name);
// !names.contains(name) 的判断,避免重复加载了 values 中指定的拓展名;
if (!names.contains(name)
&& !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
&& isActive(activateValue, url)) {
exts.add(ext);
}
}
}
Collections.sort(exts, ActivateComparator.COMPARATOR);
}
// 从拓展名称中加载
List<T> usrs = new ArrayList<T>();
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX)
&& !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) {
if (Constants.DEFAULT_KEY.equals(name)) {
if (!usrs.isEmpty()) {
exts.addAll(0, usrs);
usrs.clear();
}
} else {
T ext = getExtension(name);
usrs.add(ext);
}
}
}
if (!usrs.isEmpty()) {
exts.addAll(usrs);
}
return exts;
}
上面的获取**拓展涉及三个参数 ,分别是URL,values 拓展名,以及指定的组名;具体的匹配策略可按如下区分:
-
values 拓展名不包含剔除默认字符串("-default"):加载
Activate
注解的拓展类;- 指定组名匹配
Activate
注解的 group;-
Activate
注解的value 值匹配 URL 中的key;
-
- 指定组名匹配
-
加载 values 指定的拓展实现类
从以上大致可以判断,dubbo
将带有 Activate
注解的拓展当做 default ,我们可以在调用 getActivateExtension
方法时,在指定拓展点名称时,在里面包含 -default
,即可剔除默认的拓展实现;
依赖注入
在通过 createExtension
创建拓展点的时候,有这样一个 injectExtension(instance)
方法调用,其目的是为了注入拓展:
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
// 反射获取实例所有方法,遍历方法列表
for (Method method : instance.getClass().getMethods()) {
// 检测方法名是否具有 setter 方法特征
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
/**
* 使用 DisableInject 注解,禁止依赖注入
*/
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
Class<?> pt = method.getParameterTypes()[0];
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
// 反射调用 setter 方法,将依赖设置到目标对象中
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("fail to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
这是 Dubbo IOC
,其注入实现是将拓展实现类中带有 setter
方法特征的属性注入。需要注入的依赖属性来自 objectFactory
对象,查找该对象的实例化处,发现它是在构造函数中实例化的:
objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
这里已经在利用 SPI
的拓展特性,查看 ExtensionFactory
接口,其带有 @SPI
注解,查看其实现类,分别是 AdaptiveExtensionFactory
、SpiExtensionFactory
和 SpringExtensionFactory
。AdaptiveExtensionFactory
用于创建自适应的拓展。SpiExtensionFactory
则是用于从 dubbo SPI
中获取所需要的拓展, SpringExtensionFactory
是用于从 Spring 的 IOC
容器中获取所需的拓展。
在上面代码中,objectFactory
变量的类型为 AdaptiveExtensionFactory
,AdaptiveExtensionFactory
内部维护了一个 ExtensionFactory
列表,用于存储其他类型的 ExtensionFactory
。
总结
dubbo
自己实现了 SPI
机制,相比 Java 不灵活的加载方式,它大概提供了以下几个功能:
-
由于拓展实现类描述文件内容改写为 key-value 的形式,所以可以实现按自定义key 获取拓展实现类;
-
由于要求拓展点必须添加
@SPI
注解,而该注解又支持默认拓展实现类的指定,所以可以实现指定默认拓展实现类; -
之前在创建
createExtension
中,有一个关于cachedWrapperClasses
的操作,其实这是一种包装,提供针对拓展点实现的一种包装,有点类似于一种链。 -
关于
@Adaptive
注解,这个只有在通过getAdaptiveExtension
获取到的拓展时,才会有效;并且限制了实现类中只有一个类能够添加该注解;如果要将该注解添加在方法上,只能在拓展点的接口上添加;在调用
getAdaptiveExtension
时遵循以下逻辑:- 实现类中有一个类有该注解,返回该实现类,注解在拓展点方法上的
@Adaptive
不生效; - 实现类中没有该注解,拓展点接口上的某些方法存在该注解,那么将通过
"javassist"
创建一个对象返回(这里也是利用了SPI
的,具体可查看源码),并在调用带有@Adaptive
方法时,再去选择具体的实现类,如果调用了未注解的方法,则会得到一个异常; - 实现类中没有该注解,拓展点接口上的方法也没有该注解,抛出异常;
- 实现类中有一个类有该注解,返回该实现类,注解在拓展点方法上的
-
关于
@Activate
注解,在调用getActivateExtension
时生效;其属性 values 针对方法参数 URL 中是否存在 key,属性 group 针对方法参数 group,order 属性则用于排序,并且添加了该注解的实现类会在调用该方法时,作为 default 实现类,可以通过在方法参数中,添加 “-default” 排除。
其实对于各注解的使用不要混淆,它们只有在调用对应方法时,才会生效;比如说你在调用 getExtension
方法时, @Activate
注解并不会生效。
可以说,对于 SPI
实现,dubbo
还是做了很多工作,重点关注 ExtensionLoader
的 getExtension
、getDefaultExtension
、getAdaptiveExtension
、getActivateExtension
几个方法。
我与风来
认认真真学习,做思想的产出者,而不是文字的搬运工
错误之处,还望指出
上一篇: Dubbo SPI详解
下一篇: Dubbo SPI机制详解