MyBatis 源碼閱讀之數據庫連接
MyBatis 的配置文件所有配置會被 org.apache.ibatis.builder.xml.XMLConfigBuilder
類讀取,
我們可以通過此類來了解各個配置是如何運作的。
而 MyBatis 的映射文件配置會被 org.apache.ibatis.builder.xml.XMLMapperBuilder
類讀取。
我們可以通過此類來了解映射文件的配置時如何被解析的。
本文探討 事務管理器 和 數據源 相關代碼
配置
environment
以下是 mybatis 配置文件中 environments 節點的一般配置。
<!-- mybatis-config.xml -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
environments 節點的加載也不算複雜,它只會加載 id 爲 development 屬性值的 environment 節點。
它的加載代碼在 XMLConfigBuilder
類的 environmentsElement()
方法中,代碼不多,邏輯也簡單,此處不多講。
TransactionManager
接下來我們看看 environment 節點下的子節點。transactionManager 節點的 type 值默認提供有 JDBC 和 MANAGED ,dataSource 節點的 type 值默認提供有 JNDI 、 POOLED 和 UNPOOLED 。
它們對應的類都可以在 Configuration
類的構造器中找到,當然下面我們也一個一個來分析。
現在我們大概瞭解了配置,然後來分析這些配置與 MyBatis 類的關係。
TransactionFactory
transactionManager 節點對應 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 事務的簡單封裝;ManagedTransactionFactory
創建的事務對象是 ManagedTransaction
的實例,它本身並不控制事務,即 commit
和 rollback
都是不做任何操作,而是交由 JavaEE 容器來控制事務,以方便集成。
DataSourceFactory
DataSourceFactory
是獲取數據源的接口,也使用了 抽象工廠模式 ,代碼如下,方法極爲簡單:
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);
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
。
UnpooledDataSource
UnpooledDataSource
看名字就知道是沒有池化的特徵,相對也簡單點,以下代碼省略一些不重要的方法
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 Map<String, Driver> registeredDrivers = new ConcurrentHashMap<String, Driver>();
private String driver;
private String url;
private String username;
private String password;
private Boolean autoCommit;
// 事務隔離級別
private Integer defaultTransactionIsolationLevel;
static {
// 遍歷所有可用驅動
Enumeration<Driver> 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;
}
/*
* Method to see if the connection is usable
*
* @return True if the connection is usable
*/
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()
回滾並關閉激活連接池和空閒連接池中的連接