开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
在 《JDBC 详解(一):JDBC 基本介绍与使用》中的 DriverManager 介绍中,我们已经知道 DriverManager 支持Java标准版服务提供程序机制(SPI),并且维护了一个 registeredDrivers 列表,在获取数据库连接时会根据 JDBC URL 遍历这个列表找到合适的 Driver。
这篇文章我们来聊一下 registeredDrivers 列表在初始化时是如何寻找到具体的 Driver 的,其背后使用的 SPI 机制是什么,以及 SPI 与类加载器双亲委派机制的矛盾与解决。
SPI 机制
SPI(Service Provider Interface) ,是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件。Java 中 SPI 机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。
我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。
Java SPI 定义步骤
Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
Java SPI实现流程:
- 定义SPI 接口标准
- 提供方提供具体实现:实现方法配置在META-INF/services/${interface_name} 文件
- 开发者使用,JDK中查找服务的实现的工具类是:java.util.ServiceLoader
ServiceLoader 源码
// ServiceLoader实现了Iterable接口,可以遍历所有的服务实现者
public final class ServiceLoader<S> implements Iterable<S>
{
// 查找配置文件的目录
private static final String PREFIX = "META-INF/services/";
// 表示要被加载的服务的类或接口
private final Class<S> service;
// 这个ClassLoader用来定位,加载,实例化服务提供者
private final ClassLoader loader;
// 访问控制上下文
private final AccessControlContext acc;
// 缓存已经被实例化的服务提供者,按照实例化的顺序存储
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 迭代器
private LazyIterator lookupIterator;
}
// 服务提供者查找的迭代器
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
// hasNext方法
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
// next方法
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
};
}
// 服务提供者查找的迭代器
private class LazyIterator implements Iterator<S> {
// 服务提供者接口
Class<S> service;
// 类加载器
ClassLoader loader;
// 保存实现类的url
Enumeration<URL> configs = null;
// 保存实现类的全名
Iterator<String> pending = null;
// 迭代器中下一个实现类的全名
String nextName = null;
public boolean hasNext() {
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 (!hasNext()) {
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, x);
}
throw new Error(); // This cannot happen
}
}
- ServiceLoader实现了Iterable接口,并且内部提供了懒加载迭代器lookupIterator
- LazyIterator中的hasNext方法,静态变量PREFIX就是”META-INF/services/”目录
- 通过反射方法Class.forName()加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象
SPI 接口与 API 接口的区别
二者区别:来自stackoverflow.com/questions/2…
- The API is the description of classes/interfaces/methods/... that you call and use to achieve a goal, and
- the SPI is the description of classes/interfaces/methods/... that you extend and implement to achieve a goal.
Put differently, the API tells you what a specific class/method does for you, and the SPI tells you what you must do to conform.
Driver 加载
DriverManager 中的 Driver 加载发生在DriverManager类加载的时候,通过静态代码块执行 loadInitialDrivers()方法,会经过以下步骤:
- 系统属性中尝试获取驱动类的路径
- 通过SPI机制的工具类ServiceLoader去加载驱动类,并在 ServiceLoader.iterator().next()中完成类加载与实例化。(Driver 类在实例化时会向DriverManager 注册自己。)
- 若第一步加载类路径不为空,则再去加载系统属性中的 Driver 类
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
// 1. 系统属性中尝试获取驱动类的路径
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
// 2. 通过SPI机制的工具类ServiceLoader去加载驱动类
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 3. 加载Driver接口的服务类,META-INF/services/java.sql.Driver
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.
*/
try{
while(driversIterator.hasNext()) {
// next 方法中创建对象
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
SPI 与 ClassLoader
双亲委派机制
类加载器的基础知识点:
- 每个ClassLoader都只能加载自己所绑定目录下的资源;
- 一个类由类加载器A加载,那么这个类依赖类也应该由相同的类加载器加载
对于 JDBC 来说,Driver.class 在 rt.jar 中应该被 Bootstrap ClassLoader 加载,但是其 SPI 机制的实现类却是由 System ClassLoader 加载,即双亲委派机制无法解决此问题
ContextClassLoader
Bootstrap ClassLoader、ExtClassLoader、AppClassLoader,它们是真实存在的类,而且遵从”双亲委托“的机制。
而在 Java 中存在另外一种「虚拟」的 ContextClassLoader。contextClassLoader 只是一个成员变量,可以被设置和修改。
public class Thread implements Runnable {
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
// 默认设置为当前线程 cl 即 AppClassLoader
contextClassLoader = cl;
}
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(contextClassLoader,
Reflection.getCallerClass());
}
return contextClassLoader;
}
}
SPI 使用 ContextClassLoader
在 ServiceLoader 中,load 方法中获取 ContextClassLoader 并传入迭代器中。所以,上文中 nextService() 方法中加载 Driver.class 及其实现类的加载器是被设置的线程上下文类加载器,以此打破了双亲委派规则。
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl); // 传入加载方法
}
// 在初始化 lookupIterator 时,传入线程上下文类加载器
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}