江蘇 無錫 繆小東java
本篇從java.sql.Driver接口、java.sql.DriveManager類以及其它開源數據庫的驅動類討論JDBC中驅動加載的全過程以及JDBC的Framework如何作到「可插拔」的細節。程序員
本篇包含了不少部分的內容。如類加載器、本地方法、對象鎖、類鎖、按功能或者狀態分離鎖、安全機制,對這些內容沒有深刻討論!詳情能夠繼續關注本博客!我在上篇主要關注驅動管理器的初始化、鏈接的創建、驅動的註冊、驅動的遍列、驅動的取消註冊以及DriverManager中的日誌操做。sql
//Driver.java數據庫
package java.sql;編程
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;ide
int getMajorVersion(); //返回驅動的主版本號工具
int getMinorVersion(); //返回驅動的次版本號
boolean jdbcCompliant(); //是否兼容於JDBC標準
}
以上就是JDBC中的Driver接口,它是任何數據庫提供商的驅動類必須實現的接口,驅動類必須實現該接口中的全部方法!簡單吧!
它之因此是一個接口,就是OO中常常談到的「依賴倒轉原則(DIP-Dependence Inverse Principle)」的具體應用了!在DriverManager類中能夠看到:它使用的驅動都是Driver接口,從而依賴與高層,不依賴於實現。這樣可使用JDBC Framework管理和維護不一樣JDBC提供商的數據庫驅動。
JDBC Framework中容許加載多個數據庫的驅動!相應地,通常建議數據庫提供商的驅動必須小一點,從而保證在加載多個驅動後不會佔用太多的內存。
在Driver接口中共有以上六個方法。其中紅色的兩個相對很重要,它是供DriverManager調用的,其它四個很簡單的方法!下面簡單講述前兩個方法的意義!
Connection connect(String url, java.util.Properties info) throws SQLException方法是數據庫提供商的驅動必須實現的方法,它主要是使用指定的URL和與具體提供商相關的信息創建一個鏈接。
boolean acceptsURL(String url) throws SQLException方法也是數據庫提供商的驅動必須實現的方法,主要斷定某個該驅動是否介紹該URL。(通常在創建鏈接以前調用。詳情將DriverManager類)
DriverManager類是整個JDBC的起點!利用它能夠建立鏈接,從而完成後續的操做。在JDBC 中DriverManager是一個相對比較複雜的類,所以咱們按其只能分爲幾類介紹。本篇將DriverManager中的方法分爲3類:1.初始化;2.驅動的註冊、查詢、取消註冊;3.創建鏈接;4.日誌相關。
下面就看看它的源代碼吧!
//DriverManager.java 1.45 05/11/17
package java.sql;
import sun.misc.Service;
import java.util.Iterator;
class DriverInfo {
Driver driver;
Class driverClass;
String driverClassName;
public String toString() {
return ("driver[className=" + driverClassName + "," + driver + "]");
}
}
public class DriverManager {
// Prevent the DriverManager class from being instantiated.
private DriverManager(){}
以上是其代碼的前面部分。主要是包的定義、相關文件的導入、類的定義以及一個私有化的構造器――即該類不能夠實例化,只能夠調用其靜態方法,至關於一個工具類――一個管理驅動的工具類!還有一個就是一個輔助類DriverInfo,它包裝了驅動類Driver,包含驅動類的類和驅動類的名稱。
下面就開始DriverManager類重要方法的介紹吧!
private static java.util.Vector drivers = new java.util.Vector(); //保存多個驅動的彙集
private static boolean initialized = false; //是否初始化的標記,默認固然是否了
// 真正的初始化方法
static void initialize() {
if (initialized) { return; } //已經初始化就返回!(初始化了就算了)
initialized = true; //設置此標識符,表示已經完成初始化工做
loadInitialDrivers(); //初始化工做主要是完成全部驅動的加載
println("JDBC DriverManager initialized");
}
//初始化方法中完成加載全部系統提供的驅動的方法
private static void loadInitialDrivers() {
String drivers;
try {
drivers = (String) java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("jdbc.drivers"));
//獲得系統屬性"jdbc.drivers"對應的驅動的驅動名(這但是須要許可的哦!)。
} catch (Exception ex) {
drivers = null;
}
Iterator ps = Service.providers(java.sql.Driver.class); //從系統服務中加載驅動
while (ps.hasNext()) { //加載這些驅動,從而實例化它們
ps.next();
}
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null) { return; } //系統屬性未指定驅動則返回
while (drivers.length() != 0) { //循環過程,講解見下面
int x = drivers.indexOf(':');
String driver;
if (x < 0) {
driver = drivers;
drivers = "";
} else {
driver = drivers.substring(0, x);
drivers = drivers.substring(x+1);
}
if (driver.length() == 0) { continue; }
try {
println("DriverManager.Initialize: loading " + driver);
Class.forName(driver, true, ClassLoader.getSystemClassLoader());
//加載這些驅動,下篇會講解其細節
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}//end of while
//系統屬性"jdbc.drivers"可能有多個數據庫驅動,這些驅動的名字是以「:」分隔開的,
//上面的過程就是將此以「:」分隔的驅動,依次遍列,而後調用Class.forName依次加載
}
private static Object logSync = new Object(); //對象鎖
//下面是一個輔助方法,用於向日志中寫入信息!
public static void println(String message) {
synchronized (logSync) { //很重要的一致性編程的方法,見下面
if (logWriter != null) { //設置日誌才能夠進行下面的寫入信息
logWriter.println(message); //向logger中寫入信息
logWriter.flush();
}
}
}
//以上藍色的屬性和方法,是一致性編程(Concurent Programming)中的重要方法。
//首先明確咱們在向日志寫入信息的時候,是能夠調用其它非寫入日誌的方法的,
//只是不一樣的客戶不能同時調用該寫入方法――一個客戶正在寫入,其它必須等待寫完
//假如咱們機械地使用synchronized(this)或synchronized該寫入方法時,必然會致使效率低
//通常地,當類的中多個方法能夠分爲多個不一樣組,這些組的方法互相之間不干擾時,
//能夠爲每一個組指定一個本身的鎖,限制同一個方法被多個客戶使用,從而保證該方法的
//一致性,保證無必要的synchronized方法!
//關於一致性編程,請多關注博客中的文章
//向DriverManager 註冊指定的驅動。驅動這麼註冊請閱讀下篇!
public static synchronized void registerDriver(java.sql.Driver driver) throws SQLException {
if (!initialized) { initialize(); } //註冊前必須初始化了
DriverInfo di = new DriverInfo(); //建立一個新的驅動信息類
di.driver = driver;
di.driverClass = driver.getClass();
di.driverClassName = di.driverClass.getName(); //以上填入註冊驅動的信息
drivers.addElement(di); //將此驅動信息假如驅動彙集中
println("registerDriver: " + di);
}
//
public static synchronized Driver getDriver(String url) throws SQLException {
println("DriverManager.getDriver( "" + url + " ")");
if (!initialized) { initialize(); } //一樣必須先初始化
//本地方法,獲得調用此方法的類加載器
ClassLoader callerCL = DriverManager.getCallerClassLoader();
// 遍列全部的驅動信息,返回能理解此URL的驅動
for (int i = 0; i < drivers.size(); i++) { //遍列驅動信息的彙集
DriverInfo di = (DriverInfo)drivers.elementAt(i);
// 調用者在沒有權限加載此驅動時會忽略此驅動
if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
println(" skipping: " + di);
continue;
}
try {
println(" trying " + di);
if (di.driver.acceptsURL(url)) { //驅動能理解此URL時,返回此驅動
println("getDriver returning " + di);
return (di.driver);
}
} catch (SQLException ex) {
// Drop through and try the next driver.
}
}
println("getDriver: no suitable driver");
throw new SQLException("No suitable driver", "08001");
}
//從DriverManager 中取消註冊某個驅動。Applet僅僅可以取消註冊從它的類加載器加載的驅動
public static synchronized void deregisterDriver(Driver driver) throws SQLException {
ClassLoader callerCL = DriverManager.getCallerClassLoader();
println("DriverManager.deregisterDriver: " + driver);
int i;
DriverInfo di = null;
for (i = 0; i < drivers.size(); i++) {
di = (DriverInfo)drivers.elementAt(i);
if (di.driver == driver) { break; } //找到了某個驅動則返回,同時返回i值
}
if (i >= drivers.size()) { //所有遍列完,度沒有找到驅動則返回
println(" couldn't find driver to unload");
return;
}
//找到此驅動,但調用者不能加載此驅動,則拋出異常
if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
throw new SecurityException();
}
// 在以上全部操做後,能夠刪除此驅動了
drivers.removeElementAt(i);
}
//獲得當前全部加載的JDBC驅動的枚舉**
public static synchronized java.util.Enumeration getDrivers() {
java.util.Vector result = new java.util.Vector();
if (!initialized) { initialize(); } //該類沒有初始化時,必須完成初始化工做
//詳情請閱讀初始化部分
ClassLoader callerCL = DriverManager.getCallerClassLoader(); //獲得當前類的類加載器
for (int i = 0; i < drivers.size(); i++) { //遍列全部的驅動
DriverInfo di = (DriverInfo)drivers.elementAt(i); //獲得某個具體的驅動
// 假如調用者沒有許可加載此驅動時,忽略該驅動
if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
println(" skipping: " + di);
continue;
}
result.addElement(di.driver); //將能夠加載的驅動加入要返回的結果集
}
return (result.elements()); //返回結果集
}
private static native ClassLoader getCallerClassLoader();
//得到當前調用者的類裝載器的本地方法(關於本地方法JNI請關注本博客後續文章)
// 返回類對象。咱們使用DriverManager的本地方法getCallerClassLoader()獲得調用者的類加載器
private static Class getCallerClass(ClassLoader callerClassLoader, String driverClassName) {
// callerClassLoader爲類加載器,driverClassName爲驅動的名稱
Class callerC = null;
try {
callerC = Class.forName(driverClassName, true, callerClassLoader);
//使用指定的類裝載器定位、加載指定的驅動類,
//true表明該驅動類在沒有被初始化時會被初始化,返回此類
}catch (Exception ex) {
callerC = null; // being very careful
}
return callerC;
}
在JDBC程序中通常使用DriverManager.getConnection方法返回一個鏈接。該方法有多個變體,它們都使用了具體驅動類的connect方法實現鏈接。下面是鏈接的核心方法。
private static Connection getConnection(String url, java.util.Properties info, ClassLoader callerCL)
throws SQLException {
//當類加載器爲null時,必須檢查應用程序的類加載器
//其它在rt.jar以外的JDBC驅動類能夠今後加載驅動 /*
synchronized(DriverManager.class) { //同步當前DriverManger的類
if(callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); }
//獲得當前線程的類加載器(此句的真正含義請關注後續線程相關的文章)
}
if(url == null) { throw new SQLException("The url cannot be null", "08001"); }
println("DriverManager.getConnection( "" + url + " ")");
if (!initialized) { initialize(); } //必須初始化,將默認的驅動加入
// 遍列當前的全部驅動,並試圖創建鏈接
SQLException reason = null;
for (int i = 0; i < drivers.size(); i++) {
DriverInfo di = (DriverInfo)drivers.elementAt(i);
// 假如調用者沒有許可加載該類,忽略它
if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
//當驅動不是被當前調用者的類加載器加載時忽略此驅動
println(" skipping: " + di);
continue;
}
try {
println(" trying " + di);
Connection result = di.driver.connect(url, info); //調用某個驅動的鏈接方法創建鏈接
if (result != null) { //在創建鏈接後打印鏈接信息且返回鏈接
println("getConnection returning " + di);
return (result);
}
} catch (SQLException ex) {
if (reason == null) { reason = ex; } //第一個錯誤哦
}
}
//以上過程要麼返回鏈接,要麼拋出異常,當拋出異常會給出異常緣由,即給reason賦值
//在全部驅動都不能創建鏈接後,如有錯誤則打印錯誤且拋出該異常
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
//若根本沒有返回鏈接也沒有異常,不然打印沒有適當鏈接,且拋出異常
println("getConnection: no suitable driver");
throw new SQLException("No suitable driver", "08001");
}
//如下三個方法是上面的鏈接方法的變體,都調用了上面的鏈接方法
public static Connection getConnection(String url, java.util.Properties info) throws SQLException {
ClassLoader callerCL = DriverManager.getCallerClassLoader();
//沒有類加載器時就是該調用者的類加載器
return (getConnection(url, info, callerCL));
}
public static Connection getConnection(String url, String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
ClassLoader callerCL = DriverManager.getCallerClassLoader();
if (user != null) { info.put("user", user); }
if (password != null) { info.put("password", password); }
return (getConnection(url, info, callerCL));
}
public static Connection getConnection(String url) throws SQLException {
java.util.Properties info = new java.util.Properties();
ClassLoader callerCL = DriverManager.getCallerClassLoader();
return (getConnection(url, info, callerCL));
}
DriverManager中與日誌相關的方法有好幾個,主要分爲已被deprecated的Stream相關的方法,和建議使用的Reader、Writer相關的方法。(對應於java IO的字符流和字節流哦!由於寫入日誌的信息通常爲字符流,因此廢棄了與字節流相關的方法)
//常數。容許設置logging stream
final static SQLPermission SET_LOG_PERMISSION = new SQLPermission("setLog");
private static int loginTimeout = 0;
private static java.io.PrintWriter logWriter = null; //寫入的字符流
private static java.io.PrintStream logStream = null; //寫入的字節流
//設置驅動在試圖鏈接(log)時等待的最大時間
public static void setLoginTimeout(int seconds) { loginTimeout = seconds; }
public static int getLoginTimeout() { return (loginTimeout); }
public static java.io.PrintWriter getLogWriter() { //獲得LogWriter
return logWriter;
}
public static void setLogWriter(java.io.PrintWriter out) { //設置字符流
SecurityManager sec = System.getSecurityManager(); //取得安全管理器
if (sec != null) { sec.checkPermission(SET_LOG_PERMISSION); }
//檢查是否具備日誌寫入的權限,有權限則繼續,不然拋出異常!
logStream = null;
logWriter = out;
}
public static void setLogStream(java.io.PrintStream out) { //設置字節流
SecurityManager sec = System.getSecurityManager();
if (sec != null) { sec.checkPermission(SET_LOG_PERMISSION); }
logStream = out;
if ( out != null )
logWriter = new java.io.PrintWriter(out); //將字節流包裝爲字符流
else
logWriter = null;
}
public static java.io.PrintStream getLogStream() { //獲得字節流
return logStream;
}
}
以上對應於《教你創建簡單JDBC程序的》DriverManager.getConnection()方法。
下篇咱們將關注:數據庫提供商如何註冊本身的驅動,即關注Class.forName()方法。以及「可插拔」等概念!
本篇主要用幾個開源數據庫的驅動講述驅動是如何加載的,以及「可插拔」機制等。
因爲文章篇幅有限,本文章並非專門研究開源源代碼!所以咱們僅研究那些與數據庫驅動加載直接相關的方法。在下面的幾個開源軟件的驅動中,咱們主要關注驅動類的getConnection()方法。
如下第一個是smallsql中驅動類SSDriver的源代碼:
package smallsql.database;
import java.sql.*;
import java.util.Properties;
public class SSDriver implements Driver {
static SSDriver drv;
static {
try{
drv = new SSDriver();
java.sql.DriverManager.registerDriver(drv);
}catch(Throwable e){}
}
public Connection connect(String url, Properties info) throws SQLException {
if(!acceptsURL(url)) return null;
……
return new SSConnection( (idx > 0) ? url.substring(idx+1) : null);
}
……
}
從上面紅色的部分能夠看到:這是一個靜態語句塊(static block),這意味着該語句是在類構造完成前完成的(關於語句塊的加載請閱讀《Think in java》)。即調用class.forName(「smallsql.database.SSDriver」)語句時,會首先建立一個SSDriver的實例,而且將其向驅動管理器(DriverManager)註冊。這樣就完成驅動的註冊了。
從上面的藍色的代碼能夠看出:驅動的鏈接方法返回的是一個具體的SSConnection對象。而在前面研究的Driver接口中返回的是Connection接口,這是不茅盾的,SSConnection對象實現了Connection接口。
再下面一個是HSqlD中的驅動類的源代碼:
package org.hsqldb;
import java.sql.*;
import java.util.Properties;
import org.hsqldb.jdbc.jdbcConnection;
import org.hsqldb.persist.HsqlDatabaseProperties;
import org.hsqldb.persist.HsqlProperties;
public class jdbcDriver implements Driver {
public Connection connect(String url, Properties info) throws SQLException {
return getConnection(url, info);
}
public static Connection getConnection(String url, Properties info) throws SQLException {
HsqlProperties props = DatabaseURL.parseURL(url, true);
if (props == null) {
throw new SQLException(Trace.getMessage(Trace.INVALID_JDBC_ARGUMENT));
} else if (props.isEmpty()) {
return null;
}
props.addProperties(info);
return new jdbcConnection(props);
}
static {
try {
DriverManager.registerDriver(new jdbcDriver());
} catch (Exception e) {}
}
}
藍色的依然是創建鏈接的部分,依然返回某個具體的實現java.sql.Connection接口的對象。是設計模式中的哪一個工廠啊(通常工廠、抽象工廠、工廠方法仍是什麼都不是啊)!
紅色的一樣是一個靜態語句塊,一樣完成該驅動的註冊。
兩個開源數據庫的驅動居然如此類似,是否是其它的就不同呢!看下面是mckoi中的驅動類:
package com.mckoi;
public class JDBCDriver extends com.mckoi.database.jdbc.MDriver {
static {
com.mckoi.database.jdbc.MDriver.register();
}
public JDBCDriver() {
super();
// Or we could move driver registering here...
}
}
紅色的部分又是完成相同的工做――在類裝載完成前向驅動管理器註冊驅動,只不過此次是調用MDriver的register方法罷了。那麼該驅動創建鏈接是否也同樣呢,固然同樣啦!不信你看下面的代碼:
package com.mckoi.database.jdbc;
……
public class MDriver implements Driver {
……
private static boolean registered = false;
public synchronized static void register() {
if (registered == false) {
try {
java.sql.DriverManager.registerDriver(new MDriver());
registered = true;
}catch (SQLException e) {
e.printStackTrace();
}
}
}
public Connection connect(String url, Properties info) throws SQLException {
if (!acceptsURL(url)) { return null; }
DatabaseInterface db_interface;
int row_cache_size;
int max_row_cache_size;
……
MConnection connection = new MConnection(url, db_interface, row_cache_size, max_row_cache_size);
……
return connection;
}
}
從以上三個開源數據庫驅動的源代碼能夠看出:在調用Class.forName(「XXXDriver」)時,完成了將具體的驅動程序向JDBC API中驅動管理器的註冊,該註冊方法在類構造完成前完成,通常使用靜態語句塊。在調用DriverManager的getConnection方法時,通常先在已註冊的驅動中查找能夠了解此URL的驅動,而後調用該驅動的connect方法,從而創建鏈接,返回的鏈接都是一個實現java.sql.Connection接口的具體類。下面是該過程的時序圖。
以上是JDBC中驅動加載的時序圖。時序圖主要有如下7個動做:
1. 客戶調用Class.forName(「XXXDriver」)加載驅動。
2. 此時此驅動類首先在其靜態語句塊中初始化此驅動的實例,
3. 再向驅動管理器註冊此驅動。
4. 客戶向驅動管理器DriverManager調用getConnection方法,
5. DriverManager調用註冊到它上面的可以理解此URL的驅動創建一個鏈接,
6. 在該驅動中創建一個鏈接,通常會建立一個對應於數據庫提供商的XXXConnection鏈接對象,
7. 驅動向客戶返回此鏈接對象,不過在客戶調用的getConnection方法中返回的爲一個java.sql.Connection接口,而具體的驅動返回一個實現java.sql.Connection接口的具體類。
以上就是驅動加載的全過程。由此過程咱們能夠看出JDBC的其它一些特色。
在《教你創建簡單JDBC程序》一篇中,講述了通常JDBC的幾個步驟。經過本篇的介紹,我將此程序分爲如下幾部分:
上圖中,藍色的即爲本章前面介紹的JDBC驅動加載的細節部分。看看下面的部分:左面的很明顯吧!是java.sql包中的接口吧!它是抽象的!右邊呢?經過驅動管理器DriverManager獲得的是一個實現java.sql.Connection接口的具體類吧!(不知道啊!前面不是講過了嗎!)所以咱們能夠能夠注意到左右分別是抽象的和具體的。(這種抽象和具體的鏈接是由java的RTTI支持的,不懂能夠閱讀《Think in java》)。在接下來的結果集的處理rs也是抽象的吧!
所以,在寫JDBC程序時,即便咱們使用不一樣數據庫提供商的數據庫咱們只要改變驅動類的地址,和具體鏈接的URL及其用戶名和密碼,其它幾乎不用任何修改,就能夠完成一樣的工做!方便吧!
這意味着什麼呢?咱們實際上是在針對抽象接口編程,只要知道接口的調用順序,以及其中的主要方法,咱們就能夠迅速學會JDBC編程了!
同時,咱們只要對不一樣數據庫提供商的驅動類使用Class.forName(「XXXDriver」)就能夠加載驅動,其細節你根本不用關注――JDBC Framework已經全爲你搞定了!應用程序對於不一樣提供商的數據庫是無需太多改動的,於是其是「可插拔」的!整個J2EE就是一個高層的API――抽象接口,用戶可使用不一樣的產品提供商提供的產品,使用統一的API,從而便於程序員學習!
謝謝你們!到此結束!