1. 什么是SPI
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。
比如你有个接口,现在这个接口有多个实现类,在系统运行的时候,如何知道选择哪个实现类呢?通过 SPI 可以指定默认的配置,根据配置加载具体的某一个实现类,然后用这个实现类的实例对象。
SPI 机制一般用在插件扩展的场景,比如说你开发了一个给别人使用的开源框架,如果你想让别人自己写个插件,用到你的开源框架里面,从而扩展某个功能,这个时候 SPI 思想就用上了。这一机制为很多框架扩展提供了可能,比如在 Dubbo、JDBC 中都使用到了 SPI 机制。
2. Java 中 SPI 思想的体现
SPI 经典的思想体现,比如说 jdbc。在 Java 定义了一套 jdbc 的接口,但是 Java 并没有提供 jdbc 的实现类。但是实际上项目跑的时候,要使用 jdbc 接口的哪些实现类呢?一般来说,我们要根据自己使用的数据库,比如 mysql,你就将 mysql-jdbc-connector.jar 引入进来;oracle,你就将 oracle-jdbc-connector.jar 引入进来。所以为了方便管理,需要定制一个统一的接口,使调用者在调用数据库的时候可以方便的面向统一的接口进行编程。但是问题来了,真正使用的时候到底用哪个实现呢?从哪里找到实现类呢?这个时候就需要用到 SPI 机制。
最常见的,在我们在访问数据库时候用到的 java.sql.Driver 接口:
Java SPI 规定在 classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名。这样当我们引用了某个 jar 包的时候就可以去找这个 jar 包的 META-INF/services/ 目录,再根据接口名找到文件,然后读取文件里面的内容去进行实现类的加载与实例化。
3. Java SPI 示例
下面我们自己写一个小例子,来更加深刻的理解 SPI 机制。
首先写一个接口和两个对应的实现类:
public interface SPIService {
void say();
}
public class Nihao implements SPIService {
public void say() {
System.out.println("你好, 逗比");
}
}
public class Hello implements SPIService{
public void say() {
System.out.println("Hello");
}
}
复制代码
然后我在 META-INF/services/ 目录下建了个以接口全限定名命名的文件,内容是实现类的全限定类名,多个实现类之间用换行符分隔。具体内容如下:
com.demo.spi.Nihao
com.demo.spi.Hello
然后我们就可以通过 ServiceLoader.load 或者 Service.providers 方法拿到实现类的实例。写一个 mian 方法来测试下:
public class Main {
public static void main(String[] args) {
ServiceLoader<SPIService> serviceLoader = ServiceLoader.load(SPIService);
Iterator<SPIService> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
SPIService next = iterator.next();
next.say();
}
}
}
复制代码
运行结果:
你好
Hello
Process finished with exit code 0
4. Java SPI 源码分析
我们分析下源码,看具体做了什么,先看下 ServiceLoader 这个类的属性结构:
public final class ServiceLoader<s> implements Iterable<s>
//配置文件的路径
private static final String PREFIX = "META-INF/services/";
//加载的服务类或接口
private final Class<s> service;
//已加载的服务类缓存集合(按实例顺序)
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
//类加载器
private final ClassLoader loader;
//内部类,真正加载服务类
private LazyIterator lookupIterator;
}
复制代码
从 ServiceLoader.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();
// 实例化内部类,得到一个 LazyIterator
lookupIterator = new LazyIterator(service, loader);
}
复制代码
可以看出,上边整个过程是先找当前线程绑定的 ClassLoader,如果没有就用 SystemClassLoader,然后清除一下缓存,重要的是实例化了内部类:LazyIterator。最后返回 ServiceLoader 的实例。而 LazyIterator 其实就是 Iterator 的实现类。先看下它的具体方法:
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();
}
}
复制代码
可以看出,hasNext() 其实是调用的 hasNextService();next() 其实是调用 nextService()。先看下 hasNextService():
private boolean hasNextService() {
//第二次调用的时候,已经解析完成了,直接返回
if (nextName != null) {
return true;
}
if (configs == null) {
//META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
String fullName = PREFIX + service.getName();
//将文件路径转成URL对象
configs = loader.getResources(fullName);
}
while ((pending == null) || !pending.hasNext()) {
//解析URL文件对象,按行遍历文件内容,最后返回
pending = parse(service, configs.nextElement());
}
//拿到第一个实现类的类名
nextName = pending.next();
return true;
}
复制代码
这个方法其实就是根据约定好的路径找到接口对应的文件,并对文件内容进行加载和解析。
再看下 next() 方法:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
// 全限定类名
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 创建类的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 {
// 通过newInstance实例化
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
}
复制代码
这个方法其实就是通过全限定类名加载类,然后创建实例,把实例放入缓存集合最后返回示例。
以上就是 Java SPI 源码的实现过程,思路其实很简单:
1)首先约定好一个目录
2)根据接口名去那个目录找到文件
3)文件解析得到实现类的全限定名
4)然后循环加载实现类和创建其实例
5. Java SPI 总结
JDK 内置的 SPI 机制本身有它的优点,但由于实现比较简单,也有不少缺点。
优点
使用 Java SPI 机制的优势是实现解耦,使得接口的定义与具体业务实现分离,而不是耦合在一起。应用进程可以根据实际业务情况启用或替换具体组件。
缺点
1)不能按需加载。虽然 ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
2)获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
3)多个并发多线程使用 ServiceLoader 类的实例是不安全的。
鉴于 SPI 的诸多缺点,很多系统都是自己实现了一套类加载机制,例如 dubbo。Dubbo SPI 除了可以按需加载实现类之外,增加了 IOC 和 AOP 的特性,还有个自适应扩展机制。这部分内容以后再和大家继续分享。