Java的类加载机制的核心是双亲委派模型,双亲委派模型(不存在自定义类加载器的情况下)加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。
Java里有如下几种类加载器
引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如
rt.jar、charsets.jar等
扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR
类包
应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那
些类或引用的非核心类库与
自定义加载器:负责加载用户自定义路径下的类包
SPI的全名是Service Provider Interface,SPI:
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。
先看看SPI的实现方式,以JDBC为例:
在mysql驱动包中,存在META-INF/services/java.sql.Driver文件,内容就是mysql驱动包里对java.sql.Driver接口的实现类全类名。
项目中引入mysql驱动包后,直接通过DriverManager就可以获得mysql的Connection对象。
根据使用方式,我们提出几个问题。
1.META-INF/services/java.sql.Driver这个文件是做什么用的?
mysql的驱动包被加载的过程是什么?
-
它是如何打破双亲委派机制的?
这几个问题需要通过jdk的源码实现来回答,首先看DriverManager的静态代码块,它会在DriverManager类被加载时执行:static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
再看loadInitialDrivers方法的实现:
private static void loadInitialDrivers() {
String drivers;
try {
// 先读取系统属性
drivers = AccessController.doPrivileged(new PrivilegedAction() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 通过SPI加载驱动类
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
ServiceLoaderloadedDrivers = ServiceLoader.load(Driver.class);
IteratordriversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
// 继续加载系统属性中的驱动类
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);
// 使用AppClassloader加载
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
其中与SPI核心相关的内容已经给出注释,其余内容是从system参数中加载驱动,因为我们没有设置系统参数,所以相关逻辑不会执行,关键看以下代码:
ServiceLoader.load(Driver.class);
方法实现如下(按照方法堆栈调用依次贴出实现):
public staticServiceLoaderload(Classservice) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}public static
ServiceLoaderload(Classservice,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class 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);
}
可以看到ServiceLoader使用了一个ClassLoader是Thread.currentThread().getContextClassLoader(),这个ClassLoader如果没有专门的设置,返回的是AppClassLoader对象,这就是SPI打破双亲委派机制的关键所在,因为java.sql.Driver接口是java核心包的类,所以根据双亲委派它应该由BootstrapClassLoader来加载,但是ServiceLoader在实现SPI机制的过程中,使用AppClassLoader来加载,所以它才能成功加载到mysql的驱动类。但是此处只是返回了ServiceLoader对象,不能说明加载mysql驱动真正使用的是AppClassLoader,我们继续看,剩余的步骤是拿到ServiceLoader的遍历器对象,然后进行了遍历,可以看到上面的reload方法中初始化了一个LazyIterator对象,我们来看LazyIterator类中的关键代码(hasNext方法会调用):
先简单回复图片中的一个疑点,loader对象为什么是URLClassLoader对象而不是AppClassLoader对象,这是因为通过IDEA运行java程序,断点功能等需要IDEA通过额外的技术实现,所以使用的classloader是经过处理的,我已经测试用system.printIn.out打印Thread.currentThread().getContextClassLoader(),在IDEA的运行结果是URLClassLoader对象,但是通过java命令运行的结果是AppClassLoader对象,有疑问的小伙伴可以自行测试一下。
通过断点调试可以看到fullName等于META-INF/services/java.sql.Driver,这里与第一个问题的目录结构相呼应,也就解释了为什么要用那样的目录接口与文件命名,fullName是由常量PREFIX和service.getName()的拼装的,进而说明,ServiceLoader类是SPI的通用实现类,它不仅仅可以加载java.sql.Driver,传入响应的接口,在指定目录下创建接口全限定类名文件,并写入第三方实现类,同样可以加载,它是java SPI机制的通用实现。
下面需要看最核心的加载逻辑(next()方法会调用):
熟悉的Class.forName,关键看参数,加载的类是com.mysql.jdbc.Driver,使用的ClassLoader是URLClassLoader对象(实际运行无特殊处理是使用AppClassLoader对象)。
以上分析与debug调试过程已经充分说明了JDBC对SPI的实际应用过程。额外补充对DriverManager.getConnection方法的源码分析。
private static Connection getConnection(
String url, java.util.Properties info, Class> caller) throws SQLException {
//.....省略非关键内容
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
//....省略非关键内容
}
可以看到关键逻辑是遍历registeredDrivers拿到driver对象,尝试通过连接信息获取连接,如果连接不为空,返回连接对象。说明DriverManager可能同时持有多种数据库驱动类,会使用连接信息逐一尝试,连接成功后会返回。现在的问题转化为registeredDrivers里驱动对象是在什么时候放入的,通过IDEA的方法反调不难找到如下代码:
可以看到
mysql的驱动类的静态代码块中调用了DriverManager#registerDriver方法,将自己注册到了registeredDrivers中。