目录
- 前言
- JDK SPI
- JDBC SPI
- ServiceLoader实现原理
- 小结
- Spring+SpringBoot SPI
- 实现原理
- Debug
- 小结
- Dubbo SPI
- 如何使用
- 实现原理
前言
SPI,英文全称是Service Provider Interface,直译是“服务提供接口”或“服务提供者接口”,是一种基于ClassLoader来发现并加载服务的机制。
这里的“服务”泛指任何一个可以提供服务的功能、模块、应用或系统,这些“服务”在设计接口或规范体系时,往往会预留一些比较关键的口子或者扩展点,让调用方按照既定的规范去自由发挥实现,而这些所谓的“比较关键的口子或者扩展点”,我们就叫“服务”提供的“接口”。
一个标准的SPI,由3个组件构成,分别是:
- Service:一个公开的接口或抽象类,定义了一个抽象的功能模块;
- Service Provider:Service的一个实现类;
- ServiceLoader:SPI机制中的核心组件,负责在运行时发现并加载Service Provider。
JDK SPI
JDK自带的SPI实现,流程如图所示:
JDBC SPI
以JDBC举例,来看一下具体如何使用。
JDK在java.sql包下有个名为Driver的接口,定义了若干个方法,而具体实现则由具体的DB厂商来实现,如MySQL、Oracle等。
package java.sql;
public interface Driver {
java.sql.Connection connect(String url, java.util.Properties info) throws SQLException;
boolean acceptsURL(String url) throws SQLException;
java.sql.DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) throws SQLException;
int getMajorVersion();
int getMinorVersion();
boolean jdbcCompliant();
public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException;
}
在MySQL和Oracle的jar包中,在META-INF/services目录下有个名为java.sql.Driver的文件,里面的内容分别为com.mysql.cj.jdbc.Driver和oracle.jdbc.OracleDriver,即MySQL和Oracle里实现Driver接口的实现类的全路径。
此外,我还自定义了一个SimonLeeSQL工程,来实现Driver接口。
定义AbstractSimonLeeSqlDriver,实现Driver接口:
public abstract class AbstractSimonLeeSqlDriver implements java.sql.Driver {
@Override
public java.sql.Connection connect(String url, java.util.Properties info) {
return null;
}
@Override
public boolean acceptsURL(String url) {
return false;
}
@Override
public java.sql.DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) {
return new java.sql.DriverPropertyInfo[0];
}
@Override
public int getMajorVersion() {
return 0;
}
@Override
public int getMinorVersion() {
return 0;
}
@Override
public boolean jdbcCompliant() {
return false;
}
@Override
public java.util.logging.Logger getParentLogger() {
return null;
}
}
再定义SimonLeeSqlDriver1与SimonLeeSqlDriver2,继承AbstractSimonLeeSqlDriver:
public class SimonLeeSqlDriver1 extends AbstractSimonLeeSqlDriver {
}
public class SimonLeeSqlDriver2 extends AbstractSimonLeeSqlDriver {
}
最后在resources的META-INF/services目录下定义个名为java.sql.Driver的文件,里面的内容为如下2行:
将SimonLeeSQL工程mvn install,在A工程的pom.xml里分别引入MySQl、Oracle、SimonLeeSQL的坐标。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>21.3.0.0</version>
</dependency>
<dependency>
<groupId>com.simonlee</groupId>
<artifactId>SimonLeeSQL</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
在A工程里,使用java.util.ServiceLoader.load()和其内部的迭代器加载上述4个Driver接口的实现类。
public static void main(String[] args) {
java.util.ServiceLoader<java.sql.Driver> drivers = java.util.ServiceLoader.load(java.sql.Driver.class);
java.util.Iterator<java.sql.Driver> iterator = drivers.iterator();
while (iterator.hasNext()) {
java.sql.Driver driver = iterator.next();
System.out.println(driver);
}
}
看一下输出结果,能正确拿到4个具体实现类的实例。
在具体应用中,A工程如果想更换DB驱动,只需要在pom.xml里引入DB厂商提供的Maven坐标即可(前提是遵循了JDBC的SPI玩法),无需改动任何代码,实现了热插拔。
ServiceLoader实现原理
java.util.ServiceLoader实现了Iterable接口,并且在内部定义了LazyIterator迭代器。
private class LazyIterator implements Iterator<S> {
private Class<S> service;
private ClassLoader loader;
private Enumeration<java.net.URL> configs = null;
private Iterator<String> pending = null;
private String nextName = null;
@Override
public boolean hasNext() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// PREFIX = META-INF/services/
// service.getName() = java.sql.Driver
// fullName = META-INF/services/java.sql.Driver
String fullName = PREFIX + service.getName();
if (loader == null) {
configs = ClassLoader.getSystemResources(fullName);
} else {
// java.lang.ClassLoader.getResources()扫描所有META-INF/services/java.sql.Driver文件
// loader = Launcher$AppClassLoader
configs = loader.getResources(fullName);
}
} catch (java.io.IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// configs.nextElement() =
// jar:file:/D:/MavenRepo/com/mysql/mysql-connector-j/8.0.33/mysql-connector-j-8.0.33.jar!/META-INF/services/java.sql.Driver
// jar:file:/D:/MavenRepo/com/oracle/database/jdbc/ojdbc8/21.3.0.0/ojdbc8-21.3.0.0.jar!/META-INF/services/java.sql.Driver
// jar:file:/D:/MavenRepo/com/simonlee/SimonLeeSQL/1.0-SNAPSHOT/SimonLeeSQL-1.0-SNAPSHOT.jar!/META-INF/services/java.sql.Driver
pending = parse(service, configs.nextElement());
}
// nextName =
// com.mysql.cj.jdbc.Driver
// oracle.jdbc.OracleDriver
// com.simonlee.sql.SimonLeeSqlDriver1
// com.simonlee.sql.SimonLeeSqlDriver2
nextName = pending.next();
return true;
}
@Override
public S next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// java.lang.Class.forName(),反射机制来实例化对象
// c =
// class com.mysql.cj.jdbc.Driver
// class oracle.jdbc.OracleDriver
// class com.simonlee.sql.SimonLeeSqlDriver1
// class com.simonlee.sql.SimonLeeSqlDriver2
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.key = nextName, value = 具体实例
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated", x);
}
throw new Error();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
运行完后,providers的内容如图所示:
小结
Spring+SpringBoot SPI
相较于JDK SPI,Spring+SpringBoot的区别是:
- 配置文件路径为META-INF
- 配置文件名固定为spring.factories
- 配置文件内容为key-value,key固定为org.springframework.boot.autoconfigure.EnableAutoConfiguration,value为接口的实现类的全路径
实现原理
通过Spring里的org.springframework.core.io.support.SpringFactoriesLoader.loadFactories()读取所有jar包的META-INF/spring.factories资源文件,并从文件中读取一堆的类似EnableAutoConfiguration标识的类路径,将这些类创建对应的Spring Bean对象注入到容器中,就完成了SpringBoot的自动装配底层核心原理。
Spring里的org.springframework.core.io.support.SpringFactoriesLoader:
public class SpringFactoriesLoader {
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
static final Map<ClassLoader, Map<String, List<String>>> cache = new org.springframework.util.ConcurrentReferenceHashMap<>();
public static List<String> loadFactoryNames(Class<?> factoryType, @org.springframework.lang.Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = org.springframework.core.io.support.SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
// FACTORIES_RESOURCE_LOCATION = META-INF/spring.factories
// java.lang.ClassLoader.getResources()扫描所有META-INF/spring.factories文件
Enumeration<java.net.URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
java.net.URL url = urls.nextElement();
org.springframework.core.io.UrlResource resource = new org.springframework.core.io.UrlResource(url);
Properties properties = org.springframework.core.io.support.PropertiesLoaderUtils.loadProperties(resource);
// 遍历每个META-INF/spring.factories文件,拿到里面的key-value对
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
// 将value按半角逗号分割,转成数组
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
// 遍历数组,拿到里面的值
for (String factoryImplementationName : factoryImplementationNames) {
// result.key = org.springframework.boot.autoconfigure.EnableAutoConfiguration(包括但不限于)
// result.value = 接口的实现类的全路径
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>()).add(factoryImplementationName.trim());
}
}
}
// Replace all lists with unmodifiable lists containing unique elements
result.replaceAll((factoryType, implementations) -> implementations.stream()
.distinct()
.collect(java.util.stream.Collectors.collectingAndThen(java.util.stream.Collectors.toList(), Collections::unmodifiableList)));
// 将result放到cache里,后续调用时可直接拿到
cache.put(classLoader, result);
} catch (java.io.IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}
}
SpringBoot里的org.springframework.boot.autoconfigure.AutoConfigurationImportSelector:
public class AutoConfigurationImportSelector
implements org.springframework.context.annotation.DeferredImportSelector,
org.springframework.beans.factory.BeanClassLoaderAware,
org.springframework.context.ResourceLoaderAware,
org.springframework.beans.factory.BeanFactoryAware,
org.springframework.context.EnvironmentAware,
org.springframework.core.Ordered {
protected List<String> getCandidateConfigurations(org.springframework.core.type.AnnotationMetadata metadata,
org.springframework.core.annotation.AnnotationAttributes attributes) {
// getSpringFactoriesLoaderFactoryClass() = EnableAutoConfiguration.class
// 调用Spring里的org.springframework.core.io.support.SpringFactoriesLoader
// 从cache里获取key = org.springframework.boot.autoconfigure.EnableAutoConfiguration的value,即接口的实现类的全路径
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories." +
" If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return org.springframework.boot.autoconfigure.EnableAutoConfiguration.class;
}
}
Debug
对SimonLeeSQL工程进行改造,定义SimonLeeSqlDriver3与SimonLeeSqlDriver4,继承AbstractSimonLeeSqlDriver。
public class SimonLeeSqlDriver3 extends AbstractSimonLeeSqlDriver {
}
public class SimonLeeSqlDriver4 extends AbstractSimonLeeSqlDriver {
}
最后在resources的META-INF目录下定义个名为spring.factories的文件,里面的内容为如下2行:
将SimonLeeSQL工程重新mvn install,在A工程的pom.xml里新加引入mybatis-spring-boot-autoconfigure的坐标。
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
<version>2.3.2</version>
</dependency>
其中的META-INF/spring.factories文件如下所示:
启动A工程,Debug结果如下所示:
可以看到,cache里有key=EnableAutoConfiguration和SimonLeeTestKey的,并且key=EnableAutoConfiguration的value里有MybatisLanguageDriverAutoConfiguration、MybatisAutoConfiguration、SimonLeeSqlDriver3、SimonLeeSqlDriver4,符合预期。
小结
相较于JDK SPI,Spring+SpringBoot做了如下优化:
- 如果想让一个jar包利用SPI机制支持2个接口com.xxx.Interface1和com.xxx.Interface2,JDK需要在META-INT/services目录下创建2个文件com.xxx.Interface1和com.xxx.Interface2,而Spring+SpringBoot只需在META-INT目录下的spring.factories文件里加入对应的key即可(参考mybatis-spring-boot-autoconfigure)
- 如果想获取特定的接口的实现类,JDK由于采用List存储,需要自己进行遍历并编写各种比较代码,而Spring+SpringBoot采用Map存储,getKey即可(参考AutoConfigurationImportSelector获取key=EnableAutoConfiguration)。
参考:SpringBoot——SpringBoot SPI原理
Dubbo SPI
如何使用
自定义SimonLeeDubbo接口,并加上@org.apache.dubbo.common.extension.SPI注解。
@org.apache.dubbo.common.extension.SPI
public interface SimonLeeDubbo {
}
自定义2个类SimonLeeDubboImpl1和SimonLeeDubboImpl2,实现SimonLeeDubbo接口
public class SimonLeeDubboImpl1 implements SimonLeeDubbo {
}
public class SimonLeeDubboImpl2 implements SimonLeeDubbo {
}
最后在resources的META-INF/services目录下定义个名为com.simonlee.dubbo.SimonLeeDubbo的文件,里面的内容为如下2行:
使用org.apache.dubbo.common.extension.ExtensionLoader.getExtension()加载上述2个SimonLeeDubbo接口的实现类。
public static void main(String[] args) {
org.apache.dubbo.rpc.model.ApplicationModel applicationModel = org.apache.dubbo.rpc.model.ApplicationModel.defaultModel();
// 通过Protocol获取指定像ServiceLoader一样的加载器
org.apache.dubbo.common.extension.ExtensionLoader<SimonLeeDubbo> dubbos = applicationModel.getExtensionLoader(SimonLeeDubbo.class);
// 通过指定的名称从加载器中获取指定的实现类
SimonLeeDubbo dubbo11 = dubbos.getExtension("simonLeeDubboImpl1");
System.out.println(dubbo11);
// 再次通过指定的名称从加载器中获取指定的实现类,看看打印的引用是否创建了新对象
SimonLeeDubbo dubbo12 = dubbos.getExtension("simonLeeDubboImpl1");
System.out.println(dubbo12);
SimonLeeDubbo dubbo2 = dubbos.getExtension("simonLeeDubboImpl2");
System.out.println(dubbo2);
}
看一下输出结果,2次拿到的simonLeeDubboImpl1是同个引用,说明用到了缓存。
实现原理
// 获取该接口的所有扩展点集合
private Map<String, Class<?>> getExtensionClasses() {
// 先从缓存里面取,看看有没有该接口的扩展点集合
Map<String, Class<?>> classes = cachedClasses.get();
// 如果没有任何缓存的话
if (classes == null) {
// 则采用双检索的形式保证线程安全粒度去读取磁盘文件加载扩展点
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 真正加载扩展点的逻辑
classes = loadExtensionClasses();
// 如果加载到的话,则放入到缓存中,下一次就可以直接从缓存中取了
cachedClasses.set(classes);
}
}
}
return classes;
}
↓
// 正在加载扩展点的逻辑
private Map<String, Class<?>> loadExtensionClasses() {
// 此处省略若干行代码...
Map<String, Class<?>> extensionClasses = new HashMap<>();
for (LoadingStrategy strategy : strategies) {
// 按照每种磁盘路径的策略去加载磁盘文件
loadDirectory(extensionClasses, strategy, type.getName());
// compatible with old ExtensionFactory
if (this.type == ExtensionInjector.class) {
loadDirectory(extensionClasses, strategy, ExtensionFactory.class.getName());
}
}
return extensionClasses;
}
↓
// 查看加载磁盘的策略有哪些,发现是一个方法
private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();
↓
// 继续查看该方法的逻辑,结果发现该方法加载了 LoadingStrategy 接口的所有实现类
private static LoadingStrategy[] loadLoadingStrategies() {
// java.util.ServiceLoader.load()
return stream(load(LoadingStrategy.class).spliterator(), false)
.sorted()
.toArray(LoadingStrategy[]::new);
}
↓
// LoadingStrategy 实现类之一,重点关注 directory 方法
public class DubboInternalLoadingStrategy implements LoadingStrategy {
@Override
public String directory() {
return "META-INF/dubbo/internal/";
}
// 此处省略若干行代码...
}
↓
// LoadingStrategy 实现类之一,重点关注 directory 方法
public class DubboLoadingStrategy implements LoadingStrategy {
@Override
public String directory() {
return "META-INF/dubbo/";
}
// 此处省略若干行代码...
}
↓
// LoadingStrategy 实现类之一,重点关注 directory 方法
public class ServicesLoadingStrategy implements LoadingStrategy {
@Override
public String directory() {
return "META-INF/services/";
}
// 此处省略若干行代码...
}
private void loadDirectory(Map<String, Class<?>> extensionClasses, LoadingStrategy strategy, String type) {
loadDirectory(extensionClasses, strategy.directory(), type, strategy.preferExtensionClassLoader(), strategy.overridden(),
strategy.includedPackages(), strategy.excludedPackages(), strategy.onlyExtensionClassLoaderPackages());
String oldType = type.replace("org.apache", "com.alibaba");
loadDirectory(extensionClasses, strategy.directory(), oldType, strategy.preferExtensionClassLoader(), strategy.overridden(),
strategy.includedPackagesInCompatibleType(), strategy.excludedPackages(), strategy.onlyExtensionClassLoaderPackages());
}
↓
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst,
boolean overridden, String[] includedPackages, String[] excludedPackages, String[] onlyExtensionClassLoaderPackages) {
// dir = META-INF/services/
// type = com.simonlee.dubbo.SimonLeeDubbo
// fileName = META-INF/services/com.simonlee.dubbo.SimonLeeDubbo
String fileName = dir + type;
try {
List<ClassLoader> classLoadersToLoad = new LinkedList<>();
// try to load from ExtensionLoader's ClassLoader first
if (extensionLoaderClassLoaderFirst) {
ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
classLoadersToLoad.add(extensionLoaderClassLoader);
}
}
// load from scope model
Set<ClassLoader> classLoaders = scopeModel.getClassLoaders();
if (CollectionUtils.isEmpty(classLoaders)) {
// java.lang.ClassLoader.getSystemResources()
Enumeration<java.net.URL> resources = ClassLoader.getSystemResources(fileName);
if (resources != null) {
while (resources.hasMoreElements()) {
loadResource(extensionClasses, null, resources.nextElement(), overridden, includedPackages, excludedPackages,
onlyExtensionClassLoaderPackages);
}
}
} else {
classLoadersToLoad.addAll(classLoaders);
}
Map<ClassLoader, Set<java.net.URL>> resources = ClassLoaderResourceLoader.loadResources(fileName, classLoadersToLoad);
resources.forEach(((classLoader, urls) -> loadFromClass(
extensionClasses, overridden, urls, classLoader, includedPackages, excludedPackages, onlyExtensionClassLoaderPackages)));
} catch (Throwable t) {
logger.error("Exception occurred when loading extension class (interface: " + type + ", description file: " + fileName + ").", t);
}
}
↓
private void loadFromClass(Map<String, Class<?>> extensionClasses, boolean overridden, Set<java.net.URL> urls, ClassLoader classLoader,
String[] includedPackages, String[] excludedPackages, String[] onlyExtensionClassLoaderPackages) {
if (CollectionUtils.isNotEmpty(urls)) {
for (java.net.URL url : urls) {
loadResource(extensionClasses, classLoader, url, overridden, includedPackages, excludedPackages, onlyExtensionClassLoaderPackages);
}
}
}
↓
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL, boolean overridden,
String[] includedPackages, String[] excludedPackages, String[] onlyExtensionClassLoaderPackages) {
try {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
String line;
String clazz;
// 遍历每1行,每1行即为1个SimonLeeDubbo接口的具体实现类
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();
clazz = line.substring(i + 1).trim();
} else {
clazz = line;
}
if (StringUtils.isNotEmpty(clazz)
&& !isExcluded(clazz, excludedPackages)
&& isIncluded(clazz, includedPackages)
&& !isExcludedByClassLoader(clazz, classLoader, onlyExtensionClassLoaderPackages)) {
// java.lang.Class.forName(),反射机制来实例化对象
loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type +
", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
exceptions.put(line, e);
}
}
}
}
} catch (Throwable t) {
logger.error("Exception occurred when loading extension class (interface: " + type + ", class file: " + resourceURL + ") in " + resourceURL, t);
}
}
简单跟踪代码后,发现原来我们可以在三个路径下添加SPI机制的文件,分别为:
- META-INF/dubbo/internal/
- META-INF/dubbo/
- META-INF/services/
真相也清楚了,Dubbo框架会从这3个资源目录下去加载SPI机制的文件,只不过从路径的名称上可以看出,META-INF/dubbo/internal/存放的是Dubbo内置的一些扩展点,META-INF/services/存放的是Dubbo自身的一些业务逻辑所需要的一些扩展点,而META-INF/dubbo/存放的是上层业务系统自身的一些定制Dubbo的相关扩展点。
作者:曼特宁