亲宝软件园·资讯

展开

MyBatis基础支持DataSource实现源码解析

念念清晰 人气:0

DataSource

在数据库应用中,客户端与数据库服务端建立的连接对象(Connection)是宝贵的资源,每次请求数据库都创建连接,使用完毕后会销毁连接,这是一种很浪费资源的操作。因此Java提出了DataSource接口。可以把它当作一个连接池。程序初始化时,创建一批连接放入到连接池中,如果需要请求数据库就从连接池中取出连接对象(Connection)使用完毕后把连接归还给连接池。这样就减少了每次请求都创建、销毁连接的步骤,从而提高数据库性能。

package javax.sql;
public interface DataSource  extends CommonDataSource, Wrapper {
  // 最重要的方法
  Connection getConnection() throws SQLException;
  // 其他方法不再列出
}

Java只是在JDK1.4版本发布了该接口规范。具体实现需要用户自己实现。MyBatis中提供了3种DataSource接口的实现。

下面着重分析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方法,就是在该类中实现的逻辑。该类有几个重要属性。

接下来来看下代理对象的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种

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中

关闭连接

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

关闭过程:

小结

DataSourceFactory

获取MyBatis提供的DataSource实现,需要通过工厂DataSourceFactory接口来获取。在这里MyBatis使用了工厂方法模式。DataSourceFactory有两个实现类。分别是

我们首先来看下工厂接口定义

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对象而已。

加载全部内容

相关教程
猜你喜欢
用户评论