java的SPI机制
什么是SPI
SPI的全称是Service Provider Interface,中文名为服务提供者接口,是一种服务发现机制,基于JDK内置的动态加载实现扩展点的机制(通过在 ClassPath 路径下的 META-INF/services
文件夹查找文件,自动加载文件里所定义的类),这一机制使得框架扩展和替换组件变得容易。
SPI术语
SPI由三个组件构成:Service、Service Provider、ServiceLoader
- Service:是一个公开的接口或抽象类,定义了一个抽象的功能模块(文件名称)
- Service Provider:是Service的实现类(文件内容)
- ServiceLoader:是SPI机制中的核心组件,负责在运行时发现并加载Service Provider
SPI架构和API架构区别
SPI架构
Java SPI 设计理念是基于接口的编程(策略模式)+配置文件组合实现的动态加载机制。
API架构设置
SPI的扩展是基于jar级别,API扩展是基于类级别
SPI 优缺点
优点,可实现服务接口与服务实现的解耦:
- 服务提供者(如 springboot starter)提供出 SPI 接口。身为服务提供者,在你无法形成绝对规范强制时,适度"放权" 比较明智,适当让客户端去自定义实现
- 客户端(普通的 springboot 项目)即可通过本地注册的形式,将实现类注册到服务端,轻松实现可插拔
缺点
- Java SPI虽然使用了懒加载机制,但是其获取一个实现时,需要使用迭代器循环加载所有的实现类;
- 当需要某一个实现类时,需要通过循环一遍来获取;
- 多个并发多线程使用ServiceLoader类的实例是不安全的。
- 不能按需加载。虽然 ServiceLoader 做了延迟加载,但是只能通过遍历的方式全部获取。如果其中某些实现类很耗时,而且你也不需要加载它,那么就形成了资源浪费
- 获取某个实现类的方式不够灵活,只能通过迭代器的形式获取
Dubbo SPI 实现方式对以上两点进行了业务优化。
SPI 创建方式
SPI 运行机制
SPI的简单实现
下面我们来简单实现一个jdk的SPI的简单实现。
第一步,新建一个空maven项目 javaspi,项目结构如下:
第二步创建接口,接口全限定名为org.example.spi.SpiPlugin
。
package org.example.spi;
public interface SpiPlugin {
void execute();
}
第三步、创建接口实现类
org.example.spi.impl.SpiPluginImplA
package org.example.spi.impl;
import org.example.spi.SpiPlugin;
public class SpiPluginImplA implements SpiPlugin {
public void execute() {
System.out.println("SpiPluginImplA.execute()");
}
}
org.example.spi.impl.SpiPluginImplB
package org.example.spi.impl;
import org.example.spi.SpiPlugin;
public class SpiPluginImplB implements SpiPlugin {
public void execute() {
System.out.println("SpiPluginImplB.execute()");
}
}
第四步、在resource
目录下创建 META-INF/services
文件夹,并在该文件夹下创建名为接口全限定名的文件
org.example.spi.SpiPlugin
,并在该文件里写上需要用到的接口实现类。
org.example.spi.impl.SpiPluginImplA
org.example.spi.impl.SpiPluginImplB
第五步、验证,创建MainTest
编写main
方法验证。
package org.example;
import org.example.spi.SpiPlugin;
import java.util.ServiceLoader;
public class MainTest {
public static void main(String[] args) {
ServiceLoader<SpiPlugin> loader = ServiceLoader.load(SpiPlugin.class);
Iterator<SpiPlugin> iterator = loader.iterator();
while (iterator.hasNext()){
SpiPlugin next = iterator.next();
next.execute();
}
}
}
这里用iterator
去循环打印,更容易debugger跟踪代码。
输出内容如下:
SpiPluginImplA.execute()
SpiPluginImplB.execute()
这样一个简单的spi的demo就完成了。可以看到其中最为核心的就是通过ServiceLoader这个类来加载具体的实现类的。
SPI原理解析
通过上面简单的demo,可以看到最关键的实现就是ServiceLoader这个类,可以看下这个类的源码,如下:
package java.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.AccessController;
import java.security.AccessControlContext;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
public final class ServiceLoader<S>
implements Iterable<S>
{
//扫描目录前缀
private static final String PREFIX = "META-INF/services/";
// 被加载的类或接口
// The class or interface representing the service being loaded
private final Class<S> service;
// 用于定位、加载和实例化实现方实现的类的类加载器
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// 上下文对象
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// 按照实例化的顺序缓存已经实例化的类
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒查找迭代器
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
// 重新加载
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
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();
}
private static void fail(Class<?> service, String msg, Throwable cause)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg,
cause);
}
private static void fail(Class<?> service, String msg)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg);
}
private static void fail(Class<?> service, URL u, int line, String msg)
throws ServiceConfigurationError
{
fail(service, u + ":" + line + ": " + msg);
}
// 读取文件每一行,加入到 names 中
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
String ln = r.readLine();
if (ln == null) {
return -1;
}
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
// Parse the content of the given URL as a provider-configuration file.
// 读取文件每一行内容 加到集合中,并返回迭代器,一行就是一个接口实现类
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}
// Private inner class implementing fully-lazy provider lookup
// 私有内部类,提供对所有的service的类的加载与实例化,和遍历逻辑
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
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 = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
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.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
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);
}
}
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);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
/**
* 迭代器 ,自定义了迭代过程
* An iterator that lazily loads providers for this loader's service
*/
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
public String toString() {
return "java.util.ServiceLoader[" + service.getName() + "]";
}
}
上面的代码重点关注nextService()
和 nextService()
方法,有兴趣的读者可以自己去研究,下面贴出比较直观的spi加载的主要流程供参考:
Spring Boot中的SPI机制
Spring Boot的扩展机制之Spring Factories
在Spring中也有一种类似与Java SPI的加载机制。它在META-INF/spring.factories
文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化。
这种自定义的SPI机制是Spring Boot Starter实现的基础。
Spring Factories实现原理
spring-core
包里定义了SpringFactoriesLoader
类,这个类实现了检索META-INF/spring.factories
文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:
- loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表。
- loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表。
上面的两个方法的关键都是从指定的ClassLoader中获取spring.factories
文件,并解析得到类名列表,具体代码如下:
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
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);
}
}
从代码中我们可以知道,在这个方法中会遍历整个ClassLoader中所有jar包下的spring.factories
文件。也就是说我们可以在自己的jar中配置spring.factories
文件,不会影响到其它地方的配置,也不会被别人的配置覆盖。
spring.factories的是通过Properties解析得到的,所以我们在写文件中的内容都是安装下面这种方式配置的:
com.xxx.interface=com.xxx.classname
如果一个接口希望配置多个实现类,可以使用 ,
进行分割。
以上内容若有错误之处,欢迎各位批评指正,若有侵权,请告知删除!更多精彩内容请扫码关注,获取最新动态,一手资料,技术前沿。