摘要:源碼閱讀之數據庫連接的配置文件所有配置會被類讀取,我們可以通過此類來了解各個配置是如何運作的。也就是說的統計字段是關于整個數據源的,而一個則是針對單個連接的。
MyBatis 源碼閱讀之數據庫連接
MyBatis 的配置文件所有配置會被 org.apache.ibatis.builder.xml.XMLConfigBuilder 類讀取,
我們可以通過此類來了解各個配置是如何運作的。
而 MyBatis 的映射文件配置會被 org.apache.ibatis.builder.xml.XMLMapperBuilder 類讀取。
我們可以通過此類來了解映射文件的配置時如何被解析的。
本文探討 事務管理器 和 數據源 相關代碼配置 environment
以下是 mybatis 配置文件中 environments 節點的一般配置。
environments 節點的加載也不算復雜,它只會加載 id 為 development 屬性值的 environment 節點。
它的加載代碼在 XMLConfigBuilder 類的 environmentsElement() 方法中,代碼不多,邏輯也簡單,此處不多講。
接下來我們看看 environment 節點下的子節點。transactionManager 節點的 type 值默認提供有 JDBC 和 MANAGED ,dataSource 節點的 type 值默認提供有 JNDI 、 POOLED 和 UNPOOLED 。
它們對應的類都可以在 Configuration 類的構造器中找到,當然下面我們也一個一個來分析。
現在我們大概了解了配置,然后來分析這些配置與 MyBatis 類的關系。
TransactionFactorytransactionManager 節點對應 TransactionFactory 接口,使用了 抽象工廠模式 。MyBatis 給我們提供了兩個實現類:ManagedTransactionFactory 和 JdbcTransactionFactory ,它們分別對應者 type 屬性值為 MANAGED 和 JDBC 。
TransactionFactory 有三個方法,我們需要注意的方法只有 newTransaction() ,它用來創建一個事務對象。
void setProperties(Properties props); Transaction newTransaction(Connection conn); Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);
其中 JdbcTransactionFactory 創建的事務對象是 JdbcTransaction 的實例,該實例是對 JDBC 事務的簡單封裝,實例中 Connection 和 DataSource 對象正是事務所在的 連接 和 數據源 。
TransactionIsolationLevel 代表當前事務的隔離等級,它是一個枚舉類,簡單明了無需多言。而 autoCommit 表示是否開啟了自動提交,開啟了,則沒有事務的提交和回滾等操作的意義了。
ManagedTransactionFactory 創建的事務對象是 ManagedTransaction 的實例,它本身并不控制事務,即 commit 和 rollback 都是不做任何操作,而是交由 JavaEE 容器來控制事務,以方便集成。
DataSourceFactoryDataSourceFactory 是獲取數據源的接口,也使用了 抽象工廠模式 ,代碼如下,方法極為簡單:
public interface DataSourceFactory { /** * 可傳入一些屬性配置 */ void setProperties(Properties props); DataSource getDataSource(); }
MyBatis 默認支持三種數據源,分別是 UNPOOLED 、 POOLED 和 JNDI 。對應三個工廠類:
UnpooledDataSourceFactory 、 PooledDataSourceFactory 和 JNDIDataSourceFactory 。
其中 JNDIDataSourceFactory 是使用 JNDI 來獲取數據源。我們很少使用,并且代碼不是非常復雜,此處不討論。我們先來看看 UnpooledDataSourceFactory :
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() { this.dataSource = new UnpooledDataSource(); } @Override public void setProperties(Properties properties) { Properties driverProperties = new Properties(); // MetaObject 用于解析實例對象的元信息,如字段的信息、方法的信息 MetaObject metaDataSource = SystemMetaObject.forObject(dataSource); // 解析所有配置的鍵值對key-value,發現非預期的屬性立即拋異常,以便及時發現 for (Object key : properties.keySet()) { String propertyName = (String) key; if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) { // 添加驅動的配置屬性 String value = properties.getProperty(propertyName); driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value); } else if (metaDataSource.hasSetter(propertyName)) { // 為數據源添加配置屬性 String value = (String) properties.get(propertyName); Object convertedValue = convertValue(metaDataSource, propertyName, value); metaDataSource.setValue(propertyName, convertedValue); } else { throw new DataSourceException("Unknown DataSource property: " + propertyName); } } if (driverProperties.size() > 0) { metaDataSource.setValue("driverProperties", driverProperties); } } @Override public DataSource getDataSource() { return dataSource; } /** * 將 String 類型的值轉為目標對象字段的類型的值 */ private Object convertValue(MetaObject metaDataSource, String propertyName, String value) { Object convertedValue = value; Class> targetType = metaDataSource.getSetterType(propertyName); 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; } }
雖然代碼看起來復雜,實際上非常簡單,在創建工廠實例時創建它對應的 UnpooledDataSource 數據源。
setProperties() 方法用于給數據源添加部分屬性配置,convertValue() 方式時一個私有方法,就是處理 當 DataSource 的屬性為整型或布爾類型時提供對字符串類型的轉換功能而已。
最后我們看看 PooledDataSourceFactory ,這個類非常簡單,僅僅是繼承了 UnpooledDataSourceFactory ,然后構造方法替換數據源為 PooledDataSource 。
public class PooledDataSourceFactory extends UnpooledDataSourceFactory { public PooledDataSourceFactory() { this.dataSource = new PooledDataSource(); } }
雖然它的代碼極少,實際上都在 PooledDataSource 類中。
DataSource看完了工廠類,我們來看看 MyBatis 提供的兩種數據源類: UnpooledDataSource 和 PooledDataSource 。
UnpooledDataSourceUnpooledDataSource 看名字就知道是沒有池化的特征,相對也簡單點,以下代碼省略一些不重要的方法
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class UnpooledDataSource implements DataSource { private ClassLoader driverClassLoader; private Properties driverProperties; private static MapregisteredDrivers = new ConcurrentHashMap (); private String driver; private String url; private String username; private String password; private Boolean autoCommit; // 事務隔離級別 private Integer defaultTransactionIsolationLevel; static { // 遍歷所有可用驅動 Enumeration drivers = DriverManager.getDrivers(); while (drivers.hasMoreElements()) { Driver driver = drivers.nextElement(); registeredDrivers.put(driver.getClass().getName(), driver); } } // ...... private Connection doGetConnection(Properties properties) throws SQLException { // 每次獲取連接都會檢測驅動 initializeDriver(); Connection connection = DriverManager.getConnection(url, properties); configureConnection(connection); return connection; } /** * 初始化驅動,這是一個 同步 方法 */ private synchronized void initializeDriver() throws SQLException { // 如果不包含驅動,則準備添加驅動 if (!registeredDrivers.containsKey(driver)) { Class> driverType; try { // 加載驅動 if (driverClassLoader != null) { driverType = Class.forName(driver, true, driverClassLoader); } else { driverType = Resources.classForName(driver); } Driver driverInstance = (Driver)driverType.newInstance(); // 注冊驅動代理到 DriverManager DriverManager.registerDriver(new DriverProxy(driverInstance)); // 緩存驅動 registeredDrivers.put(driver, driverInstance); } catch (Exception e) { throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e); } } } private void configureConnection(Connection conn) throws SQLException { // 設置是否自動提交事務 if (autoCommit != null && autoCommit != conn.getAutoCommit()) { conn.setAutoCommit(autoCommit); } // 設置 事務隔離級別 if (defaultTransactionIsolationLevel != null) { conn.setTransactionIsolation(defaultTransactionIsolationLevel); } } private static class DriverProxy implements Driver { private Driver driver; DriverProxy(Driver d) { this.driver = d; } /** * Driver 僅在 JDK7 中定義了本方法,用于返回本驅動的所有日志記錄器的父記錄器 * 個人也不是十分明確它的用法,畢竟很少會關注驅動的日志 */ public Logger getParentLogger() { return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); } // 其他方法均為調用 driver 對應的方法,此處省略 } }
這里 DriverProxy 僅被注冊到 DriverManager 中,這是一個代理操作,但源碼上并沒有什么特別的處理代碼,我也不懂官方為什么在這里加代理,有誰明白的可以留言相互討論。這里的其他方法也不是非常復雜,我都已經標有注釋,應該都可以看懂,不再細說。
以上便是 UnpooledDataSource 的初始化驅動和獲取連接關鍵代碼。
PooledDataSource接下來我們來看最后一個類 PooledDataSource ,它也是直接實現 DataSource ,不過因為擁有池化的特性,它的代碼復雜不少,當然效率比 UnpooledDataSource 會高出不少。
PooledDataSource 通過兩個輔助類 PoolState 和 PooledConnection 來完成池化功能。
PoolState 是記錄連接池運行時的狀態,定義了兩個 PooledConnection 集合用于記錄空閑連接和活躍連接。
PooledConnection 內部定義了兩個 Connection 分別表示一個真實連接和代理連接,還有一些其他字段用于記錄一個連接的運行時狀態。
先來詳細了解一下 PooledConnection
/** * 此處使用默認的訪問權限 * 實現了 InvocationHandler */ class PooledConnection implements InvocationHandler { private static final String CLOSE = "close"; private static final Class>[] IFACES = new Class>[] { Connection.class }; /** hashCode() 方法返回 */ private final int hashCode; private final Connection realConnection; private final Connection proxyConnection; // 省略 checkoutTimestamp、createdTimestamp、lastUsedTimestamp private boolean valid; /* * Constructor for SimplePooledConnection that uses the Connection and PooledDataSource passed in * * @param connection - the connection that is to be presented as a pooled connection * @param dataSource - the dataSource that the connection is from */ 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; this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this); } /* * 設置連接狀態為不正常,不可使用 */ public void invalidate() { valid = false; } /* * 查看連接是否可用 * * @return 如果可用則返回 true */ public boolean isValid() { return valid && realConnection != null && dataSource.pingConnection(this); } /** * 自動上一次使用后經過的時間 */ public long getTimeElapsedSinceLastUse() { return System.currentTimeMillis() - lastUsedTimestamp; } /** * 存活時間 */ public long getAge() { return System.currentTimeMillis() - createdTimestamp; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { // 對于 close() 方法,將連接放回池中 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); } } } private void checkConnection() throws SQLException { if (!valid) { throw new SQLException("Error accessing PooledConnection. Connection is invalid."); } } }
本類實現了 InvocationHandler 接口,這個接口是用于 JDK 動態代理的,在這個類的構造器中 proxyConnection 就是創建了此代理對象。
來看看 invoke() 方法,它攔截了 close() 方法,不再關閉連接,而是將其繼續放入池中,然后其他已實現的方法則是每次調用都需要檢測連接是否合法。
而 PoolState 類,這個類實際上沒什么可說的,都是一些統計字段,沒有復雜邏輯,不討論; 需要注意該類是針對一個 PooledDataSource 對象統計的 。
也就是說 PoolState 的統計字段是關于整個數據源的,而一個 PooledConnection 則是針對單個連接的。
最后我們回過頭來看 PooledDataSource 類,數據源的操作就只有兩個,獲取連接,釋放連接,先來看看獲取連接
public class PooledDataSource implements DataSource { private final UnpooledDataSource dataSource; @Override public Connection getConnection() throws SQLException { return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection(); } @Override public Connection getConnection(String username, String password) throws SQLException { return popConnection(username, password).getProxyConnection(); } /** * 獲取一個連接 */ private PooledConnection popConnection(String username, String password) throws SQLException { boolean countedWait = false; PooledConnection conn = null; long t = System.currentTimeMillis(); int localBadConnectionCount = 0; // conn == null 也可能是沒有獲得連接,被通知后再次走流程 while (conn == null) { synchronized (state) { // 是否存在空閑連接 if (!state.idleConnections.isEmpty()) { // 池里存在空閑連接 conn = state.idleConnections.remove(0); } else { // 池里不存在空閑連接 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) { /* * Just log a message for debug and continue to execute the following statement * like nothing happened. Wrap the bad connection with a new PooledConnection, * this will help to not interrupt current executing thread and give current * thread a chance to join the next competition for another valid/good database * connection. At the end of this loop, bad {@link @conn} will be set as null. */ log.debug("Bad connection. Could not roll back"); } } conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); 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) { // 檢測連接是否可用 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()); // 激活連接池數+1 state.activeConnections.add(conn); state.requestCount++; state.accumulatedRequestTime += System.currentTimeMillis() - t; } else { // 連接壞掉了,超過一定閾值則拋異常提醒 state.badConnectionCount++; localBadConnectionCount++; conn = null; if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) { // 省略日志 throw new SQLException( "PooledDataSource: Could not get a good connection to the database."); } } } } } if (conn == null) { // 省略日志 throw new SQLException( "PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); } return conn; } }
上面的代碼都已經加了注釋,總體流程不算復雜:
while => 連接為空
能否直接從池里拿連接 => 可以則獲取連接并返回
不能,查看池里的連接是否沒滿 => 沒滿則創建一個連接并返回
滿了,查看池里最早的連接是否超時 => 超時則強制該連接回滾,然后獲取該連接并返回
未超時,等待連接可用
檢測連接是否可用
釋放連接操作,更為簡單,判斷更少
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 newConn = new PooledConnection(conn.getRealConnection(), this); state.idleConnections.add(newConn); newConn.setCreatedTimestamp(conn.getCreatedTimestamp()); newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp()); // 該對象已經不能用于連接了 conn.invalidate(); if (log.isDebugEnabled()) { log.debug("Returned connection " + newConn.getRealHashCode() + " to pool."); } state.notifyAll(); } else { state.accumulatedCheckoutTime += conn.getCheckoutTime(); if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } // 關閉連接 conn.getRealConnection().close(); if (log.isDebugEnabled()) { log.debug("Closed connection " + conn.getRealHashCode() + "."); } conn.invalidate(); } } else { if (log.isDebugEnabled()) { log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection."); } state.badConnectionCount++; } } }
部分碼注釋已添加,這里就說一下總體流程:
從活動池中移除連接
如果該連接可用
連接池未滿,則連接放回池中
滿了,回滾,關閉連接
總體流程大概就是這樣
以下還有兩個方法代碼較多,但邏輯都很簡單,稍微說明一下:
pingConnection() 執行一條 SQL 檢測連接是否可用。
forceCloseAll() 回滾并關閉激活連接池和空閑連接池中的連接
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/72851.html
摘要:從使用到原理學習線程池關于線程池的使用,及原理分析分析角度新穎面向切面編程的基本用法基于注解的實現在軟件開發中,分散于應用中多出的功能被稱為橫切關注點如事務安全緩存等。 Java 程序媛手把手教你設計模式中的撩妹神技 -- 上篇 遇一人白首,擇一城終老,是多么美好的人生境界,她和他歷經風雨慢慢變老,回首走過的點點滴滴,依然清楚的記得當初愛情萌芽的模樣…… Java 進階面試問題列表 -...
摘要:源碼閱讀之的配置文件所有配置會被類讀取,我們可以通過此類來了解各個配置是如何運作的。是用于項目中存在多種數據庫時區分同一條對應的數據庫。可以這樣認為,在中的和組合才是一條的唯一標識。如果發現自己的沒被正確識別,可以查看方法是否和預期一致。 MyBatis 源碼閱讀之 databaseId MyBatis 的配置文件所有配置會被 org.apache.ibatis.builder.xml...
摘要:簡介我從七月份開始閱讀源碼,并在隨后的天內陸續更新了篇文章。考慮到超長文章對讀者不太友好,以及拆分文章工作量也不小等問題。經過兩周緊張的排版,一本小小的源碼分析書誕生了。我在寫系列文章中,買了一本書作為參考,這本書是技術內幕。 1.簡介 我從七月份開始閱讀MyBatis源碼,并在隨后的40天內陸續更新了7篇文章。起初,我只是打算通過博客的形式進行分享。但在寫作的過程中,發現要分析的代碼...
閱讀 2142·2021-10-12 10:11
閱讀 843·2021-10-09 09:41
閱讀 3757·2021-09-09 11:37
閱讀 1933·2021-09-08 10:41
閱讀 2634·2019-08-30 12:58
閱讀 2369·2019-08-30 10:58
閱讀 1272·2019-08-26 13:40
閱讀 4098·2019-08-26 13:36