Java SPI 机制
Java SPI 机制
什么是SPI机制
在Java生态系统中,SPI(Service Provider Interface)是一种服务发现机制,它允许第三方为一个接口或抽象类提供具体的实现。这种机制广泛应用于框架和库的设计中,为开发者提供了极大的灵活性和扩展性。
SPI机制是Java平台的一个特性,用于将接口实现类的全限定名配置在文件中,并由JDK提供的API去加载这些实现类。这是一种解耦合的方式,通过定义服务提供者接口,然后让第三方来实现这个接口,最终达到服务动态查找的目的。
SPI机制主要原理
SPI的核心在于 java.util.ServiceLoader
类,它是用来加载服务提供者的工厂类。当程序需要某个接口的具体实现时,可以通过 ServiceLoader
加载配置文件中指定的实现类。配置文件通常位于 META-INF/services/
目录下,文件名即为接口的全限定名,文件内容则是实现类的全限定名。
以获取数据库驱动为例,当初次调用 DriverManager.getConnection(url);
时会初始化 DriverManager
类,在其中存在如下静态代码块:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
其中, loadInitialDrivers()
方法中存在如下代码:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// ......
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
其实现核心为第一行代码,这里会通过SPI机制加载服务提供者配置的实现类,并创建为 Driver.class
的实现类。内部代码如下:
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;
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
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");
// cl为当前线程获取到的类加载器,此时不为null,因此loader即为当前线程的类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// 默认情况下 System.getSecurityManager() 返回null,此时 acc为null
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
至此,第一行代码执行完成。随后,调用前一行代码返回的 loadedDrivers
获取迭代器。其中 iterator()
方法实现如下:
public Iterator<S> iterator() {
return new Iterator<S>() {
// 这里为knownProviders赋值,初始状态下为空
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
// 初次调用时,knownProviders为空,此时返回lookupIterator.hasNext()
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
// 初次调用时,knownProviders为空,此时返回lookupIterator.next()
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
随后,系统调用 driversIterator.hasNext()
和 driversIterator.next()
两个方法。如上代码注释,初次调用时,knownProviders为空,此时返回 lookupIterator.hasNext()
。
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() {
// 初次调用时,nextName为null
if (nextName != null) {
return true;
}
// 初次调用时,configs为null
if (configs == null) {
try {
// PREFIX 为 ServiceLoader中定义的前缀,值为 META-INF/services/
// PREFIX + service.getName() 为 META-INF/services/ + 接口名
// 在加载数据库驱动时,这里读取的是 META-INF/services/java.sql.Driver
String fullName = PREFIX + service.getName();
// loader 为前面传进来的当前线程的类加载器,不为null
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 根据提供的信息获取资源文件
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 初次执行时,pending为null
while ((pending == null) || !pending.hasNext()) {
// 初次执行时不会进入if代码块
if (!configs.hasMoreElements()) {
return false;
}
// 调用ServiceLoader中的parse方法
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
// 初次调用时会执行这里的代码
public boolean hasNext() {
// 分析上文可知,此处进入if。方法调用 hasNextService()
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
}
- ServiceLoader#parse()
// ServlceLoader中的parse方法
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
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();
}
// Parse a single line from the given configuration file, adding the name
// on the line to the names list.
//
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
// 按行读取文件内容
// 在mysql驱动中为 com.mysql.cj.jdbc.Driver
String ln = r.readLine();
// 执行一系列校验和处理
if (ln == null) {
return -1;
}
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
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);
}
// 如果该类没有被加载过,则加入到names中
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
至此,前面 driversIterator.hasNext()
相关代码执行完毕。随后调用 driversIterator.next();
代码实现如下:
private S nextService() {
// nextName为前面获取的数据,不为null
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 中
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
至此,整个通过SPI机制获取实现类的过程结束。
SPI机制在spring中的应用
在springboot环境下,创建一个starter时需要在 src/main/resources/META-INF/
目录下添加 spring.factories
文件,在这个文件中可以配置当前starter的启动配置类。以 druid-spring-boot-starter
为例,其配置文件如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
这里的实现类似于SPI机制,在文件中定义了druid需要注入的配置类,区别在于这里上层为注解,springboot实现了自己的扫描机制,且配置文件统一命名为 spring.factories