MyBatis 源码分析 - 内置数据源
1.簡介
本篇文章將向大家介紹 MyBatis 內置數據源的實現邏輯。搞懂這些數據源的實現,可使大家對數據源有更深入的認識。同時在配置這些數據源時,也會更清楚每種屬性的意義和用途。因此,如果大家想知其然,也知其所以然。那么接下來就讓我們一起去探索 MyBatis 內置數據源的源碼吧。
MyBatis 支持三種數據源配置,分別為 UNPOOLED、POOLED 和 JNDI。并提供了兩種數據源實現,分別是 UnpooledDataSource 和 PooledDataSource。在三種數據源配置中,UNPOOLED 和 POOLED 是我們最常用的兩種配置。至于 JNDI,MyBatis 提供這種數據源的目的是為了讓其能夠運行在 EJB 或應用服務器等容器中,這一點官方文檔中有所說明。由于 JNDI 數據源在日常開發中使用甚少,因此,本篇文章不打算分析 JNDI 數據源相關實現。大家若有興趣,可自行分析。接下來,本文將重點分析 UNPOOLED 和 POOLED 兩種數據源。其他的就不多說了,進入正題吧。
?2.內置數據源初始化過程
在詳細分析 UnpooledDataSource 和 PooledDataSource 兩種數據源實現之前,我們先來了解一下數據源的配置與初始化過程。現在看數據源是如何配置的,如下:
| 1 2 3 4 5 6 | <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 配置文件解析過程一文中分析過,這里就不贅述了。下面我們來看一下數據源工廠類的實現邏輯。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | 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();}@Overridepublic 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;}@Overridepublic DataSource getDataSource() {return dataSource;} } |
以上是 UnpooledDataSourceFactory 的源碼分析,除了 setProperties 方法稍復雜一點,其他的都比較簡單,就不多說了。下面看看 PooledDataSourceFactory 的源碼。
| 1 2 3 4 5 6 7 | public class PooledDataSourceFactory extends UnpooledDataSourceFactory {public PooledDataSourceFactory() {// 創建 PooledDataSourcethis.dataSource = new PooledDataSource();} } |
以上就是 PooledDataSource 類的所有源碼,PooledDataSourceFactory 繼承自 UnpooledDataSourceFactory,復用了父類的邏輯,因此它的實現很簡單。
關于兩種數據源的創建過程就先分析到這,接下來,我們去探究一下兩種數據源是怎樣實現的。
?3.UnpooledDataSource
UnpooledDataSource,從名稱上即可知道,該種數據源不具有池化特性。該種數據源每次會返回一個新的數據庫連接,而非復用舊的連接。由于 UnpooledDataSource 無需提供連接池功能,因此它的實現非常簡單。核心的方法有三個,分別如下:
下面我將按照順序分節對相關方法進行分析,由于 configureConnection 方法比較簡單,因此我把它和 doGetConnection 放在一節中進行分析。下面先來分析 initializeDriver 方法。
?3.1 初始化數據庫驅動
回顧我們一開始學習使用 JDBC 訪問數據庫時的情景,在執行 SQL 之前,通常都是先獲取數據庫連接。一般步驟都是加載數據庫驅動,然后通過 DriverManager 獲取數據庫連接。UnpooledDataSource 也是使用 JDBC 訪問數據庫的,因此它獲取數據庫連接的過程也大致如此,只不過會稍有不同。下面我們一起來看一下。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // -☆- UnpooledDataSource private 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 方法主要包含三步操作,分別如下:
這三步都是都是常規操作,比較容易理解。上面代碼中出現了緩存相關的邏輯,這個是用于避免重復注冊驅動。因為 initializeDriver 放阿飛并不是在 UnpooledDataSource 初始化時被調用的,而是在獲取數據庫連接時被調用的。因此這里需要做個檢測,避免每次獲取數據庫連接時都重新注冊驅動。這個是一個比較小的點,大家看代碼時注意一下即可。下面看一下獲取數據庫連接的邏輯。
?3.2 獲取數據庫連接
在使用 JDBC 時,我們都是通過 DriverManager 的接口方法獲取數據庫連接。本節所要分析的源碼也不例外,一起看一下吧。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | // -☆- UnpooledDataSource public 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 的定義。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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);}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {...}// 省略部分代碼 } |
下面再來看看 PoolState 的定義。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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 獲取連接時,如果空閑鏈接列表里有連接時,可直接取用。那如果沒有空閑連接怎么辦呢?此時有兩種解決辦法,要么創建新連接,要么等待其他連接完成任務。具體怎么做,需視情況而定。下面我們深入到源碼中一探究竟。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | 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; } |
上面代碼冗長,過程比較復雜,下面把代碼邏輯梳理一下。從連接池中獲取連接首先會遇到兩種情況:
對于第一種情況,處理措施就很簡單了,把連接取出返回即可。對于第二種情況,則要進行細分,會有如下的情況。
對于上面兩種情況,第一種情況比較好處理,直接創建新的連接即可。至于第二種情況,需要再次進行細分。
對于第一種情況,我們直接將超時連接強行中斷,并進行回滾,然后復用部分字段重新創建 PooledConnection 即可。對于第二種情況,目前沒有更好的處理方式了,只能等待了。下面用一段偽代碼演示各種情況及相應的處理措施,如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | if (連接池中有空閑連接) {1. 將連接從空閑連接集合中移除 } else {if (活躍連接數未超出限制) {1. 創建新連接} else {1. 從活躍連接集合中取出第一個元素2. 獲取連接運行時長if (連接超時) {1. 將連接從活躍集合中移除2. 復用原連接的成員變量,并創建新的 PooledConnection 對象} else {1. 線程進入等待狀態2. 線程被喚醒后,重新執行以上邏輯}} }1. 將連接添加到活躍連接集合中 2. 返回連接 |
最后用一個流程圖大致描繪 popConnection 的邏輯,如下:
?4.3 回收連接
相比于獲取連接,回收連接的邏輯要簡單的多。回收連接成功與否只取決于空閑連接集合的狀態,所需處理情況很少,因此比較簡單。下面看一下相關的邏輯。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | 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();}// 創建新的 PooledConnectionPooledConnection 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 中的代理邏輯。相關代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // -☆- PooledConnection public 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 兩種內置數據源進行了較為詳細的分析,總的來說,這兩種數據源的源碼都不是很難理解。大家在閱讀源碼的過程中,首先應搞懂源碼的主要邏輯,然后再去分析一些邊邊角角的邏輯。不要一開始就陷入各種細節中,容易迷失方向。
好了,到此本文就結束了。若文章有錯誤不妥之處,希望大家指明。最后,感謝大家閱讀我的文章。
?附錄:MyBatis 源碼分析系列文章列表
| 2018-07-16 | MyBatis 源碼分析系列文章導讀 |
| 2018-07-20 | MyBatis 源碼分析 - 配置文件解析過程 |
| 2018-07-30 | MyBatis 源碼分析 - 映射文件解析過程 |
| 2018-08-17 | MyBatis 源碼分析 - SQL 的執行過程 |
| 2018-08-19 | MyBatis 源碼分析 - 內置數據源 |
| 2018-08-25 | MyBatis 源碼分析 - 緩存原理 |
| 2018-08-26 | MyBatis 源碼分析 - 插件機制 |
- 本文鏈接:?https://www.tianxiaobo.com/2018/08/19/MyBatis-源碼分析-內置數據源/
http://www.tianxiaobo.com/2018/08/19/MyBatis-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-%E5%86%85%E7%BD%AE%E6%95%B0%E6%8D%AE%E6%BA%90/?
總結
以上是生活随笔為你收集整理的MyBatis 源码分析 - 内置数据源的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MyBatis 源码分析 - SQL 的
- 下一篇: MyBatis 源码分析 - 缓存原理