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