JavaWeb的內容大致包括:
我記得18年6月開始JavaWeb系列的寫作,截止目前,還剩下JDBC和Filter/Listener。速度有點超乎預期地慢...不過隨著自己開發經驗的增長以及對知識點的理解,相信後面出活會越來越快。
JDBC本身是非常簡單的,就是一組API介面,只要導入對應的驅動包(JDBC實現類)就可以對資料庫進行增刪改查。但是其中涉及到很多問題,比如:
等等。不論培訓班視頻還是書籍,講到JDBC時都會把上面的內容串聯起來,這使得它的難度瞬時翻了幾番。
主要內容:
在說JDBC之前,必須先聊聊數據持久化。
持久化
把數據保存到可掉電式存儲設備中以供之後使用。
大多數情況下,數據持久化意味著將內存中的數據保存到磁碟中加以「固化」。而持久化的實現過程大多通過各種關係資料庫完成。當然,也可以存入磁碟文件或者XML數據文件(崔老師JavaWeb day14練習中,使用了XML充當「資料庫」)。
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有三個大步驟:
我們上面的代碼,僅完成了第一步。
上面testDriver()方法中的第一句:
//1. 創建一個 Driver 實現類的對象 Driver driver = new com.mysql.jdbc.Driver();
這是一個典型的面向介面編程。
我們先看左邊的Driver介面:
接著我們再來看看右邊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();最終目的是為了driver.connect()獲得Connection,這一點很合理。但是為啥還要在類中搞一個靜態代碼塊去註冊驅動?沒有這一步,我不照樣獲得Connection嗎?
DriverManager
其實,之所以com.mysql.jdbc.Driver類中的靜態代碼塊要去註冊驅動,是因為獲取Connection通常有兩種辦法:
通常後者更常用。而所謂的驅動管理器,就是DriverManager。通過DriverManager.getConnection()即可獲取。
此刻,我們發現腦中瞬間多了兩個疑問:
不急,先上代碼,看看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);
}
上面示例中,最關鍵的一句代碼是:
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):
也知道了com.mysql.jdbc.Driver會通過靜態代碼塊,將自己註冊到了DriverManager。
那麼,問題就來了:driver.connect()和DriverManager.getConnection()都可以獲得連接,它們之間有什麼聯繫嗎?
來看一下這個同名的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的三大步驟:
這一篇只涉及如何獲取Connection,下篇才真正開始CRUD以及代碼抽取。
最後,我自己看源碼時,發現一個問題:Driver介面有個getParentLogger(),但是NonRegisteringDriver並沒有實現這個方法(其他方法都實現了)。
這是為啥呢?
2019-5-5 20:42:45
推薦閱讀: