SPI
SPI(Service Provider Interface),,是Java提供的一套用来被第三方实现或者扩展的接口,定义一个服务接口,服务提供者来提供这个服务接口的实现,服务使用者通过SPI来发现这些服务提供者提供的实现然后使用这些服务,从而将服务的实现和服务的使用解耦,使得服务的实现可插拔
最常见的就是Jdbc中的驱动类,不同的数据库产品需要使用不同的驱动,jdbc定义Driver接口,不同的数据库产品提供该接口的使用,驱动使用者通过SPI来发现可用的驱动实现类,从而使用这些驱动,来和数据库进行交互
java提供了ServiceLoader来提供SPI的服务发现功能,在jdbc、mybatis等都有使用
服务提供
这里以jdbc的Driver为例,mysql-connector-java-8.0.23.jar这个jar包的META-INF/servcies下面有一个以接口java.sql.Driver命名的文件
文件内容如下
这样服务使用者通过SPI机制就能够发现这个Driver接口的实现类,从而使用这个实现类
总结一下使用流程:
- 在META-INF/services目录下创建一个文件,文件的名称为接口的全路径名
- 在上面创建的文件中,填入当前jar包提供的该接口的实现类的全路径名
服务发现
jdk通过ServiceLoader来实现服务的发现,下面看下如何使用ServiceLoader来发现服务
使用
这里以jdbc驱动管理类DriverManager为例,看下是如何使用ServiceLoader的
// 获取Driver类的ServiceLoadewr
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
// 遍历每个Driver的实现类,这里会对这些实现类进行类的加载
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
源码分析
属性
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;
构造函数
主要就是简单的属性赋值,然后调用了reload
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();
}
reload
public void reload() {
// 清空发现的provider缓存
providers.clear();
// 创建一个实现类的迭代器
lookupIterator = new LazyIterator(service, loader);
}
iterator
ServiceLoader通过返回一个迭代器,并且使用这个迭代器来进行遍历,从而完成服务的发现和服务实现类的加载
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();
}
};
}
刚开始看到这里的时候,产生了疑惑,考虑如下情况:
第一次调用,缓存中不存在服务提供者,然后会从配置文件中查找服务提供者,并且会将这个服务提供者添加到缓存中
第二次调用,此时缓存中不为空,如果从缓存的迭代器中返回之前刚添加的服务提供者,那么不是会抛ConcurrentModificationException吗,但是实际运行的时候并不会抛异常
原因在于如果一开始缓存中不存在服务提供者,那么缓存的迭代器的hasNext会一直返回false,即使后续向缓存中添加了服务提供者,也会一直返回false,因此一直不会调用缓存迭代器的next方法,因此不会抛出异常
因此上面的代码不会出现这次从缓存中获取服务提供者,下次从配置文件中获取服务提供者,会一直从一个地方来获取服务提供者,要么从缓存中要么从配置文件中
下面主要看LazyIterator的方法
LazyIterator.hasNext
主要是会调用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);
}
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
// configs用来保存所有spi服务提供者文件的url
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);
}
}
// 如果pending为空或者pending没有内容了,会尝试从下一个服务提供者文件中来加载服务提供者
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 对下一个服务提供者文件进行解析,并且将文件中的服务提供者作为迭代器的内容
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
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;
}
// 下面对文件内容进行一些处理,然后将处理后的内容添加到names中
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;
}
LazyIterator.next
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
}
总结
总结下服务发现这里的执行流程:
- 找到所有/META-INF/resources目录下所有以接口全路径名称命名的文件
- 懒加载式遍历这些文件,每次读取一个文件,将该文件中每行内容作为一个实现类的全路径,将这些内容填充到迭代器中;当通迭代器的next方法遍历完当前文件的所有内容之后,读取下一个文件
- 在遍历所有实现类的时候,会加载该实现类,然后实例化,将实例放到map中,map的key为全路径名称,value为实例