前言
该篇主要是为了后面讲解 dubbo SPI 机制做个铺垫。要想了解 dubbo SPI 机制,首先需要了解 java SPI 机制。
什么是 SPI ?
SPI 全称为 Service Provider Interface,是一种服务发现机制。其本质是将接口的实现类的全限定名(包名+类名)配置在文件中,并由服务加载器读取配置文件,加载实现类。这样就可以实现在运行时,动态为接口替换实现类。可以理解为一种“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。是 JDK 内置的一种机制。
为什么需要 SPI ?
在面向对象的编程里,一般都是基于接口编程,模块之前不对实现类进行硬编码。因为如果基于实现类进行编码,当需要替换实现类时,需要修改代码。违反了可插拔原则。其有点类似于IOC原理,其主要思想可以说是为了解藕。
使用场景
调用者可以根据实际需要,启动、扩展、替换框架实现的策略。
比较常见的例子:
- JDBC 加载不同类型数据库的驱动类。
- SLF4J 加载不同提供商的日志实现类。
- Spring
- Dubbo
如何使用
新建一个文件夹 META-INF/services
,放在classpath下面。以maven项目为例,放在resources目录下。
定义一个接口和二个实现类
HelloService
package com.joyxj.spi;
/**
* 定义一个接口,用于spi
* @author xiaoj
* @version 1.0
*
*/
public interface HelloService {
void sayHello();
}
二个实现类
HelloServiceImpl
package com.joyxj.spi;
/**
* Hello Service 实现类
*
* @author xiaoj
* @version 1.0
*/
public class HelloServiceImpl implements HelloService {
@Override
public void sayHello() {
System.out.println("hello");
}
}
HelloServiceSecondImpl
package com.joyxj.spi;
/**
* Hello Service 实现类
*
* @author xiaoj
* @version 1.0
*/
public class HelloServiceSecondImpl implements HelloService {
@Override
public void sayHello() {
System.out.println("你好");
}
}
在META-INF/services
目录下新建文件com.joyxj.spi.HelloService
,其名称为包名+类名。
其目录结构如下:
- resources
- META-INF
- services
- com.joyxj.spi.HelloService
其内容为接口实现类的全限定名。如果有多个,每行一个。
com.joyxj.spi.HelloServiceImpl
com.joyxj.spi.HelloServiceSecondImpl
测试
package com.joyxj.spi;
import java.util.Iterator;
import java.util.ServiceLoader;
/**
* JAVA SPI 测试
* @author xiaoj
* @version 1.0
*/
public class HelloServiceSpiTest {
public static void main(String[] args) {
ServiceLoader<HelloService> helloServicesLoaders = ServiceLoader.load(HelloService.class);
Iterator<HelloService> iterator = helloServicesLoaders.iterator();
while (iterator.hasNext()) {
iterator.next().sayHello();
}
}
}
输出:
hello
你好
源码分析
从上面示例中可以看出 JAVA SPI 的核心类是 ServiceLoader
,下面就开始逐步分析这个类的源码。以下分析基于JDK 1.8。
首先看下这个类的类签名和属性定义。
public final class ServiceLoader<S>
implements Iterable<S>
{
// 需要加载资源文件的目录。
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
// 用于定位、加载和实例化 providers 的类加载器
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
// 创建ServiceLoader时的访问控制上下文
private final AccessControlContext acc;
// Cached providers, in instantiation order
// 基于实例的顺序缓存类或接口的实现实例,其中Key为实现类的全限定名。
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
// 当前的懒查找迭代器
private LazyIterator lookupIterator;
// 省略其它
}
- 首先应用程序通过调用 ServiceLoader.load()方法创建一个实例。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();
lookupIterator = new LazyIterator(service, loader);
}
从上面代码可以看出,实例化操作是要做了一些初始化操作。其并没有去加载META-INF/services/
下的文件。
ServiceLoader<S> load(Class<S> service,ClassLoader loader)
是典型的静态工厂方法,通过 ServiceLoader
提供的私有构造方法创建实例。
- 回到ServiceLoader类的定义上面,发现其实现了Iterable接口。下面看其iterator()方法。
public Iterator<S> iterator() {
// 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();
}
};
}
iterator()是iterator接口的匿名实现,其核心主要就在判断先从缓存中读取,如果没有则从lookupIterator中获取。
- lookupIterator是一个懒加载迭代器,是一个
LazyIterator
对象。而LazyIterator
是ServiceLoader
的一个私有的内部类。该内部类也实现了Iterator接口。
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;
}
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 {
// 反射获得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());
// 加入缓存
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
// Iterable的方法,其主要是调用hasNextService
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);
}
}
// // Iterable的方法,其主要是调用nextService
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
主要是去加载资源文件,然后parse(service, configs.nextElement())
方法去解析资源文件。
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();
}
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;
}
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);
}
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
parse
会一次性读取完资源文件,但是其不会创建相应的实现类的实例对象。只有当调用上面的nextService()方法时才会创建相应的实例。从而实现了一个懒加载。
小结
JAVA SPI 被广泛用于第三方插件式类库的加载,最常见的就是JDBC等。理解JAVA SPI 有助于设计实现和编写可扩展良好的可插拔的第三方类库。