参考资料:敖丙_dubbo的spi
什么是SPI
SPI:Service Provider Interface,服务提供者接口。从字面上,很难理解这玩意有啥用。
举个栗子,市面上有五花八门的数据库,每种数据库底层协议各不相同,我们总不能针对每个数据库都开发出不同的api访问吧。所以,需要有一个接口,定义一些访问数据库的规范协议,所有数据库都按照这个接口开发,这个接口就是java.sql.Driver
。既然数据库厂商都根据接口开发对应的实现类,但是存在一个问题,真正使用时,到底用哪个实现类,从哪里找到这个实现类?
答案是,大家约定好把实现类的配置写在某个地方,要使用时去约定位置查找。
Java SPI
Java SPI就是约定在classpath的META-INF/services/
目录下创建一个以服务接口命名的文件,文件内容是具体实现类的全限定名。
以下是MySQL的做法
Java SPI示例
我们定义一个接口,接着创建两个实现类,并在类路径的META-INF/services目录下创建服务接口全限定名的文件。
public interface IHelloSPI {
void say();
}
@Slf4j
public class HelloSPIImpl1 implements IHelloSPI {
@Override
public void say() {
log.info(" --- >> 我是实现类1");
}
}
@Slf4j
public class HelloSPIImpl2 implements IHelloSPI {
@Override
public void say() {
log.info(" --- >> 我是实现类2");
}
}
com.drh.springboot_hello.spi.java.HelloSPIImpl1
com.drh.springboot_hello.spi.java.HelloSPIImpl2
public class JavaSPITest {
public static void main(String[] args) {
// load方法会创建一个ServiceLoader和LazyIterator对象
ServiceLoader<IHelloSPI> serviceLoader = ServiceLoader.load(IHelloSPI.class);
// 获取到LazyIterator对象
Iterator<IHelloSPI> iterator = serviceLoader.iterator();
// hasNext方法会解析接口文件内容,遍历每行,获取到实现类的全限定名
while (iterator.hasNext()) {
// next方法,根据全限定名,创建class对象,然后反射创建实例对象
iterator.next().say();
}
}
}
输出结果
Java SPI源码分析
在上面的代码可以看到,ServiceLoader#load
是Java SPI的入口,我们跟踪代码看下做了什么操作。
从上面的调用过程可以看到,先找到当前线程绑定的类加载器,如果没有就使用系统加载器。然后清除一下缓存,接着创建一个ServiceLoader和LazyIterator。
查看LazyIterator发现,它是Iterator的子类。因此,可以推断serviceLoader#iterator
获取到的实际上是LazyIterator实例对象。
我们继续来看,LazyIterator#hasNext
方法
private static final String PREFIX = "META-INF/services/";
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 找到文件位置
String fullName = PREFIX + service.getName();
// 加载文件
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 按行遍历文件
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 解析内容
pending = parse(service, configs.nextElement());
}
// 赋值给nextName,等会nextService方法会用到这个属性
nextName = pending.next();
return true;
}
可以看到,LazyIterator#hasNextService
方法先根据约定路径找到接口文件,接着按照行遍历解析文件内容。
接着,我们看下LazyIterator#next
方法
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
// 获取hasNextService解析文件内容得到行数据
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 实现类的class对象
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
// 创建实现类实例对象,并强转成接口引用
S p = service.cast(c.newInstance());
// 保存实例对象到缓存
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
LazyIterator#nextService
根据全限定名获取到实现类class对象,通过反射创建实例对象,并放入缓存,最后返回结果。
总结: Java SPI依靠ServiceLoader实现,调用load方法时,会创建一个ServiceLoader对象,以及其内部属性LazyIterator对象。接着,ServiceLoader获取迭代器,即LazyIterator。
调用迭代器#hasNext方法,到
META-INF/services
目录查找以服务接口全限定名来命名的文件,按行遍历,获取到实现类的全限定名。最后,调用next方法,根据类全限定名调用Class.forName和newInstance等方法,创建实例对象并存储到缓存。
Duboo SPI
由于Java SPI会把所有实现类都实例化对象,如果系统中不需要用到这个对象,会造成资源浪费。为了实现按需加载,dubbo自己实现了一个SPI,通过指定的名称来加载对应的具体实现类。
我们先来看下dubbo对配置文件目录的约定,分为三类目录:
META-INF/services/
:该目录用来兼容Java SPIMETA-INF/dubbo/
:该目录存放用户自定义的配置文件META-INF/dubbo/internal/
:该目录存放dubbo内部使用的SPI配置文件
Dubbo SPI示例
指定一个服务接口,并使用@SPI
注解表示使用dubbo的SPI。接着在META-INF/dubbo/
目录下创建以服务接口命名的文件,内容以key-value形式表示参数与实现类的关系。
@SPI
public interface IHelloDubboSPI {
void say();
}
@Slf4j
public class HelloDubboSPIImpl implements IHelloDubboSPI {
@Override
public void say() {
log.info(" --- >> dubbo spi 示例。。。");
}
}
impl1=spi.dubbo.HelloDubboSPIImpl
@SpringBootTest
public class DubboSPITest {
@Test
public void testDubboSPI() {
ExtensionLoader<IHelloDubboSPI> extensionLoader = ExtensionLoader.getExtensionLoader(IHelloDubboSPI.class);
IHelloDubboSPI impl1 = extensionLoader.getExtension("impl1");
impl1.say();
}
}
Dubbo源码分析
从上面的测试代码看到,大致流程是先通过接口类对象找到对应的ExtensionLoader,然后再通过getExtension方法得到指定名称的具体实现类对象。
先看下ExtensionLoader#getExtensionLoader
方法
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
// 忽略一堆参数验证
ExtensionLoader<T> loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
if (loader == null) {
// 注意:new ExtensionLoader(type) -> 这个方法里面用到了自适应扩展
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));
loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
}
return loader;
}
private ExtensionLoader(Class<?> type) {
this.type = type;
this.objectFactory = type == ExtensionFactory.class ?
null : (ExtensionFactory)getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension();
}
getExtensionLoader
方法很简单,先从缓存里面找是否已经存在指定类型的ExtensionLoader,如果没有就创建一个并放到缓存,key是class对象,最后返回ExtensionLoader对象。
接着,我们看下获取具体实现类对象,ExtensionLoader#getExtension
public T getExtension(String name) {
// 省略部分代码
// 从缓存中获取指定实现类对象
Holder<Object> holder = (Holder)this.cachedInstances.get(name);
if (holder == null) {
this.cachedInstances.putIfAbsent(name, new Holder());
holder = (Holder)this.cachedInstances.get(name);
}
Object instance = holder.get();
if (instance == null) { // 双重检查
synchronized(holder) {
instance = holder.get();
if (instance == null) {
// 关键!!创建实例对象
instance = this.createExtension(name);
holder.set(instance);
}
}
}
return instance;
}
从代码中看到,重点是this.createExtension(name)
,继续往下看
private T createExtension(String name) {
// 获取实现类的class对象
Class<?> clazz = (Class)this.getExtensionClasses().get(name);
if (clazz == null) {
throw this.findException(name);
} else {
try {
// 判断缓存是否已经有实例对象
T instance = EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
// 没有的话,调用newInstance方法创建并放入缓存
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = EXTENSION_INSTANCES.get(clazz);
}
// setter 依赖注入
this.injectExtension(instance);
Set<Class<?>> wrapperClasses = this.cachedWrapperClasses;
Class wrapperClass;
// 如果有包装类,就包装下
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
for(Iterator var5 = wrapperClasses.iterator(); var5.hasNext();
instance = this.injectExtension(wrapperClass.getConstructor(this.type).newInstance(instance))) {
wrapperClass = (Class)var5.next();
}
}
return instance;
} catch (Throwable var7) {
throw new IllegalStateException("xxx");
}
}
}
方法逻辑很清晰,就是先找到实现类,然后看下缓存有没有实例对象,如果没有,就通过反射创建,然后执行set方法依赖注入。
到这里,我们继续看下怎么找到实现类。
查找实现类
我们继续看下ExtensionLoader#getExtensionClasses
private Map<String, Class<?>> getExtensionClasses() {
Map<String, Class<?>> classes = (Map)this.cachedClasses.get();
if (classes == null) {
synchronized(this.cachedClasses) {
classes = (Map)this.cachedClasses.get();
if (classes == null) {
// 关键!!先从缓存中查找,没有则调用loadExtensionClasses方法
classes = this.loadExtensionClasses();
this.cachedClasses.set(classes);
}
}
}
return classes;
}
private Map<String, Class<?>> loadExtensionClasses() {
// dubbo的SPI注解,读取它的值
SPI defaultAnnotation = (SPI)this.type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
String value = defaultAnnotation.value();
// 检验SPI接口定义的名称
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 " + this.type.getName() + ": " +
Arrays.toString(names));
}
if (names.length == 1) {
// SPI的value作为服务接口的默认实现类
this.cachedDefaultName = names[0];
}
}
}
// 查找约定的三个目录
Map<String, Class<?>> extensionClasses = new HashMap();
this.loadDirectory(extensionClasses, "META-INF/dubbo/internal/");
this.loadDirectory(extensionClasses, "META-INF/dubbo/");
this.loadDirectory(extensionClasses, "META-INF/services/");
return extensionClasses;
}
loadDirectory
会扫描约定的三个目录,找到服务接口文件,解析读取文件内容,把这些实现类对象都加载进来,通过loadClass做缓存处理。
在执行loadClass
方法前,已经加载实现类,Class.forName(xxx),loadClass方法只是根据实现类上面的注解做不同的缓存操作,分别有Adaptive、WrapperClass和普通类三种。
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) {
// 省略部分代码
clazz.getConstructor();
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);
}
Class<?> c = extensionClasses.get(n);
if (c == null) {
extensionClasses.put(n, clazz);
}
}
}
}
总结
Dubbo SPI的分析就到此结束吧,至于Adaptive、WrapperClass的分析可以看敖丙的文章,暂时消化不了。最后,我们来个总结吧
要得到指定名称的实例对象,需要先创建接口对应的ExtensionLoader,接着调用getExtension方法,根据方法参数中的名字去找到指定的子类对象。过程是,先到约定的META-INF/service
、META-INF/dubbo
、META-INF/dubbo/internal
目录下,找到以服务接口命名的文件,解析文件内容,把所有的实现类都先加载进来(类加载),并使用HashMap存储name与实现类class对象的对应关系,key->name,value->class对象。最后,根据getExtension方法参数,从map里面找到对应的class对象,通过反射创建实例对象并返回。
总结:
dubbo SPI的实现原理和Java SPI很类似,都是约定目录,以服务接口作为文件名。不同点是dubbo SPI会指定名称与实现类的关系。
注意:加载类和实例化对象是两回事。Java SPI是加载实现类并实例化对象,而Dubbo SPI是加载类,按需实例化。