开篇
SPI全称为Service Provider Interface,是一种服务提供机制,比如在现实中我们经常会有这种场景,就是对于一个规范定义方而言(可以理解为一个或多个接口),具体的服务实现方是不可知的(可以理解为对这些接口的实现类),那么在定义这些规范的时候,就需要规范定义方能够通过一定的方式来获取到这些服务提供方具体提供的是哪些服务,而SPI就是进行这种定义的。
JDK SPI例子
说明:
-
首先规范制定方会定义一个接口org.apache.jdk.spi.example.IHello 。
-
其次在项目目录下的META-INF/service名称为org.apache.jdk.spi.example.IHello的文件,包含SPI实现接口全路径。
-
通过ServiceLoader加载访问调用即可。
-
对于jdk的SPI,其主要存在两个问题,为每个接口提供的服务一般尽量只提供一个,因为jdk的SPI默认会将所有目标文件中定义的所有子类都读取到返回使用;当定义多个子类实现时,无法动态的根据配置来使用不同的配置。
---- 定义接口
package org.apache.jdk.spi.example;
public interface IHello {
void sayHello();
}
---- 定义实现1
package org.apache.jdk.spi.example;
public class HelloImpl1 implements IHello {
@Override
public void sayHello() {
System.out.println("我是Impl1");
}
}
---- 定义实现2
package org.apache.jdk.spi.example;
public class HelloImpl2 implements IHello {
@Override
public void sayHello() {
System.out.println("我是Impl2");
}
}
---- META-INF/services目录文件 org.apache.jdk.spi.example.IHello
org.apache.jdk.spi.example.HelloImpl1
org.apache.jdk.spi.example.HelloImpl2
---- 测试文件内容
package org.apache.jdk.spi.example;
import java.util.Iterator;
import java.util.ServiceLoader;
public class ServiceLoaderDemo {
public static void main(String[] args){
ServiceLoader<IHello> s = ServiceLoader.load(IHello.class);
Iterator<IHello> iHelloIterator = s.iterator();
while (iHelloIterator.hasNext()) {
IHello iHello = iHelloIterator.next();
iHello.sayHello();
}
}
}
Dubbo SPI例子
-
定义PlantsWater的接口并通过@SPI注解进行注解,注解可选择带默认值。
-
将watering()方法使用@Adaptive注解进行了标注,表示该方法在自动生成的子类中是需要动态实现的方法。
-
增加grant()方法是为了表明不带@Adaptive在自动生成的子类方法内部会抛出异常。
-
为PlantsWater增加两个实现,AppleWater和BananaWater,实际调用通过参数控制。
-
在META-INF/dubbo下创建一个文件,该文件的名称是目标接口的全限定名,这里是org.apache.dubbo.spi.example.PlantsWater,在该文件中需要指定该接口所有可提供服务的子类。
-
定义主函数ExtensionLoaderDemo模拟SPI调用的验证。
----定义基础应用类
public interface Fruit {}
public class Apple implements Fruit {}
public class Banana implements Fruit{}
----定义SPI类
@SPI("banana")
public interface PlantsWater {
Fruit grant();
@Adaptive
String watering(URL url);
}
public class AppleWater implements PlantsWater {
public Fruit grant() {
return new Apple();
}
public String watering(URL url) {
System.out.println("watering apple");
return "watering finished";
}
}
public class BananaWater implements PlantsWater {
public Fruit grant() {
return new Banana();
}
public String watering(URL url) {
System.out.println("watering banana");
return "watering success";
}
}
----resources文件 org.apache.dubbo.spi.example.PlantsWater
apple=org.apache.dubbo.spi.example.AppleWater
banana=org.apache.dubbo.spi.example.BananaWater
------测试代码内容
public class ExtensionLoaderDemo {
public static void main(String[] args) {
// 首先创建一个模拟用的URL对象
URL url = URL.valueOf("dubbo://192.168.0.101:20880?plants.water=apple");
// 通过ExtensionLoader获取一个PlantsWater对象,getAdaptiveExtension已经加载了所有SPI类
PlantsWater plantsWater = ExtensionLoader.getExtensionLoader(PlantsWater.class)
.getAdaptiveExtension();
// 使用该PlantsWater调用其"自适应标注的"方法,获取调用结果
String result = plantsWater.watering(url);
System.out.println(result);
}
}
-----实际输出内容
十月 11, 2019 7:48:51 下午 org.apache.dubbo.common.logger.LoggerFactory info
信息: using logger: org.apache.dubbo.common.logger.jcl.JclLoggerAdapter
watering apple
watering finished
Process finished with exit code 0
JDK 和 Dubbo SPI简单对比
Dubbo 的扩展点加载是基于JDK 标准的 SPI 扩展点发现机制增强而来的,Dubbo 改进了 JDK 标准的 SPI 的以下问题:
-
JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
-
如果扩展点加载失败,就失败了,给用户没有任何通知。比如:JDK 标准的ScriptEngine,如果Ruby ScriptEngine 因为所依赖的 jruby.jar 不存在,导致 Ruby ScriptEngine 类加载失败,这个失败原因被吃掉了,当用户执行 ruby 脚本时,会报空指针异常,而不是报Ruby ScriptEngine不存在。
-
增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。
Dubbo SPI实现原理
dubbo对于SPI的实现主要是在ExtensionLoader这个类中,这个类主要有三个方法:
- getExtension():主要用于获取名称为name的对应的子类的对象,这里如果子类对象如果有AOP相关的配置,这里也会对其进行封装;
- getAdaptiveExtension():使用定义的装饰类来封装目标子类,具体使用哪个子类可以在定义的装饰类中通过一定的条件进行配置;
- getExtensionLoader():加载当前接口的子类并且实例化一个ExtensionLoader对象。
public T getExtension(String name);
public T getAdaptiveExtension();
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type);
getExtension()
- getExtension()方法的主要作用是获取name对应的子类对象返回。
- 其实现方式是首先读取定义文件中的子类,然后根据不同的子类对象的功能的不同,比如使用@Adaptive修饰的装饰类和用于AOP的Wrapper类,将其封装到不同的缓存中。
- 最后根据传入的name获取其对应的子类对象,并且使用相应的Wrapper类对其进行封装。
如下是getExtension()方法的源码:
public T getExtension(String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
// 如果名称为true,则返回默认的子类对象,这里默认的子类对象的name定义在目标接口的@SPI注解中
if ("true".equals(name)) {
return getDefaultExtension();
}
// 查看当前是否已经缓存有保存目标对象的实例的Holder对象,缓存了则直接返回,
// 没缓存则创建一个并缓存起来
final Holder<Object> holder = getOrCreateHolder(name);
Object instance = holder.get();
// 如果无法从Holder中获取目标对象的实例,则使用双检查法为目标对象创建一个实例
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 创建name对应的子类对象的实例
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
public T getDefaultExtension() {
getExtensionClasses();
if (StringUtils.isBlank(cachedDefaultName) || "true".equals(cachedDefaultName)) {
return null;
}
// 通过cachedDefaultName去获取对应的子类实例
return getExtension(cachedDefaultName);
}
private void cacheDefaultExtensionName() {
// cachedDefaultName取自SPI的参数当中
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation == null) {
return;
}
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
if (names.length > 1) {
throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
if (names.length == 1) {
cachedDefaultName = names[0];
}
}
}
- 关于对于目标对象的获取,首先是从缓存里取,没取到才会进行创建。
- 这里需要说明的是,如果传入的name为true,那么就会返回默认的子类实例,而默认的子类实例是通过其名称进行映射的,该名称存储在目标接口的@SPI注解中。
createExtension()方法的源码:
private T createExtension(String name) {
// 获取当前名称对应的子类类型,如果不存在,则抛出异常
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
// 获取当前class对应的实例,如果缓存中不存在,则实例化一个并缓存起来
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 为生成的实例通过其set方法注入对应的实例,这里实例的获取方式不仅可以通过SPI的方式
// 也可以通过Spring的bean工厂获取
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
for (Class<?> wrapperClass : wrapperClasses) {
// 实例化各个wrapper对象,并将目标对象通过wrapper的构造方法传入,
// 另外还会通过wrapper对象的set方法对其依赖的属性进行注入
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}
在createExtension()方法中,其主要做了三件事:
- 加载定义文件中的各个子类,然后将目标name对应的子类返回后进行实例化。
- 通过目标子类的set方法为其注入其所依赖的bean,这里既可以通过SPI,也可以通过Spring的BeanFactory获取所依赖的bean,injectExtension(instance)。
- 获取定义文件中定义的wrapper对象,然后使用该wrapper对象封装目标对象,并且还会调用其set方法为wrapper对象注入其所依赖的属性。
关于wrapper对象,这里需要说明的是,其主要作用是为目标对象实现AOP。wrapper对象有两个特点:
- a. 与目标对象实现了同一个接口;
- b. 有一个以目标接口为参数类型的构造函数。这也就是上述createExtension()方法最后封装wrapper对象时传入的构造函数实例始终可以为instance实例的原因。
getExtensionClasses()方法的源码
private Map<String, Class<?>> getExtensionClasses() {
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 加载定义文件,并且将定义的类按照功能缓存在不同的属性中,即:
// a. 目标class类型缓存在cachedClasses;
// b. wrapper的class类型缓存在cachedWrapperClasses;
// c. 用于装饰的class类型缓存在cachedAdaptiveClass;
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
private Map<String, Class<?>> loadExtensionClasses() {
// 获取目标接口上通过@SPI注解定义的默认子类对应的名称,并将其缓存在cachedDefaultName中
cacheDefaultExtensionName();
// 分别在META-INF/dubbo/internal、META-INF/dubbo、META-INF/services目录下
// 获取定义文件,并且读取定义文件中的内容,这里主要是通过META-INF/dubbo/internal
// 获取目标定义文件
Map<String, Class<?>> extensionClasses = new HashMap<>();
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
return extensionClasses;
}
private void cacheDefaultExtensionName() {
// 获取目标接口上通过@SPI注解定义的默认子类对应的名称,并将其缓存在cachedDefaultName中
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation == null) {
return;
}
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
if (names.length > 1) {
throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
if (names.length == 1) {
cachedDefaultName = names[0];
}
}
}
- loadExtensionClasses()主要是分别从三个目录中读取定义文件,读取该文件,并且进行缓存。
loadDirectory()方法的源码:
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
String fileName = dir + type;
try {
Enumeration<java.net.URL> urls;
ClassLoader classLoader = findClassLoader();
// 加载定义文件
if (classLoader != null) {
urls = classLoader.getResources(fileName);
} else {
urls = ClassLoader.getSystemResources(fileName);
}
if (urls != null) {
while (urls.hasMoreElements()) {
// 对定义文件进行遍历,依次加载定义文件的内容
java.net.URL resourceURL = urls.nextElement();
loadResource(extensionClasses, classLoader, resourceURL);
}
}
} catch (Throwable t) {
logger.error("Exception occurred when loading extension class (interface: " +
type + ", description file: " + fileName + ").", t);
}
}
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
try {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
final int ci = line.indexOf('#');
if (ci >= 0) {
line = line.substring(0, ci);
}
line = line.trim();
if (line.length() > 0) {
try {
String name = null;
int i = line.indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type +