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

作者:念念清晰 时间:2023-01-16 09:58:07 

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() &lt; 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 &gt; 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() &lt; poolMaximumIdleConnections &amp;&amp; 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();
 }
}

来源:https://juejin.cn/post/7195883375995289657

标签:MyBatis,基础支持,DataSource
0
投稿

猜你喜欢

  • Android XListView下拉刷新和上拉加载更多

    2022-11-01 19:07:45
  • C#中 Json 序列化去掉null值的方法

    2022-02-02 12:13:52
  • Spring Boot + Vue 前后端分离开发之前端网络请求封装与配置

    2021-05-28 02:48:41
  • Android SurfaceView画板操作

    2022-07-10 15:12:23
  • Mybatis 逆向工程的三种方法详解

    2023-08-10 22:27:20
  • C#使用timer实现的简单闹钟程序

    2023-07-30 00:24:28
  • SSH框架网上商城项目第2战之基本增删查改、Service和Action的抽取

    2023-06-21 19:16:23
  • JAVA中常见异常类

    2021-11-09 09:47:20
  • Java实现邮件找回密码功能

    2022-08-18 07:12:57
  • Java基础之二叉搜索树的基本操作

    2023-07-08 10:07:07
  • 详解Android App中创建ViewPager组件的方法

    2023-07-12 00:46:14
  • SpringBoot 如何从容器中获取对象

    2023-12-06 08:20:40
  • C++ 关于MFC多线程编程的注意事项

    2023-02-17 22:34:44
  • C#调用易语言写的Dll文件方法

    2023-07-21 01:56:10
  • ionic App 解决android端在真机上tab处于顶部的问题

    2022-05-11 21:56:57
  • C# Winform消息通知系统托盘气泡提示框ToolTip控件

    2023-01-13 23:31:02
  • C# TrackBar拖动条改变滑块颜色

    2021-12-22 22:11:07
  • Android利用爬虫实现模拟登录的实现实例

    2021-05-24 18:47:24
  • Android RollPagerView实现轮播图

    2023-06-12 21:10:32
  • Java中的CAS和ABA问题说明

    2021-08-19 11:08:18
  • asp之家 软件编程 m.aspxhome.com