SPI 服务发现机制
SPI 是 Java JDK 内部提供的一种服务发现机制。
- SPI->Service Provider Interface, 服务提供接口,是 Java JDK 内置的一种服务发现机制
- 通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类
[⚠️注意事项]:
面向对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行编码。如果涉及实现类就会违反可插拔的原则,针对于模块装配,Java SPI 提供了为某个接口寻找服务的实现机制。
SPI 规范
- 使用约定:
[1]. 编写服务提供接口,可以是抽象接口和函数接口,JDK1.8 之后推荐使用函数接口
[2]. 在 jar 包的 META-INF/services/ 目录里创建一个以服务接口命名的文件。其实就是实现该服务接口的具体实现类。
提供一个目录:META-INF/services/
放到 ClassPath 下面
[3]. 当外部程序装配这个模块的时候,就能通过该 Jar 包 META-INF/services/ 配置文件找到具体的实现类名,并装载实例化,完成模块注入。
目录下放置一个配置文件:文件名是需要拓展的接口全限定名称
文件内部为要实现的接口实现类
文件必须为 UTF- 8 编码
[4]. 寻找服务接口实现,不用在代码中提供,而是利用 JDK 提供服务查找工具类:java.util.ServiceLoader 类来加载使用:
ServiceLoader.load(xxx.class)
ServiceLoader<XXXInterface> loads = ServiceLoader.load(xxx.class)
SPI 源码分析
[1].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>
{//[1]. 初始化定义全局配置文件路径 Path
private static final String PREFIX = "META-INF/services/";
//[2]. 初始化定义加载的服务类或接口
private final Class<S> service;
//[3]. 初始化定义类加载器
private final ClassLoader loader;
//[4]. 初始化定义访问控制上下文
private final AccessControlContext acc;
//[5]. 初始化定义加载服务类的缓存集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
//[6]. 初始化定义私有内部 LazyIterator 类,真正加载服务类的实现类
private LazyIterator lookupIterator;
// 私有化有参构造 -> ServiceLoader(Class<S> svc, ClassLoader cl)
private ServiceLoader(Class<S> svc, ClassLoader cl) {//[1]. 实例化服务接口 ->Class<S>
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//[2]. 实例化类加载器 ->ClassLoader
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
//[3]. 实例化访问控制上下文 ->AccessControlContext
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
//[4]. 回调函数 ->reload
reload();}
public void reload() {//[1]. 清空缓存实例集合
providers.clear();
//[2]. 实例化私有内部 LazyIterator 类 ->LazyIterator
lookupIterator = new LazyIterator(service, loader);
}
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);
}
}
2.LazyIterator 源码:
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();
}
}
使用举例
[1].Dubbo SPI 机制:
META-INF/dubbo.internal/xxx= 接口全限定名
Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。
Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下。
与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外,在测试 Dubbo SPI 时,需要在 Robot 接口上标注 @SPI 注解。
[2].Cache SPI 机制:
META-INF/service/javax.cache.spi.CachingProvider=xxx
[3]Spring SPI 机制:
META-INF/services/org.apache.commons.logging.LogFactory=xxx
[4].SpringBoot SPI 机制:
META-INF/spring.factories/org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx
在 springboot 的自动装配过程中,最终会加载 META-INF/spring.factories 文件,而加载的过程是由 SpringFactoriesLoader 加载的。从 CLASSPATH 下的每个 Jar 包中搜寻所有 META-INF/spring.factories 配置文件,然后将解析 properties 文件,找到指定名称的配置后返回
源码:
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// spring.factories 文件的格式为:key=value1,value2,value3
// 从所有的 jar 包中找到 META-INF/spring.factories 文件
// 然后从文件中解析出 key=factoryClass 类名称的所有 value 值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {String factoryClassName = factoryClass.getName();
// 取得资源文件的 URL
Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<String>();
// 遍历所有的 URL
while (urls.hasMoreElements()) {URL url = urls.nextElement();
// 根据资源文件 URL 解析 properties 文件,得到对应的一组 @Configuration 类
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String factoryClassNames = properties.getProperty(factoryClassName);
// 组装数据,并返回
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
}
[5]. 自定义序列化实现 SPI:META-INF/services/xxx= 接口全限定名
参考学习 Java SPI 和 Dubbo SPI 机制源码, 自己动手实现序列化工具类等
版权声明:本文为博主原创文章,遵循相关版权协议,如若转载或者分享请附上原文出处链接和链接来源。