本文只討論 JDBC 驅動加載問題。
1 JDBC概述
1.1 什麼是 JDBC
JDBC
一般指Java 數據庫連接
,(Java Database Connectivity
,簡稱爲JDBC
);JDBC
是 Java 語言中用來規範客戶端程序
如何來訪問數據庫
的應用程序接口
;其提供瞭如查詢、更新數據庫中數據的方法,我們常說的 JDBC 是面向關係型數據庫的。
1.2 JDBC 基本結構圖
一般情況下,在 application
中進行數據庫連接,調用 JDBC 接口
:
- 首先,需要將指定的 JDBC 驅動實現加載到 jvm 中;
- 然後,再進行使用對應具體的數據庫驅動類、結合具體的數據庫連接參數,建立數據庫連接。
基本的結構圖如下:
1.3 案例問題的分析與解決
1.3.1 問題描述
//jdk6之後,無需再顯式執行Class.forName
Class.forName("oracle.jdbc.driver.OracleDriver");
// com.mysql.cj.jdbc.Driver [mysql:mysql-connector-java:8.0.28]
// com.mysql.jdbc.Driver [mysql:mysql-connector-java:5.1.33]
// org.gjt.mm.mysql.Driver extends com.mysql.jdbc.Driver [mysql:mysql-connector-java:5.1.33] 【 8.0.1.33 中無此Driver類】
// com.amazon.opendistroforelasticsearch.jdbc.Driver [com.amazon.opendistroforelasticsearch.client:opendistro-sql-jdbc:1.12.0.0]
// oracle.jdbc.driver.OracleDriver [com.clickhouse:clickhouse-jdbc:0.3.2]
Connection connection = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl","xxx","xxxx");
我第一次寫此代碼時,就有很多疑問,但是後來也沒研究,今天我把當時的疑問列出
-
1 "oracle.jdbc.driver.OracleDriver" 和"jdbc:oracle:thin" 有什麼關係,爲什麼要寫兩遍
-
2
Class.forName
註冊驅動後,沒有返回值,是怎麼回事 -
3
Class.forName
爲什麼 此行在jdk6版本中 又不需要了呢 -
4
DriverManager.getConnection()
怎麼使用的驅動的呢
1.3.2 問題分析:源碼分析
要回答上邊的問題,我們先來看看這些代碼的源碼
- 1 要使用jdbc連接 oracle,必須先加載驅動,Class.forName就是加載驅動
Class.forName 會初始化 oracle.jdbc.driver.OracleDriver這個類 (關於類的加載過程可自行搜索),初始化時自動執行類的靜態代碼塊
// oracle.jdbc.driver.OracleDriver
static{
...
if (defaultDriver == null) {
defaultDriver = new oracle.jdbc.OracleDriver();
DriverManager.registerDriver(defaultDriver);
}
這樣就驅動就註冊了
- 2 jdk6爲什麼不需要 執行了Class.forName這行代碼了呢
大家先了解一下ServiceLoader 。
簡單來說就是在驅動jar包配置文件中,指定oracle.jdbc.driver.OracleDriver實現java.sql.Driver接口;然後DriverManager在初始化的時候,自動掃描所有jar包中實現了java.sql.Driver的類 ,並初始化 此實現類
//DriverManager
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
...
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//搜索服務的實現類(驅動實例)
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();// 此處初始化 並註冊了
}
} catch(Throwable t) {
// Do nothing
}
return null;
//ServiceLoader
public S next() {
if (acc == null) {
return nextService();
} else {
...
//ServiceLoader
private S nextService() {
...
try {
c = Class.forName(cn, false, loader);
...
也就是說 DriverManger藉助 ServiceLoader 找到驅動 並註冊了,所以不需要再手工註冊
- 3 驅動註冊了,DriverManager中 驅動怎麼被使用呢
//caller = Reflection.getCallerClass()
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
....
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
//callerCL 是方法調用類或者當前線程的classLoader,會在isDriverAllowed中使用
...
// 所有已經註冊驅動都保存在registeredDrivers,這是個CopyOnWriteArrayList
for(DriverInfo aDriver : registeredDrivers) {
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
//jdbc:oracle:thin:@localhost:1521:orcl 最終是由驅動實現類使用
Connection con = aDriver.driver.connect(url, info);
...
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
//使用不同的classLoader加載出來的驅動類是不相等的,此處就利用這點判斷權限
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}
遍歷 registeredDrivers (所有已經註冊驅動都在這裏), 使用 DriverManager.getConnection 方法所在類的 classLoader(如果爲空,則使用當前線程上下文的classLoaer),去加載驅動類 ,然後和registeredDrivers裏邊的比較 ,如果相等,則此驅動是有權限被 使用。
注意:此處 遍歷 registeredDrivers時,只要找到一個有權限的,就立即返回。
如果使用相同方式註冊多個了驅動,調用的是哪個驅動呢? registeredDrivers中第一個有權限且能正確連接上的的 ;那麼程序裏邊 如果要連接多個數據庫,使用jdbc怎麼操作?
1.4 驅動加載
從案例中回來,看看驅動是如何加載的?
1.4.1 加載驅動
驅動是一個 class,將驅動進行加載到 jvm 中與加載普通類一樣,使用 Class.forName("driverName") 進行加載
//加載MySQL 數據庫驅動
Class.forName("com.mysql.jdbc.Driver");
//加載Oracle數據庫驅動
Class.forName("oracle.jdbc.driver.OracleDriver");
1.4.2 java.sql.Driver
接口
驅動。
首先,需要實現 java.sql.Driver 接口,連接不同數據庫的驅動類不同,但每個驅動類都需要提供一個實現了
java.sql.Driver
接口的類。
然後,在程序中由驅動器管理類 java.sql.DriverManager 去調用這些 Driver 實現
java.sql.Driver
接口源碼
package java.sql;
import java.util.logging.Logger;
public interface Driver {
// 試圖創建一個給定 url 的數據庫連接,創建 Connection 對象
Connection connect(String url, java.util.Properties info) throws SQLException;
// 驅動程序是否可以打開 url 連接,判斷 url 是否符合協議,符合協議形式的 url 纔可以
boolean acceptsURL(String url) throws SQLException;
// 獲取驅動的屬性信息
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) throws SQLException;
// 獲取驅動主版本號
int getMajorVersion();
// 獲取驅動的次版本號
int getMinorVersion()#;
// 驅動程序是否是一個真正的 JDBC Compliant
boolean jdbcCompliant();
public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}
- 手動加載 Driver
// 加載 mysql 驅動類,並實例化
Driver driver = (Driver)Class.forName("com.mysql.jdbc.Driver);
// 判斷 url 是否符合 mysql 的 url 形式
boolean flag = driver.acceptsURL("jdbc:mysql://localhost:3306/test");
// 創建數據庫連接
String url = "jdbc:mysql://localhost:3306/test";
Properties props = new Properties();
props.put("user", "root");
props.put("password", "root");
Connection connection = driver.connect(url, props);
1.4.3 DriverManager
驅動器管理類
如果有多個驅動 Driver,JDBC 提供了 DriverManager
對多個驅動進行統一管理
DriverManager
可以註冊和刪除加載的驅動程序,根據給定的 url 獲取符合 url 協議的 Driver 或者建立 Connection 連接
- DriverManager 源碼
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers(); // 加載配置在 jdbc.drivers 系統變量中的驅動
println("JDBC DriverManager initialized");
}
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;
}
...... 省略一部分代碼
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);
}
}
}
使用 Class.forName("driverName")
加載驅動類到 jvm
的時候,同時會執行這個驅動類中的靜態代碼塊,創建一個 Driver
實例;
然後,調用 DriverManager
進行註冊 Driver。
DriverManager 在第一次被調用時,它會被加載到內存中,然後執行它內部的 static 靜態代碼塊,執行其中的 loadIntialDrivers() 靜態方法,加載配置在 jdbc.driver 中的 Driver,配置在 jdbc.drivers 中的驅動 driver 會先被加載
- 註冊 Driver 到 DriverManager
在加載某一個 Driver 類時,應該創建自己的實例並像 DriverManager 中註冊自己的實例。
比如 com.mysql.jdbc.Driver 的源碼,使用靜態代碼塊進行實現
在我們使用 Class.forName("com.mysql.jdbc.Driver")
方法獲取它的 Class
對象,com.mysql.jdbc.Driver
就會被 jvm 加載,連接,初始化。
初始化就會執行其中的靜態代碼塊,執行下面的
java.sql.DriverManager.registerDriver(new Driver())
進行註冊
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
- 註冊實例源碼
將要註冊的驅動信息放到一個
DriverInfo
中,然後再放到一個 List 中,在後面連接時候會用到
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been added to our list */
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
1.5 案例:建立MySQL數據庫連接
MySQL JDBC驅動版本
- com.mysql.cj.jdbc.Driver [mysql:mysql-connector-java:8.0.28] [√]
- com.mysql.jdbc.Driver [mysql:mysql-connector-java:5.1.33] [X]
- org.gjt.mm.mysql.Driver extends com.mysql.jdbc.Driver [mysql:mysql-connector-java:5.1.33] [X]
【 8.0.1.33 中無此Driver類】
@Override
protected synchronized Connection createConnection() {
// JDK 1.6 之後支持 JDBC 驅動包(內的 META-INF/services/java.sql.Driver) 利用 ServiceLoader 機制 自動註冊數據庫驅動
//this.registerDriver();
if (this.connection == null) {
try {
//step1 準備參數
String url = this.getDataSource().getDatasourceUrl();
String username = this.getDataSource().getDatasourceUser();
String password = this.getDataSource().getDatasourcePassword();
Properties properties = new Properties();
//JDBC DriverManager 所約定的屬性固定名稱: user
properties.put("user", username);
//JDBC DriverManager 所約定的屬性固定名稱: password
properties.put("password", password);
//step2 根據 url 獲取指定的 數據庫驅動
Driver driver = DriverManager.getDriver(url);
logger.debug("current datasource info is : {}", this.getDataSource().toString());
logger.debug("Success to get driver from DriverManager for jdbcUrl: {}", url);
//step3 使用指定的數據庫驅動連接對應種類的數據庫
//方法1: (暫不使用)基於DriverManager(找到符合權限)建立數據庫連接
//this.connection = DriverManager.getConnection(url, username, password);
//方法2: 使用指定驅動器連接數據庫 | 本步驟爲避免出現偶現問題:當 mysql 數據源的用戶名、密碼等配置錯誤時,因JDBC DriverManager的管理機制會報ES錯誤:URISyntaxException: URL does not begin with the mandatory prefix jdbc:elasticsearch://.
if(driver instanceof com.mysql.cj.jdbc.Driver){
//檢查給定的URL是否有效
boolean acceptable = driver.acceptsURL(url);
//使用指定的驅動器連接指定的數據庫
this.connection = driver.connect(url, properties);
logger.debug("driver.acceptsURL(url): {}", acceptable);
logger.debug("connection = driver.connect(url, properties): {}", connection);
} else {//不允許使用其他種類、其他版本的數據庫驅動
logger.error("Prohibit the use of other types and versions of database driver! currentDriver: {}", driver);
}
} catch (SQLException exception) {
logger.error("fail to register mysql jdbc connection because `SQLException`!");
logger.error(String.format("error info :%s", exception.toString()));
throw new RuntimeException(exception);
}
}
return this.connection;
}
@Override
protected void registerDriver() {
String driverClass = this.getDataSource().getDatasourceDriverClass();
if(REGISTERED_JDBC_DRIVERS.contains(driverClass)==false){
String driver = this.getDataSource().getDatasourceDriverClass();
driver = (driver == null)?DEFAULT_JDBC_DRIVER:driver;
try {
//方式1
Class.forName(driver);
//方式2
//Class clazz = Class.forName(driver);
//Object driverObject = clazz.newInstance();// 等效於: new com.mysql.jdbc.Driver()
//DriverManager.registerDriver((Driver) driverObject);
//DriverManager.registerDriver(new com.mysql.jdbc.Driver());
//DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
} catch (ClassNotFoundException exception) {
logger.error("fail to register mysql jdbc driver because `ClassNotFoundException`!");
logger.error(String.format("error info :%s", exception.toString()));
throw new RuntimeException(exception);
} catch (Exception exception) {
logger.error(String.format("fail to register mysql jdbc driver,error info : %s", exception.toString()));
throw new RuntimeException(exception);
}
logger.info("success to register mysql jdbc driver!");
REGISTERED_JDBC_DRIVERS.add(driverClass);
}
}
X 參考文獻
- JDBC/DriverManager原理--註冊驅動 - CSDN
- ServiceLoader詳解 - 博客園
- JDBC 驅動加載原理解析 - CSDN
- [數據庫/Java SE]MySQL驅動包(mysql-connector-java.jar)問題[com.mysql.jdbc.Driver/org.gjt.mm.mysql.Driver/com.mysql.cj.jdbc.Driver] - 博客園/千千寰宇
- 深入理解JDBC設計模式: DriverManager 解析 - 博客園
- JDBC數據庫驅動及原理 - CSDN
- Class.forName的加載類底層實現 - 掘金
- Class.forName()用法詳解 - CSDN
- Java中Class.forName()用法詳解 - CSDN
- acceptsURL 方法 (SQLServerDriver) - Microsoft