JDBC對于java的開發人員來說并不陌生,它封裝了ODBC,簡化了java連接配接資料庫的開發,本文我從部分JDBC的源碼入手來闡述一下JDBC。
橋接模式是由Gang of four整理的23種設計模式中的一種,JDBC是橋接模式一個典型的例子。了解JDBC源碼,能讓我們更好地了解橋接模式的意圖和實作;了解橋接模式,能讓我們更清楚JDBC設計的優越之處。
首先我們先來看下橋接模式的意圖,它旨在将抽象部分與實作部分分離,使其可以獨立地變化,廢話不多說,我們直接看一張UML圖:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiclRnblN0LclHdpZXYyd2LcBzNvwVZ2x2bzNXak9CX90TQNNkRrFlQKBTSvwFbslmZvwFMwQzLcVmepNHdu9mZvwFVywUNMZTY18CX052bm9CX9sGROhXRE5UenpWTmZEWjZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39TOzIDNykjMwEjNxETM3EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
其實最初我在看這句意圖的時候非常的郁悶,作為一名開發人員,對抽象與實作的第一反應就是接口實作或者類繼承,接口和實作類獨立地變化本身就是一件讓我覺得很郁悶的事情,但是看了這張UML圖之後便恍然大悟了,這裡的抽象與實作是聚合關系,也就是我們口頭常說的調用者和被調用者,這樣一來橋接模式的意圖就很清楚了,其實某種意義上就是解耦了某個功能的抽象定義和具體實作,讓其變成了兩個互相獨立的子產品。
讓我們回到JDBC的源碼,首先請大家打開JDK API,翻到java.sql包:
可以發現,java.sql包不同于其他幾乎所有的包,它基本可以說沒有定義多少類,但是定義了大量的接口,而由各個資料庫公司去寫這些接口的實作類:
可以看到,sum公司僅僅是提出了一系列接口規範,資料庫公司做出實作,然後當你真的需要連接配接某個資料庫時,你需要先添加資料庫公司釋出的jar包,如jdbc-mysql.jar,然後在使用時先加載Driver:
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
System.out.println("找不到驅動程式類 ,加載驅動失敗!");
e.printStackTrace();
}
然後就可以通過DriverManager去擷取連接配接:
try {
conn = (Connection) DriverManager.getConnection(url , username , password );
} catch (SQLException e) {
System.out.println("資料庫連接配接失敗!");
e.printStackTrace();
} try {
conn = (Connection) DriverManager.getConnection(url , username , password );
} catch (SQLException e) {
System.out.println("資料庫連接配接失敗!");
e.printStackTrace();
}
這裡的conn的定義依舊是以java.sql中定義的接口來聲明的,也就是說,我們在代碼中依舊使用的是接口,隻不過通過了最初的加載Driver讓添加的jar包中的類成為了真正的實作,但是這個加載的過程究竟是怎麼實作的呢???
這裡需要牽扯到一個别的知識,Class.forName和ClassLoader類都能用于加載java類獲得Class對象,那這二者究竟有什麼差別呢?
答案是,ClassLoader隻能将類填放進入JVM方法區,在Class.newInstance的時候才會加載類中的代碼塊,但是Class.forName可以直接加載類中的靜态代碼塊,然後我們來看下com.mysql.jdbc.Driver類:
static
{
try
{
DriverManager.registerDriver(new Driver());
}
catch(SQLException E)
{
throw new RuntimeException("Can't register driver!");
}
}
類在被Class.forName的時候就加載了這一塊代碼塊,将Driver類本身的對象注冊進入DriverManager
/**
* Registers the given driver with the {@code DriverManager}.
* A newly-loaded driver class should call
* the method {@code registerDriver} to make itself
* known to the {@code DriverManager}. If the driver is currently
* registered, no action is taken.
*
* @param driver the new JDBC Driver that is to be registered with the
* {@code DriverManager}
* @param da the {@code DriverAction} implementation to be used when
* {@code DriverManager#deregisterDriver} is called
* @exception SQLException if a database access error occurs
* @exception NullPointerException if {@code driver} is null
* @since 1.8
*/
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);
}
可以看到注冊的過程無非是把Driver對象存放進入了變量registeredDrivers中,這個變量原本是個List,具體不說這個變量了。
而當我們getConnection的時候:
// Worker method called by the public getConnection() methods.
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
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 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!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// if we got here nobody could connect.
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");
}
他将會周遊所有注冊的driver,并從driver加載connection,
回到com.mysql.jdbc.Driver,我們可以看到它繼承了NonRegisteringDriver類,然後這個類中實作了connect方法:
public Connection connect(String url, Properties info)
throws SQLException
{
if(url != null)
{
if(StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:loadbalance://"))
return connectLoadBalanced(url, info);
if(StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:replication://"))
return connectReplicationConnection(url, info);
}
Properties props = null;
if((props = parseURL(url, info)) == null)
return null;
if(!"1".equals(props.getProperty("NUM_HOSTS")))
return connectFailover(url, info);
try
{
com.mysql.jdbc.Connection newConn = ConnectionImpl.getInstance(host(props), port(props), props, database(props), url);
return newConn;
}
catch(SQLException sqlEx)
{
throw sqlEx;
}
catch(Exception ex)
{
SQLException sqlEx = SQLError.createSQLException((new StringBuilder()).append(Messages.getString("NonRegisteringDriver.17")).append(ex.toString()).append(Messages.getString("NonRegisteringDriver.18")).toString(), "08001", null);
sqlEx.initCause(ex);
throw sqlEx;
}
}
至此整個加載流程應該已經清楚了,在Class.forName的過程中執行了靜态代碼塊,在靜态代碼塊中注冊了Driver對象,在getConnection中取出注冊的Driver并調用其connect方法,在mysql中connect方法的傳回值是com.mysql.jdbc.Connection類的對象了,但是我們在調用過程中依舊是用其接口定義,這裡充分運用了面向對象多态的性質。
再描述一遍,jdbc的類族設計是由sum公司設計了一套接口,再由各個資料庫公司實作接口,我們在調用的過程中隻需要使用接口去定義,然後在加載Driver的過程中底層代碼會給我們選擇好接口真正的實作類,以此來實作真正的資料庫連接配接,此後所有的方法,包括擷取statement等等,都是由接口聲明調用,但是底層傳回的是接口實作類。用這種橋接的模式,我們可以很輕松地在不同的資料庫連接配接中進行轉化,隻需要修改Driver加載的類,如果把加載類的聲明放入配置檔案中,更是不需要重新去編譯,可以很友善地在不同資料庫間進行轉化。
其實這樣的java設計規範由别的公司進行實作的設計非常多,比如j2ee規範,假設沒有j2ee規範,每個web容器各有各的接口,那麼當我們需要把tomcat下跑的代碼移植到jboss下的時候就不得不把代碼全部重寫一遍,這無疑是很不合理的,是以接口雖然沒有做出任何實作,但是其規範的作用在這浩瀚的IT世界裡卻是不可缺少的。