Java SPI
1 什么是Java SPI
SPI(Service Provider Interface),是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。
Java SPI提供了这样的一个机制:为某个接口寻找服务实现的机制。
比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java SPI机制可以为某个接口寻找服务实现。
Java SPI 实际上是 基于接口的编程+策略模式+约定配置文件 组合实现的动态加载机制,在JDK中通过工具类 java.util.ServiceLoader来实现服务的查找。
2 实现SPI 需要遵循的标准
-
在
classpath下创建一个目录,该目录命名必须是:META-INF/service -
在该目录下创建一个文件,文件名必须是
扩展接口的全路径名称 -
文件内容是该扩展接口的
所有实现类 -
通过
java.util.ServiceLoader的加载机制来发现服务
3 SPI的实际应用
SPI扩展机制应用场景有很多,比如Common-Logging,JDBC,Dubbo等等。
在JDBC场景中,在Java中定义了接口java.sql.Driver,有MySQL驱动、Oracle驱动等,具体的实现都是由不同厂商提供的。
MySQL的实现
以MySQL为例,java.sql.Driver的实现类是com.mysql.jdbc.Driver。
在MySQL的jar包mysql-connector-java-{version}.jar中,可以发现:
- 有META-INF/services目录
- 目录下有一个名字为java.sql.Driver的文件
- 文件内容是com.mysql.cj.jdbc.Driver
服务发现
在使用JDBC获取连接时,会调用DriverManager.getConnection()获取连接对象,而在Driver类初始化时会使用ServiceLoader动态获取classpath下注册的驱动实现。
4 SPI Demo
定义一个接口
package com.artemis.spi;
public interface Pet {
void name();
}
定义不同的实现类
实现一:
public class Dog implements Pet {
@Override
public void name() {
System.out.println("dog");
}
}
实现二:
public class Cat implements Pet {
@Override
public void name() {
System.out.println("cat");
}
}
在META-INF/services/目录创建文件
测试及输出
public static void main(String[] args) {
ServiceLoader<Pet> pets = ServiceLoader.load(Pet.class);
for (Pet pet : pets) {
pet.name();
}
}
-----------------------------
dog
cat
Process finished with exit code 0
5 源码分析
5.1 调用ServiceLoader.load方法
ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括:
- loader(ClassLoader类型,类加载器)
- acc(AccessControlContext类型,访问控制器)
- providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类)
- lookupIterator(实现迭代器功能)
load方法
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader(Reflection.getCallerClass(), service, cl);
}
ServiceLoader的成员变量
public final class ServiceLoader<S> implements Iterable<S>
{
// 查找配置文件的目录
private static final String PREFIX = "META-INF/services/";
// 表示要被加载的服务的类或接口
private final Class<S> service;
// 这个ClassLoader用来定位,加载,实例化服务提供者
private final ClassLoader loader;
// 访问控制上下文
private final AccessControlContext acc;
// 缓存已经被实例化的服务提供者,按照实例化的顺序存储
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 迭代器
private LazyIterator lookupIterator;
}
5.2 通过迭代器接口获取对象实例
ServiceLoader实现了Iterable接口,可以遍历所有的服务实现者。
如果knownProviders中缓存有实现对象的实例,则返回,否则在LazyIterator lookupIterator中进行查找。
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
// hasNext方法
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext(); // lookupIterator中查找
}
// next方法
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
};
}
5.3 加载对象
- 读取
META-INF/services/下的配置文件,获得所有能被实例化的类的名称 - 通过反射方法
Class.forName()加载类对象,并用instance()方法将类实例化。 - 把实例化后的类缓存到
providers(LinkedHashMap类型)中, 然后返回实例对象。
LazyIterator类
- configs 存放服务实现类的资源配置抽象
- pending 存放服务实现类的全名字符串
- nextName 是下一个可获取的服务实现类的全名字符串
- 加载资源使用到 classLoader 的 getSystemResources 和 getResources 方法
Java里的资源抽象使用类 URL 来唯一标识,无论是本地文件 (file:///) 还是网络文件(http(s)://。
// 服务提供者查找的迭代器
private class LazyIterator implements Iterator<S> {
// 服务提供者接口
Class<S> service;
// 类加载器
ClassLoader loader;
// 保存实现类的url
Enumeration<URL> configs = null;
// 保存实现类的全名
Iterator<String> pending = null;
// 迭代器中下一个实现类的全名
String nextName = null;
public boolean hasNext() {
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;
}
public S next() {
if (!hasNext()) {
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, x);
}
throw new Error(); // This cannot happen
}
}
6 SPI 的不足
- 需要遍历所有的实现,并实例化,然后我们在循环中才能找到需要的实现
- 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们
- 扩展如果依赖其他的扩展,做不到自动注入和装配
- 不提供类似于Spring的IOC和AOP功能
- 扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的Java SPI不支持