详解 SPI 机制
2024-03-28 10:32:53 # Technical # JavaBase

SPI(Service Provider Interface)是 JDK 内置的一种「服务提供发现机制」,用于支持在运行时发现和加载服务实现。SPI 机制允许开发者编写服务接口(Service Interface),然后为这个接口提供一个或多个实现,这些实现可以在运行时动态地被发现和加载

SPI 简单示例

假设有四个项目,分别为:Web 项目、Common项目、File 项目和 DB 项目。web 项目负责对外提供服务,Common项目是一个公共项目,File 项目负责内部文件管理,DB 项目负责内部的数据库管理。Web项目依赖于 Common项目、File项目以及 DB 项目,File 项目和 DB 项目依赖于 Common项目。

graph BT
A(Common项目) --> B(Web项目)
C(File项目) --> B
A --> C
D(DB项目) --> B
A --> D

现需要对外提供搜索服务,搜索可能是文件,可能是数据库

Common 项目定义一个搜索接口

1
2
3
public interface Search {
Object searchSomething(String keyword);
}

File 项目基于文件搜索的实现

1
2
3
4
5
6
7
public class FileSearch implements Search {
@Override
public Object searchSomething(String keyword) {
System.out.println("文件搜索 "+keyword);
return null;
}
}

并到 resources 下创建目录 META-INF/services,然后创建一个名为 temp.common.service.Search 的文件,文件内容为:temp.file.service.FileSearch

DB 项目基于数据库搜索的实现

1
2
3
4
5
6
7
public class DBSearch implements Search {
@Override
public Object searchSomething(String keyword) {
System.out.println("数据库搜索 "+keyword);
return null;
}
}

同 File 项目,到 resources 下创建目录 META-INF/services,然后创建一个名为 temp.common.service.Search 的文件,文件内容为:temp.db.service.FileSearch

将上面的三个项目打包上传至 maven 仓库,然后 Web 项目更新依赖,尝试调用:

1
2
3
4
5
6
7
8
9
10
public class SPITest {
public static void main(String[] args) {
ServiceLoader<Search> s = ServiceLoader.load(Search.class);
Iterator<Search> iterator = s.iterator();
while (iterator.hasNext()) {
Search search = iterator.next();
search.searchSomething("hello spi");
}
}
}

Output:

1
2
文件搜索 hello spi
数据库搜索 hello spi

日后还可扩展如:视频搜索、音频搜索等等服务,只需按规则实现方法,并配置相应的文件即可

SPI 相关应用

JDBC DriverManger

在 JDBC4.0 之前,需要手动加载驱动 Class.forName("xxx.xxx.jdbc.Driver"),而之后就不需要手动加载,直接通过 DriverManager 获取数据库连接 DriverManager.getConnection(url,username,password),其原理便是利用了 SPI

直接查看 DriverManger 的源码,找到一个静态代码块

1
2
3
4
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

查看 loadInitialDrivers() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static void loadInitialDrivers() {
...
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
...
}

抽丝剥茧,关键方法在第 6 行 ServiceLoader.load(Driver.class)

1
2
3
4
5
6
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 调用另一个重载的方法
return ServiceLoader.load(service, cl);
}

ServiceLoader.load(service, cl)

1
2
3
4
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
// 直接new了一个ServiceLoader对象
return new ServiceLoader<>(service, loader);
}

再看 ServiceLoader 构造方法

1
2
3
4
5
6
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}

接着看 reload()

1
2
3
4
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}

又初始化了一个私有内部类 LazyIterator,到这里 ServiceLoader.load(Driver.class) 算是执行完毕了,但是也没看到相关加载驱动的代码,只能接着往下看

获取 ServiceLoader 的迭代器 loadedDrivers.iterator()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public Iterator<S> iterator() {
return new Iterator<S>() {
// providers是一个 ServiceLoader的LinkedHashMap属性,存放着k-String v-S
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();

// 迭代器是否有下一个元素
public boolean hasNext() {
// 先判断providers,有直接返回「有」
if (knownProviders.hasNext())
return true;
// providers中没有,判断lookupIterator
// 这个lookupIterator很熟悉,就是前面的LazyIterator
return lookupIterator.hasNext();
}

// 获取迭代器下一个元素
public S next() {
// 先尝试从providers中获取
if (knownProviders.hasNext())
return knownProviders.next().getValue();
// providers中没有,从lookupIterator(LazyIterator)中获取
return lookupIterator.next();
}
};
}

逻辑的最终都指向了上面的那个私有内部类 —— LazyIterator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
private class LazyIterator implements Iterator<S> {
// 需要加载的服务类(此处是jdbc驱动)
Class<S> service;
// 类加载器
ClassLoader loader;
// 配置文件枚举
Enumeration<URL> configs = null;
// String类型的迭代器
Iterator<String> pending = null;
// 下一个元素的名字
String nextName = null;

// 前面所用的构造方法(lookupIterator = new LazyIterator(service, loader);)
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}

// 判断是否有下一个元素
private boolean hasNextService() {
// 下一个元素名称不为空就直接返回true
if (nextName != null) {
return true;
}
// 配置为空,说明需要初始化,需要读取配置(仅执行一次)
if (configs == null) {
try {
// PREFIX:META-INF/services/ service.getName():java.sql.Driver
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());
}
// 获取服务名称(com.mysql.cj.jdbc.Driver)
nextName = pending.next();
return true;
}

// 获取服务
private S nextService() {
// 先调用上面的方法判断下,是否还有
if (!hasNextService())
throw new NoSuchElementException();
// 获取服务名称
String cn = nextName;
// 将下一个服务名称置为null,等下次迭代时再赋值
nextName = null;
// 待加载的服务
Class<?> c = null;
try {
// 加载服务
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中(相当于缓存起来)
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
}

谜题到这基本可以揭开,驱动的加载是在后面的迭代中加载的

1
2
3
4
5
6
7
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}

由上可知,ServiceLoader 会根据要加载服务的全类名去 META-INF/services/ 下面找相匹配的文件,然后读取文件内容,最后根据文件内容中配置的服务实现全类名去加载服务实例

JDBC 驱动的选择

通常一个项目中会有多个 jdbc 驱动,如:Oracle、MySQL、PostgreSQL 等,而从上面的分析可知,java 会将所有的驱动都加载并缓存起来,那么没有手动调用 Class.forName("xxx.xxx.jdbc.Driver") 的话,jdbc 是如何选择的呢

直接看获取连接的代码 DriverManager.getConnection(url, user, password)

1
2
3
4
5
6
7
8
9
10
11
12
public static Connection getConnection(String url, String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();

if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}

return (getConnection(url, info, Reflection.getCallerClass()));
}

调用的是另一个重载方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {
...
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}

} else {
println(" skipping: " + aDriver.getClass().getName());
}

}
...
}

抽丝剥茧,关键代码就是这个循环内 for(DriverInfo aDriver : registeredDrivers),用一个看似很傻的办法,一个个驱动去尝试连接,连接成功就返回

其实驱动会在连接前的第一步就判断这个连接的 url 是不是与自己相符的,符合的才会尝试建立连接,所以这里并不是每个驱动都实实在在地去尝试连接

而这个 registeredDrivers 是每个驱动在初始化时,自己调用 DriverManager.registerDriver(driver) 方法注册的

Common-Logging

common-logging 是 apache 的一个开源项目。也称 Jakarta Commons Logging,缩写 JCL。同 slf4j 一样,是一个日志系统的门面,但没有 slf4j 流行

首先,日志实例是通过 LogFactory 的 getLog(String) 方法创建的

1
2
3
public static getLog(Class clazz) throws LogConfigurationException {
return getFactory().getInstance(clazz);
}

LogFactory 是一个抽象类,它负责加载具体的日志实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
public static org.apache.commons.logging.LogFactory getFactory() throws LogConfigurationException {
// Identify the class loader we will be using
ClassLoader contextClassLoader = getContextClassLoaderInternal();

if (contextClassLoader == null) {
// This is an odd enough situation to report about. This
// output will be a nuisance on JDK1.1, as the system
// classloader is null in that environment.
// ...
}

// Return any previously registered factory for this class loader
org.apache.commons.logging.LogFactory factory = getCachedFactory(contextClassLoader);
if (factory != null) {
return factory;
}

// ...

// classpath根目录下寻找commons-logging.properties
Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);

// classpath根目录下commons-logging.properties是否配置use_tccl
ClassLoader baseClassLoader = contextClassLoader;
if (props != null) {
String useTCCLStr = props.getProperty(TCCL_KEY);
if (useTCCLStr != null) {
// The Boolean.valueOf(useTCCLStr).booleanValue() formulation
// is required for Java 1.2 compatibility.
if (Boolean.valueOf(useTCCLStr).booleanValue() == false) {
// Don't use current context classloader when locating any
// LogFactory or Log classes, just use the class that loaded
// this abstract class. When this class is deployed in a shared
// classpath of a container, it means webapps cannot deploy their
// own logging implementations. It also means that it is up to the
// implementation whether to load library-specific config files
// from the TCCL or not.
baseClassLoader = thisClassLoader;
}
}
}

// 这里真正开始决定使用哪个factory
// 首先,尝试查找vm系统属性org.apache.commons.logging.LogFactory,其是否指定factory
// ...
try {
String factoryClass = getSystemProperty(FACTORY_PROPERTY, null);
if (factoryClass != null) {
// ...
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
} else {
// ...
}
} catch (SecurityException e) {
// ...
// ignore
} catch (RuntimeException e) {
// This is not consistent with the behaviour when a bad LogFactory class is
// specified in a services file.
//
// One possible exception that can occur here is a ClassCastException when
// the specified class wasn't castable to this LogFactory type.
// ...
throw e;
}

// 第二 尝试使用java spi服务发现机制,载META-INF/services下寻找org.apache.commons.logging.LogFactory实现
if (factory == null) {
// ...
try {
// META-INF/services/org.apache.commons.logging.LogFactory, SERVICE_ID
final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);

if (is != null) {
// This code is needed by EBCDIC and other strange systems.
// It's a fix for bugs reported in xerces
BufferedReader rd;
try {
rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
} catch (java.io.UnsupportedEncodingException e) {
rd = new BufferedReader(new InputStreamReader(is));
}

String factoryClassName = rd.readLine();
rd.close();

if (factoryClassName != null && !"".equals(factoryClassName)) {
// ...
factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader);
}
} else {
// is == null
// ...
}
} catch (Exception ex) {
// note: if the specified LogFactory class wasn't compatible with LogFactory
// for some reason, a ClassCastException will be caught here, and attempts will
// continue to find a compatible class.
// ...
}
}

// 第三,尝试从classpath根目录下的commons-logging.properties中查找org.apache.commons.logging.LogFactory属性指定的factory
if (factory == null) {
if (props != null) {
// ...
String factoryClass = props.getProperty(FACTORY_PROPERTY);
if (factoryClass != null) {
// ...
factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
// TODO: think about whether we need to handle exceptions from newFactory
} else {
// ...
}
} else {
// ...
}
}

// 最后,使用后备factory实现,org.apache.commons.logging.impl.LogFactoryImpl
if (factory == null) {
// Note: unlike the above code which can try to load custom LogFactory
// implementations via the TCCL, we don't try to load the default LogFactory
// implementation via the context classloader because:
// * that can cause problems (see comments in newFactory method)
// * no-one should be customising the code of the default class
// Yes, we do give up the ability for the child to ship a newer
// version of the LogFactoryImpl class and have it used dynamically
// by an old LogFactory class in the parent, but that isn't
// necessarily a good idea anyway.
factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader);
}

if (factory != null) {
// Always cache using context class loader.
cacheFactory(contextClassLoader, factory);

if (props != null) {
Enumeration names = props.propertyNames();
while (names.hasMoreElements()) {
String name = (String) names.nextElement();
String value = props.getProperty(name);
factory.setAttribute(name, value);
}
}
}
return factory;
}

整体步骤如下:

  1. 从 vm 查找系统属性 org.apache.commons.logging.LogFactory
  2. 使用 SPI 机制,找到 org.apache.commons.logging.LogFactory 的实现
  3. 查找 classpath 根目录 commons-logging.properties 的 org.apache.commons.logging.LogFactory 属性是否指定 factory 实现
  4. 使用默认 factory 实现,org.apache.commons.logging.impl.LogFactoryImpl

插件体系

Eclipse 使用 OSGi 作为插件系统的基础,动态添加新插件和停止现有插件,以动态的方式管理组件生命周期。

一般来说,插件的文件结构必须在指定目录下包含以下三个文件:

  • META-INF/MANIFEST.MF: 项目基本配置信息,版本、名称、启动器等
  • build.properties: 项目的编译配置信息,包括,源代码路径、输出路径
  • plugin.xml:插件的操作配置信息,包含弹出菜单及点击菜单后对应的操作执行类等

当 eclipse 启动时,会遍历 plugins 文件夹中的目录,扫描每个插件的清单文件 MANIFEST.MF,并建立一个内部模型来记录它所找到的每个插件的信息,就实现了动态添加新的插件。

这也意味着是 eclipse 制定了一系列的规则,像是文件结构、类型、参数等。插件开发者遵循这些规则去开发自己的插件,eclipse 并不需要知道插件具体是怎样开发的,只需要在启动的时候根据配置文件解析、加载到系统里就好了,是 SPI 思想的一种体现

Spring Factories

在 Spring 项目中,如果想要被 Spring 管理的 Bean 不在 Spring 的包扫描路径下,有两种解决方式:

  • 使用 @Import 注解
  • 使用 spring.factories 文件

与 SPI 类似,Spring Factories 需要在 resources/META-INF 下的 spring.factories 文件中配置服务的全类名

spring-core 包里定义了 SpringFactoriesLoader,与 Java 的 ServiceLoader 类似,这个类实现了 检索 META-INF/spring.factories 文件,并获取文件配置 的功能。这个类中定义了两个对外的方法:

  • loadFactories:查找具体实现,返回对象列表
  • loadFactoryName:查询具体实现名称,返回类名列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
public final class SpringFactoriesLoader {
// 配置文件的地址常量
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// 日志实例
private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap();

private SpringFactoriesLoader() {}

public static <T> List<T> loadFactories(Class<T> factoryClass, @Nullable ClassLoader classLoader) {
Assert.notNull(factoryClass, "'factoryClass' must not be null");
ClassLoader classLoaderToUse = classLoader;
// 如果类加载器为空,就获取当前类的类加载器
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
// 调用 loadFactoryNames 方法加载给定工厂类的实现类名列表
List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
if (logger.isTraceEnabled()) {
logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
}
List<T> result = new ArrayList(factoryNames.size());
Iterator var5 = factoryNames.iterator();
// 遍历工厂类名列表
while(var5.hasNext()) {
String factoryName = (String)var5.next();
// 调用 instantiateFactory 方法实例化每个工厂类,并将实例添加到结果列表中
result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
}
// 使用 Spring 的排序工具对结果列表进行排序
AnnotationAwareOrderComparator.sort(result);
return result;
}


/**
* 使用给定的类加载器从"META-INF/spring.factories"加载给定类型的工厂实现的完全限定类名
*/
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

/**
* 加载 META-INF/spring.factories 文件中的配置信息,将工厂类名和对应的实现类名存储在一个多
* 值映射中
*/
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 尝试从缓存中获取给定类加载器的工厂配置信息
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
// 获取指定类加载器或系统类加载器中的所有spring.factories文件的URL
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
// 创建一个 LinkedMultiValueMap 用于存储工厂配置信息,这里采用多值映射
LinkedMultiValueMap result = new LinkedMultiValueMap();
// 遍历所有 META-INF/spring.factories 文件的 URL
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
// 使用 URL 创建一个 UrlResource 对象,该对象表示一个可读的资源
UrlResource resource = new UrlResource(url);
// 加载 UrlResource 对象表示的资源文件,得到其中的配置信息,这里假定文件内容是以键值对的形式存储的
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 遍历配置信息的键值对
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
// 获取当前配置信息的键值对
Entry<?, ?> entry = (Entry)var6.next();
// 获取工厂类名,并去除两端的空格
String factoryClassName = ((String)entry.getKey()).trim();
// 将工厂类对应的值(可能是逗号分隔的多个类名)拆分成数组
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;
// 将拆分后的每个类名添加到多值映射中,键是工厂类名,值是对应的类名
for(int var11 = 0; var11 < var10; ++var11) {
String factoryName = var9[var11];
result.add(factoryClassName, factoryName.trim());
}
}
}
// 将加载的工厂配置信息放入缓存中
cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}

/**
* 实例化工厂
*/
private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
try {
// 实例化对象
Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);
// 确定此Class对象表示的类或接口是否与指定Class参数表示的类或接口相同
if (!factoryClass.isAssignableFrom(instanceClass)) {
throw new IllegalArgumentException("Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");
} else {
// 获取给定类和参数的可访问构造函数
return ReflectionUtils.accessibleConstructor(instanceClass, new Class[0]).newInstance();
}
} catch (Throwable var4) {
throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), var4);
}
}
}

SpringBoot3.0 之后不再使用 SpringFactoriesLoader,而是 Spring 重新从 META-INFO/spring/ 目录下的 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中读取,其本质还是不变的,只是文件路径和文件名的改变

ShardingSphere

ShardingSphere 在保持架构完整性与功能稳定性的情况下,为满足用户不同场景的实际需求,采用了 SPI 的方式进行扩展,动态将用户自定义的实现类加载其中。

ShardingSphere SPI 功能介绍

SPI 的缺点

  • 不能按需加载
  • 获取方式不够灵活,只能通过 Iterator 获取,不能通过指定某个参数获取对应实现类
  • ServiceLoader 里面的方法都是非线程安全的