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() 回滚并关闭激活链接池和空闲链接池中的链接
相关文章
相关标签/搜索