使用jdbc创建数据库连接时,业务代码一般需要执行以下代码:
Class.forName("com.mysql.jdbc.Driver");//mysql驱动
Connection conn= DriverManager.getConnection("jdbc:mysql://ip:3306/db","user","pwd");
接下来我们以mysql驱动为例,研究jdbc驱动是如何工作的。
1、驱动加载分析:
1)Class.forName(“com.mysql.jdbc.Driver”):
在早期的jdbc中,在创建连接之前必须执行该代码。当执行Class.forName(“com.mysql.jdbc.Driver”)后,会加载、类初始化(执行static代码)mysql驱动类,代码如下:
package com.mysql.jdbc;
import com.mysql.jdbc.NonRegisteringDriver;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException arg0) {
throw new RuntimeException("Can\'t register driver!");
}
}
}
生成驱动实例对象,并向DriverManager注册(将实例添加到list中),代码如下:
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver));
} else {
throw new NullPointerException();
}
}
2)DriverManager类:
调用DriverManager.registerDriver()方法之前肯定得先初始化DriverManager类,代码如下:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
loadInitialDrivers方法会通过两种方式加载驱动实现类:
- 使用Class.forName加载jdbc.drivers系统变量中指定的驱动实现类;
- 使用SPI加载驱动实现类;
private static void loadInitialDrivers() {
String drivers;
try {//1、获取系统变量中的驱动实现类名,然后通过下面的Class.forName加载
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
//2、使用SPI获取驱动实现类
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
//通过Class.forName加载jdbc.drivers系统变量中指定的驱动类
if (drivers != null && !drivers.equals("")) {
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {}
}
}
}
注意:如果配置了jdbc.drivers变量,使用Class.forName加载驱动类时需要传入系统ClassLoader,因为DriverManager类是boot类加载器加载的,如果直接调用Class.forName(aDriver),就会使用boot类加载器来加载驱动,显然是加载不到的。
说明:
- 由于高版本jdk中的jdbc使用了SPI加载驱动实现类,所以不需要在前面显示的通过Class.forName("com.mysql.jdbc.Driver");来加载驱动类了。(写了也不会出错)
- 在以前的低版本中如果不写Class.forName("com.mysql.jdbc.Driver");,就需要设置jdbc.drivers系统变量,二者的用途是一样的,都是使用Class.forName方法来加载指定的驱动类;(驱动类一旦加载会执行其static代码,将驱动实例对象注册到DriverManager中)
3)SPI之ServiceLoader.load():
在ServiceLoader.load时,根据传入的接口类,遍历META-INF/services目录下的以该类命名的文件中的所有类,并实例化返回,同样当加载驱动类时,会调用驱动类的static代码,将其注册到jdbc的DriverManager中。ServiceLoader是spi机制的一个实现,具体见:javascript:void(0)
通过SPI加载、实例化这些驱动程序,需要注意一点:
可能是因为驱动程序类可能不存在,即可能有带有服务类别的打包驱动程序作为java.sql.Driver的实现,但实际的类可能会丢失。在这种情况下,java.util.ServiceConfigurationError会在运行时由VM试图定位时抛出并加载服务。所以,这里使用try catch块来捕获那些运行时错误,如果驱动程序在classpath中不可用,但是打包为服务,并且该服务在classpath中。
2、获取驱动连接分析:
1)getConnection():
public static Connection getConnection(String url,String user,
String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) info.put("user", user);
if (password != null) info.put("password", password);
return (getConnection(url, info, Reflection.getCallerClass()));
}
将调用类的classLoader传给了getConnection方法(调用类是我们的业务代码,所以其类加载器就是系统类加载器),目的是后面再去加载数据库的driver,做一下验证(isDriverAllowed方法)。
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//caller为空,从线程上下文获取(默认是系统类加载器)
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized (DriverManager.class) {
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {//从注册的list中获取驱动示例
// 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!
return (con);
}
} catch (SQLException ex) {
if (reason == null) reason = ex;
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
if (reason != null) {// if we got here nobody could connect.
println("getConnection failed: " + reason);
throw reason;
}
throw new SQLException("No suitable driver found for "+ url, "08001");
}
说明:getConnection方法就是从registerdDrivers(是个list)中遍历所有的驱动实现类,驱动内部会根据url协议来判断是否该创建对应的数据库连接;如果所有驱动都无法解析url创建连接,则抛出异常。
2)isDriverAllowed()方法:
该方法是通过“调用类的类加载器”(一般来说就是系统类加载器)再初始化一次驱动类,其目的是为了确保事先注册的驱动与当前的驱动是通过同一个类加载器加载的。
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}
总结:
使用jdbc创建具体数据库驱动库的连接,主要思路是:加载驱动类,在驱动类的static代码中按照jdbc规范,将其实例注册到DriverManager中,在创建连接时遍历注册的list,先进行驱动实例的校验,然后根据url协议创建出连接。加载驱动类的方式有:
- 设置jdbc.drivers的系统变量;
- 使用Class.forName指定驱动类;
- 使用SPI加载驱动类;