mybatis 源码系列(五) 数据源DataSource
更多mybatis 源码系列文章可关注我的博客,点击前往
在第四章节中,我们分析里数据库驱动Driver的加载方式,其中有提到mybatis的数据源,我们都知道,Java中的SQL规范java.sql.DataSource
是一个接口,而我们在生产环境中一般都是基于数据库的连接池技术来获取数据库连接以操作数据库的.
通常为我们所知的主流数据源主要有:druid、c3p0、dbcp,HikariCP等等
这些都是帮助我们实现的在数据库连接池技术上非常好的技术中间件,我们只需要引入相关的Jar包即可引用
类型
而这一节我们主要研究mybatis给我们提供的默认数据源
mybatis中的数据源主要位于org.apache.ibatis.datasource
包下,主要有三种
- UnpooledDataSource:非数据库连接池的数据源,每次获取数据库Connection对象都会创建,不会使用数据库连接池
- PooledDataSource:数据库连接池数据源
- JndiDataSource:通过容器获取数据源
先来看整个类图关系
数据源工厂
先来看mybatis中的数据源工厂父类,DataSourceFactory.java
/**
* @author Clinton Begin
*/
public interface DataSourceFactory {
/***
* 设置数据源属性
* @param props
*/
void setProperties(Properties props);
/***
* 获取当前数据源实例
* @return
*/
DataSource getDataSource();
}
数据源工厂中主要提供了两个方法:
- 设置数据源属性
- 获取数据源
有此两个方法,我们来看它的具体实现
UnpooledDataSourceFactory
非数据库连接池的数据源工厂,UnpooledDataSourceFactory.java
/**
* @author Clinton Begin
*/
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();
//通过反射,获取dataSource的元数据对象,此处dataSource目标是UnpooledDataSource
//关于元数据对象,我们在后面章节研究,此处不做深究
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;
}
//other...
}
从数据工厂的源码中我们看到,非数据库连接池的数据源最终返回的是UnpooledDataSource
setProperties方法的主要逻辑:
- 声明驱动属性配置,从源配置中赋值驱动属性
- 通过反射获取UnpooledDataSource的元数据对象,对齐属性进行赋值
- 关于mybatis的元数据对象MetaObject我们不在这章深究,只需要知道这么回事即可,知道他的作用(动态根据Properties对象赋值目标对象UnpooledDataSource的属性)
所以通过源码,我们在使用非绑定数据源的方式如下:
//创建数据源工厂
UnpooledDataSourceFactory unpooledDataSourceFactory=new UnpooledDataSourceFactory();
String driver="com.mysql.cj.jdbc.Driver";
String url="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true";
String username="root";
String password="123456";
//赋值properties
Properties properties=new Properties();
properties.setProperty("driver",driver);
properties.setProperty("url",url);
properties.setProperty("username",username);
properties.setProperty("password",password);
unpooledDataSourceFactory.setProperties(properties);
//如果使用的是UnpooledDataSource数据源,则以上properties属性赋值需要使用UnpooledDataSource的属性值
//获取数据源
DataSource dataSource=unpooledDataSourceFactory.getDataSource();
通过以上代码的方式,我们就能拿到DataSource的实例,从而获取数据库连接Connection对象
UnpooledDataSource
此时,我们来看UnpooledDataSource
的具体实现
UnpooledDataSource
主要包含的属性
属性 | 说明 |
---|---|
driverClassLoader | 当前驱动类的ClassLoader实例 |
driverProperties | 驱动类的属性 |
registeredDrivers | 注册驱动类 |
driver | 数据库驱动 |
url | 数据库连接地址 |
username | 用户名 |
password | 密码 |
autoCommit | 是否自动提交 |
defaultTransactionIsolationLevel | 事务隔离级别 |
非数据池的获取数据库连接方式很简单,代码如下:
@Override
public Connection getConnection() throws SQLException {
return doGetConnection(username, password);
}
/***
* 获取数据连接对象
* @param properties
* @return
* @throws SQLException
*/
private Connection doGetConnection(Properties properties) throws SQLException {
//初始化Driver驱动
initializeDriver();
//获取连接
Connection connection = DriverManager.getConnection(url, properties);
//配置连接属性,主要是是否自动提交和事务隔离级别
configureConnection(connection);
return connection;
}
/****
* 配置Connection连接对象的属性
* @param conn
* @throws SQLException
*/
private void configureConnection(Connection conn) throws SQLException {
//是否自动提交
if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
conn.setAutoCommit(autoCommit);
}
//设置事务级别
if (defaultTransactionIsolationLevel != null) {
conn.setTransactionIsolation(defaultTransactionIsolationLevel);
}
}
获取数据库连接Connection
对象的方式是每次都通过DriverManager
来获取连接,不做连接池、缓存等处理.
PooledDataSourceFactory
通过上面的程序类图,我们其实已经知道,PooledDataSourceFactory其实是继承自UnPooledDataSourceFactory
来看代码:
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
public PooledDataSourceFactory() {
//dataSource数据源此处为PooledDataSource
this.dataSource = new PooledDataSource();
}
}
PooledDataSource
源码
来看使用连接池的数据源
/**
* 这是一个简单,同步,线程安全的数据库连接池。
*
* @author Clinton Begin
*/
public class PooledDataSource implements DataSource {
private static final Log log = LogFactory.getLog(PooledDataSource.class);
/***
* 连接池状态
*/
private final PoolState state = new PoolState(this);
private final UnpooledDataSource dataSource;
// OPTIONAL CONFIGURATION FIELDS
/***
* 连接池活动最大连接数
*/
protected int poolMaximumActiveConnections = 10;
/***
* 连接池最大空闲连接数量
*/
protected int poolMaximumIdleConnections = 5;
/***
* 最大checkout时间,默认20秒
*/
protected int poolMaximumCheckoutTime = 20000;
/***
* 等待时间
*/
protected int poolTimeToWait = 20000;
/***
* 连接池本地最大死的连接差值
*/
protected int poolMaximumLocalBadConnectionTolerance = 3;
protected String poolPingQuery = "NO PING QUERY SET";
/***
* 是否启用ping操作
*/
protected boolean poolPingEnabled;
protected int poolPingConnectionsNotUsedFor;
/***
* 连接执行类别代码
*/
private int expectedConnectionTypeCode;
public PooledDataSource() {
dataSource = new UnpooledDataSource();
}
public PooledDataSource(UnpooledDataSource dataSource) {
this.dataSource = dataSource;
}
//other
}
从源码中我们看到:
- 连接池数据源中以UnPooledDataSource为基础,构造函数传入的也是非连接池数据源
- 添加了连接池的相关基础属性,主要包含活动连接数、空闲连接数、等待时间等待
来看连接池获取数据源的方式:
/***
* 获取数据库连接
* @return
* @throws SQLException
*/
@Override
public Connection getConnection() throws SQLException {
return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}
/***
* 获取连接池的代理连接对象PooledConnection
*
*/
private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();
int localBadConnectionCount = 0;
while (conn == null) {
synchronized (state) {
//空閑连接不为空
if (!state.idleConnections.isEmpty()) {
// Pool has available connection
//空空闲连接集合中获取一个连接,并移除空闲连接集合
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {
//空闲连接集合数为空
// Pool does not have available connection
if (state.activeConnections.size() < poolMaximumActiveConnections) {
//**连接数小于最大活动连接数,则创建一个连接(从DataSource数据源中获取一个新的连接)
// Can create new connection
conn = new PooledConnection(dataSource.getConnection(), this);
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else {
//无法创建新的数据库连接
// Cannot create new connection
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
//当前连接的检查时间大于连接池默认check时间
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// Can claim overdue connection
//当前连接为逾期连接
//逾期连接数量+1
state.claimedOverdueConnectionCount++;
//累计连接时间++
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
//累计check时间++
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 happend.
Wrap the bad connection with a new PooledConnection, this will help
to not intterupt current executing thread and give current thread a
chance to join the next competion 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();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
//等待
// Must wait
try {
if (!countedWait) {
//等待数量+1
state.hadToWaitCount++;
//等待标志位置为true
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
//object的wait方法
state.wait(poolTimeToWait);
//累计等待时间
state.accumulatedWaitTime += System.currentTimeMillis() - wt;
} catch (InterruptedException e) {
break;
}
}
}
}
if (conn != null) {
// ping to server and check the connection is valid or not
if (conn.isValid()) {
//当前PoolConnection对象连接可用
//判断真实Connection是否自动提交,如果为false,则回滚当前事务.
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
//设置当前连接hash
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
//添加到活动连接集合中
state.activeConnections.add(conn);
//请求数+1
state.requestCount++;
//累计请求时间
state.accumulatedRequestTime += System.currentTimeMillis() - t;
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
state.badConnectionCount++;
localBadConnectionCount++;
conn = null;
if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Could not get a good connection to the database.");
}
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
}
}
if (conn == null) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
return conn;
}
流程图
通过获取此链接的方式,整理流程图如下:
逻辑
从上面获取连接对象的代码中,程序执行逻辑如下:
- 首先获取的代理数据库连接PooledConnection,该对象通过维护真是Connection,并通过JDK的动态代理产生真实的数据库Connection连接对象
- 循环获取PooledConnection,知道获取得到为止
- 首先拿到连接池状态锁,判断空闲连接池是否为空,如果不为空,获取第一个空闲连接(同时remove从空闲池集合中remove第一个),并返回
- 如果空闲连接池为空,判断当前**连接池大小是否小于连接池最大连接数,如果小于则new一个新的PooledConnection代理连接对象
- 如果连接池最大连接数已满,获取第一个连接池代理对象,判断该代理对象是否预期(连接池预期时间默认20秒),如果当前连接已预期,从活动连接池中移除该连接,回滚当前连接事务,使用当前代理对象的Connection,作为创建新的PooledConnection对象的参数,老的连接置为不可用
- 如果第一个池对象并未预期,则等待,根据连接池的等待时间进行等待
- 拿到PooledConnection对象后,对当前连接判断是否有效,有效的方法验证主要包括当前连接是否关闭,如果只想query操作,并执行,执行拿到结果则当前连接为可用连接
- 拿到真实Connection对象,判断是否自动提交,如果为false,则回滚当前Connection的事务,最后将该连接加入到连接池活动连接集合中返回
- 当我们得到PooledConnection后,因为最终要返回的是Connection对象,随意调用池代理连接的getProxyConnect()方法获取代理对象
- 获取代理对象时,首先判断当前Connection的方法,如果是调用close()方法,则首先会进入释放连接的逻辑,从当前活动连接池中移除该对象,判断空闲池空间足够,如果可用,加入空闲连接池,调用notifyAll(),唤醒wait线程,如果空闲连接池已满,则真实调用Connection的close()方法,并在之前回滚事务,关闭该连接
JndiDataSourceFactory
关于Jndi数据源,工作中几乎没有用到过,印象中是通过J2EE容器来创建数据源,为此我还是特地搜索学习记录一下
首先,什么是JNDI?
JNDI(Java Naming and Directory Interface)是Java技术中指定的API,它为使用Java编程语言编写的应用程序提供命名和目录功能.它专为使用Java对象模型的Java平台而设计。使用JNDI,基于Java技术的应用程序可以存储和检索任何类型的命名Java对象,此外,JNDI还提供了执行标准目录操作的方法,例如将属性与对象相关联以及使用其属性搜索对象
JNDI也是独立于任何特定命名或目录服务实现而定义的,它使应用程序能够使用通用API访问不同的,可能是多个命名和目录服务,可以在此通用API后面无缝插入不同的命名和目录服务提供程序。这使基于Java技术的应用程序能够利用各种现有命名和目录服务中的信息,例如LDAP,NDS,DNS和NIS(YP),以及使应用程序能够与传统软件和系统共存
使用JNDI作为工具,您可以构建新的功能强大且可移植的应用程序,这些应用程序不仅可以利用Java的对象模型,而且还可以与部署它们的环境良好集成。
通过在网络范围内共享有关用户,机器,网络,服务和应用程序的各种信息,JNDI在Intranet和Internet中发挥着至关重要的作用
可以看官网解释,我们知道Jndi是Java为我们提供的标准的Api,用于提供命名或目录服务
这就好比我们将我们的数据源写在外部的配置文件中同等道理,我们在Jndi配置了一个数据源,命名为com/mybatis/prodDataSource
,而开发无需要知道该数据源地址,用户名密码等,甚至连接池也不用关心,只需要使用Jndi提供的api,就能初始化拿到数据源的Connection连接,进行数据库的操作。
这样做的好处:
- 开发在不同的环境(dev、prod)中可以使用相同的JNDI命名,这样在部署时,不用为了环境的不同,而变更相关的应用程序配置
- 可以最小化需要知道访问生产数据库的凭据的人数。只有Java EE应用服务器需要知道您是否使用JNDI
当然,随着Spring Boot应用框架给我们提供的各环境部署的策略,目前Jndi这些技术已经很少有人使用了.