注:代码环境基于 JDK 1.8
一、SPI 是什么?
SPI(Service Provider Interface):是一个可以被第三方扩展或实现的 API,它可以用来实现框架扩展和可替换的模块,优势是实现解耦。简单来说就是推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。若在代码里涉及具体的实现类就违反了可挺拔的原则。从而 java SPI 提供了这种服务发现机制:为某个接口寻找服务实现的机制。
二、SPI 与 API 的区别
- API 直接为提供了功能,使用 API 就能完成任务。
- API 和 SPI 都是相对的概念,差别只在语义上,API 直接被应用开发人员使用,SPI 被框架扩张人员使用。
- API 大多数情况下,都是实现方来制定接口并完成对接口的不同实现,调用方仅仅依赖却无权选择不同实现。SPI 是调用方来制定接口,实现方来针对接口来实现不同的实现。调用方来选择自己需要的实现方。
三、SPI 使用及示例
- 服务调用方通过 ServiceLoader.load 加载服务接口的实现类实例
- 服务提供方实现服务接口后, 在自己Jar包的 META-INF/services 目录下新建一个接口名全名的文件, 并将具体实现类全名写入。
示例:
1. 创建接口:
public interface Search {
List<String> searchDoc(String keyword);
}
2. 创建 DatabaseSearch 实现类:
public class DatabaseSearch implements Search {
@Override
public List<String> searchDoc(String keyword) {
System.out.printf("数据库搜索:" + keyword);
return null;
}
}
3. 创建 FileSearch 实现类:
public class FileSearch implements Search {
@Override
public List<String> searchDoc(String keyword) {
System.out.println("文件搜索:" + keyword);
return null;
}
}
4. META-INF.services 中创建接口全限定名文件:spi.learn.Search:
spi.learn.FileSearch
spi.learn.DatabaseSearch
5. 测试类:
public static void main(String[] args) {
ServiceLoader<Search> s = ServiceLoader.load(Search.class);
Iterator<Search> iterator = s.iterator();
while (iterator.hasNext()) {
Search search = iterator.next();
search.searchDoc("spi");
}
}
-----输出:-----
文件搜索:spi
数据库搜索:spi
四、源码解读
先来看下 ServiceLoader 类的全局变量:
//spi 默认加载的路径
private static final String PREFIX = "META-INF/services/";
// 表示正在被加载的类或接口
// The class or interface representing the service being loaded
private final Class<S> service;
// 用于定位、装入和实例化提供程序的类加载器
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// 权限控制上下文
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// 基于实例的顺序缓存类的实现实例,其中Key为实现类的全限定类名
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 当前的"懒查找"迭代器,ServiceLoader的核心
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
ServiceLoader.load(Search.class) 加载入口:
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) {
// 因为 ServiceLoader 的构造为私有,这里只能依赖此静态方法来访问私有构造实例化,典型的静态工厂方法。
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// svc 为 null,抛 NullPointerException
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 若没有指定加载器,默认使用系统加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// Java安全管理器
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
// 清空实例化好的缓存。
providers.clear();
// "懒查找",ServiceLoader 的核心。
lookupIterator = new LazyIterator(service, loader);
}
LazyIterator 为 ServiceLoader 的核心,来看看具体源码:
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
// 加载资源的URL集合
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;
}
// 资源为null,尝试加载
if (configs == null) {
try {
// 资源名称,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;
}
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 {
// 实例并强转
S p = service.cast(c.newInstance());
// 实例完成,添加缓存,Key:实现类全限定类名,Value:实现类实例
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();
}
}
LazyIterator 机制总结:LazyIterator 也实现了 Iterator接口的实现,Lazy特性体现在只有在使用 ServiceLoader 调用 iterator() 方法获取 Iterator 接口匿名实现类后, 再调用 hasNext() 方法时,才会"懒判断"或者"懒加载"下一个实现类的实例。调用的入口,也就是示例 main 方法中 while 那一步,我们再看下 iterator() 的 Iterator 匿名实现类源码:
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();
}
public S next() {
if (knownProviders.hasNext()) return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
调用链分析完,最后看下 hasNextService 中解析限定名文件的 parse 方法,主要检查文件内容的字符合法性、缓存过滤避免重复加载。
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
InputStream in = null;
BufferedReader r = null;
// 存放 META-INF/services 下文件中的实现类的全类名
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
// 解析完返回迭代器
return names.iterator();
}
//具体解析资源文件中每一行内容
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names)
throws IOException, ServiceConfigurationError {
String ln = r.readLine();
if (ln == null) {
return -1; //-1表示解析完成
}
// 如果存在'#'字符,截取第一个'#'字符串之前的内容,'#'字符之后的属于注释内容
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
//不合法的标识:' '、'\t'
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
//判断第一个 char 是否一个合法的 Java 起始标识符
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
//判断所有其他字符串是否属于合法的Java标识符
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
//不存在则缓存
if (!providers.containsKey(ln) && !names.contains(ln)) names.add(ln);
}
return lc + 1;
}
五、总结
优点:
- 使用 Java SPI 机制的优势是实现解耦,使第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
- 相比使用提供接口 jar 包供第三方服务使用的方式,SPI 使得源框架不必关心接口的实现类的路径,可以不使用硬编码 import 导入实现类。
缺点:
- 虽然 ServiceLoader 使用了懒加载,但结果还是通过遍历获取,基本上可以说是全部实例化了一遍,所以说,这个懒加载机制在此场景下是浪费的。
- 由于是遍历获取,所以获取实现类的方式不够灵活。
- 多个并发多线程使用 ServiceLoader 类的实例是不安全的。