MyBatis基础支持DataSource实现源码解析
作者:念念清晰
DataSource
在数据库应用中,客户端与数据库服务端建立的连接对象(Connection)是宝贵的资源,每次请求数据库都创建连接,使用完毕后会销毁连接,这是一种很浪费资源的操作。因此Java提出了DataSource接口。可以把它当作一个连接池。程序初始化时,创建一批连接放入到连接池中,如果需要请求数据库就从连接池中取出连接对象(Connection)使用完毕后把连接归还给连接池。这样就减少了每次请求都创建、销毁连接的步骤,从而提高数据库性能。
package javax.sql; public interface DataSource extends CommonDataSource, Wrapper { // 最重要的方法 Connection getConnection() throws SQLException; // 其他方法不再列出 }
Java只是在JDK1.4版本发布了该接口规范。具体实现需要用户自己实现。MyBatis中提供了3种DataSource接口的实现。
- UnpooledDataSource
- PooledDataSource
- JNDI方式的接口(不在本文讨论范围)
下面着重分析1和2这两种DataSource的实现。
UnpooledDataSource
UnpooledDataSource顾名思义,他是非池化的DataSource,说白了和普通的Connection没什么区别。通过UnpooledDataSource过去连接每次都需要重新创建一个Connection。我们来看下它的getConnection实现方法。
public Connection getConnection() throws SQLException { return doGetConnection(username, password); } private Connection doGetConnection(Properties properties) throws SQLException { initializeDriver(); Connection connection = DriverManager.getConnection(url, properties); configureConnection(connection); return connection; }
在UnpooledDataSource#getConnection方法中,调用了doGetConnection方法,参数是username和password,该方法也就是通过用户名和密码获取数据库连接的意思。doGetConnection具体实现就使用了DriverManager来获取连接对象。这是JDBC原生获取连接对象的方式。
值得一说的是:UnpooledDataSource的其他方法都是基于DriverManager实现的。也就是说,使用UnpooledDataSource作为连接池的话等价于没有使用连接池。
PooledDataSource
PooledDataSource才是真正意义上的连接池,它提供了连接池的大小(默认10)、最大活跃连接数量、空闲连接数量等蚕食设置。并且对Connection对象进行了JDK动态代理,重写了Connection的close
方法。使得Connection对象在调用close方法是不是真正的关闭连接,而是把自定义关闭行为,MyBatis的关闭逻辑就是把Connection对象归还连接池。
我们先看下PooledDataSource的几个重要字段信息
public class PooledDataSource implements DataSource { // PooledDataSource真正管理连接状态的是PoolState,后面会详细说明 private final PoolState state = new PoolState(this); // UnpooledDataSource上面说过和普通的Connection无异 private final UnpooledDataSource dataSource; //正在使用连接的数量 protected int poolMaximumActiveConnections = 10; //空闲连接数 protected int poolMaximumIdleConnections = 5; //在被强制返回之前,池中连接被检查的时间 protected int poolMaximumCheckoutTime = 20000; //这是给连接池一个打印日志状态机会的低层次设置,还有重新 尝试获得连接, 这些情况下往往需要很长时间 为了避免连接池没有配置时静默失 败)。 protected int poolTimeToWait = 20000; //发送到数据的侦测查询,用来验证连接是否正常工作,并且准备 接受请求。默认是“NO PING QUERY SET” ,这会引起许多数据库驱动连接由一 个错误信息而导致失败 protected String poolPingQuery = "NO PING QUERY SET"; //开启或禁用侦测查询 protected boolean poolPingEnabled = false; //用来配置 poolPingQuery 多次时间被用一次 protected int poolPingConnectionsNotUsedFor = 0; private int expectedConnectionTypeCode; }
这些字段主要记录了连接池的重要信息:连接池大小、空闲时最大连接数、最大活跃连接数、超时时间等。而整整揭开PooledDataSource获取连接对象的神秘面纱还需要介绍两个类。PooledConnection和PoolState
PooledConnection
PooledConnection实现了InvocationHandler接口,他是用来做JDK动态代理的。前文提到过,mybatis使用JDK动态代理重写了Connection对象的close方法,就是在该类中实现的逻辑。该类有几个重要属性。
- private PooledDataSource dataSource; // dataSource的副本
- private Connection realConnection; // 真实连接对象
- private Connection proxyConnection; // 实际返回的代理对象
接下来来看下代理对象的invoke方法是如何重写close方法的。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); //如果调用close的话,忽略它,反而将这个connection加入到池中 if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { dataSource.pushConnection(this); return null; } return method.invoke(realConnection, args); // 其他逻辑省略.... }
在invoke方法中判断下执行的方法名称是否是Close,如果是,就不再执行原来的close方法了,而是执行PooledDataSource 的pushConnection方法!从方法名可以看出方法的作用是:把连接push到连接池PooledDataSource 中。pushConnection的逻辑后文详细说明
PoolState
上文提到PooledDataSource并不管理连接对象。那么程序初始化的时候创建的一批连接存放到哪里了呢?答案是存在PoolState对象中,而PooledDataSource有一个属性就是PoolState。也就是说PooledDataSource是通过PoolState来管理连接池的。
一批连接在Java中就是一个List集合嘛。那么我们想一下PoolState都需要怎么管理连接呢?首先根据连接的状态,可以把连接分为2种
- 空闲连接
protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>();
- 活跃连接
protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>();
PoolState中两个List属性分别存储空闲连接和活跃连接。需要连接的时候就从idleConnections
列表中取,关联连接时就把连接从activeConnections
中移到idleConnections
中。
PoolState中还有一些其他的统计信息字段,比如 请求次数、请求的总时间、总连接数等这些属性比较简单就不再列出了
获取连接
介绍完PooledConnection和PoolState这两个类后,我们来看下PooledDataSource是怎么获取连接的。获取连接的逻辑在PooledDataSource#getConnection方法中,getConnection方法只是一个壳子,具体调用逻辑在popConnection方法。我们来看一下(我只列出了重要逻辑)
public Connection getConnection() throws SQLException { return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection(); } private PooledConnection popConnection(String username, String password) throws SQLException { //最外面是while死循环,如果一直拿不到connection,则不断尝试 while (conn == null) { synchronized (state) { if (!state.idleConnections.isEmpty()) { //如果有空闲的连接的话,返回第一个空闲连接 conn = state.idleConnections.remove(0); } else { //如果没有空闲的连接 if (state.activeConnections.size() < poolMaximumActiveConnections) { //如果activeConnections太少,那就new一个PooledConnection conn = new PooledConnection(dataSource.getConnection(), this); } else { //如果activeConnections已经很多了,那不能再new了 //取得activeConnections列表的第一个(最老的) PooledConnection oldestActiveConnection = state.activeConnections.get(0); long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); if (longestCheckoutTime > poolMaximumCheckoutTime) { //如果checkout时间过长,则这个connection标记为overdue(过期) //删掉最老的连接,然后再new一个新连接 conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); oldestActiveConnection.invalidate(); } else { //如果checkout时间不够长,没办法,只能等待,在此分支会记录一些统计信息 } } } if (conn != null) { if (conn.isValid()) { //如果已经拿到connection,则记录一些统计信息 } else { //如果没拿到,统计信息:坏连接+1 state.badConnectionCount++; localBadConnectionCount++; conn = null; //如果好几次都拿不到,就放弃了,抛出异常 } } } } return conn; }
在popConnection中
- 从PoolState对象的空闲连接列表中获取连接,如果有空闲连接就返回。
- 从PoolState对象的活跃连接列表中获取连接,如果连接数小于最大活跃数,则new一个连接返回。如果没有只能等待其他线程释放连接再进行获取
- 无论是否获取到连接,对连接进行一些信息统计并记录到PoolState对象中。一旦尝试获取连接的时间超过了阈值,就会放弃获取连接抛出异常
关闭连接
在PooledConnection小节中见到,PooledConnection重写了Connection的close方法。当调用Connection的close方法时真正执行的逻辑是PooledDataSource的pushConnection方法。该代码逻辑很简单,大体上说,就是把连接从活跃列表中删除,加入到空闲列表中。具体实现如下
protected void pushConnection(PooledConnection conn) throws SQLException { synchronized (state) { //先从activeConnections中删除此connection 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(); } //new一个新的Connection,加入到idle列表 PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this); state.idleConnections.add(newConn); //通知其他线程可以来抢connection了 state.notifyAll(); } else { //否则,即空闲的连接已经足够了 state.accumulatedCheckoutTime += conn.getCheckoutTime(); //那就将connection关闭就可以了,获取真正的connection对象并且关闭 conn.getRealConnection().close(); conn.invalidate(); } } } }
关闭过程:
- 空闲连接数<最大空闲连接数 则新建一个连接存放到PoolState的空闲列表中并通知其他线程可以来抢Connection对象
- 如果PoolState的空闲列表是满的,那只能获取真正的connection对象并将其关闭了。
小结
- PooledDataSource真正意义上实现了DataSource接口。具有连接池的意义
- PooledDataSource通过PooledConnection和PoolState来管理连接池中的连接
- PooledConnection重写了Connection对象的close方法。调用Connection的close方法时并不会真正的关闭连接,而是先要进行归还连接的操作。
- PoolState是对连接列表状态的管理。它有两个List属性,分别存储了活跃连接列表和空闲连接列表
DataSourceFactory
获取MyBatis提供的DataSource实现,需要通过工厂DataSourceFactory
接口来获取。在这里MyBatis使用了工厂方法模式。DataSourceFactory有两个实现类。分别是
- UnpooledDataSourceFactory
- PooledDataSourceFactory
我们首先来看下工厂接口定义
public interface DataSourceFactory { //设置属性,被XMLConfigBuilder所调用 void setProperties(Properties props); //生产数据源,直接得到javax.sql.DataSource DataSource getDataSource(); }
其中最重要的方法就是getDataSource,它很直观,通过工厂对象的该方法可以获取DataSource实现。
UnpooledDataSourceFactory
UnpooledDataSourceFactory获取dataSource的方法非常简单直观。
首先,构造方法里里new了一个UnpooledDataSource对象存放到工厂的属性中
然后,getDataSource直接返回该对象即可。具体实现如下
public class UnpooledDataSourceFactory implements DataSourceFactory { protected DataSource dataSource; public UnpooledDataSourceFactory() { this.dataSource = new UnpooledDataSource(); } public DataSource getDataSource() { return dataSource; } }
PooledDataSourceFactory
PooledDataSourceFactory就有意思了,想偷懒,直接继承自UnpooledDataSourceFactory。只需要在构造方法中new一个PooledDataSource对象,再通过getDataSource方法获取即可。
public class PooledDataSourceFactory extends UnpooledDataSourceFactory { //数据源换成了PooledDataSource public PooledDataSourceFactory() { this.dataSource = new PooledDataSource(); } }
结语
个人感觉mybatis提供的DataSourceFactory的实现类有点鸡肋。可以说还是new对象。我们知道工厂模式创建的一般都是比较复杂的对象,是用来帮助开发者屏蔽复杂的细节。而mybatis的这两个实现都只是new对象而已。
以上就是MyBatis基础支持DataSource实现源码解析的详细内容,更多关于MyBatis基础支持DataSource的资料请关注脚本之家其它相关文章!