Mybatis 的连接池技术
我们在前面的 WEB 课程中也学习过类似的连接池技术,而在 Mybatis 中也有连接池技术,但是它采用的是自己的连接池技术。在 Mybatis 的 SqlMapConfig.xml 配置文件中,通过 <dataSourcetype=”pooled”>来实现 Mybatis 中连接池的配置。
1. Mybatis 连接池的分类
在 Mybatis 中我们将它的数据源 dataSource 分为以下几类:
可以看出 Mybatis 将它自己的数据源分为三类:
- UNPOOLED 不使用连接池的数据源
- POOLED 使用连接池的数据源
- JNDI 使用 JNDI 实现的数据源
相应地,MyBatis内部分别定义了实现了java.sql.DataSource接口的UnpooledDataSource,PooledDataSource类来表示 UNPOOLED、POOLED类型的数据源。
- PooledDataSource和UnpooledDataSource都实现了DataSource接口。
- PooledDataSource有一个UnpooledDataSource的引用。
- 当PooledDataSource创建Connection对象时,还是通过UnpooledDataSource来创建。
- PooledDataSource提供了一种缓存连接池机制。
在这三种数据源中,我们一般采用的是 POOLED 数据源(很多时候我们所说的数据源就是为了更好的管理数据库连接,也就是我们所说的连接池技术)。
为了方便,以下案例的SqlMapConfig.xml中都将要添加包扫描
<!--别名配置-->
<typeAliases>
<package name="com.itheima.domain" />
</typeAliases>
<!--映射文件指定-->
<mappers>
<package name="com.itheima.mapper" />
</mappers>
2. Mybatis 中数据源的配置
我们的数据源配置就是在 SqlMapConfig.xml 文件中,具体配置如下:
<!--数据源配置-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="UNPOOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
MyBatis 在初始化时,解析此文件,根据 的 type 属性来创建相应类型的的数据源DataSource,即:
- type=”UNPOOLED” : MyBatis 会创建 UnpooledDataSource 实例
- type=”POOLED”:MyBatis 会创建 PooledDataSource 实例
- type=”JNDI”:MyBatis 会从 JNDI 服务上查找 DataSource 实例,然后返回使用
1. Mybatis 中 DataSource 的存取
MyBatis是通过工厂模式来创建数据源DataSource对象的,MyBatis定义了抽象的工厂接口 :org.apache.ibatis.datasource.DataSourceFactory,通过其 getDataSource()方法返回数据源DataSource。
下面是 DataSourceFactory 源码,具体如下:
public interface DataSourceFactory {
void setProperties(Properties props);
DataSource getDataSource();
}
MyBatis创建了DataSource实例后,会将其放到Configuration对象内的Environment对象中,供以后使用。
SqlSessionFactoryBuilder.build(inputstream)的时候会初始化创建数据源信息,具体分析过程如下:
- 先进入 XMLConfigBuilder 类中,可以找到如下代码:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
- 我们用DEBUG模式执行查询,在XMLConfigBuilder.java的parse()方法断点监控分析configuration对象的environment 属性,结果如下:
2. Mybatis 数据源UNPOOLED
SqlMapConfig.xml配置
<dataSource type="UNPOOLED">
执行查询,并没有发现这句日志记录Returned connection 155361948 to pool 测试日志如下:
2018-07-05 20:01:47,207 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Opening JDBC Connection
2018-07-05 20:01:47,812 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@272113c4]
2018-07-05 20:01:47,819 [main] DEBUG [com.itheima.mapper.UserMapper.findAll] - ==> Preparing: SELECT * FROM USER
2018-07-05 20:01:47,951 [main] DEBUG [com.itheima.mapper.UserMapper.findAll] - <== Total: 6
User{id=41, username='老王', birthday=Tue Feb 27 17:47:08 CST 2018, sex='男', address='北京'}
User{id=48, username='小马宝莉', birthday=Thu Mar 08 11:44:00 CST 2018, sex='女', address='北京修正'}
2018-07-05 20:01:47,956 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@272113c4]
2018-07-05 20:01:47,964 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@272113c4]
UnpooledDataSource源码分析
源码中的getConnection会调用doGetConnection(String username, String password)方法,该方法中又调用doGetConnection(Properties properties)方法,doGetConnection(Properties properties)方法源码如下:
private Connection doGetConnection(Properties properties) throws SQLException {
//该方法实际上是注册驱动
initializeDriver();
//获取连接对象
Connection connection = DriverManager.getConnection(url, properties);
configureConnection(connection);
return connection;
}
initializeDriver()方法是用于注册驱动,代码如下:
private synchronized void initializeDriver() throws SQLException {
if (!registeredDrivers.containsKey(driver)) {
Class<?> driverType;
try {
if (driverClassLoader != null) {
//注册驱动
driverType = Class.forName(driver, true, driverClassLoader);
} else {
//....
}
//....
} catch (Exception e) {
throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
}
}
}
3. Mybatis 数据源POOLED
SqlMapConfig.xml配置
<dataSource type="POOLED">
执行查询,会有一句将Connection回收到连接池日志记录Returned connection 155361948 to pool 测试日志如下:
2018-07-05 19:55:49,888 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Opening JDBC Connection
2018-07-05 19:55:50,466 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Created connection 155361948.
2018-07-05 19:55:50,466 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@942a29c]
2018-07-05 19:55:50,469 [main] DEBUG [com.itheima.mapper.UserMapper.findAll] - ==> Preparing: SELECT * FROM USER
2018-07-05 19:55:50,662 [main] DEBUG [com.itheima.mapper.UserMapper.findAll] - <== Total: 6
User{id=41, username='老王', birthday=Tue Feb 27 17:47:08 CST 2018, sex='男', address='北京'}
User{id=42, username='小二王', birthday=Fri Mar 02 15:09:37 CST 2018, sex='女', address='北京金燕龙'}
2018-07-05 19:55:50,668 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@942a29c]
2018-07-05 19:55:50,668 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@942a29c]
2018-07-05 19:55:50,668 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Returned connection 155361948 to pool.
PooledDataSource源码分析
下面是PooledDataSource连接获取的源代码:
@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();
}
源码中的getConnection会调用popConnection(String username,String password)方法,其中popConnection方法源码如下:
private PooledConnection popConnection(String username, String password) throws SQLException {
//.....
while (conn == null) {
synchronized (state) {
//先去idleConnections查找是否有空闲的连接
if (!state.idleConnections.isEmpty()) {
// Pool has available connection
conn = state.idleConnections.remove(0);
//...
} else {
//如果idleConnections没有空闲的连接,查询activeConnections中的连接是否满了
// Pool does not have available connection
if (state.activeConnections.size() < poolMaximumActiveConnections) {
//如果没满就创建新的
// Can create new connection
conn = new PooledConnection(dataSource.getConnection(), this);
//...
} else {
// Cannot create new connection
//如果activeConnections中连接满了就取出活动连接池的第一个,也就是最早创建的
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// Can claim overdue connection
//查询最早创建的是否过期,如果过期了就移除他并创建新的
//....
} else {
// 还未过期,就必须等待,再次重复上述步骤
//.....
}
}
}
if (conn != null) {
// .......
}
}
}
return conn;
}
流程如下图:
4. Mybatis 的事务控制
1. JDBC 中事务的回顾
在 JDBC 中我们可以通过手动方式将事务的提交改为手动方式,通过 setAutoCommit()方法就可以调整。
通过 JDK 文档,我们找到该方法如下:
那么我们的 Mybatis 框架因为是对 JDBC 的封装,所以 Mybatis 框架的事务控制方式,本身也是用 JDBC的 setAutoCommit()方法来设置事务提交。
2. Mybatis 中事务提交方式
Mybatis 中事务的提交方式,本质上就是调用 JDBC 的 setAutoCommit()来实现事务控制。
我们运行之前所写的代码:
测试类
@Test
public void testSaveUser() {
User user = new User("张三",new Date(),"男","深圳市");
//增加有用户
userDao.saveUser(user);
}
UserDaoImpl中的saveUsser()方法需要手动执行sqlSession.commit()提交
public int saveUser(User user) {
//获得SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
//执行保存
int insert = sqlSession.insert("com.itheima.dao.IUserDao.saveUser",user);
//提交
sqlSession.commit();
//关闭资源
sqlSession.close();
return insert;
}
日志信息
DEBUG [org.apache.ibatis.logging.LogFactory] - Logging initialized using 'class org.apache.ibatis.logging.log4j.Log4jImpl' adapter.
DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Opening JDBC Connection
DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@2a556333]
[com.itheima.mapper.UserMapper.saveUser] - ==> Preparing: INSERT INTO USER (username,birthday,sex,address)VALUES(?,?,?,?)
DEBUG [com.itheima.mapper.UserMapper.saveUser] - ==> Parameters: 张三(String), 2018-07-05 22:29:20.974(Timestamp), 男(String), 深圳市(String)
DEBUG [com.itheima.mapper.UserMapper.saveUser] - <== Updates: 1
DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@2a556333]
[main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@2a556333]
2018-07-05 22:29:21,471 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@2a556333]
这是我们的 Connection 的整个变化过程,通过分析我们能够发现之前的 CUD操作过程中,我们都要手动进行事务的提交,原因是 setAutoCommit()方CUD操作中,必须通过 sqlSession.commit()方法来执行提交操作。
3. Mybatis 自动提交事务的设置
通过上面的研究和分析,现在我们一起思考,为什么 CUD 过程中必须使用 sqlSession.commit()提交事务?主要原因就是在连接池中取出的连接,都会将调用 connection.setAutoCommit(false)方法,这样我们就必须使用 sqlSession.commit()方法,相当于使用了 JDBC 中的 connection.commit()方法实现事务提交。
明白这一点后,我们现在一起尝试不进行手动提交,一样实现 CUD 操作。
测试类
@Test
public void testSaveUser() {
User user = new User("张三",new Date(),"男","深圳市");
//增加有用户
userDao.saveUser(user);
}
UserDaoImpl中的saveUsser()方法注释手动执行sqlSession.commit()提交
public int saveUser(User user) {
//获得SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(true);
//执行保存
int insert = sqlSession.insert("com.itheima.dao.IUserDao.saveUser",user);
//提交
//sqlSession.commit();
//关闭资源
sqlSession.close();
return insert;
}
所对应的 DefaultSqlSessionFactory 类的源代码:
@Override
public SqlSession openSession(boolean autoCommit) {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit);
}
日志信息
DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Opening JDBC Connection
DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Created connection 2108753062.
DEBUG [com.itheima.mapper.UserMapper.saveUser] - ==> Preparing: INSERT INTO USER (username,birthday,sex,address)VALUES(?,?,?,?)
DEBUG [com.itheima.mapper.UserMapper.saveUser] - ==> Parameters: 张三(String), 2018-07-05 23:03:01.121(Timestamp), 男(String), 深圳市(String)
DEBUG [com.itheima.mapper.UserMapper.saveUser] - <== Updates: 1
DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@7db12bb6]
DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Returned connection 2108753062 to pool.
我们发现,此时事务就设置为自动提交了,同样可以实现 CUD 操作时记录的保存。虽然这也是一种方式,但就编程而言,设置为自动提交方式为 false 再根据情况决定是否进行提交,这种方式更常用。因为我们可以根据业务情况来决定提交是否进行提交。
4. JNDI数据源配置
创建一个web工程[war包],pom.xml如下
<!--引入相关依赖-->
<dependencies>
<!--MyBatis依赖包-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<!--MySQL驱动包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
<scope>runtime</scope>
</dependency>
<!--日志包-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<!--测试包-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
<!--servlet-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<!--jsp-->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
在webapp目录下创建META-INF/context.xml
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<!--
<Resource
name="jdbc/eesy_mybatis" 数据源的名称
type="javax.sql.DataSource" 数据源类型
auth="Container" 数据源提供者
maxActive="20" 最大活动数
maxWait="10000" 最大等待时间
maxIdle="5" 最大空闲数
username="root" 用户名
password="1234" 密码
driverClassName="com.mysql.jdbc.Driver" 驱动类
url="jdbc:mysql://localhost:3306/eesy_mybatis" 连接url字符串
/>
-->
<Resource
name="jdbc/web_jndi"
type="javax.sql.DataSource"
auth="Container"
maxActive="20"
maxWait="10000"
maxIdle="5"
username="root"
password="123456"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf8"
/>
</Context>
SqlMapConfig.xml配置
<!--数据源配置-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="JNDI">
<property name="data_source" value="java:comp/env/jdbc/web_jndi" />
</dataSource>
</environment>
</environments>
新建JNDIServlet
private RoleMapper roleMapper;
private SqlSession session;
private InputStream is;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//读取配置文件
is = Resources.getResourceAsStream("SqlMapConfig.xml");
//创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//通过SqlSessionBuilder对象构建一个SqlSessionFactory
SqlSessionFactory sqlSessionFactory = builder.build(is);
//通过SqlSessionFactory构建一个SqlSession接口的代理实现类
session = sqlSessionFactory.openSession(true);
//通过SqlSession实现增删改查
roleMapper = session.getMapper(RoleMapper.class);
List<Role> roles = roleMapper.findRoleUserList();
for (Role role : roles) {
System.out.println(role);
}
//关闭资源
session.close();
is.close();
}