1. 什么是 SPI
1. 背景
在面向对象的设计准则中,个别举荐模块之间基于接口编程,通常状况下调用方模块是不会感知到被调用方模块的外部具体实现。一旦代码外面波及具体实现类,就违反了开闭准则。如果须要替换一种实现,就须要批改代码。
为了实现在模块拆卸的时候不必在程序外面动静指明,这就须要一种服务发现机制。Java SPI 就是提供了这样一个机制:为某个接口寻找服务实现的机制。这有点相似 IOC 的思维,将拆卸的控制权移交到了程序之外。
SPI
英文为 Service Provider Interface
字面意思就是:“服务提供者的接口”,我的了解是:专门提供给服务提供者或者扩大框架性能的开发者去应用的一个接口。
SPI 将服务接口和具体的服务实现拆散开来,将服务调用方和服务实现者解耦,可能晋升程序的扩展性、可维护性。批改或者替换服务实现并不需要批改调用方。
2. 应用场景
很多框架都应用了 Java 的 SPI 机制,比方:数据库加载驱动,日志接口,以及 dubbo 的扩大实现等等。
3. SPI 和 API 有啥区别
说到 SPI 就不得不说一下 API 了,从狭义上来说它们都属于接口,而且很容易混同。上面先用一张图阐明一下:
个别模块之间都是通过通过接口进行通信,那咱们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。
当实现方提供了接口和实现,咱们能够通过调用实现方的接口从而领有实现方给咱们提供的能力,这就是 API,这种接口和实现都是放在实现方的。
当接口存在于调用方这边时,就是 SPI,由接口调用方确定接口规定,而后由不同的厂商去杜绝这个规定对这个接口进行实现,从而提供服务,举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,而后当初需要量产了,而市面上有好几家芯片制造业公司,这个时候,只有 H 公司指定好了这芯片生产的规范(定义好了接口标准),那么这些单干的芯片公司(服务提供者)就依照规范交付自家特色的芯片(提供不同计划的实现,然而给进去的后果是一样的)。
2. 实战演示
Spring 框架提供的日志服务 SLF4J 其实只是一个日志门面(接口),然而 SLF4J 的具体实现能够有几种,比方:Logback、Log4j、Log4j2 等等,而且还能够切换,在切换日志具体实现的时候咱们是不须要更改我的项目代码的,只须要在 Maven 依赖外面批改一些 pom 依赖就好了。
这就是依赖 SPI 机制实现的,那咱们接下来就实现一个繁难版本的日志框架。
1. Service Provider Interface
新建一个 Java 我的项目 service-provider-interface
目录构造如下:
├─.idea
└─src
├─META-INF
└─org
└─spi
└─service
├─Logger.java
├─LoggerService.java
├─Main.java
└─MyServicesLoader.java
新建 Logger 接口,这个就是 SPI,服务提供者接口,前面的服务提供者就要针对这个接口进行实现。
package org.spi.service;
public interface Logger {void info(String msg);
void debug(String msg);
}
接下来就是 LoggerService 类,这个次要是为服务使用者(调用方)提供特定性能的。如果存在纳闷的话能够先往后面持续看。
package org.spi.service;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
public class LoggerService {private static final LoggerService SERVICE = new LoggerService();
private final Logger logger;
private final List<Logger> loggerList;
private LoggerService() {ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);
List<Logger> list = new ArrayList<>();
for (Logger log : loader) {list.add(log);
}
// LoggerList 是所有 ServiceProvider
loggerList = list;
if (!list.isEmpty()) {
// Logger 只取一个
logger = list.get(0);
} else {logger = null;}
}
public static LoggerService getService() {return SERVICE;}
public void info(String msg) {if (logger == null) {System.out.println("info 中没有发现 Logger 服务提供者");
} else {logger.info(msg);
}
}
public void debug(String msg) {if (loggerList.isEmpty()) {System.out.println("debug 中没有发现 Logger 服务提供者");
}
loggerList.forEach(log -> log.debug(msg));
}
}
新建 Main 类(服务使用者,调用方),启动程序查看后果。
package org.spi.service;
public class Main {public static void main(String[] args) {LoggerService service = LoggerService.getService();
service.info("Hello SPI");
service.debug("Hello SPI");
}
}
程序后果:
info 中没有发现 Logger 服务提供者
debug 中没有发现 Logger 服务提供者
将整个程序间接打包成 jar 包,能够间接通过 IDEA 将我的项目打包成一个 jar 包。
2. Service Provider
接下来新建一个我的项目用来实现 Logger 接口
新建我的项目 service-provider
目录构造如下:
├─.idea
├─lib
│ └─service-provider-interface.jar
└─src
├─META-INF
│ └─services
│ └─org.spi.service.Logger
└─org
└─spi
└─provider
└─Logback.java
新建 Logback 类
package org.spi.provider;
import org.spi.service.Logger;
public class Logback implements Logger {
@Override
public void info(String msg) {System.out.println("Logback info 的输入:" + msg);
}
@Override
public void debug(String msg) {System.out.println("Logback debug 的输入:" + msg);
}
}
将 service-provider-interface
的 jar 导入我的项目中。
新建 lib 目录,而后将 jar 包拷贝过去,再增加到我的项目中。
再点击 OK。
接下来就能够在我的项目中导入 jar 包外面的一些类和办法了,就像 JDK 工具类导包一样的。
实现 Logger 接口,在 src 目录下新建 META-INF/services
文件夹,而后新建文件 org.spi.service.Logger
(SPI 的全类名),文件外面的内容是:org.spi.provider.Logback
(Logback 的全类名,即 SPI 的实现类的包名 + 类名)。
这是 JDK SPI 机制 ServiceLoader 约定好的规范
接下来同样将 service-provider
我的项目打包成 jar 包,这个 jar 包就是服务提供方的实现。通常咱们导入 maven 的 pom 依赖就有点相似这种,只不过咱们当初没有将这个 jar 包公布到 maven 公共仓库中,所以在须要应用的中央只能手动的增加到我的项目中。
3. 成果展现
接下来再回到 service-provider-interface
我的项目。
导入 service-provider
jar 包,从新运行 Main 办法。
运行后果如下:
Logback info 的输入:Hello SPI
Logback debug 的输入:Hello SPI
阐明导入 jar 包中的实现类失效了。
通过应用 SPI 机制,能够看出 服务(LoggerService)和 服务提供者两者之间的耦合度非常低,如果须要替换一种实现(将 Logback 换成另外一种实现),只须要换一个 jar 包即可。这不就是 SLF4J 原理吗?
如果某一天需要变更了,此时须要将日志输入到音讯队列,或者做一些别的操作,这个时候齐全不须要更改 Logback 的实现,只须要新增一个 服务实现(service-provider)能够通过在本我的项目外面新增实现也能够从内部引入新的服务实现 jar 包。咱们能够在服务 (LoggerService) 中抉择一个具体的 服务实现
(service-provider) 来实现咱们须要的操作。
loggerList.forEach(log -> log.debug(msg));
或者
loggerList.get(1).debug(msg);
loggerList.get(2).debug(msg);
这里须要先了解一点:ServiceLoader 在加载具体的 服务实现 的时候会去扫描所有包下 src 目录的 META-INF/services
的内容,而后通过反射去生成对应的对象,保留在一个 list 列表外面,所以能够通过迭代或者遍历的形式失去你须要的那个 服务实现。
3. ServiceLoader
想要应用 Java 的 SPI 机制是须要依赖 ServiceLoader 来实现的,那么咱们接下来看看 ServiceLoader 具体是怎么做的:
ServiceLoader 是 JDK 提供的一个工具类,位于 package java.util;
包下。
A facility to load implementations of a service.
这是 JDK 官网给的正文:一种加载服务实现的工具。
再往下看,咱们发现这个类是一个 final 类型的,所以是不可被继承批改,同时它实现了 Iterable 接口。之所以实现了迭代器,是为了不便后续咱们可能通过迭代的形式失去对应的服务实现。
public final class ServiceLoader<S> implements Iterable<S>{xxx...}
能够看到一个相熟的常量定义:
private static final String PREFIX = "META-INF/services/";
上面是 load 办法:能够发现 load 办法反对两种重载后的入参;
public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader) {return new ServiceLoader<>(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();}
public void reload() {providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
依据代码的调用程序,在 reload() 办法中是通过一个外部类 LazyIterator 实现的。先持续往下面看。
ServiceLoader 实现了 Iterable 接口的办法后,具备了迭代的能力,在这个 iterator 办法被调用时,首先会在 ServiceLoader 的 Provider 缓存中进行查找,如果缓存中没有命中那么则在 LazyIterator 中进行查找。
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(); // 调用 LazyIterator}
public S next() {if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next(); // 调用 LazyIterator}
public void remove() {throw new UnsupportedOperationException();
}
};
}
在调用 LazyIterator 时,具体实现如下:
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 {
// 通过 PREFIX(META-INF/services/)和类名 获取对应的配置文件,失去具体的实现类
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;
}
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);
}
}
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}
4. 总结
其实不难发现,SPI 机制的具体实现实质上还是通过反射实现的。即:咱们依照规定将要裸露对外应用的具体实现类在 META-INF/services/
文件下申明。
其实 SPI 机制在很多框架中都有利用:Spring 框架的基本原理也是相似的反射。还有 dubbo 框架提供同样的 SPI 扩大机制。
通过 SPI 机制可能大大地进步接口设计的灵活性,然而 SPI 机制也存在一些毛病,比方:
- 遍历加载所有的实现类,这样效率还是绝对较低的;
- 当多个 ServiceLoader 同时 load 时,会有并发问题。