JavaWeb的內容大致包括:

  • Tomcat(Tomcat外傳)
  • Servlet、JSP(淺談JSP)
  • Session、Cookie(Cookie與Session)
  • JDBC
  • Filter/Listener
  • AJAX、JSON(AJAX與JSON)

我記得18年6月開始JavaWeb系列的寫作,截止目前,還剩下JDBC和Filter/Listener。速度有點超乎預期地慢...不過隨著自己開發經驗的增長以及對知識點的理解,相信後面出活會越來越快。

JDBC本身是非常簡單的,就是一組API介面,只要導入對應的驅動包(JDBC實現類)就可以對資料庫進行增刪改查。但是其中涉及到很多問題,比如:

  • Connection的管理(連接池)
  • 冗餘代碼的抽取(獲取、釋放連接)
  • 結果集的封裝(映射Pojo欄位)

等等。不論培訓班視頻還是書籍,講到JDBC時都會把上面的內容串聯起來,這使得它的難度瞬時翻了幾番。

主要內容:

  • JDBC簡介
  • 基本環境搭建
  • Driver
  • DriverManager
  • 代碼優化

JDBC簡介

在說JDBC之前,必須先聊聊數據持久化。

持久化

把數據保存到可掉電式存儲設備中以供之後使用。

大多數情況下,數據持久化意味著將內存中的數據保存到磁碟中加以「固化」。而持久化的實現過程大多通過各種關係資料庫完成。當然,也可以存入磁碟文件或者XML數據文件(崔老師JavaWeb day14練習中,使用了XML充當「資料庫」)。

圖片來自尚矽谷JDBC

JDBC

資料庫是實現持久化的一種途徑,而JDBC則是通向資料庫的橋樑。

通俗地講,JDBC就是一組API(包括少量類),為訪問不同資料庫提供了統一的途徑,為開發者屏蔽了一些細節問題。比如,我們都知道瀏覽器發送HTTP請求訪問伺服器,但其實請求底層仍是TCP協議。同樣的,訪問資料庫底層也通過TCP協議。你知道怎麼與資料庫建立TCP連接嗎?一部分科班讀者可能對計算機網路非常熟悉,但是大部分像我這樣的野生程序員可能壓根沒想過這個問題。所幸,這些具體的實現,各大資料庫產商已經替我們做了。

驅動

JDBC是Java制定的介面,資料庫產商依照該介面編寫與自家資料庫配套的實現類。比如MySQL、Oracle、SqlServer等都有自己的不同實現,這些實現類的集合既是我們籠統意義上的「驅動」。

面向介面編程

在代碼中直接new具體的驅動類,會使程序高度耦合。比如,後期如果要切換資料庫(雖然很少),就要臨時調換驅動類,需要修改源碼,不符合開閉原則。而面向介面編程,實際上就是一種「多態」。屏蔽具體的實現,只需調用介面方法,傳入規定的參數即可得到預期的返回值。切換資料庫驅動並不影響程序運行結果。


基本環境搭建

上面說過,JDBC只是一組介面,具體實現交給驅動。所以,要使用JDBC完成CRUD,必須先導入具體的資料庫驅動。本次我們以MySQL為例,所以導入MySQL的資料庫驅動。

pom.xml

<dependencies>
<!--MySQL資料庫驅動-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<!--Junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>compile</scope>
</dependency>
</dependencies>

代碼結構

DriverDemo

public class DriverDemo {
@Test
public void testDriver() throws SQLException {
//1. 創建一個 Driver 實現類的對象
Driver driver = new com.mysql.jdbc.Driver();

//2. 準備連接資料庫的基本信息: url, user, password
String url = "jdbc:mysql://192.168.136.128:3306/test";
Properties info = new Properties();
info.put("user", "root");
info.put("password", "root");

//3. 調用 Driver 介面的 connect(url, info) 獲取資料庫連接
Connection connection = driver.connect(url, info);
System.out.println(connection);
}
}

運行結果

使用JDBC有三個大步驟:

  • 連接資料庫
  • 執行SQL語句
  • 獲得結果

我們上面的代碼,僅完成了第一步。


Driver

上面testDriver()方法中的第一句:

//1. 創建一個 Driver 實現類的對象
Driver driver = new com.mysql.jdbc.Driver();

這是一個典型的面向介面編程。

我們先看左邊的Driver介面:

介面的方法並不多,我們在上面用到了其中的connect()方法

接著我們再來看看右邊MySQL的Driver實現類:

怎麼回事?!

只有一個靜態代碼塊和無參構造。不對啊,我們明明在程序中調用了driver.connect(),怎麼連connect方法都沒了?!

仔細一看,原來MySQL的Driver類還繼承了NonRegisteringDriver,它實現了Driver介面的全部方法:

connect()的核心代碼就一句:

Connection newConn = com.mysql.jdbc.ConnectionImpl.getInstance(host(props), port(props), props, database(props), url);

總之,就是根據給定的url和用戶名密碼,返回一個與資料庫關聯的Connection。JDBC相當於是程序與資料庫之間的橋樑。得到Connect代表橋已經建好,此刻已經可以通車了。

由於com.mysql.jdbc.Driver繼承了NonRegisteringDriver,所以它也可以調用connect方法(繼承父類方法),上面我們已經用過,確實可行。

現在我們把注意力集中到com.mysql.jdbc.Driver中獨有的代碼:靜態代碼塊+無參構造。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Cant register driver!");
}
}

/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}

空參構造的作用,僅僅是為了能夠new com.mysql.jdbc.Driver()。但重點不在於new本身,而是new這個Driver的時候,JVM會去載入這個Driver實現類。此時會自動執行靜態代碼塊中的代碼,也就是:

java.sql.DriverManager.registerDriver(new Driver());

注釋已經說得很明白,就是把這個Driver註冊到DriverManager。

所以,我們使用Driver獲取連接的整體邏輯是:

  • new com.mysql.jdbc.Driver()
  • 類被載入進內存
  • 靜態代碼塊執行:java.sql.DriverManager.registerDriver(newDriver());
  • com.mysql.jdbc.driver實例被註冊進DriverManager
  • 調用com.mysql.jdbc.driver實例的connect()方法獲得Connection

new com.mysql.jdbc.driver();最終目的是為了driver.connect()獲得Connection,這一點很合理。但是為啥還要在類中搞一個靜態代碼塊去註冊驅動?沒有這一步,我不照樣獲得Connection嗎?


DriverManager

其實,之所以com.mysql.jdbc.Driver類中的靜態代碼塊要去註冊驅動,是因為獲取Connection通常有兩種辦法:

  • 根據具體的Driver實現類直接獲取
  • 通過驅動管理器獲取

通常後者更常用。而所謂的驅動管理器,就是DriverManager。通過DriverManager.getConnection()即可獲取。

此刻,我們發現腦中瞬間多了兩個疑問:

  1. DriverManager是個啥呀?咋還能getConnection()了呢?
  2. 還有,將Driver註冊進DriverManager又能咋地?driver.connect()不也好使嗎,照樣獲得了Connection,為啥要整出一個DriverManager?

不急,先上代碼,看看DriverManager如何獲得Connection。

@Test
public void testDriverManager() throws Exception{
//1. 驅動的全類名
String driverClass = "com.mysql.jdbc.Driver";
//2. 準備連接資料庫的基本信息: url, user, password
String url = "jdbc:mysql://192.168.136.128:3306/test";
String user = "root";
String password = "root";

//2. 載入資料庫驅動程序(對應的 Driver 實現類中有註冊驅動的靜態代碼塊)
Class.forName(driverClass);

//3. 通過 DriverManager 的 getConnection() 方法獲取資料庫連接
Connection connection =
DriverManager.getConnection(url, user, password);
System.out.println(connection);

}

上面示例中,最關鍵的一句代碼是:

//2. 載入資料庫驅動程序(對應的 Driver 實現類中有註冊驅動的靜態代碼塊)
Class.forName(driverClass);

JVM何時會載入一個類呢?

大體可以歸為兩種情況:

被動載入(Driver方式)

//JVM發現要new一個com.mysql.jdbc.Driver實例,而此時內存中沒有com.mysql.jdbc.Driver位元組碼對象,就會去載入該類
Driver driver = new com.mysql.jdbc.Driver();

主動載入(DriverManager方式)

//通過Class對象主動觸發JVM載入com.mysql.jdbc.Driver類
Class.forName("com.mysql.jdbc.Driver");

我們已經分析過,不論是主動還是被動,只要類被載入,靜態代碼塊都會執行執行。也就是說,com.mysql.jdbc.Driver都會被註冊進DriverManager。

Driver方式獲取Connection確實和「註冊」操作沒有直接關係,但是DriverManager方式與「註冊」關係密切。我們來分析一下源碼。

我們之前已經查看過com.mysql.jdbc.Driver的源碼,知道了connect()方法的具體實現(繼承自NonRegisteringDriver):

這一句代碼,底層屏蔽了好多細節,直接連上資料庫返回了Connection

也知道了com.mysql.jdbc.Driver會通過靜態代碼塊,將自己註冊到了DriverManager。

那麼,問題就來了:driver.connect()和DriverManager.getConnection()都可以獲得連接,它們之間有什麼聯繫嗎?

1.DriverManager內部維護一個容器,該容器存儲所有已註冊的Driver
2.通過DriverManager也可以得到連接

來看一下這個同名的getConnection()方法具體做了啥:

//私有方法,只能內部調用:獲取資料庫連接
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//callerCl是類載入器
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;

//循環遍歷容器中所有已註冊的Driver
for(DriverInfo aDriver : registeredDrivers) {
//isDriverAllowed()方法會使用callerCl嘗試載入每一個Driver,載入成功返回true
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
//底層還是使用了Driver本身的connect方法,獲取Connection
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");
}

//載入驅動類,載入失敗返回false
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;
}

小結

DriverManager是個啥呀?咋還能getConnection()了呢?

DriverManager是java.sql包下的一個類,內部維護著一個CopyOnWriteArrayList用於存放已經註冊的驅動實例。之所以能getConnection(),是因為底層會循環遍歷所有驅動,找到當前註冊的驅動後調用driver.connect()獲得Connection。

還有,將Driver註冊進DriverManager又能咋地?driver.connect()不也好使嗎,照樣獲得了Connection,為啥要整出一個DriverManager?

其實也沒為啥,人家設計的初衷就是為了支持註冊多個(多種)驅動,通過DriverManager可以管理多個驅動程序。所以它叫「驅動管理器」(DriverManager)。

靜態代碼塊註冊驅動到DriverManager對於Driver方式獲取Connection是多餘的,但是對於DriverManager獲取Connection是必須的。


代碼優化

把url、user、password、driver移到jdbc.properties文件中,做成可配置。

@Test
public void getConnection() throws Exception{
//1. 準備連接資料庫的 4 個字元串.
//1). 創建 Properties 對象
Properties properties = new Properties();

//2). 獲取 jdbc.properties 對應的輸入流
InputStream in =
this.getClass().getClassLoader().getResourceAsStream("jdbc.properties");

//3). 載入 2) 對應的輸入流
properties.load(in);

//4). 具體決定 user, password 等4 個字元串.
String user = properties.getProperty("user");
String password = properties.getProperty("password");
String jdbcUrl = properties.getProperty("jdbcUrl");
String driver = properties.getProperty("driver");

//2. 載入資料庫驅動程序(對應的 Driver 實現類中有註冊驅動的靜態代碼塊)
Class.forName(driver);

//3. 通過 DriverManager 的 getConnection() 方法獲取資料庫連接
Connection connection = DriverManager.getConnection(jdbcUrl, user, password);

System.out.println(connection);
}

JDBC的三大步驟:

  • 連接資料庫
  • 執行SQL語句
  • 獲得結果

這一篇只涉及如何獲取Connection,下篇才真正開始CRUD以及代碼抽取。

最後,我自己看源碼時,發現一個問題:Driver介面有個getParentLogger(),但是NonRegisteringDriver並沒有實現這個方法(其他方法都實現了)。

這是為啥呢?

打開源碼,IDEA提示錯誤...

2019-5-5 20:42:45

推薦閱讀:

相关文章