原生的JDBC中Driver驱动本身只是一个接口,是Java给所有数据库操作提供了的Driver接口,至于具体的实现,是由不同数据库类型去实现的。例如,MySQL的mysql-connector-java-5.1.47.jar中
Driver接口具体实现:
package java.sql;
import java.util.logging.Logger;
public interface Driver {
Connection connect(String url, java.util.Properties info) throws SQLException;
boolean acceptsURL(String url) throws SQLException;
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
throws SQLException;
int getMajorVersion();
int getMinorVersion();
boolean jdbcCompliant();
public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}
然后Java有一个DriverManager类来管理所有继承了Driver接口的实现类,字段registeredDrivers
package java.sql;
public class DriverManager {
// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
// 验证当前获取连接的类的类加载器和当前驱动的类加载器是不是同一个
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来获取连接,Driver是需要先向DriverManager中进行注册的。
以前我们使用JDBC的方法(未破坏双亲委派机制)
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "1234");
我们手动的使用Class.forName()进行驱动类的加载,类加载器是使用的调用当前方法所用的类加载器。
我们看下mysql对Driver接口的实现
package com.mysql.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {}
static {
try {
// 将drive写入 DriverManager::registeredDrivers
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
Class.forName()加载会使类被初始化(cinit执行static代码块),所以Mysql就将自己的驱动注册到了DriverManager中了,还有一点需要注意,Class.forName()方法加载一个类,使用的类加载器是其调用者的类加载器。
现在我们使用JDBC的方式(对双亲委派机制产生了破坏)
在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就不需要我们手动的去加载驱动了,我们只需要直接获取连接就可以了。
具体的spi的实现过程可以去查询其他文章,我只做简单分析
1.扫描所有的jar包,去找META-INF/services/java.sql.Driver文件中获取具体的实现类名
2.利用Class.forName("")去加载该类
那么问题就来了,Class.forName()加载用的是调用者的Classloader,这个调用者DriverManager是在rt.jar中的,是由启动类加载器进行加载的,而com.mysql.jdbc.Driver肯定不在<JAVA_HOME>/jre/lib下(可以通过System.getProperty("sun.boot.class.path")获取启动类加载器加载的路径),所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。
这时候该怎么办?因为存在这样的问题,所以这个时候就引入线程上下文件类加载器(Thread Context ClassLoader)。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。下面看看JDBC中是怎么去应用的呢
DriverManager新增了静态代码块,也就是说在DriveManager类被初始化时就会调用该方法
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
//通过SPI加载驱动类 AccessController.doPrivileged(new PrivilegedAction<Void>() {
//下面的这个run方法通过线程上下文加载器就会自动加载Driver的实现类
public Void run() {
//我们在下面分析一下ServiceLoader源代码
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
/**
driversIterator.next()最终就是调用 java.util.ServiceLoader类中的Class.forName(DriverName, false, loader)方法。传给 forName 的 loader不能是BootrapLoader,应当是线程上下文加载器
*/
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);
}
}
}
然后我们看下ServiceLoader.load()的具体实现:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器去加载驱动类,这里就破坏了双亲委派机制
// ServiceLoader也是由启动类进行加载的
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
可以看到核心就是拿到线程上下文类加载器,然后构造了一个ServiceLoader,后续的具体查找过程,我们不再深入分析,这里只要知道这个ServiceLoader已经拿到了线程上下文类加载器即可。
接下来,ServiceLoader的loadInitialDrivers()方法中有一句driversIterator.next(),它的具体实现如下:
private class LazyIterator implements Iterator<S>
{
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = "META-INF/services/" + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else /**
ClassLoader中的getResources()方法是从所有jar包中查找符合条件的。
只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名
*/
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.forName(cn, false, loader);进行加载
如在mysql-connector-java-5.1.25.jar包下的META-INF下有一个配置文件
java.sql.Driver中配置由com.mysql.jdbc.Driver
*/ 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 }
}
现在,我们成功的做到了通过线程上下文类加载器拿到了应用程序类加载器(或者自定义的然后塞到线程上下文中的),同时我们也查找到了厂商在自己的jar包中声明的驱动具体实现类名,这样我们就可以成功的在rt.jar包中的DriverManager中成功的加载了放在第三方应用程序包中的类了。
总结
这个时候我们再看下整个mysql的驱动加载过程:
获取线程上下文类加载器,从而也就获得了应用程序类加载器(也可能是自定义的类加载器)
从META-INF/services/java.sql.Driver文件中获取java.sql.Driver接口的实现类名“com.mysql.jdbc.Driver”
通过线程上下文类加载器去加载这个Driver类,从而避开了双亲委派模型的弊端
很明显,spi服务确确实实是破坏了双亲委派模型的,毕竟做到了父级类加载器加载了子级路径中的类。
————————————————
版权声明:本文为CSDN博主「P19777」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/P19777/article/details/100829154
https://www.cnblogs.com/liuligang/p/10519771.html