JDK SPI 、Spring SPI、Dubbo SPI机制
共 49030字,需浏览 99分钟
·
2021-08-06 06:10
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
JDK SPI机制
SPI(Service Provider Interface),是一种将服务接口与服务实现分离以达到解耦可拔插、大大提升了程序可扩展性的机制。
约定(我觉得称之为规范更合适):
1. 制定统一的规范(比如 java.sql.Driver)
2. 服务提供商提供这个规范具体的实现,在自己jar包的META-INF/services/目录里创建一个以服务接口命名的文件,内容是实现类的全命名(比如:com.mysql.jdbc.Driver)。
3. 平台引入外部模块的时候,就能通过该jar包META-INF/services/目录下的配置文件找到该规范具体的实现类名,然后装载实例化,完成该模块的注入。
这个机制最大的优点就是无须在代码里指定,进而避免了代码污染,实现了模块的可拔插。
JDK SPI的一个典型案例就是 java.sql.Driver 了
我们最熟悉的代码
// 加载驱动
Class.forName("com.mysql.jdbc.Driver");
// 获取连接
Connection connection = DriverManager.getConnection("url", "user", "password");
我们进入DriverManager类,里面有个静态代码块,这部分的内容会先执行。
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
加载并初始化驱动
private static void loadInitialDrivers() {
// ...
// 如果驱动被打包作为服务提供者,则加载它。
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 1. load
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 2. 获取Loader的迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
// 3. 调用next方法
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
// ...
}
看 ServiceLoader#load方法
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// load
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
// new 一个 ServiceLoader,参数为服务接口Class和类加载器
return new ServiceLoader<>(service, loader);
}
可以看出,上面主要是获取类加载器并新建ServiceLoader的过程,没有加载实现类的动作。现在看:ServiceLoader#iterator 方法
public Iterator<S> iterator() {
// 这里是个Iterator的匿名内部类,重写了一些方法
return new Iterator<S>() {
// 已存在的提供者
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
// 先检查缓存
if (knownProviders.hasNext())
return true;
// 缓存没有,走 java.util.ServiceLoader.LazyIterator#hasNext 方法
return lookupIterator.hasNext();
}
public S next() {
// 同样先检查缓存
if (knownProviders.hasNext())
return knownProviders.next().getValue();
// 缓存没有,走 java.util.ServiceLoader.LazyIterator#next 方法
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
看 ServiceLoader.LazyIterator#hasNext 方法
// java.util.ServiceLoader.LazyIterator#hasNext
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 获取全路径:META-INF/services/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;
}
// 解析的返回值是一个 Iterator<String> 类型,其中的String代表文件里配置的实现类全限定名,比如:com.mysql.jdbc.Driver
pending = parse(service, configs.nextElement());
}
// 把nextName初始化
nextName = pending.next();
return true;
}
看 ServiceLoader.LazyIterator#next 方法
// java.util.ServiceLoader.LazyIterator#next
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
// 进入 java.util.ServiceLoader.LazyIterator#nextService 方法
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
// com.mysql.jdbc.Driver
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 加载 com.mysql.jdbc.Driver 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 {
// 实例化并转换成 com.mysql.jdbc.Driver 对象
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
}
所以,DriverManager做了什么事:
// 1. 获取类加载器并创建ServiceLoader对象
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 2. 创建迭代器并覆盖hasNext和next方法
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
// 3. 这个方法主要是读取配置文件,获取其中实现类的全限定名。
while(driversIterator.hasNext()) {
// 4. 这个方法主要是根据全限定名去生成一个实例
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
另外你也能发现,ServiceLoader只提供了遍历的方式来获取目标实现类,没有提供按需加载的方法,这也是常说的不足之处。
自己实现
定义一个接口
package com.demo.spi;
public interface Funny {
void deal();
}
写一个实现类
package com.demo.spi;
public class FunnyImpl implements Funny {
@Override
public void deal() {
System.out.println("FunnyImpl");
}
}
配置
文件内容
执行测试
@Test
public void test(){
ServiceLoader<Funny> serviceLoader = ServiceLoader.load(Funny.class);
serviceLoader.forEach(Funny::deal);
}
输出
Spring SPI机制
对于Spring的SPI机制主要体现在SpringBoot中。我们知道SpringBoot的启动包含new SpringApplication和执行run方法两个过程,new的时候有这么个逻辑:(getSpringFactoriesInstances)
这个方法走到里面,无非 1. 加载类的全限定名列表。2. 根据类名通过反射实例化。
重点在于:SpringFactoriesLoader.loadFactoryNames(type, classLoader)
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
// 获取类的全限定名
String factoryClassName = factoryClass.getName();
// 1. 执行loadSpringFactories,这里只传入了类加载器,肯定是要获取全部配置
// 2. getOrDefault,获取指定接口的实现类名称列表,如果没有则返回一个空列表
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 先检查缓存
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
// 获取类路径下所有META-INF/spring.factories
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
// 把加载的配置转换成Map<String, List<String>>格式
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
它还提供了实例化的方法:SpringFactoriesLoader.loadFactories(factoryClass, classLoader) 利用反射实现,理解起来也不困难,不做解释了
对比ServiceLoader:
1. 都是XXXLoader。命名格式都一样。
2. 一个是加载 META-INF/services/ 目录下的配置;一个是加载 META-INF/spring.factories 固定文件的配置。思路都一样。
3. 两个都是利用ClassLoader和ClassName来完成操作的。不同的是Java的ServiceLoader加载配置和实例化都是自己来实现,并且不能按需加载;SpringFactoriesLoader既可以单独加载配置然后按需实例化也可以实例化全部。
如何实现一个Spring-Boot-Starter?
先看一下SpringBoot的相关要点。
这个是SpringBoot的 @SpringBootApplication 注解,里面还有一个 @EnableAutoConfiguration 注解,开启自动配置的。
可以发现这个自动配置注解在另一个工程,而这个工程里也有个spring.factories文件,如下图:
我们知道在SpringBoot的目录下有个spring.factories文件,里面都是一些接口的具体实现类,可以由SpringFactoriesLoader加载。
那么同样的道理,spring-boot-autoconfigure模块也能动态加载了。看一下其中的内容:
我们看到有个接口【org.springframework.boot.autoconfigure.EnableAutoConfiguration】,有N个配置类,都是自动配置相关的,那么我们可以猜一猜,是不是模仿一下这样的配置,我们就可以丝滑进入了?
为了证明这一点,我们看一下 dubbo-spring-boot-starter 的结构
你会发现starter项目没有实现类,只有个pom文件。
它的关键在于引入了一个 autoconfigure 依赖。
这个里面配置了Spring的 spring.factories 其中只有一个配置
看到这里你是不是感觉到了什么:
1. 新建一个只有pom的starter工程,引入写好的自动配置模块。
2. 自动配置模块配置 spring.factories 文件,格式如上。
3. 具体实现自动配置逻辑。
这样当SpringBoot启动加载配置类的时候,就会把这些第三方的配置一起加载。
所以我认为 Starter的作用就是在启动之前把第三方的配置加载到容器中,具体表现就是无须用户手动配置即可使用。【如果不准确望指正】
具体实现一个starter
两个maven工程,目录如下:
custom-spring-boot-autoconfigure↓↓↓↓↓
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>custom-spring-boot-autoconfigure</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
</dependencies>
</project>
CustomAutoConfiguration
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnClass(name = "org.springframework.web.servlet.DispatcherServlet")
public class CustomAutoConfiguration {
@Bean
public CustomConfig customConfig(){
System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!第三方自定义的配置");
return new CustomConfig();
}
}
CustomConfig
/**
* 自定义配置
*/
public class CustomConfig {
private String value = "Default";
}
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.demo.CustomAutoConfiguration
一共四个文件
custom-spring-boot-starter↓↓↓↓
只有pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>custom-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.demo</groupId>
<artifactId>custom-spring-boot-autoconfigure</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
最后把这两个工程install
下面是新建一个SpringBoot项目,然后引入咱们自定义的starter依赖。
<dependency>
<groupId>com.demo</groupId>
<artifactId>custom-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
然后启动就可以啦,可以看见,用户引入依赖之后无需手动配置就可以使用第三方插件
看下工程依赖
Dubbo SPI机制
上面介绍了两种SPI,到Dubbo这里就不难理解了吧?
思路都是处理类+约定配置 ,在Dubbo里,约定扩展配置在 META-INF/dubbo/internal/ 目录下
在Dubbo源码里也可以发现,核心类是 ExtensionLoader
private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
private static final ProxyFactory PROXY_FACTORY = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
getExtensionLoader
// 这个方法就是获取一个特定类型的ExtensionLoader的实例
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
}
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
}
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type (" + type +
") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
}
// 先从缓存中取
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;
}
下面看getExtension方法
public T getExtension(String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
if ("true".equals(name)) {
// 获取默认的扩展类
return getDefaultExtension();
}
// Holder用来持有目标对象
final Holder<Object> holder = getOrCreateHolder(name);
// 获取实例
Object instance = holder.get();
// 没有的话,双重检查并创建实例
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
看createExtension方法
private T createExtension(String name) {
// 1. 获取扩展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);
}
// 2. IOC注入依赖
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
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 + ") couldn't be instantiated: " + t.getMessage(), t);
}
}
1. 获取所有扩展类
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;
}
// 对于loadDirectory,每个目录有两个执行逻辑,因为目前要兼容alibaba老版本
private Map<String, Class<?>> loadExtensionClasses() {
cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
// 加载目录:META-INF/dubbo/internal/
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
// 加载目录:META-INF/dubbo/
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
// 加载目录:META-INF/services/
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
return extensionClasses;
}
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
// 文件路径,比如 META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol
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;
// 按照 = 分割,前面是name,后面是类全限定名
int i = line.indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
// 加载Class
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
} 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);
}
}
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error occurred when loading extension class (interface: " +
type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + " is not subtype of interface.");
}
// 检测目标类上是否有 Adaptive 注解
if (clazz.isAnnotationPresent(Adaptive.class)) {
// 设置 cachedAdaptiveClass缓存
cacheAdaptiveClass(clazz);
// 检测 clazz 是否是 Wrapper 类型
} else if (isWrapperClass(clazz)) {
// 存储 clazz 到 cachedWrapperClasses 缓存中
cacheWrapperClass(clazz);
} else {
// 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常
clazz.getConstructor();
if (StringUtils.isEmpty(name)) {
// 如果 name 为空,则尝试从 Extension 注解中获取 name,或使用小写的类名作为 name
name = findAnnotationName(clazz);
if (name.length() == 0) {
throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
}
}
String[] names = NAME_SEPARATOR.split(name);
if (ArrayUtils.isNotEmpty(names)) {
// 存储 name 到 Activate 注解对象的映射关系
cacheActivateClass(clazz, names[0]);
for (String n : names) {
// 存储 Class 到名称的映射关系
cacheName(clazz, n);
// 存储名称到 Class 的映射关系
saveInExtensionClass(extensionClasses, clazz, n);
}
}
}
}
这部分有些注释直接拿的官网,总结下来这部分:
1. 根据约定的路径比如【META-INF/dubbo/internal/】加载文件URl。
2. 读取并解析每一个文件内容,具体为按行读取,去掉注释,按等号分割为 name-class全限定名键值对。
3. 根据类全限定名生成Class对象,根据Class对象的的特点进行相关的缓存以及name到Class对象的缓存。
2. IOC注入依赖
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
// 这里判断方法是否是setter方法,Dubbo目前只处理setter的IOC
if (isSetter(method)) {
// 如果标注了@DisableInject,则不进行注入
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
// 获取 setter 方法参数类型
Class<?> pt = method.getParameterTypes()[0];
// 基本类型跳过
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
// 获取属性名,比如 setName 方法对应属性名 name
String property = getSetterProperty(method);
// 从 ObjectFactory 中获取依赖对象
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
// 通过反射调用 setter 方法设置object依赖
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("Failed to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
这部分容易理解,核心部分利用反射实现,其它前置处理做了一些校验。
而使用Dubbo SPI的话也是一样的套路,扩展类实现 + 配置 + ExtensionLoader
作者 | 露娜妹
来源 | cnblogs.com/LUA123/p/12460869.html