Java 基础知识之 SPI
发布日期:2021-06-29 12:02:44 浏览次数:3 分类:技术文章

本文共 6946 字,大约阅读时间需要 23 分钟。

什么是 SPI?

SPI,全称 Service Provider Interface,即服务提供者接口,是Java中用于提供给第三方实现的接口。

如何使用SPI?

SPI 符合面向接口编程的范式,使用接口的用户无需了解底层的实现即可直接使用接口所提供的服务。使用 SPI 需要遵守如下的约定。

  1. 服务提供者完成接口的实现,实现类存在一个不带参数的构造器。
  2. 服务提供者在 classpath 下的 META-INF/services 目录下创建和接口名称一致的文件,文件的内容为接口的实现类全限定名,如果有多个则每行为一个实现类的全限定名,字符 # 及之后的字符串为注释。
  3. 接口的使用者使用 java.util.ServiceLoader#load(java.lang.Class<S>) 获取接口实现类。

SPI 示例

SPI的使用场景有多种,如JDBC,Spring,Dubbo,以JDBC为例,MySQL JDBC 驱动下可以看到 java.sql.Dirver的配置文件。

MySQL JDBC 驱动 SPI 配置文件那么,驱动如何被加载的呢?当我们调用 java.sql.DriverManager#getConnection(java.lang.String)方法获取java.sql.Connection时会触发 DriverManager 类的加载。DriverManager 类加载时会执行静态代码块的代码。查看 DriverManager 类的相关源码如下。

public class DriverManager {
static {
loadInitialDrivers(); println("JDBC DriverManager initialized"); } private static void loadInitialDrivers() {
String drivers; try {
drivers = AccessController.doPrivileged(new PrivilegedAction
() {
public String run() {
return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) {
drivers = null; } AccessController.doPrivileged(new PrivilegedAction
() {
public Void run() {
//在此处使用 ServiceLoader 加载 Driver 的实现 ServiceLoader
loadedDrivers = ServiceLoader.load(Driver.class); Iterator
driversIterator = loadedDrivers.iterator(); try{
while(driversIterator.hasNext()) {
driversIterator.next(); } } catch(Throwable t) {
// Do nothing } return null; } }); println("DriverManager.initialize: jdbc.drivers = " + drivers); if (drivers == null || drivers.equals("")) {
return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver); //此处加载不同厂商的驱动 Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex); } } }}

可以看到 DriverManager 类被加载到 JVM 时会使用 java.util.ServiceLoader#load(java.lang.Class<S>)方法加载SPI Driver的实现类,然后循环使用 java.lang.Class#forName(java.lang.String, boolean, java.lang.ClassLoader) 方法加载驱动。

ServiceLoader 源码浅析

SPI 的实现类使用 java.util.ServiceLoader#load(java.lang.Class<S>)方法获取,ServiceLoader的源码只有500多行,底层使用了java.lang.ClassLoader#getResources或者 java.lang.ClassLoader#getSystemResources 获取 classpath 下的 SPI 资源文件,然后进行逐行解析文件内容获取 SPI 实现类,并且使用了懒加载的方式。获取 classpath 下资源文件的方式参见

接下来看 ServiceLoader的实现,先看 ServiceLoader 的成员变量。

//ServiceLoader 实现了接口 Iterable ,说明可以作为迭代器使用public final class ServiceLoader    implements Iterable{
//classpath 下文件前缀 private static final String PREFIX = "META-INF/services/"; //服务的接口或类 private final Class service; //实例化服务提供者的类加载器 private final ClassLoader loader; //ServiceLoader 被创建时的访问控制上下文 private final AccessControlContext acc; // 服务提供者缓存 private LinkedHashMap
providers = new LinkedHashMap<>(); // 懒加载的迭代器,ServiceLoader使用该迭代器懒加载。 private LazyIterator lookupIterator;}

方法java.util.ServiceLoader#load(java.lang.Class<S>)最终调用 ServiceLoader 的构造方法对成员变量进行初始化,当我们循环获取 SPI 实现时会调用方法java.util.ServiceLoader#iterator,跟踪源码如下

public Iterator iterator() {
return new Iterator() {
//已经加载过的Provider Iterator
> knownProviders = providers.entrySet().iterator(); public boolean hasNext() {
//如果已加载过的Provider中没有更多,则尝试查找 if (knownProviders.hasNext()) return true; return lookupIterator.hasNext(); } public S next() {
如果已加载过的Provider中没有更多,则尝试从查找到的迭代器中获取 if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); } public void remove() {
throw new UnsupportedOperationException(); } }; }

可以看到,对 ServiceProvider 的迭代最后又会调用 LazyIterator

迭代器的方法,LazyIterator 是 ServiceProvider的静态内部类,前面实例化 ServiceProvider 时会对LazyIterator 进行实例化。LazyItertor 核心源码如下

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 表示某个文件中的 SPI 实现类集合,parse方法用于解析文件,最终循环调用方法java.util.ServiceLoader#parseLine 校验文件中的行是否合法 pending = parse(service, configs.nextElement()); } //nextName 表示下一个待获取的实现类的全限定名 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 {
//在此调用无参数构造方法实例化 SPI 实现类,因此 SPI 实现类应包含一个无参数的构造方法 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 }

LazyIterator 类通过对类路径下SPI 文件的解析获取到实现类,然后进行加载,解析过程最终调用了方法java.util.ServiceLoader#parseLine,该方法主要解析文件中的每一行字符串,主要进行校验字符串是否合法,在此不做分析。

总结

SPI 通过对实现的来源进行约定,可以将接口与实现进行解耦,使得服务的使用者不必关心具体的实现在哪个地方即可获取到实现,但是使用SPI必须一次加载所有的实现类,无法根据条件获取某一个具体的实现。

转载地址:https://blog.csdn.net/zzuhkp/article/details/106443550 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Java 中如何获取 classpath 下资源文件?
下一篇:Java 核心技术之序列化 Serializable

发表评论

最新留言

感谢大佬
[***.8.128.20]2024年04月10日 15时11分35秒