写点什么

java 培训:MyBatis 的架构与原理分析

作者:@零度
  • 2022 年 3 月 02 日
  • 本文字数:17035 字

    阅读完需:约 56 分钟

1 引言

本文主要讲解 JDBC 怎么演变到 Mybatis 的渐变过程,重点讲解了为什么要将 JDBC 封装成 Mybaits 这样一个持久层框架 。再而论述 Mybatis 作为一个数据持久层框架本身有待改进之处。

2 JDBC 实现查询分析

我们先看看我们最熟悉也是最基础的通过 JDBC 查询数据库数据,一般需要以下七个步骤:

加载 JDBC 驱动;

建立并获取数据库连接;

创建 JDBC Statements 对象;

设置 SQL 语句的传入参数;

执行 SQL 语句并获得查询结果;

对查询结果进行转换处理并将处理结果返回;

释放相关资源(关闭 Connection,关闭 Statement,关闭 ResultSet);

以下是具体的实现代码:

public static List<Map<String,Object>> queryForList(){

Connection connection = null;

ResultSet rs = null;

PreparedStatement stmt = null;

List<Map<String,Object>> resultList = new ArrayList<Map<String,Object>>();

try {

// 加载 JDBC 驱动

Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();

String url = "jdbc:oracle:thin:@localhost:1521:ORACLEDB";

String user = "trainer";

String password = "trainer";

// 获取数据库连接

connection = DriverManager.getConnection(url,user,password);

String sql = "select * from userinfo where user_id = ? ";

// 创建 Statement 对象(每一个 Statement 为一次数据库执行请求)

stmt = connection.prepareStatement(sql);

// 设置传入参数

stmt.setString(1, "zhangsan");

// 执行 SQL 语句

rs = stmt.executeQuery();

// 处理查询结果(将查询结果转换成 List<Map>格式)

ResultSetMetaData rsmd = rs.getMetaData();

int num = rsmd.getColumnCount();

while(rs.next()){

Map map = new HashMap();

for(int i = 0;i < num;i++){

String columnName = rsmd.getColumnName(i+1);

map.put(columnName,rs.getString(columnName));

}

resultList.add(map);

}

} catch (Exception e) {

e.printStackTrace();

} finally {

try {

// 关闭结果集

if (rs != null) {

rs.close();

rs = null;

}

// 关闭执行

if (stmt != null) {

stmt.close();

stmt = null;

}

if (connection != null) {

connection.close();

connection = null;

}

} catch (SQLException e) {

e.printStackTrace();

}

}

return resultList;

}

3 JDBC 演变到 Mybatis 过程 #

上面我们看到了实现 JDBC 有七个步骤,哪些步骤是可以进一步封装的,减少我们开发的代码量。

3.1 第一步优化:连接获取和释放 ##

1、 问题描述:

数据库连接频繁的开启和关闭本身就造成了资源的浪费,影响系统的性能 。

解决问题:

数据库连接的获取和关闭我们可以使用数据库连接池来解决资源浪费的问题 。北京java培训通过连接池就可以反复利用已经建立的连接去访问数据库了。减少连接的开启和关闭的时间。

2、问题描述:

但是现在连接池多种多样,可能存在变化 ,有可能采用 DBCP 的连接池,也有可能采用容器本身的 JNDI 数据库连接池。

解决问题:

我们可以通过 DataSource 进行隔离解耦 ,我们统一从 DataSource 里面获取数据库连接,DataSource 具体由 DBCP 实现还是由容器的 JNDI 实现都可以 ,所以我们将 DataSource 的具体实现通过让用户配置来应对变化。

3.2 第二步优化:SQL 统一存取 ##

1、问题描述:

我们使用 JDBC 进行操作数据库时,SQL 语句基本都散落在各个 JAVA 类中 ,这样有三个不足之处:

第一,可读性很差,不利于维护以及做性能调优。

第二,改动 Java 代码需要重新编译、打包部署。

第三,不利于取出 SQL 在数据库客户端执行(取出后还得删掉中间的 Java 代码,编写好的 SQL 语句写好后还得通过+号在 Java 进行拼凑)。

解决问题:

我们可以考虑不把 SQL 语句写到 Java 代码中,那么把 SQL 语句放到哪里呢?首先需要有一个统一存放的地方,我们可以将这些 SQL 语句统一集中放到配置文件或者数据库里面(以 key-value 的格式存放) 。然后通过 SQL 语句的 key 值去获取对应的 SQL 语句。

既然我们将 SQL 语句都统一放在配置文件或者数据库中,那么这里就涉及一个 SQL 语句的加载问题 。

3.3 第三步优化:传入参数映射和动态 SQL

1、问题描述:

很多情况下,我们都可以通过在 SQL 语句中设置占位符来达到使用传入参数的目的,这种方式本身就有一定局限性,它是按照一定顺序传入参数的,要与占位符一一匹配。但是,如果我们传入的参数是不确定的 (比如列表查询,根据用户填写的查询条件不同,传入查询的参数也是不同的,有时是一个参数、有时可能是三个参数),那么我们就得在后台代码中自己根据请求的传入参数去拼凑相应的 SQL 语句 ,这样的话还是避免不了在 Java 代码里面写 SQL 语句的命运 。既然我们已经把 SQL 语句统一存放在配置文件或者数据库中了,怎么做到能够根据前台传入参数的不同,动态生成对应的 SQL 语句呢?

解决问题:

第一,我们先解决这个动态问题,按照我们正常的程序员思维是,通过 if 和 else 这类的判断来进行是最直观的 ,这个时候我们想到了 JSTL 中的这样的标签,那么,能不能将这类的标签引入到 SQL 语句中呢?假设可以,那么我们这里就需要一个专门的 SQL 解析器来解析这样的 SQL 语句,但是,if 判断的变量来自于哪里呢?传入的值本身是可变的,那么我们得为这个值定义一个不变的变量名称,而且这个变量名称必须和对应的值要有对应关系,可以通过这个变量名称找到对应的值,这个时候我们想到了 key-value 的 Map。解析的时候根据变量名的具体值来判断。

假如前面可以判断没有问题,那么假如判断的结果是 true,那么就需要输出的标签里面的 SQL 片段,但是怎么解决在标签里面使用变量名称的问题呢?这里我们需要使用一种有别于 SQL 的语法来嵌入变量(比如使用#变量名#) 。这样,SQL 语句经过解析后就可以动态的生成符合上下文的 SQL 语句。

还有,怎么区分开占位符变量和非占位变量?有时候我们单单使用占位符是满足不了的,占位符只能为查询条件占位,SQL 语句其他地方使用不了。这里我们可以使用 #变量名 #表示占位符变量,使用变量名表示非占位符变量 。

3.4 第四步优化:结果映射和结果缓存

1、问题描述:

执行 SQL 语句、获取执行结果、对执行结果进行转换处理、释放相关资源是一整套下来的。假如是执行查询语句,那么执行 SQL 语句后,返回的是一个 ResultSet 结果集,这个时候我们就需要将 ResultSet 对象的数据取出来,不然等到释放资源时就取不到这些结果信息了。

我们从前面的优化来看,以及将获取连接、设置传入参数、执行 SQL 语句、释放资源这些都封装起来了,只剩下结果处理这块还没有进行封装,如果能封装起来,每个数据库操作都不用自己写那么一大堆 Java 代码,直接调用一个封装的方法就可以搞定了。

解决问题:

我们分析一下,一般对执行结果的有哪些处理,有可能将结果不做任何处理就直接返回,也有可能将结果转换成一个 JavaBean 对象返回、一个 Map 返回、一个 List 返回等 `,结果处理可能是多种多样的。从这里看,我们必须告诉 SQL 处理器两点:第一,需要返回什么类型的对象;第二,需要返回的对象的数据结构怎么跟执行的结果映射 ,这样才能将具体的值 copy 到对应的数据结构上。

接下来,我们可以进而考虑对 SQL 执行结果的缓存来提升性能 。缓存数据都是 key-value 的格式,那么这个 key 怎么来呢?怎么保证唯一呢?即使同一条 SQL 语句几次访问的过程中由于传入参数的不同,得到的执行 SQL 语句也是不同的。那么缓存起来的时候是多对。但是 SQL 语句和传入参数两部分合起来可以作为数据缓存的 key 值 。

3.5 第五步优化:解决重复 SQL 语句问题

1、问题描述:

由于我们将所有 SQL 语句都放到配置文件中,这个时候会遇到一个 SQL 重复的问题 ,几个功能的 SQL 语句其实都差不多,有些可能是 SELECT 后面那段不同、有些可能是 WHERE 语句不同。有时候表结构改了,那么我们就需要改多个地方,不利于维护。

解决问题:

当我们的代码程序出现重复代码时怎么办?将重复的代码抽离出来成为独立的一个类,然后在各个需要使用的地方进行引用 。对于 SQL 重复的问题,我们也可以采用这种方式,通过将 SQL 片段模块化,将重复的 SQL 片段独立成一个 SQL 块,然后在各个 SQL 语句引用重复的 SQL 块 ,这样需要修改时只需要修改一处即可。

4 Mybaits 有待改进之处

1、问题描述:

Mybaits 所有的数据库操作都是基于 SQL 语句,导致什么样的数据库操作都要写 SQL 语句 。一个应用系统要写的 SQL 语句实在太多了。

改进方法:

我们对数据库进行的操作大部分都是对表数据的增删改查,很多都是对单表的数据进行操作,由这点我们可以想到一个问题:单表操作可不可以不写 SQL 语句,通过 JavaBean 的默认映射器生成对应的 SQL 语句 ,比如:一个类 UserInfo 对应于 USER_INFO 表, userId 属性对应于 USER_ID 字段。这样我们就可以通过反射可以获取到对应的表结构了,拼凑成对应的 SQL 语句显然不是问题 。

5 MyBatis 框架整体设计


5.1 接口层-和数据库交互的方式

MyBatis 和数据库的交互有两种方式:

使用传统的 MyBatis 提供的 API;

使用 Mapper 接口;

5.1.1 使用传统的 MyBatis 提供的 API

这是传统的传递 Statement Id 和查询参数给 SqlSession 对象,使用 SqlSession 对象完成和数据库的交互 ;MyBatis 提供了非常方便和简单的 API,供用户实现对数据库的增删改查数据操作,以及对数据库连接信息和 MyBatis 自身配置信息的维护操作。


上述使用 MyBatis 的方法,是创建一个和数据库打交道的 SqlSession 对象,然后根据 Statement Id 和参数来操作数据库 ,这种方式固然很简单和实用,但是它不符合面向对象语言的概念和面向接口编程的编程习惯 。由于面向接口的编程是面向对象的大趋势,MyBatis 为了适应这一趋势,增加了第二种使用 MyBatis 支持接口(Interface)调用方式。

5.1.2 使用 Mapper 接口

MyBatis 将配置文件中的每一个节点抽象为一个 Mapper 接口:

这个接口中声明的方法和节点中的<select|update|delete|insert> 节点项对应,即<select|update|delete|insert> 节点的 id 值为 Mapper 接口中的方法名称,parameterType 值表示 Mapper 对应方法的入参类型 ,而 resultMap 值则对应了 Mapper 接口表示的返回值类型或者返回结果集的元素类型 。



根据 MyBatis 的配置规范配置好后,通过 SqlSession.getMapper(XXXMapper.class)方法,MyBatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个 Mapper 实例 ,我们使用 Mapper 接口的某一个方法时,MyBatis 会根据这个方法的方法名和参数类型,确定 Statement Id,底层还是通过 SqlSession.select("statementId",parameterObject);或者 SqlSession.update("statementId",parameterObject); 等等来实现对数据库的操作,MyBatis 引用 Mapper 接口这种调用方式,纯粹是为了满足面向接口编程的需要 。(其实还有一个原因是在于,面向接口的编程,使得用户在接口上可以使用注解来配置 SQL 语句,这样就可以脱离 XML 配置文件,实现“0 配置”)。

5.2 数据处理层

数据处理层可以说是 MyBatis 的核心 ,从大的方面上讲,它要完成两个功能:

通过传入参数构建动态 SQL 语句;

SQL 语句的执行以及封装查询结果集成 List;

5.2.1 参数映射和动态 SQL 语句生成

动态语句生成可以说是 MyBatis 框架非常优雅的一个设计,MyBatis 通过传入的参数值,使用 Ognl 来动态地构造 SQL 语句 ,使得 MyBatis 有很强的灵活性和扩展性。

参数映射指的是对于 java 数据类型和 jdbc 数据类型之间的转换:这里有包括两个过程:查询阶段 ,我们要将 java 类型的数据,转换成 jdbc 类型的数据,通过 preparedStatement.setXXX() 来设值;另一个就是对 resultset 查询结果集的 jdbcType 数据转换成 java 数据类型 。

5.2.2 SQL 语句的执行以及封装查询结果集成 List

动态 SQL 语句生成之后,MyBatis 将执行 SQL 语句,并将可能返回的结果集转换成 List 列表。

MyBatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换 ,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询 。

5.3 框架支撑层

1、事务管理机制

事务管理机制对于 ORM 框架而言是不可缺少的一部分 ,事务管理机制的质量也是考量一个 ORM 框架是否优秀的一个标准。

2、连接池管理机制

由于创建一个数据库连接所占用的资源比较大,对于数据吞吐量大和访问量非常大的应用而言,连接池的设计就显得非常重要 。

3、缓存机制

为了提高数据利用率和减小服务器和数据库的压力,MyBatis 会对于一些查询提供会话级别的数据缓存 ,会将对某一次查询,放置到 SqlSession 中,在允许的时间间隔内,对于完全相同的查询,MyBatis 会直接将缓存结果返回给用户,而不用再到数据库中查找。

4、SQL 语句的配置方式

传统的 MyBatis 配置 SQL 语句方式就是使用 XML 文件进行配置的,但是这种方式不能很好地支持面向接口编程的理念,为了支持面向接口的编程,MyBatis 引入了 Mapper 接口的概念,面向接口的引入,对使用注解来配置 SQL 语句成为可能,用户只需要在接口上添加必要的注解即可,不用再去配置 XML 文件了 ,但是,目前的 MyBatis 只是对注解配置 SQL 语句提供了有限的支持,某些高级功能还是要依赖 XML 配置文件配置 SQL 语句。

5.4 引导层

引导层是配置和启动 MyBatis 配置信息的方式 。MyBatis 提供两种方式来引导 MyBatis :基于 XML 配置文件的方式和基于 Java API 的方式 。

5.5 主要构件及其相互关系

从 MyBatis 代码实现的角度来看,MyBatis 的主要的核心部件有以下几个:

SqlSession:作为 MyBatis 工作的主要顶层 API,表示和数据库交互的会话,完成必要数据库增删改查功能;

Executor:MyBatis 执行器,是 MyBatis 调度的核心,负责 SQL 语句的生成和查询缓存的维护;

StatementHandler:封装了 JDBC Statement 操作,负责对 JDBC statement 的操作,如设置参数、将 Statement 结果集转换成 List 集合。

ParameterHandler:负责对用户传递的参数转换成 JDBC Statement 所需要的参数;

ResultSetHandler:负责将 JDBC 返回的 ResultSet 结果集对象转换成 List 类型的集合;

TypeHandler:负责 java 数据类型和 jdbc 数据类型之间的映射和转换;

MappedStatement:MappedStatement 维护了一条<select|update|delete|insert>节点的封装;

SqlSource:负责根据用户传递的 parameterObject,动态地生成 SQL 语句,将信息封装到 BoundSql 对象中,并返回;

BoundSql:表示动态生成的 SQL 语句以及相应的参数信息;

Configuration:MyBatis 所有的配置信息都维持在 Configuration 对象之中;

它们的关系如下图所示:



6 SqlSession 工作过程分析

1、开启一个数据库访问会话---创建 SqlSession 对象

SqlSession sqlSession = factory.openSession();



MyBatis 封装了对数据库的访问,把对数据库的会话和事务控制放到了 SqlSession 对象中

2、为 SqlSession 传递一个配置的 Sql 语句的 Statement Id 和参数,然后返回结果:

List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",params);

上述的"com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",是配置在 EmployeesMapper.xml 的 Statement ID,params 是传递的查询参数。

让我们来看一下 sqlSession.selectList()方法的定义:

public <E> List<E> selectList(String statement, Object parameter) {

return this.selectList(statement, parameter, RowBounds.DEFAULT);

}

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {

try {

//1.根据 Statement Id,在 mybatis 配置对象 Configuration 中查找和配置文件相对应的 MappedStatement

MappedStatement ms = configuration.getMappedStatement(statement);

//2. 将查询任务委托给 MyBatis 的执行器 Executor

List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

return result;

} catch (Exception e) {

throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);

} finally {

ErrorContext.instance().reset();

}

}

MyBatis 在初始化的时候,会将 MyBatis 的配置信息全部加载到内存中,使用 org.apache.ibatis.session.Configuration 实例来维护 。使用者可以使用 sqlSession.getConfiguration()方法来获取。MyBatis 的配置文件中配置信息的组织格式和内存中对象的组织格式几乎完全对应的 。

上述例子中的:

<select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" >

select

EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY

from LOUIS.EMPLOYEES

<if test="min_salary != null">

where SALARY < #{min_salary,jdbcType=DECIMAL}

</if>

</select>

加载到内存中会生成一个对应的 MappedStatement 对象,然后会以 key="com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary" ,value 为 MappedStatement 对象的形式维护到 Configuration 的一个 Map 中 。当以后需要使用的时候,只需要通过 Id 值来获取就可以了。

从上述的代码中我们可以看到 SqlSession 的职能是:SqlSession 根据 Statement ID, 在 mybatis 配置对象 Configuration 中获取到对应的 MappedStatement 对象,然后调用 mybatis 执行器来执行具体的操作 。

3、MyBatis 执行器 Executor 根据 SqlSession 传递的参数执行 query()方法(由于代码过长,读者只需阅读我注释的地方即可):

/**

* BaseExecutor 类部分代码

*

*/

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {

// 1. 根据具体传入的参数,动态地生成需要执行的 SQL 语句,用 BoundSql 对象表示

BoundSql boundSql = ms.getBoundSql(parameter);

// 2. 为当前的查询创建一个缓存 Key

CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);

return query(ms, parameter, rowBounds, resultHandler, key, boundSql);

}

@SuppressWarnings("unchecked")

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {

ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());

if (closed) throw new ExecutorException("Executor was closed.");

if (queryStack == 0 && ms.isFlushCacheRequired()) {

clearLocalCache();

}

List<E> list;

try {

queryStack++;

list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;

if (list != null) {

handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);

} else {

// 3.缓存中没有值,直接从数据库中读取数据

list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

}

} finally {

queryStack--;

}

if (queryStack == 0) {

for (DeferredLoad deferredLoad : deferredLoads) {

deferredLoad.load();

}

deferredLoads.clear(); // issue #601

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {

clearLocalCache(); // issue #482

}

}

return list;

}

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {

List<E> list;

localCache.putObject(key, EXECUTION_PLACEHOLDER);

try {

//4. 执行查询,返回 List 结果,然后 将查询的结果放入缓存之中

list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

} finally {

localCache.removeObject(key);

}

localCache.putObject(key, list);

if (ms.getStatementType() == StatementType.CALLABLE) {

localOutputParameterCache.putObject(key, parameter);

}

return list;

}

/**

*

* SimpleExecutor 类的 doQuery()方法实现

*

*/

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {

Statement stmt = null;

try {

Configuration configuration = ms.getConfiguration();

//5. 根据既有的参数,创建 StatementHandler 对象来执行查询操作

StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);

//6. 创建 java.Sql.Statement 对象,传递给 StatementHandler 对象

stmt = prepareStatement(handler, ms.getStatementLog());

//7. 调用 StatementHandler.query()方法,返回 List 结果集

return handler.<E>query(stmt, resultHandler);

} finally {

closeStatement(stmt);

}

}

上述的 Executor.query()方法几经转折,最后会创建一个 StatementHandler 对象,然后将必要的参数传递给 StatementHandler ,使用 StatementHandler 来完成对数据库的查询,最终返回 List 结果集。

从上面的代码中我们可以看出,Executor 的功能和作用是:

根据传递的参数,完成 SQL 语句的动态解析,生成 BoundSql 对象,供 StatementHandler 使用;

为查询创建缓存,以提高性能;

创建 JDBC 的 Statement 连接对象,传递给 StatementHandler 对象,返回 List 查询结果;

4、StatementHandler 对象负责设置 Statement 对象中的查询参数、处理 JDBC 返回的 resultSet,将 resultSet 加工为 List 集合返回:

接着上面的 Executor 第六步,看一下:prepareStatement() 方法的实现:

/**

*

* SimpleExecutor 类的 doQuery()方法实现

*

*/

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {

Statement stmt = null;

try {

Configuration configuration = ms.getConfiguration();

StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);

// 1.准备 Statement 对象,并设置 Statement 对象的参数

stmt = prepareStatement(handler, ms.getStatementLog());

// 2. StatementHandler 执行 query()方法,返回 List 结果

return handler.<E>query(stmt, resultHandler);

} finally {

closeStatement(stmt);

}

}

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {

Statement stmt;

Connection connection = getConnection(statementLog);

stmt = handler.prepare(connection);

//对创建的 Statement 对象设置参数,即设置 SQL 语句中 ? 设置为指定的参数

handler.parameterize(stmt);

return stmt;

}

以上我们可以总结 StatementHandler 对象主要完成两个工作:

对于 JDBC 的 PreparedStatement 类型的对象,创建的过程中,我们使用的是 SQL 语句字符串会包含 若干个? 占位符,我们其后再对占位符进行设值。StatementHandler 通过 parameterize(statement)方法对 Statement 进行设值;

StatementHandler 通过 Listquery(Statement statement, ResultHandler resultHandler)方法来完成执行 Statement,和将 Statement 对象返回的 resultSet 封装成 List;

5、StatementHandler 的 parameterize(statement) 方法的实现:

/**

* StatementHandler 类的 parameterize(statement) 方法实现

*/

public void parameterize(Statement statement) throws SQLException {

// 使用 ParameterHandler 对象来完成对 Statement 的设值

parameterHandler.setParameters((PreparedStatement) statement);

}

/**

*

* ParameterHandler 类的 setParameters(PreparedStatement ps) 实现

* 对某一个 Statement 进行设置参数

*/

public void setParameters(PreparedStatement ps) throws SQLException {

ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());

List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

if (parameterMappings != null) {

for (int i = 0; i < parameterMappings.size(); i++) {

ParameterMapping parameterMapping = parameterMappings.get(i);

if (parameterMapping.getMode() != ParameterMode.OUT) {

Object value;

String propertyName = parameterMapping.getProperty();

if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params

value = boundSql.getAdditionalParameter(propertyName);

} else if (parameterObject == null) {

value = null;

} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {

value = parameterObject;

} else {

MetaObject metaObject = configuration.newMetaObject(parameterObject);

value = metaObject.getValue(propertyName);

}

// 每一个 Mapping 都有一个 TypeHandler,根据 TypeHandler 来对 preparedStatement 进行设置参数

TypeHandler typeHandler = parameterMapping.getTypeHandler();

JdbcType jdbcType = parameterMapping.getJdbcType();

if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();

// 设置参数

typeHandler.setParameter(ps, i + 1, value, jdbcType);

}

}

}

}

从上述的代码可以看到,StatementHandler 的 parameterize(Statement) 方法调用了 ParameterHandler 的 setParameters(statement) 方法,ParameterHandler 的 setParameters(Statement)方法负责 根据我们输入的参数,对 statement 对象的 ? 占位符处进行赋值。

6、StatementHandler 的 Listquery(Statement statement, ResultHandler resultHandler)方法的实现:

/**

* PreParedStatement 类的 query 方法实现

*/

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {

//1.调用 preparedStatemnt。execute()方法,然后将 resultSet 交给 ResultSetHandler 处理

PreparedStatement ps = (PreparedStatement) statement;

ps.execute();

//2. 使用 ResultHandler 来处理 ResultSet

return resultSetHandler.<E> handleResultSets(ps);

}

从上述代码我们可以看出,StatementHandler 的 Listquery(Statement statement, ResultHandler resultHandler)方法的实现,是调用了 ResultSetHandler 的 handleResultSets(Statement) 方法。ResultSetHandler 的 handleResultSets(Statement) 方法会将 Statement 语句执行后生成的 resultSet 结果集转换成 List 结果集:

/**

* ResultSetHandler 类的 handleResultSets()方法实现

*

*/

public List<Object> handleResultSets(Statement stmt) throws SQLException {

final List<Object> multipleResults = new ArrayList<Object>();

int resultSetCount = 0;

ResultSetWrapper rsw = getFirstResultSet(stmt);

List<ResultMap> resultMaps = mappedStatement.getResultMaps();

int resultMapCount = resultMaps.size();

validateResultMapsCount(rsw, resultMapCount);

while (rsw != null && resultMapCount > resultSetCount) {

ResultMap resultMap = resultMaps.get(resultSetCount);

//将 resultSet

handleResultSet(rsw, resultMap, multipleResults, null);

rsw = getNextResultSet(stmt);

cleanUpAfterHandlingResultSet();

resultSetCount++;

}

String[] resultSets = mappedStatement.getResulSets();

if (resultSets != null) {

while (rsw != null && resultSetCount < resultSets.length) {

ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);

if (parentMapping != null) {

String nestedResultMapId = parentMapping.getNestedResultMapId();

ResultMap resultMap = configuration.getResultMap(nestedResultMapId);

handleResultSet(rsw, resultMap, null, parentMapping);

}

rsw = getNextResultSet(stmt);

cleanUpAfterHandlingResultSet();

resultSetCount++;

}

}

return collapseSingleResultList(multipleResults);

}

7 MyBatis 初始化机制

7.1 MyBatis 的初始化做了什么

任何框架的初始化,无非是加载自己运行时所需要的配置信息。MyBatis 的配置信息,大概包含以下信息,其高层级结构如下:



MyBatis 的上述配置信息会配置在 XML 配置文件中,那么,这些信息被加载进入 MyBatis 内部,MyBatis 是怎样维护的呢?

MyBatis 采用了一个非常直白和简单的方式---使用 org.apache.ibatis.session.Configuration 对象作为一个所有配置信息的容器,Configuration 对象的组织结构和 XML 配置文件的组织结构几乎完全一样 (当然,Configuration 对象的功能并不限于此,它还负责创建一些 MyBatis 内部使用的对象,如 Executor 等,这将在后续的文章中讨论)。如下图所示:



Configuration 对象的组织结构和 XML 配置文件的组织结构几乎完全一样

MyBatis 根据初始化好 Configuration 信息,这时候用户就可以使用 MyBatis 进行数据库操作了。可以这么说,MyBatis 初始化的过程,就是创建 Configuration 对象的过程 。

MyBatis 的初始化可以有两种方式:

基于 XML 配置文件:基于 XML 配置文件的方式是将 MyBatis 的所有配置信息放在 XML 文件中,MyBatis 通过加载并 XML 配置文件,将配置文信息组装成内部的 Configuration 对象。

基于 Java API:这种方式不使用 XML 配置文件,需要 MyBatis 使用者在 Java 代码中,手动创建 Configuration 对象,然后将配置参数 set 进入 Configuration 对象中。

接下来我们将通过基于 XML 配置文件方式的 MyBatis 初始化,深入探讨 MyBatis 是如何通过配置文件构建 Configuration 对象,并使用它。

7.2 基于 XML 配置文件创建 Configuration 对象

现在就从使用 MyBatis 的简单例子入手,深入分析一下 MyBatis 是怎样完成初始化的,都初始化了什么。看以下代码:

String resource = "mybatis-config.xml";

InputStream inputStream = Resources.getResourceAsStream(resource);

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession sqlSession = sqlSessionFactory.openSession();

List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo");

有过 MyBatis 使用经验的读者会知道,上述语句的作用是执行

com.foo.bean.BlogMapper.queryAllBlogInfo 定义的 SQL 语句,返回一个 List 结果集。总的来说,上述代码经历了 mybatis 初始化 -->创建 SqlSession -->执行 SQL 语句 返回结果三个过程。

上述代码的功能是根据配置文件 mybatis-config.xml 配置文件,创建 SqlSessionFactory 对象,然后产生 SqlSession,执行 SQL 语句。而 mybatis 的初始化就发生在第三句:SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 现在就让我们看看第三句到底发生了什么。

1、MyBatis 初始化基本过程:

SqlSessionFactoryBuilder 根据传入的数据流生成 Configuration 对象,然后根据 Configuration 对象创建默认的 SqlSessionFactory 实例。

初始化的基本过程如下序列图所示:



由上图所示,mybatis 初始化要经过简单的以下几步:

调用 SqlSessionFactoryBuilder 对象的 build(inputStream)方法;

SqlSessionFactoryBuilder 会根据输入流 inputStream 等信息创建 XMLConfigBuilder 对象;

SqlSessionFactoryBuilder 调用 XMLConfigBuilder 对象的 parse()方法;

XMLConfigBuilder 对象返回 Configuration 对象;

SqlSessionFactoryBuilder 根据 Configuration 对象创建一个 DefaultSessionFactory 对象;

SqlSessionFactoryBuilder 返回 DefaultSessionFactory 对象给 Client,供 Client 使用。

SqlSessionFactoryBuilder 相关的代码如下所示:

public SqlSessionFactory build(InputStream inputStream) {

return build(inputStream, null, null);

}

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {

try {

//2. 创建 XMLConfigBuilder 对象用来解析 XML 配置文件,生成 Configuration 对象

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);

//3. 将 XML 配置文件内的信息解析成 Java 对象 Configuration 对象

Configuration config = parser.parse();

//4. 根据 Configuration 对象创建出 SqlSessionFactory 对象

return build(config);

} catch (Exception e) {

throw ExceptionFactory.wrapException("Error building SqlSession.", e);

} finally {

ErrorContext.instance().reset();

try {

inputStream.close();

} catch (IOException e) {

// Intentionally ignore. Prefer previous error.

}

}

}

// 从此处可以看出,MyBatis 内部通过 Configuration 对象来创建 SqlSessionFactory,用户也可以自己通过 API 构造好 Configuration 对象,调用此方法创 SqlSessionFactory

public SqlSessionFactory build(Configuration config) {

return new DefaultSqlSessionFactory(config);

}

上述的初始化过程中,涉及到了以下几个对象:

SqlSessionFactoryBuilder :SqlSessionFactory 的构造器,用于创建 SqlSessionFactory,采用了 Builder 设计模式

Configuration :该对象是 mybatis-config.xml 文件中所有 mybatis 配置信息

SqlSessionFactory:SqlSession 工厂类,以工厂形式创建 SqlSession 对象,采用了 Factory 工厂设计模式

XMLConfigBuilder :负责将 mybatis-config.xml 配置文件解析成 Configuration 对象,共 SqlSessonFactoryBuilder 使用,创建 SqlSessionFactory

2、创建 Configuration 对象的过程:接着上述的 MyBatis 初始化基本过程讨论,java培训机构当 SqlSessionFactoryBuilder 执行 build()方法,调用了 XMLConfigBuilder 的 parse()方法,然后返回了 Configuration 对象 。那么 parse()方法是如何处理 XML 文件,生成 Configuration 对象的呢?

  • (1)XMLConfigBuilder 会将 XML 配置文件的信息转换为 Document 对象 ,而 XML 配置定义文件 DTD 转换成 XMLMapperEntityResolver 对象 ,然后将二者封装到 XpathParser 对象中,XpathParser 的作用是提供根据 Xpath 表达式获取基本的 DOM 节点 Node 信息的操作 。如下图所示:

  • (2)之后 XMLConfigBuilder 调用 parse()方法:会从 XPathParser 中取出节点对应的 Node 对象,然后解析此 Node 节点的子 Node:properties, settings, typeAliases,typeHandlers, objectFactory, objectWrapperFactory, plugins, environments,databaseIdProvider, mappers:

public Configuration parse() {

if (parsed) {

throw new BuilderException("Each XMLConfigBuilder can only be used once.");

}

parsed = true;

//源码中没有这一句,只有 parseConfiguration(parser.evalNode("/configuration"));

//为了让读者看得更明晰,源码拆分为以下两句

XNode configurationNode = parser.evalNode("/configuration");

parseConfiguration(configurationNode);

return configuration;

}

/**

* 解析 "/configuration"节点下的子节点信息,然后将解析的结果设置到 Configuration 对象中

*/

private void parseConfiguration(XNode root) {

try {

//1.首先处理 properties 节点

propertiesElement(root.evalNode("properties")); //issue #117 read properties first

//2.处理 typeAliases

typeAliasesElement(root.evalNode("typeAliases"));

//3.处理插件

pluginElement(root.evalNode("plugins"));

//4.处理 objectFactory

objectFactoryElement(root.evalNode("objectFactory"));

//5.objectWrapperFactory

objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));

//6.settings

settingsElement(root.evalNode("settings"));

//7.处理 environments

environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631

//8.database

databaseIdProviderElement(root.evalNode("databaseIdProvider"));

//9.typeHandlers

typeHandlerElement(root.evalNode("typeHandlers"));

//10.mappers

mapperElement(root.evalNode("mappers"));

} catch (Exception e) {

throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);

}

}

注意:在上述代码中,还有一个非常重要的地方,就是解析 XML 配置文件子节点的方法 mapperElements(root.evalNode("mappers")), 它将解析我们配置的 Mapper.xml 配置文件,Mapper 配置文件可以说是 MyBatis 的核心,MyBatis 的特性和理念都体现在此 Mapper 的配置和设计上。

  • (3)然后将这些值解析出来设置到 Configuration 对象中:解析子节点的过程这里就不一一介绍了,用户可以参照 MyBatis 源码仔细揣摩,我们就看上述的 environmentsElement(root.evalNode("environments")); 方法是如何将 environments 的信息解析出来,设置到 Configuration 对象中的:

/**

* 解析 environments 节点,并将结果设置到 Configuration 对象中

* 注意:创建 envronment 时,如果 SqlSessionFactoryBuilder 指定了特定的环境(即数据源);

* 则返回指定环境(数据源)的 Environment 对象,否则返回默认的 Environment 对象;

* 这种方式实现了 MyBatis 可以连接多数据源

*/

private void environmentsElement(XNode context) throws Exception {

if (context != null)

{

if (environment == null)

{

environment = context.getStringAttribute("default");

}

for (XNode child : context.getChildren())

{

String id = child.getStringAttribute("id");

if (isSpecifiedEnvironment(id))

{

//1.创建事务工厂 TransactionFactory

TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));

DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));

//2.创建数据源 DataSource

DataSource dataSource = dsFactory.getDataSource();

//3. 构造 Environment 对象

Environment.Builder environmentBuilder = new Environment.Builder(id)

.transactionFactory(txFactory)

.dataSource(dataSource);

//4. 将创建的 Envronment 对象设置到 configuration 对象中

configuration.setEnvironment(environmentBuilder.build());

}

}

}

}

private boolean isSpecifiedEnvironment(String id)

{

if (environment == null)

{

throw new BuilderException("No environment specified.");

}

else if (id == null)

{

throw new BuilderException("Environment requires an id attribute.");

}

else if (environment.equals(id))

{

return true;

}

return false;

}

  • (4)返回 Configuration 对象:将上述的 MyBatis 初始化基本过程的序列图细化:

7.3 基于 Java API 手动加载 XML 配置文件创建 Configuration 对象,并使用 SqlSessionFactory 对象 ##

我们可以使用 XMLConfigBuilder 手动解析 XML 配置文件来创建 Configuration 对象,代码如下:

String resource = "mybatis-config.xml";

InputStream inputStream = Resources.getResourceAsStream(resource);

// 手动创建 XMLConfigBuilder,并解析创建 Configuration 对象

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, null,null);

Configuration configuration=parse();

// 使用 Configuration 对象创建 SqlSessionFactory

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

// 使用 MyBatis

SqlSession sqlSession = sqlSessionFactory.openSession();

List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo");

7.4 涉及到的设计模式

初始化的过程涉及到创建各种对象,所以会使用一些创建型的设计模式。在初始化的过程中,Builder 模式运用的比较多 。

7.4.1 Builder 模式应用 1:SqlSessionFactory 的创建

对于创建 SqlSessionFactory 时,会根据情况提供不同的参数,其参数组合可以有以下几种 :



根据情况提供不同的参数,创建 SqlSessionFactory

由于构造时参数不定,可以为其创建一个构造器 Builder,将 SqlSessionFactory 的构建过程和表示分开 :



MyBatis 将 SqlSessionFactoryBuilder 和 SqlSessionFactory 相互独立

7.4.2 Builder 模式应用 2:数据库连接环境 Environment 对象的创建

在构建 Configuration 对象的过程中,XMLConfigBuilder 解析 mybatis XML 配置文件节点节点时,会有以下相应的代码:

private void environmentsElement(XNode context) throws Exception {

if (context != null) {

if (environment == null) {

environment = context.getStringAttribute("default");

}

for (XNode child : context.getChildren()) {

String id = child.getStringAttribute("id");

//是和默认的环境相同时,解析之

if (isSpecifiedEnvironment(id)) {

TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));

DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));

DataSource dataSource = dsFactory.getDataSource();

//使用了 Environment 内置的构造器 Builder,传递 id 事务工厂和数据源

Environment.Builder environmentBuilder = new Environment.Builder(id)

.transactionFactory(txFactory)

.dataSource(dataSource);

configuration.setEnvironment(environmentBuilder.build());

}

}

}

}

在 Environment 内部,定义了静态内部 Builder 类:

public final class Environment {

private final String id;

private final TransactionFactory transactionFactory;

private final DataSource dataSource;

public Environment(String id, TransactionFactory transactionFactory, DataSource dataSource) {

if (id == null) {

throw new IllegalArgumentException("Parameter 'id' must not be null");

}

if (transactionFactory == null) {

throw new IllegalArgumentException("Parameter 'transactionFactory' must not be null");

}

this.id = id;

if (dataSource == null) {

throw new IllegalArgumentException("Parameter 'dataSource' must not be null");

}

this.transactionFactory = transactionFactory;

this.dataSource = dataSource;

}

public static class Builder {

private String id;

private TransactionFactory transactionFactory;

private DataSource dataSource;

public Builder(String id) {

this.id = id;

}

public Builder transactionFactory(TransactionFactory transactionFactory) {

this.transactionFactory = transactionFactory;

return this;

}

public Builder dataSource(DataSource dataSource) {

this.dataSource = dataSource;

return this;

}

public String id() {

return this.id;

}

public Environment build() {

return new Environment(this.id, this.transactionFactory, this.dataSource);

}

}

public String getId() {

return this.id;

}

public TransactionFactory getTransactionFactory() {

return this.transactionFactory;

}

public DataSource getDataSource() {

return this.dataSource;

}

}

文章来源于码农小助手

用户头像

@零度

关注

关注尚硅谷,轻松学IT 2021.11.23 加入

还未添加个人简介

评论

发布
暂无评论
java培训:MyBatis的架构与原理分析_mybatis_@零度_InfoQ写作平台