Java SPI 到底是什么
一、Java 扩展机制
在介绍 SPI 机制之前,首先要了解 Java 的扩展机制(The extension mechanism)。“扩展机制” 指的是一种标准(或规范),通过遵循这种标准,用户可以自定义接口,达到丰富功能的目的。“扩展”的表现形式,就是一组 Java 包或者 Java 类。“扩展” 就像热拔插设备一样,Java 可以在运行时加载,而不需要提前在代码中声明扩展类的全类名或者路径。
二、SPI 介绍
SPI 就是 Java 扩展机制的具体实现方式。SPI 全称为 Service Provider Interface,其中
- Service: 指提供某种功能的类或接口。Service 可以定义接口,然后获取对应的实现类。Service 本身并不会实现具体的功能,而是调用具体的实现,这些具体的实现就是 Service Provider。以词典功能为例,词典服务的功能就是,用户输入一个单词,服务输出查询结果,但是底层的中文、英文词库,并不是 Service 的范畴,
- Service provider interface(SPI):指 Service 定义的接口或抽象类。
- Service provider: SPI 的具体实现,也是为 Service 的底层依赖。Provider 通常有扩展的需求,比如在词典服务中新增德文、法文词库。
三、使用场景
Java SPI机制是一种服务提供者发现的机制,适用于需要在多个实现中选择一个进行使用的场景。
- 加载数据库驱动程序。JDBC为了实现可插拔的数据库驱动,在Java.sql.Driver接口中定义了一组标准的API规范,而具体的数据库厂商则需要实现这个接口,以提供自己的数据库驱动程序。在Java中,JDBC驱动程序的加载就是通过SPI机制实现的。
- Spring 框架。Spring框架中的Bean加载机制就使用了SPI思想,通过读取classpath下的META-INF/spring.factories文件来加载各种自定义的Bean。
- Dubbo 框架。Dubbo框架也使用了SPI思想,通过接口注解@SPI声明扩展点接口,并在classpath下的META-INF/dubbo目录中提供实现类的配置文件,来实现扩展点的动态加载。
四、应用实战
本小节介绍如何实现一个“词典服务”,通过示例来体会 SPI 的效果。代码已上传 github: https://github.com/neuqDarcy/java-spi-demo.
(例子来自于 Oracle 官网,用到了 ant, 有一些学习成本,笔者用 maven 重新实现了功能。)
简单介绍下功能背景,“词典服务”的功能是翻译词汇,针对不同的场景,比如通用词典、专业词典,我们可以分别实现对应的词库,而这些词库是可拔插的、容易扩展的,稍后可以看到效果。
项目结构
├── DictionaryDemo 客户端,可以调用和验证功能。
│ ├── pom.xml
│ ├── src
│ │ └── main
│ │ └── java
│ │ ├── DictionaryDemo.java
│ │ └── dictionary
│ └── target
│ ├── classes
│ │ └── DictionaryDemo.class
│ └── generated-sources
│ └── annotations
├── DictionaryServiceProvider 实际上对应于上文提到的 Service
│ ├── pom.xml
│ ├── src
│ │ └── main
│ │ └── java
│ │ ├── DictionaryService.java 对应于上文提到的 Service
│ │ └── dictionary
│ │ └── api
│ │ └── Dictionary.java 对应于上文提到的 Service Provider Interface(SPI)
│ └── target
│ ├── classes
│ │ ├── DictionaryService.class
│ │ └── dictionary
│ │ └── api
│ │ └── Dictionary.class
│ └── generated-sources
│ └── annotations
├── ExtendedDictionary 扩展词库1
│ ├── pom.xml
│ ├── src
│ │ └── main
│ │ ├── java
│ │ │ └── dictionary
│ │ │ └── ExtendedDictionary.java 扩展类,对应于 Service Provider
│ │ └── resources
│ │ └── META-INF
│ │ └── services
│ │ └── dictionary.api.Dictionary 配置文件
│ └── target
│ ├── classes
│ │ ├── META-INF
│ │ │ └── services
│ │ │ └── dictionary.api.Dictionary
│ │ └── dictionary
│ │ └── ExtendedDictionary.class
│ └── generated-sources
│ └── annotations
├── GeneralDictionary 扩展词库2
│ ├── pom.xml
│ ├── src
│ │ └── main
│ │ ├── java
│ │ │ └── dictionary
│ │ │ └── GeneralDictionary.java 扩展类,对应于 Service Provider
│ │ └── resources
│ │ └── META-INF
│ │ └── services
│ │ └── dictionary.api.Dictionary
│ └── target
│ ├── classes
│ │ ├── META-INF
│ │ │ └── services
│ │ │ └── dictionary.api.Dictionary
│ │ └── dictionary
│ │ └── GeneralDictionary.class
│ └── generated-sources
│ └── annotations
└── pom.xml
步骤详解
1. 定义 SPI
package dictionary.spi;
public interface Dictionary {
public String getDefinition(String word);
}
2. 定义服务类
package dictionary;
import dictionary.spi.Dictionary;
import java.util.SortedMap;
import java.util.TreeMap;
public class GeneralDictionary implements Dictionary {
private SortedMap<String, String> map;
public GeneralDictionary() {
map = new TreeMap<String, String>();
map.put(
"book",
"a set of written or printed pages, usually bound with " +
"a protective cover");
map.put(
"editor",
"a person who edits");
}
@Override
public String getDefinition(String word) {
return map.get(word);
}
}
3. 实现 Srvice Provider
package dictionary;
import dictionary.spi.Dictionary;
import java.util.SortedMap;
import java.util.TreeMap;
public class ExtendedDictionary implements Dictionary {
private SortedMap<String, String> map;
public ExtendedDictionary() {
map = new TreeMap<String, String>();
map.put(
"xml",
"a document standard often used in web services, among other " +
"things");
map.put(
"REST",
"an architecture style for creating, reading, updating, " +
"and deleting data that attempts to use the common " +
"vocabulary of the HTTP protocol; Representational State " +
"Transfer");
}
@Override
public String getDefinition(String word) {
return map.get(word);
}
}
4. 注册 Service Provider
在 META-INF/services 路径下(必须是这个路径),
- 创建配置文件,文件名为 SPI 接口的全类名(必须)。本例中是 dictionary.api.Dictionary
- 配置文件内容为,Service Provider 的全类名(必须)。本例中是 dictionary.ExtendedDictionary
5. 实现客户端
package dictionary;
import dictionary.DictionaryService;
public class DictionaryDemo {
public static void main(String[] args) {
DictionaryService dictionary = DictionaryService.getInstance();
System.out.println(DictionaryDemo.lookup(dictionary, "book"));
System.out.println(DictionaryDemo.lookup(dictionary, "editor"));
System.out.println(DictionaryDemo.lookup(dictionary, "xml"));
System.out.println(DictionaryDemo.lookup(dictionary, "REST"));
}
public static String lookup(DictionaryService dictionary, String word) {
String outputString = word + ": ";
String definition = dictionary.getDefinition(word);
if (definition == null) {
return outputString + "Cannot find definition for this word.";
} else {
return outputString + definition;
}
}
}
六、验证功能
- 可以通过 maven 来控制是否加载扩展类。这里我们做对照实验, DictionaryDemo 引用 ExtendedDictionary 依赖,不引用 GeneralDictionary 依赖。
- 执行 DictionaryDemo.java 方法即可。
- 执行结果:
book:Cannot find definition for this word!!
editor:Cannot find definition for this word!!
xml:可延伸标记语言
REST:一种接口风格
从可以看到,ExtendedDictionary 正常提供服务,GeneralDictionary 没有提供服务。这是符合预期。
如果需要挂载 GeneralDictionary 也很简单,直接在 POM 文件中引用 GeneralDictionary 即可,不需要改动代码逻辑。
五、源码解析
Java 版本是 1.8
核心类
- java.util.ServiceLoader
- 内部类 LazyIterator。关注点比较多,先把整体代码贴上来。
// Private inner class implementing fully-lazy provider lookup
//
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);
}
}
关注点
- 还记得配置文件一定要放在 "META-INF/services/" 文件夹下吗,答案在这里。此处是“约定大于配置” 思想的体现。
private static final String PREFIX = "META-INF/services/";
- 还记得 SPI 常见的使用方式吗?是通过迭代器,获取到具体实现的。SPI 在内部实现了迭代器,其中有两点可以关注。一,在迭代器 next() 方法时才生成实现的对象,这种机制被称为“懒加载”。二,SPI 是通过反射的方式,根据“配置路径 + 配置内容”,生成实现类。
2.1 SPI 的常见使用方式
2.2 判断配置文件中的 Service Provider 是否存在public String getDefinition(String word) { String definition = null; Iterator<Dictionary> dictionaries = loader.iterator(); while (definition == null && dictionaries.hasNext()) { Dictionary d = dictionaries.next(); definition = d.getDefinition(word); } return definition; }
2.3 通过反射,生成实现类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; }
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 }