1. 前言
我为什么想要分享 DriverManager 相关的内容,是因为我们团队近期在使用 Seatunnel 作为数据采集的中间件,在测试使用中我们遇到了一个关于驱动的问题,我就仔细的看了一下 DriverManager 相关的源码,分享给大家。
2. 先加载驱动,再获取连接
下面的这两句 Java 大家应该不陌生,一个是加载驱动,另一个是获取连接。
Class.forName("com.taosdata.jdbc.rs.RestfulDriver");
DriverManager.getConnection(jdbcUrl);
下面我们先来看一下 Class.forName("com.taosdata.jdbc.rs.RestfulDriver")
背后的东西。
我先来给出一个结论:上面的代码是完成对 RestfulDriver 的加载,以及完成驱动的注册。
Class.forName("com.taosdata.jdbc.rs.RestfulDriver")
的作用是加载并初始化名为 com.taosdata.jdbc.rs.RestfulDriver
的类,而按照 JDBC 的规范,自定义 Driver 在被初始化的过程中需要主动的注册到DriverManager中(通常使用DriverManager.registerDriver(new RestfulDriver()))。
下面是我找到的一个驱动的注册代码
3. 直接获取连接
其实我们按照下面的方式也是可以获取到数据库连接的。
注意:如果注册驱动的类加载器不是获取链接时的类加载器,那么就会报:No suitable driver found for ...
DriverManager.getConnection(jdbcUrl);
DriverManager 会通过 JdbcUrl 动态的选择合适的驱动,下面我们来看一下 DriverManager 是如何动态的选择合适的驱动的,即 getConnection 背后的行为。
3.1. DriverManage r中有三个 getConnection 的重载方法:
// 根据 Url 和 Properties 参数创建连接
public static Connection getConnection(String url,
java.util.Properties info) throws SQLException {...}
// 根据 Url 、 用户名 、密码 创建连接
public static Connection getConnection(String url,
String user, String password) throws SQLException {...}
// 根据 usrl 创建连接
public static Connection getConnection(String url) throws SQLException {...}
使用第一个重载方法时,一般会将用户名、密码登认证信息存放到 info 中;使用第三个重载方法时,一般会吧认证信息拼接到url中。至于驱动提供厂商具体怎么解析判断是其内部的事情。
第 2、3 个重载方法最终都会调用第一个重载方法,下面我们来解读一下第一个重载方法:
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
// 获取类加载器
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// 如果类加载器为空,那么获取当前线程的类加载器
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
// 记录异常
SQLException reason = null;
// 遍历已经注册过的驱动,这个地方大家可能会存在疑惑:我们并没有使用Class.forName("com.taosdata.jdbc.rs.RestfulDriver")
// 去主动的注册驱动,那么DriverManager中的registeredDrivers是怎么完成注册的?
// 这个问题我们先放一下,我会在 3.2 中进行解释
for(DriverInfo aDriver : registeredDrivers) {
// 检测驱动是不是被允许,这里只是检测registeredDrivers是否真的存在于上下文代码中
// 具体怎么检测的,我们先放一下,我们会在 3.3 中进行解释
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
// 调用注册到registeredDrivers中的驱动的connect方法(即各个厂商提供的驱动程序)
Connection con = aDriver.driver.connect(url, info);
// 如果con不为null,则说明成功获取的数据库连接。
// 上文中我们说过getConnection方法会动态的获取合适的驱动,就是通过此处代码片段实现的
// 首先,在getConnection中尝试遍历了所有的已经注册的驱动,并调用了驱动的connect方法,尝试获取了连接
// 如果connect方法的返回值为null,或者抛出了异常,那么说明该驱动不是合适的驱动
// 有因为该段代码块被try-catch,所以即时抛出异常,循环也会继续,直到找到合适的驱动或循环结束。
// 说白了,DriverManager 取了个巧,通过校验厂商的connect方法会不会抛出异常或返回null,来判断驱动是否合适。
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());
}
}
// 能到这,说明已经获取连接失败了
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
}
我们可以看到,getConnect 的的过程是基于已经完成注册的驱动的动态匹配,getConnect 利用已经试错的方式去遍历已经注册过的驱动的集合,通过调用驱动的connect方法,判断connect执行过程会不会抛出异常,或者返回值是否为 null 来判断是否找到合适的驱动,直到遍历了所有的驱动或者找到了合适的驱动。
3.2. 驱动是如何完成注册的
DriverManager 是利用 ServiceLoader 完成对驱动厂商提供的驱动的加载,ServiceLoader 可以来加载 META-INF/services/
下配置好的服务实现类的实例。例如:META-INF/services/java.sql.Driver
。
ServiceLoader<Driver> drivers= ServiceLoader.load(Driver.class);
for (Driverdemo : drivers) {
System.out.println(drivers.toString());
}
规则就是 META-INF/services/java.sql.Driver
中指明的类必须实现同一个接口,接口的全路径必须是文件名(如:java.sql.Driver)
com.taosdata.jdbc.TSDBDriver
com.taosdata.jdbc.rs.RestfulDriver
在 DriverManager 中存在一段如下静态代码块,其目的就是完成对厂商驱动的加载,厂商驱动会利用驱动中的代码代码块实现对自己驱动的实例化对象的注册,即注册到 DriverManager 中的 registeredDrivers。
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
// 使用 AccessController.doPrivileged() 方法执行一些可能具有安全权限限制的代码块
// 这个写法确实比较严谨啊
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
// 获取Java的系统属性值`jdbc.drivers`,目的后面再讲
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
// 下面方法中的代码就是ServiceLoader完成对驱动厂商提供的驱动的加载,即寻找META-INF/services/java.sql.Drive
// 并完成对java.sql.Drive中配置的类的加载。
// 具体的实现原理就不细讲了,最终会通过Class.forName和newInstance()完成对类的加载,源码大概是这么做的
// Class<?> c = Class.forName(cn, false, loader);
// c.newInstance();
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
// 下面的代码就是对Java的系统属性值`jdbc.drivers`,的内容进行切分,并对切分的内容进行逐个的使用
// Class.forName 完成对目标对象的加载,由此可见我们可以通过将驱动类路径使用“:”拼接并存储到
// Java的系统属性`jdbc.drivers`中,DriverManager也是可以完成对驱动的加载的。
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 注意第二个参数是true,表明会进行对类的初始化,即类中的静态代码块会被执行。
Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
通过上述的静态代码块我们可以得出,驱动的注册过程由两种:一种是利用 ServiceLoader 的规则扫描特定的文件完成对文件中指定类的加载和初始化。另一种是通过将驱动的类路径写入到 Java 的系统属性值 jdbc.drivers
中,显式的使用 Class.forName
完成对指定类的加载和初始化。
这两种方式都需要驱动实现类进行配合才能完成驱动的注册,即需要驱动实现类在静态代码块中主动的完成对自己的注册。
3.3. isDriverAllowed()的作用
下面我们来谈一下DriverManager中的isDriverAllowed()都做了什么,顾名思义,其目的是检查驱动是否被允许,至于被允许什么,请看下文:
该方法由两个重载方法,第一个方法的目的就是为了获取参数caller的类加载其,我们主要关注第二个重载方法。
private static boolean isDriverAllowed(Driver driver, Class<?> caller) {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
return isDriverAllowed(driver, callerCL);
}
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
// 获取isDriverAllowed中第一个参数的全类名,并设置初始化该类(会执行静态方法),拿到class
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
// 判断拿到的class是否与该方法的第一个参数相同
// 其目的是为了确保事先注册的驱动与当前的驱动是通过同一个类加载器加载的
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}
从上面的代码中我们可以看到,isDriverAllowed 方法的目的就是为了校验已经注册的驱动的合法性,如果驱动被注册时使用的类加载器和调用 getConnection()所使用类的的类加载不是同一个,那么驱动将不被允许使用。
所以我更推荐在获取数据库连接前,先显式的将所需的驱动注册一下,避免因类加载器的问题导致驱动不可用(虽然这种情况不常见)。
4. 尾语
我看到驱动的这种注册方式,我非常有感触,因为我在项目中就用过类似的方式——策略模式的准备工作,只不过我当时不是用的静态代码块完成的注册么而是使用的 spring 的 InitializingBean
完成的注册。
虽然注册驱动的方式有很多种,但是还是建议使用最规范的方式( Class.forName("xxx.xx.XDriver")
)完成所需驱动的注册,避免潜在的BUG。