MyBatis 源碼閱讀之數據庫鏈接

MyBatis 源碼閱讀之數據庫鏈接

MyBatis 的配置文件全部配置會被 org.apache.ibatis.builder.xml.XMLConfigBuilder 類讀取,
咱們能夠經過此類來了解各個配置是如何運做的。
而 MyBatis 的映射文件配置會被 org.apache.ibatis.builder.xml.XMLMapperBuilder 類讀取。
咱們能夠經過此類來了解映射文件的配置時如何被解析的。java

本文探討 事務管理器數據源 相關代碼

配置

environment

如下是 mybatis 配置文件中 environments 節點的通常配置。sql

<!-- 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 值默認提供有 JDBCMANAGED ,dataSource 節點的 type 值默認提供有 JNDIPOOLEDUNPOOLED
它們對應的類均可以在 Configuration 類的構造器中找到,固然下面咱們也一個一個來分析。apache

如今咱們大概瞭解了配置,而後來分析這些配置與 MyBatis 類的關係。緩存

TransactionFactory

transactionManager 節點對應 TransactionFactory 接口,使用了 抽象工廠模式 。MyBatis 給咱們提供了兩個實現類:ManagedTransactionFactoryJdbcTransactionFactory ,它們分別對應者 type 屬性值爲 MANAGED 和 JDBC 。mybatis

TransactionFactory 有三個方法,咱們須要注意的方法只有 newTransaction() ,它用來建立一個事務對象。app

void setProperties(Properties props);

Transaction newTransaction(Connection conn);

Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);

其中 JdbcTransactionFactory 建立的事務對象是 JdbcTransaction 的實例,該實例是對 JDBC 事務的簡單封裝,實例中 ConnectionDataSource 對象正是事務所在的 鏈接數據源
TransactionIsolationLevel 表明當前事務的隔離等級,它是一個枚舉類,簡單明瞭無需多言。而 autoCommit 表示是否開啓了自動提交,開啓了,則沒有事務的提交和回滾等操做的意義了。ide

ManagedTransactionFactory 建立的事務對象是 ManagedTransaction 的實例,它自己並不控制事務,即 commitrollback 都是不作任何操做,而是交由 JavaEE 容器來控制事務,以方便集成。oop

DataSourceFactory

DataSourceFactory 是獲取數據源的接口,也使用了 抽象工廠模式 ,代碼以下,方法極爲簡單:ui

public interface DataSourceFactory {

    /**
     * 可傳入一些屬性配置
     */
    void setProperties(Properties props);

    DataSource getDataSource();
}

MyBatis 默認支持三種數據源,分別是 UNPOOLEDPOOLEDJNDI 。對應三個工廠類:
UnpooledDataSourceFactoryPooledDataSourceFactoryJNDIDataSourceFactory

其中 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 提供的兩種數據源類: UnpooledDataSourcePooledDataSource

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 經過兩個輔助類 PoolStatePooledConnection 來完成池化功能。
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;
    }
}

上面的代碼都已經加了註釋,整體流程不算複雜:

  1. while => 鏈接爲空

    1. 可否直接從池裏拿鏈接 => 能夠則獲取鏈接並返回
    2. 不能,查看池裏的鏈接是否沒滿 => 沒滿則建立一個鏈接並返回
    3. 滿了,查看池裏最先的鏈接是否超時 => 超時則強制該鏈接回滾,而後獲取該鏈接並返回
    4. 未超時,等待鏈接可用
  2. 檢測鏈接是否可用

釋放鏈接操做,更爲簡單,判斷更少

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++;
        }
    }
}

部分碼註釋已添加,這裏就說一下整體流程:

  1. 從活動池中移除鏈接
  2. 若是該鏈接可用

    1. 鏈接池未滿,則鏈接放回池中
    2. 滿了,回滾,關閉鏈接

整體流程大概就是這樣

如下還有兩個方法代碼較多,但邏輯都很簡單,稍微說明一下:

  • pingConnection() 執行一條 SQL 檢測鏈接是否可用。
  • forceCloseAll() 回滾並關閉激活鏈接池和空閒鏈接池中的鏈接
相關文章
相關標籤/搜索