MyBatis 源碼閱讀之數據庫連接

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

現在我們大概瞭解了配置,然後來分析這些配置與 MyBatis 類的關係。

TransactionFactory

transactionManager 節點對應 TransactionFactory 接口,使用了 抽象工廠模式 。MyBatis 給我們提供了兩個實現類:ManagedTransactionFactoryJdbcTransactionFactory ,它們分別對應者 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 的實例,它本身並不控制事務,即 commitrollback 都是不做任何操作,而是交由 JavaEE 容器來控制事務,以方便集成。

DataSourceFactory

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

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

    /*
     * 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;
    }
}

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

  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() 回滾並關閉激活連接池和空閒連接池中的連接
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章