MyBatis 源碼分析 - 內置數據源(一)

MyBatis 源碼分析 - 內置數據源(一)

來自專欄猿論7 人贊了文章

1.簡介

本篇文章將向大家介紹 MyBatis 內置數據源的實現邏輯。搞懂這些數據源的實現,可使大家對數據源有更深入的認識。同時在配置這些數據源時,也會更清楚每種屬性的意義和用途。因此,如果大家想知其然,也知其所以然。那麼接下來就讓我們一起去探索 MyBatis 內置數據源的源碼吧。

MyBatis 支持三種數據源配置,分別為 UNPOOLED、POOLED 和 JNDI。並提供了兩種數據源實現,分別是 UnpooledDataSource 和 PooledDataSource。在三種數據源配置中,UNPOOLED 和 POOLED 是我們最常用的兩種配置。至於 JNDI,MyBatis 提供這種數據源的目的是為了讓其能夠運行在 EJB 或應用伺服器等容器中,這一點官方文檔中有所說明。由於 JNDI 數據源在日常開發中使用甚少,因此,本篇文章不打算分析 JNDI 數據源相關實現。大家若有興趣,可自行分析。接下來,本文將重點分析 UNPOOLED 和 POOLED 兩種數據源。其他的就不多說了,進入正題吧。

2.內置數據源初始化過程

在詳細分析 UnpooledDataSource 和 PooledDataSource 兩種數據源實現之前,我們先來了解一下數據源的配置與初始化過程。現在看數據源是如何配置的,如下:

<dataSource type="UNPOOLED|POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql..."/> <property name="username" value="root"/> <property name="password" value="1234"/></dataSource>

數據源的配置是內嵌在 <environment> 節點中的,MyBatis 在解析 <environment> 節點時,會一併解析數據源的配置。MyBatis 會根據具體的配置信息,為不同的數據源創建相應工廠類,通過工廠類即可創建數據源實例。關於數據源配置的解析以及數據源工廠類的創建過程,我在 MyBatis 配置文件解析過程一文中分析過,這裡就不贅述了。下面我們來看一下數據源工廠類的實現邏輯。

public class UnpooledDataSourceFactory implements DataSourceFactory { private static final String DRIVER_PROPERTY_PREFIX = "driver."; private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length(); protected DataSource dataSource; public UnpooledDataSourceFactory() { // 創建 UnpooledDataSource 對象 this.dataSource = new UnpooledDataSource(); } @Override public void setProperties(Properties properties) { Properties driverProperties = new Properties(); // 為 dataSource 創建元信息對象 MetaObject metaDataSource = SystemMetaObject.forObject(dataSource); // 遍歷 properties 鍵列表,properties 由配置文件解析器傳入 for (Object key : properties.keySet()) { String propertyName = (String) key; // 檢測 propertyName 是否以 "driver." 開頭 if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) { String value = properties.getProperty(propertyName); // 存儲配置信息到 driverProperties 中 driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value); } else if (metaDataSource.hasSetter(propertyName)) { String value = (String) properties.get(propertyName); // 按需轉換 value 類型 Object convertedValue = convertValue(metaDataSource, propertyName, value); // 設置轉換後的值到 UnpooledDataSourceFactory 指定屬性中 metaDataSource.setValue(propertyName, convertedValue); } else { throw new DataSourceException("Unknown DataSource property: " + propertyName); } } if (driverProperties.size() > 0) { // 設置 driverProperties 到 UnpooledDataSourceFactory 的 driverProperties 屬性中 metaDataSource.setValue("driverProperties", driverProperties); } } private Object convertValue(MetaObject metaDataSource, String propertyName, String value) { Object convertedValue = value; // 獲取屬性對應的 setter 方法的參數類型 Class<?> targetType = metaDataSource.getSetterType(propertyName); // 按照 setter 方法的參數類型進行類型轉換 if (targetType == Integer.class || targetType == int.class) { convertedValue = Integer.valueOf(value); } else if (targetType == Long.class || targetType == long.class) { convertedValue = Long.valueOf(value); } else if (targetType == Boolean.class || targetType == boolean.class) { convertedValue = Boolean.valueOf(value); } return convertedValue; } @Override public DataSource getDataSource() { return dataSource; }}

以上是 UnpooledDataSourceFactory 的源碼分析,除了 setProperties 方法稍複雜一點,其他的都比較簡單,就不多說了。下面看看 PooledDataSourceFactory 的源碼。

public class PooledDataSourceFactory extends UnpooledDataSourceFactory { public PooledDataSourceFactory() { // 創建 PooledDataSource this.dataSource = new PooledDataSource(); }}

以上就是 PooledDataSource 類的所有源碼,PooledDataSourceFactory 繼承自 UnpooledDataSourceFactory,復用了父類的邏輯,因此它的實現很簡單。

關於兩種數據源的創建過程就先分析到這,接下來,我們去探究一下兩種數據源是怎樣實現的。

3.UnpooledDataSource

UnpooledDataSource,從名稱上即可知道,該種數據源不具有池化特性。該種數據源每次會返回一個新的資料庫連接,而非復用舊的連接。由於 UnpooledDataSource 無需提供連接池功能,因此它的實現非常簡單。核心的方法有三個,分別如下:

  1. initializeDriver - 初始化資料庫驅動
  2. doGetConnection - 獲取數據連接
  3. configureConnection - 配置資料庫連接

下面我將按照順序分節對相關方法進行分析,由於 configureConnection 方法比較簡單,因此我把它和 doGetConnection 放在一節中進行分析。下面先來分析 initializeDriver 方法。

3.1 初始化資料庫驅動

回顧我們一開始學習使用 JDBC 訪問資料庫時的情景,在執行 SQL 之前,通常都是先獲取資料庫連接。一般步驟都是載入資料庫驅動,然後通過 DriverManager 獲取資料庫連接。UnpooledDataSource 也是使用 JDBC 訪問資料庫的,因此它獲取資料庫連接的過程也大致如此,只不過會稍有不同。下面我們一起來看一下。

// -- UnpooledDataSourceprivate synchronized void initializeDriver() throws SQLException { // 檢測緩存中是否包含了與 driver 對應的驅動實例 if (!registeredDrivers.containsKey(driver)) { Class<?> driverType; try { // 載入驅動類型 if (driverClassLoader != null) { // 使用 driverClassLoader 載入驅動 driverType = Class.forName(driver, true, driverClassLoader); } else { // 通過其他 ClassLoader 載入驅動 driverType = Resources.classForName(driver); } // 通過反射創建驅動實例 Driver driverInstance = (Driver) driverType.newInstance(); /* * 註冊驅動,注意這裡是將 Driver 代理類 DriverProxy 對象註冊到 DriverManager 中的, * 而非 Driver 對象本身。DriverProxy 中並沒什麼特別的邏輯,就不分析。 */ DriverManager.registerDriver(new DriverProxy(driverInstance)); // 緩存驅動類名和實例 registeredDrivers.put(driver, driverInstance); } catch (Exception e) { throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e); } }}

如上,initializeDriver 方法主要包含三步操作,分別如下:

  1. 載入驅動
  2. 通過反射創建驅動實例
  3. 註冊驅動實例

這三步都是都是常規操作,比較容易理解。上面代碼中出現了緩存相關的邏輯,這個是用於避免重複註冊驅動。因為 initializeDriver 放阿飛並不是在 UnpooledDataSource 初始化時被調用的,而是在獲取資料庫連接時被調用的。因此這裡需要做個檢測,避免每次獲取資料庫連接時都重新註冊驅動。這個是一個比較小的點,大家看代碼時注意一下即可。下面看一下獲取資料庫連接的邏輯。

3.2 獲取資料庫連接

在使用 JDBC 時,我們都是通過 DriverManager 的介面方法獲取資料庫連接。本節所要分析的源碼也不例外,一起看一下吧。

// -- UnpooledDataSourcepublic Connection getConnection() throws SQLException { return doGetConnection(username, password);}private Connection doGetConnection(String username, String password) throws SQLException { Properties props = new Properties(); if (driverProperties != null) { props.putAll(driverProperties); } if (username != null) { // 存儲 user 配置 props.setProperty("user", username); } if (password != null) { // 存儲 password 配置 props.setProperty("password", password); } // 調用重載方法 return doGetConnection(props);}private Connection doGetConnection(Properties properties) throws SQLException { // 初始化驅動 initializeDriver(); // 獲取連接 Connection connection = DriverManager.getConnection(url, properties); // 配置連接,包括自動提交以及事務等級 configureConnection(connection); return connection;}private void configureConnection(Connection conn) throws SQLException { if (autoCommit != null && autoCommit != conn.getAutoCommit()) { // 設置自動提交 conn.setAutoCommit(autoCommit); } if (defaultTransactionIsolationLevel != null) { // 設置事務隔離級別 conn.setTransactionIsolation(defaultTransactionIsolationLevel); }}

如上,上面方法將一些配置信息放入到 Properties 對象中,然後將資料庫連接和 Properties 對象傳給 DriverManager 的 getConnection 方法即可獲取到資料庫連接。

好了,關於 UnpooledDataSource 就先說到這。下面分析一下 PooledDataSource,它的實現要複雜一些。

4.PooledDataSource

PooledDataSource 內部實現了連接池功能,用於復用資料庫連接。因此,從效率上來說,PooledDataSource 要高於 UnpooledDataSource。PooledDataSource 需要藉助一些輔助類幫助它完成連接池的功能,所以接下來,我們先來認識一下相關的輔助類。

4.1 輔助類介紹

PooledDataSource 需要藉助兩個輔助類幫其完成功能,這兩個輔助類分別是 PoolState 和 PooledConnection。PoolState 用於記錄連接池運行時的狀態,比如連接獲取次數,無效連接數量等。同時 PoolState 內部定義了兩個 PooledConnection 集合,用於存儲空閑連接和活躍連接。PooledConnection 內部定義了一個 Connection 類型的變數,用於指向真實的資料庫連接。以及一個 Connection 的代理類,用於對部分方法調用進行攔截。至於為什麼要攔截,隨後將進行分析。除此之外,PooledConnection 內部也定義了一些欄位,用於記錄資料庫連接的一些運行時狀態。接下來,我們來看一下 PooledConnection 的定義。

class PooledConnection implements InvocationHandler { private static final String CLOSE = "close"; private static final Class<?>[] IFACES = new Class<?>[]{Connection.class}; private final int hashCode; private final PooledDataSource dataSource; // 真實的資料庫連接 private final Connection realConnection; // 資料庫連接代理 private final Connection proxyConnection; // 從連接池中取出連接時的時間戳 private long checkoutTimestamp; // 資料庫連接創建時間 private long createdTimestamp; // 資料庫連接最後使用時間 private long lastUsedTimestamp; // connectionTypeCode = (url + username + password).hashCode() private int connectionTypeCode; // 表示連接是否有效 private boolean valid; public PooledConnection(Connection connection, PooledDataSource dataSource) { this.hashCode = connection.hashCode(); this.realConnection = connection; this.dataSource = dataSource; this.createdTimestamp = System.currentTimeMillis(); this.lastUsedTimestamp = System.currentTimeMillis(); this.valid = true; // 創建 Connection 的代理類對象 this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {...} // 省略部分代碼}

下面再來看看 PoolState 的定義。

public class PoolState { protected PooledDataSource dataSource; // 空閑連接列表 protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>(); // 活躍連接列表 protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>(); // 從連接池中獲取連接的次數 protected long requestCount = 0; // 請求連接總耗時(單位:毫秒) protected long accumulatedRequestTime = 0; // 連接執行時間總耗時 protected long accumulatedCheckoutTime = 0; // 執行時間超時的連接數 protected long claimedOverdueConnectionCount = 0; // 超時時間累加值 protected long accumulatedCheckoutTimeOfOverdueConnections = 0; // 等待時間累加值 protected long accumulatedWaitTime = 0; // 等待次數 protected long hadToWaitCount = 0; // 無效連接數 protected long badConnectionCount = 0;}

上面對 PooledConnection 和 PoolState 的定義進行了一些注釋,這兩個類中有很多欄位用來記錄運行時狀態。但在這些欄位並非核心,因此大家知道每個欄位的用途就行了。關於這兩個輔助類的介紹就先到這

4.2 獲取連接

前面已經說過,PooledDataSource 會將用過的連接進行回收,以便可以復用連接。因此從 PooledDataSource 獲取連接時,如果空閑鏈接列表裡有連接時,可直接取用。那如果沒有空閑連接怎麼辦呢?此時有兩種解決辦法,要麼創建新連接,要麼等待其他連接完成任務。具體怎麼做,需視情況而定。下面我們深入到源碼中一探究竟。

public Connection getConnection() throws SQLException { // 返回 Connection 的代理對象 return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();}private PooledConnection popConnection(String username, String password) throws SQLException { boolean countedWait = false; PooledConnection conn = null; long t = System.currentTimeMillis(); int localBadConnectionCount = 0; while (conn == null) { synchronized (state) { // 檢測空閑連接集合(idleConnections)是否為空 if (!state.idleConnections.isEmpty()) { // idleConnections 不為空,表示有空閑連接可以使用 conn = state.idleConnections.remove(0); } else { /* * 暫無空閑連接可用,但如果活躍連接數還未超出限制 *(poolMaximumActiveConnections),則可創建新的連接 */ if (state.activeConnections.size() < poolMaximumActiveConnections) { // 創建新連接 conn = new PooledConnection(dataSource.getConnection(), this); } else { // 連接池已滿,不能創建新連接 // 取出運行時間最長的連接 PooledConnection oldestActiveConnection = state.activeConnections.get(0); // 獲取運行時長 long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); // 檢測運行時長是否超出限制,即超時 if (longestCheckoutTime > poolMaximumCheckoutTime) { // 累加超時相關的統計欄位 state.claimedOverdueConnectionCount++; state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime; state.accumulatedCheckoutTime += longestCheckoutTime; // 從活躍連接集合中移除超時連接 state.activeConnections.remove(oldestActiveConnection); // 若連接未設置自動提交,此處進行回滾操作 if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { try { oldestActiveConnection.getRealConnection().rollback(); } catch (SQLException e) {...} } /* * 創建一個新的 PooledConnection,注意, * 此處復用 oldestActiveConnection 的 realConnection 變數 */ conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); /* * 復用 oldestActiveConnection 的一些信息,注意 PooledConnection 中的 * createdTimestamp 用於記錄 Connection 的創建時間,而非 PooledConnection * 的創建時間。所以這裡要復用原連接的時間信息。 */ conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp()); conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp()); // 設置連接為無效狀態 oldestActiveConnection.invalidate(); } else { // 運行時間最長的連接並未超時 try { if (!countedWait) { state.hadToWaitCount++; countedWait = true; } long wt = System.currentTimeMillis(); // 當前線程進入等待狀態 state.wait(poolTimeToWait); state.accumulatedWaitTime += System.currentTimeMillis() - wt; } catch (InterruptedException e) { break; } } } } if (conn != null) { /* * 檢測連接是否有效,isValid 方法除了會檢測 valid 是否為 true, * 還會通過 PooledConnection 的 pingConnection 方法執行 SQL 語句, * 檢測連接是否可用。pingConnection 方法的邏輯不複雜,大家可以自行分析。 * 另外,官方文檔在介紹 POOLED 類型數據源時,也介紹了連接有效性檢測方面的 * 屬性,有三個:poolPingQuery,poolPingEnabled 和 * poolPingConnectionsNotUsedFor。關於這三個屬性,大家可以查閱官方文檔 */ if (conn.isValid()) { if (!conn.getRealConnection().getAutoCommit()) { // 進行回滾操作 conn.getRealConnection().rollback(); } conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password)); // 設置統計欄位 conn.setCheckoutTimestamp(System.currentTimeMillis()); conn.setLastUsedTimestamp(System.currentTimeMillis()); state.activeConnections.add(conn); state.requestCount++; state.accumulatedRequestTime += System.currentTimeMillis() - t; } else { // 連接無效,此時累加無效連接相關的統計欄位 state.badConnectionCount++; localBadConnectionCount++; conn = null; if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) { throw new SQLException(...); } } } } } if (conn == null) { throw new SQLException(...); } return conn;}

上面代碼冗長,過程比較複雜,下面把代碼邏輯梳理一下。從連接池中獲取連接首先會遇到兩種情況:

  1. 連接池中有空閑連接
  2. 連接池中無空閑連接

對於第一種情況,處理措施就很簡單了,把連接取出返回即可。對於第二種情況,則要進行細分,會有如下的情況。

  1. 活躍連接數沒有超出最大活躍連接數
  2. 活躍連接數超出最大活躍連接數

對於上面兩種情況,第一種情況比較好處理,直接創建新的連接即可。至於第二種情況,需要再次進行細分。

  1. 活躍連接的運行時間超出限制,即超時了
  2. 活躍連接未超時

對於第一種情況,我們直接將超時連接強行中斷,並進行回滾,然後復用部分欄位重新創建 PooledConnection 即可。對於第二種情況,目前沒有更好的處理方式了,只能等待了。下面用一段偽代碼演示各種情況及相應的處理措施,如下:

if (連接池中有空閑連接) { 1. 將連接從空閑連接集合中移除} else { if (活躍連接數未超出限制) { 1. 創建新連接 } else { 1. 從活躍連接集合中取出第一個元素 2. 獲取連接運行時長 if (連接超時) { 1. 將連接從活躍集合中移除 2. 復用原連接的成員變數,並創建新的 PooledConnection 對象 } else { 1. 線程進入等待狀態 2. 線程被喚醒後,重新執行以上邏輯 } }}1. 將連接添加到活躍連接集合中2. 返回連接

最後用一個流程圖大致描繪 popConnection 的邏輯,如下:

4.3 回收連接

相比於獲取連接,回收連接的邏輯要簡單的多。回收連接成功與否只取決於空閑連接集合的狀態,所需處理情況很少,因此比較簡單。下面看一下相關的邏輯。

protected void pushConnection(PooledConnection conn) throws SQLException { synchronized (state) { // 從活躍連接池中移除連接 state.activeConnections.remove(conn); if (conn.isValid()) { // 空閑連接集合未滿 if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) { state.accumulatedCheckoutTime += conn.getCheckoutTime(); // 回滾未提交的事務 if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } // 創建新的 PooledConnection PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this); state.idleConnections.add(newConn); // 復用時間信息 newConn.setCreatedTimestamp(conn.getCreatedTimestamp()); newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp()); // 將原連接置為無效狀態 conn.invalidate(); // 通知等待的線程 state.notifyAll(); } else { // 空閑連接集合已滿 state.accumulatedCheckoutTime += conn.getCheckoutTime(); // 回滾未提交的事務 if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } // 關閉資料庫連接 conn.getRealConnection().close(); conn.invalidate(); } } else { state.badConnectionCount++; } }}

上面代碼首先將連接從活躍連接集合中移除,然後再根據空閑集合是否有空閑空間進行後續處理。如果空閑集合未滿,此時復用原連接的欄位信息創建新的連接,並將其放入空閑集合中即可。若空閑集合已滿,此時無需回收連接,直接關閉即可。pushConnection 方法的邏輯並不複雜,就不多說了。

我們知道獲取連接的方法 popConnection 是由 getConnection 方法調用的,那回收連接的方法 pushConnection 是由誰調用的呢?答案是 PooledConnection 中的代理邏輯。相關代碼如下:

// -- PooledConnectionpublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); // 檢測 close 方法是否被調用,若被調用則攔截之 if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { // 將回收連接中,而不是直接將連接關閉 dataSource.pushConnection(this); return null; } else { try { if (!Object.class.equals(method.getDeclaringClass())) { checkConnection(); } // 調用真實連接的目標方法 return method.invoke(realConnection, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } }}

在上一節中,getConnection 方法返回的是 Connection 代理對象,不知道大家有沒有注意到。代理對象中的方法被調用時,會被上面的代理邏輯所攔截。如果代理對象的 close 方法被調用,MyBatis 並不會直接調用真實連接的 close 方法關閉連接,而是調用 pushConnection 方法回收連接。同時會喚醒處於睡眠中的線程,使其恢復運行。整個過程並不複雜,就不多說了。

4.4 小節

本章分析了 PooledDataSource 的部分源碼及一些輔助類的源碼,除此之外,PooledDataSource 中還有部分源碼沒有分析,大家若有興趣,可自行分析。好了,關於 PooledDataSource 的分析就先到這。

5.總結

本篇文章對 MyBatis 兩種內置數據源進行了較為詳細的分析,總的來說,這兩種數據源的源碼都不是很難理解。大家在閱讀源碼的過程中,首先應搞懂源碼的主要邏輯,然後再去分析一些邊邊角角的邏輯。不要一開始就陷入各種細節中,容易迷失方向。

好了,到此本文就結束了。若文章有錯誤不妥之處,希望大家指明。最後,感謝大家閱讀我的文章。

本作品採用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議進行許可。

作者:田小波

鏈接:imooc.com/article/70184

來源:慕課網

本文原創發佈於慕課網 ,轉載請註明出處,謝謝合作


推薦閱讀:

有哪些好笑的關於程序員的笑話?

如何防止自己被人肉搜索到?

面試必備之樂觀鎖與悲觀鎖

搞定計算機網路面試,看這篇就夠了(補充版)

哪些 Python 庫讓你相見恨晚?

作為程序員的你,常用的工具軟體有哪些?


推薦閱讀:

Mybatis SQL技巧
MyBatis簡介與入門
Mybatis 插件
SSM的基礎配置

TAG:科技 | MyBatis | 計算機科學 |