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)的时候会初始化创建数据源信息,具体分析过程如下:

  1. 先进入 XMLConfigBuilder 类中,可以找到如下代码:
public Configuration parse() {
    if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}
  1. 我们用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();
}