mybatis

架构分析

应用架构分析

Mybatis 是一个优秀的持久层框架,底层基于 JDBC 实现与数据库的交互,并在
JDBC 操作的基础上做了封装和优化,其应用架构如图所示:

Mybatis 之所以能够成为互联网项目中持久层应用的翘楚,其核心竞争力应该是它灵活的 SQL 定制,参数及结果集的映射。

产品架构分析

互联网项目中的任何一个框架都可以看成是一个产品,每个产品都有它自己的产品架构,Mybatis 也不例外,它的产品架构主要可以从接口应用,SQL
处理以及基础服务支撑等几个角度进行分析。如下图所示:

所有想成为平台架构师的程序员,在应用一个互联网框架的过程中都应对框架的设计理念,实现思路有一个很好的认知,并基于认知强化实践过程,拓展产品架构思维。

技术架构分析

mybatis配置的核心均是通过读取配置xml文件或注解annotation来实现配置的加载。这些配置均会放在各种集合中(如下图所示),而这些集合将会被
configuration类综合管理

  • mybatis的API架构

mybatis会暴露一个API主要为用户操作的API,SqlSession通过SqlSessionFactoryBactory工厂创建来执行sql操作

Mybatis实践

SqlSessionFactory

  • SqlSessionFactory 工厂对象创建分析
    1. 系统底层会通过流读取所有mybatis配置以及mapper文件
    2. 这些数据存储到对应的对象或集合中,几乎所有的数据均由configuration对象管理
    3. 上述操作均是通过构造者XmlConfigBuilder和一系列构造者实现,并将数据存储到configuration中
1
2
3
4
5
6
7
8
9
10
11
12
13
@Before
public void init()throws IOException{
//所有框架都有配置(xml,annotation,properties,...)
//1)对于文件需要实用IO进行读取
//2)对于注解需要借助反射进行读取
InputStream in=Resources.getResourceAsStream("mybatis-configs.xml");
//InputStream in=getClass().getClassLoader().getResourceAsStream("mybatis-configs.xml");
//假如是读取文件获取了流对象,后续需要对流对象中的内容进行读取和解析
//1)SqlSessionFactoryBuilder对象底层会首先借助xml解析器对流进行相关解析
//2)SqlSessionFactoryBuilder对象会对解析内容进行封装
factory=new SqlSessionFactoryBuilder().build(in);
//系统底层建造者模式构建工厂对象(此对象构建过程相对复杂)
}

  • SqlSessionFactory 创建之时序图分析

XmlConfigBuilder分析

XmlConfigBuilder中主要做了两种操作

  1. 读取流(读取为document对象)
  2. 赋值configuration
  • 流的读取位于xmlConfigBuilder的构造方法中,其主要职责就是已document的方式读出流,这里后续不做具体分析;
  • 后续会进行数据封装,参考封装


OpenSession

  • SqlSession对象生成分析
    1. 获取数据源,生成TransactionFactory(数据源在加载配置的时候就已经初始化)
    2. 初始化需要的Executor

SqlSession

  • SqlSession 对象应用过程分析

    1. SqlSession默认都是使用defaultSqlSession
    2. SqlSession会通过Configuration对象获取存储的MappedStatement,即mapper文件写的sql
      • SqlSession是通过配置文件命名空间+sql的ID来匹配对应map的key查询MappedStatement
      • MapperStatememt保存对应mapper文件指定的sql及相关sql配置
    3. SqlSession通过内部的Executor执行器去执行sql的查询(当开启缓存是会优先执行CachedExecutor)
    4. BaseExecutor是mybatis的默认缓存执行器,它是一个装饰者装饰其它默认执行器,因此当其它执行器执行时均会执行它
    5. 最终通过封装过的StatementHandler来执行sql(statement)类似JDBC,参考JDBC

mapper

  • Mapper接口对象应用方式分析
    1. mybatis会通过mapper接口对象的路径和方法获取到statement的key值以便获取到MapperStatement
    2. 它是由JDK动态代理来实现的

  • Mapper 接口代理对象创建分析

  • 模拟mapper代理实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;

/**
* 代理工厂对象,用于生产代理对象
*/
public class ProxyFactory {
Class<?> dao;
ProxyFactory(Class<?> dao){
this.dao = dao;
}
Object newInstance(InvocationHandler handler){
return Proxy.newProxyInstance(dao.getClassLoader(),new Class<?>[]{dao},handler);
}
}

/**
* 用户自定义mapper接口
*/
interface MybatisDao{
List<Object> selectAll();
}

/**
* 代理工具实现类,用于生产代理对象以及代理方法逻辑实现
*/
class MybatisProxyHandler implements InvocationHandler{
SqlSession sqlSession;
Class<?> daoInterface;
MybatisProxyHandler(SqlSession sqlSession,Class<?> daoInterface){
this.sqlSession = sqlSession;
this.daoInterface = daoInterface;
}
//具体代理操作,mybatis并没真正意义上用代理方法,仅仅只是用来获取mapper接口的路径
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("invocation handler");
String className = daoInterface.getName();
String methodName = method.getName();
String statement = className+methodName;
sqlSession.query(statement);
return null;
}
}
//模拟sqlSession接口
interface SqlSession{
Object query(String statement);
}
//模拟DefaultSqlSession
class DefaultSqlSession implements SqlSession{

@Override
public Object query(String statement) {
System.out.println("query data from statement:"+statement);
return null;
}
}
//测试
class test{
public static void main(String[] args) {
SqlSession sqlSession = new DefaultSqlSession();
Class<?> daoInterfase = MybatisDao.class;

InvocationHandler invocationHandler = new MybatisProxyHandler(sqlSession,daoInterfase);
ProxyFactory factory = new ProxyFactory(daoInterfase);
MybatisDao proxy = (MybatisDao)factory.newInstance(invocationHandler);

proxy.selectAll();

}
}
  • Mapper 代理创建之时序图分析

  • Mapper 代理对象数据访问应用过程分析
    1. 可见基于接口实现mybatis的sql调度也是通过基础的session.selectList(statement);操作来实现的

生命周期

正确的使用mybatisAPI需要事先了解各个API对象的生命周期,以便我们知道他们之间的地位关系,错误的使用会导致非常严重的并发问题。

提示:依赖注入框架可以创建线程安全的、基于事务的 SqlSession 和映射器,并将它们直接注入到你的 bean 中,因此可以直接忽略它们的生命周期。
如果对如何通过依赖注入框架使用 MyBatis 感兴趣,可以研究一下 MyBatis-Spring 或 MyBatis-Guice 两个子项目

  • SqlSessionFactoryBuilder:这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此
    SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个
    SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。

其它对象应用分析

数据封装及初始化分析

mapper加载分析

XmlConfigBuilder

  • 内部的parseConfiguration方法可以清晰的找到所有数据的封装方法,以mapper为例

image-20200927152115775

XMLMapperBuilder

  • 我们都知道mybatis_config主配置文件中的mappers存储的是所有mapper文件的路径,myubatis基于这个路径找到具体的每个mapper文件
  • mapper文件的读取和封装类似mybatis的主配置文件,都是由builder类的构造方法负责读取流,parse()
    方法负责封装数据,因此具体的封装要看parse()方法;这里可以看到框架重新启用了一个构造者XMLMapperBuilder去封装数据。

image-20200927152934471

进入parse()方法可以看到它会从mapper配置文件的根目录标签“mapper”开始解析。

  • 通过isResourceLoaded(),addLoadedResource()方法实现配置文件的缓存
  • 后续parsePendingResultMaps(),parsePendingCacheRefs(),parsePendingStatements()方法负责重新加载初始化配置数据,即二次加载,这里不做详细描述
  • mapper配置文件封装操作在configurationElement()方法

image-20200927161642394

如下图所示,可以看到是对mapper下的各种属性的封装和初始化,包括命名空间,结果集,参数集等

  • sql语句封装具体在buildStatementFromContext()方法

image-20200928105833920

XMLStatementBuilder

逐步进入方法,(其中会有包含mybatis多数据库支持的逻辑方法,在以后的章节中会聊)最终找到XMLStatementBuilder,这个构造者用于封装具体
select|insert|update|delete标签中的属性和语句

  • 可以清晰的看见它通过parseStatementNode()方法完成数据封装

image-20200928110255555

最终可以找到它会用到MapperBuilderAssistant类来辅助封装数据

  • MapperBuilderAssistant在初始化XMLMapperBuilder对象时的构造函数中初始化,即一个mapper文件对应一个builderAssistant
  • 方法中会用到langDriver.createSqlSource(configuration, context, parameterTypeClass);
    来将sql语句封装成SqlSource类型,这是一种基于组合模式的数据存储类型。
  • 利用applyCurrentNamespace()方法生产key,该key将作为以后查询这个sql的索引
  • addMappedStatement()方法最终具体实现了sql 数据封装,即将SqlSource等类型数据加入主配置Configuration中

image-20200929153050360

MapperStatment.Builder

  1. 在封装方法中可以找到新启动了一个MappereStatment
    的内部类Builder,这是构造者模式的一个变种,用于构造MapperStatment
  2. 最后通过configuration的addMappedStatement()
    方法将MapperStatment自己加入Configuration.MapperStatments的Map集合中以便configuration主配置文件管理,将来当我们要执行某个sql时,便会根据id为key来查找要执行的sql。

image-20200929160402307

总结

XmlConfigBuilder作为总构造者,负责加载调度其它构造者参与进程。其中XmlMapperBuilder
专项作为mapper配置文件的启动类,它会逐个从mybatis主配置文件中的mapper路径来读取流,解析成document最终通过调度其下的一系列方法来参与构造。
XmlStatementBuilder是mapperBuilder下主要用来封装<insert/>|<update/>|<delete/>|<select/>
语句中的sql以及参数和结果集的,它会负责读取标签中的配置,最终通过MapperStatment的内部类构造者MapperStatment.Builder将数据封装到configuration下。每个sql及sql的配置都交由MapperStatment管理,而MapperStatment则被configuration类的一个Map封装。

builder包含关系如下:
XmlConfigBuilder
└─XmlMapperBuilder
└─XmlStatementBuilder
└─MapperStatment.Builder


Sql标签及多数据库支持

当我们在MapperConfiguration配置文件中添加如下配置时,默认开启多数据库支持;此时,mybatis会根据当前链接的数据库来为Configuration配置匹配正确的数据库配置(
从下列配置中匹配,即mysql数据库匹配mysql)。随后我们可在mapper文件中的所有sql标签添加databaseId属性,此时,mybatis只会读取符合databaseId属性的sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mybatis-config.xml
<databaseIdProvider type="DB_VENDOR">
<property name="Oracle" value="oracle"/>
<property name="MySQL" value="mysql"/>
</databaseIdProvider>
-----------------------------------
<!-- 当我们连接mysql数据库时,一下两条sql均会被加载 -->
mapper.xml
<sql id="selectAll" databaseId="mysql">
select * from student
</sql>
<select id="selectStudentsBySQLFragment" resultType="org.example.pojo.Student">
<include refid="selectAll"></include>
<if test="list != null and list.size > 0">
where id in
<foreach collection="list" open="(" close=")" separator="," item="item">
#{item.id}
</foreach>
</if>
</select>

多数据库配置识别封装

该方法位于XMLConfigBuilder.java这个构建者中,核心方法是最后两行

  • databaseIdProvider.getDatabaseId(environment.getDataSource());:可以看出它会获取数据源,随后在方法中执行以下两个操作:
    • 从数据源中获取数据源名称,此例中即:”MySQL”
    • 匹配上文的</databaseIdProvider>标签内的配置,当能匹配上时则进行辅助;相反,没有匹配则不作配置;
  • configuration.setDatabaseId(databaseId);进行配置

image-20201022143827466

多数据库配置使用

下文中会分别解析Sql标签的实现原理以及databaseId实现多数据配置的使用原理

  • 在上文的xml文件的Mapper配置中,以<sql/>标签为例,它的databaseId为“mysql”

在构建者XMLMapperBuilder中,找到Sql的封装方法,这里不做截图:

  • sqlElement(context.evalNodes("/mapper/sql"));

进入有databaseId配置的方法,注意:这里获取的databaseId是先前从主配置文件中封装的

image-20201022145732372

该方法有以下几步

  1. 获取属性值,
    1. id:用于生成key值id,将来座位索引
    2. databaseId:会用于匹配先前在主配置文件中配置并加载的databaseId
  2. 生成ID:这个ID会成为索引key来方便以后进行调用
  3. 判断匹配两个databaseID是否一样,若一样则进行存储

image-20201022150112491

总结:

多数据库配置指的是当mybatis连接特定数据库时,只读取与这个数据库相关的sql(前提是我们要配置databaseId);它的配置存储在Configuration主配置类的databaseId变量中;程序读取mapper文件后会一一获取它们的databaseId属性来逐一判断是否加载;

关于sql标签的调用

上文用到sql标签的存储来演示多数据库配置,那sql标签是如何被调用的呢?

上文中提到sql标签中的内容会被存到一个sqlFragments的集合中,这个集合虽然是XmlMapperBuilder构建者的成员变量,但是它的初始化实际上是在Configuration主配置中实现的。因此可得出,在特定配置文件配置的sqlNode是可以全局使用它的,
但是sql标签的加载必须先于include标签的加载

image-20201022151753420

调用位于具体的<select/><insert/><delete/><update/>标签加载方法中,在SqlSource对象生成之前;(SqlSource对象是实现动态sql,数据拼接的基础;

该方法之前有分析过,可参考XMLStatementBuilder

image-20201023154457577

内部实现

  • 该方法运用递归的方式逐一查找每个子节点,找到那些需要替换的include标签进行替换
  • 同时当有符合${}表达式的参数时执行注入(注意:该操作只作用于sql标签中OGNL表达式)

image-20201023163938806

动态Mappere分析

sql封装结构

树和mapper文件对应关系

mybatis需要在用户内存空间中暂存sql数据,以便后续及时调用,而存在内存空间的结构需要仰仗mybatis的Sql数据结构

mybatis中sql的数据结构是mybatis框架的重要组成部分,实现动态sql,sql参数传递等操作均要依赖于优秀的数据结构,下面将介绍mybatis框架实现在这种数据结构的方案

mybatis的sql数据结构基于组合模式实现,这种模式将对象组合成树形结构以表示“部分-整体”的层次结构,而在mybatis的sql数据封装中,会将每个
<insert/>|<update/>|<delete/>|<select/>标签中的sql内容封装到这种树型结构中(即:每条sql为一棵树结构),如下图所示。

这些树最终将封装到MappereStatment封装对象中,受Configuration对象的MappedStatements集合管理。

sqlTree_1

树的实现

  • 标准的组合模式分为三个结构

    1. Component :组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件。
    2. Leaf:叶子对象。叶子结点没有子结点。
    3. Composite:容器对象,定义有枝节点行为,用来存储子部件,在Component接口中实现与子部件有关操作,如增加(add)和删除(
      remove)等。
  • 在mybatis中,上述组合部件对应的类如下

    • Component 作为整个树的接口,由SqlNode实现,它下面有一个公共方法apply()
      用于执行所有SqlNode的默认行为,默认行为视SqlNode实现对象类型而定后续会继续分析
    • Leaf子叶对象则会有很多种,它们各有各自的作为,在mapper文件中对应<insert/>|<update/>|<delete/>|<select/>
      标签中的每一行内容。都继承自SqlNode并实现不同的apply()方法,常见的类包括:TextSqlNode、IfSqlNode、StaticTextSqlNode等。
    • Composite容器对象由MixedSqlNode实现,同样也继承自SqlNode接口,实现自定义的apply()方法。容器用于承载各种Leaf子叶对象
    • 此外DynamicContext对象作为apply()方法的参数,作用是封装具体的sql操作,如sql的获取和拼接等

容器MixedSqlNode

1
2
3
4
5
6
7
8
9
10
11
public class MixedSqlNode implements SqlNode {
//SqlNode容器用集合实现
private final List<SqlNode> contents;

public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}

//默认方法是遍历调用所有子叶节点中的apply()方法
public boolean apply(DynamicContext context) {...}
}

子叶StaticTextSqlNode

用于封装静态sql语句,mybatis会识别sql语句是否有${}符号,若没有动态sql符号则新建StaticTextSqlNode对象存储这条sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author Clinton Begin
*/
public class StaticTextSqlNode implements SqlNode {
//字符串对象用于存储sql
private final String text;

public StaticTextSqlNode(String text) {
this.text = text;
}

@Override
//拼接本条sql
public boolean apply(DynamicContext context) {
context.appendSql(this.text);
return true;
}

}

子叶TextSqlNode

用于封装动态sql语句,与StaticTextSqlNode相反,当有${}符号时自动封装语句到该对象

  • 内部会用到两个内部类来辅助完成sql拼接和动态sql判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

public class TextSqlNode implements SqlNode {
//存储sql文本
private final String text;
//注入过滤器,用于过滤输入参数
private final Pattern injectionFilter;

public TextSqlNode(String text) {
this(text, null);
}

public TextSqlNode(String text, Pattern injectionFilter) {
this.text = text;
this.injectionFilter = injectionFilter;
}
//利用内部类DynamicCheckerTokenParser负责判断动态是否是动态sql
public boolean isDynamic() {...}

//利用内部类BindingTokenParser负责${}参数拼接
public boolean apply(DynamicContext context) {...}

private static class BindingTokenParser implements TokenHandler {...}

private static class DynamicCheckerTokenParser implements TokenHandler {...}

}

动态sql标签

子叶节点sql中,均是以xml节点形式存在,这种节点会利用ognl类库来辅助完成实现test属性的解析

子叶标签IfSqlNode
  • 节点SqlNode中会多出一个表达式解析器用于解析和判断节点中test属性对应的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class IfSqlNode implements SqlNode {
//表达式解析器,用于解析if标签中的test属性内容
private final ExpressionEvaluator evaluator;
//test存储if标签中的test属性内容
private final String test;
//封装下一级sqlNode,一般都是MixedSqlNode
private final SqlNode contents;

public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
}

//利用表达式解析器里面的evaluateBoolean()方法解析判断test属性中的值是否符合要求
//若符合要求则继续执行其下的sqlNode
//<if test="name != null and name != ''"/>
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
contents.apply(context);
return true;
}
return false;
}

}

下面会给出案例,两个if节点会封装成如下形式

1
2
3
4
5
6
7
8
9
10

<select id="selectStudentsIf" resultType="org.example.pojo.Student">
select * from student where 1=1
<if test="name != null and name != ''">
and name like '%' #{name} '%'
</if>
<if test="age > 0">
and age > #{age}
</if>
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
MixedSqlNode:
content[
TextSqlNode: "select * from student where 1=1",
ifSqlNode: test: "name != null and name != ''"
MixedSqlNode: content[
StaticTextSqlNode: "and name like '%' #{name} '%'"
],
ifSqlNode: test: "age > 0"
MixedSqlNode: content[
StaticTextSqlNode: "and name like '%' #{name} '%'"
]
]
]
子叶标签TrimSqlNode
案例

要知道TrimSqlNode首先需要了解<trim/>标签的作用,trim标记是一个格式化的标记,可以完成set或者是where标记的功能

详情可参考如下案例

1
2
3
4
5
6
7
8
9
10
11
12
13
select * from user

  
<trim prefix="WHERE" prefixoverride="AND |OR">

    
<if test="name != null and name.length()>0">AND name=#{name}</if>

    
<if test="gender != null and gender.length()>0">AND gender=#{gender}</if>

  
</trim>

假如说name和gender的值都不为null的话打印的SQL为:select * from user where and name = ‘xx’ and gender = ‘xx’

在删除线标记的地方是不存在第一个and的,上面两个属性的意思如下:

  • prefix:前缀+ prefixoverride:去掉第一个and或者是or
1
2
3
4
5
6
7
8
9
10
11
12
13
 update user

  
<trim prefix="set" suffixoverride="," suffix=" where id = #{id} ">

    
<if test="name != null and name.length()>0">name=#{name} ,</if>

    
<if test="gender != null and gender.length()>0">gender=#{gender} ,</if>

  
</trim>

假如说name和gender的值都不为null的话打印的SQL为:update user set name=’xx’ , gender=’xx’ , where id=’x’

在删除线标记的地方不存在逗号,而且自动加了一个set前缀和where后缀,上面三个属性的意义如下,其中prefix意义如上:

  • suffixoverride:去掉最后一个逗号(也可以是其他的标记,就像是上面前缀中的and一样)
  • suffix:后缀

原文链接

原理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class TrimSqlNode implements SqlNode {
//封装下级节点
private final SqlNode contents;
//封装对应的标签属性
private final String prefix;
private final String suffix;
private final List<String> prefixesToOverride;
private final List<String> suffixesToOverride;

private final Configuration configuration;

//解析这个<trim/>标签并转化成对应的sql
public boolean apply(DynamicContext context) {
//修改传参,将DynamicContext替换为FilteredDynamicContext,以便后续实现属性suffixoverride的功能将多余的符号剔除
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
//继续执行其内部的其它SqlNode
boolean result = contents.apply(filteredDynamicContext);
//它会调用内部类的方法applyAll()去实现sql的重拼接
filteredDynamicContext.applyAll();
return result;
}

private class FilteredDynamicContext extends DynamicContext {

public void applyAll() {
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
if (trimmedUppercaseSql.length() > 0) {
//分别实现prefix和suffix属性的功能,通过字符串操作重新拼接sql
applyPrefix(sqlBuffer, trimmedUppercaseSql);
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
delegate.appendSql(sqlBuffer.toString());
}
}
子叶标签WhereSqlNode

WhereSqlNode实际上是基于trim实现,因此WhereSqlNode继承TrimSqlNode共享它的所有方法,不同之处在于自动为TrimSqlNode添加了需要的初始化属性。

1
2
3
4
5
6
7
8
9
public class WhereSqlNode extends TrimSqlNode {
//配置需要删除的prefixOverride参数,通trim标签的prefixoverride属性
private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
//调用父类的构造方法,默认传where语句需要的参数
public WhereSqlNode(Configuration configuration, SqlNode contents) {
super(configuration, contents, "WHERE", prefixList, null, null);
}

}
子叶节点SetSqlNode

原理同上

1
2
3
4
5
6
7
8
9
public class SetSqlNode extends TrimSqlNode {

private static List<String> suffixList = Arrays.asList(",");

public SetSqlNode(Configuration configuration,SqlNode contents) {
super(configuration, contents, "SET", null, null, suffixList);
}

}
子叶节点ForEachSqlNode
案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!--
if标签中的array表示数组,
collection表示要遍历的集合类型。其中array表示数组,list表示集合。
open、close、separator为对遍历内容的SQL拼接
-->
<select id="selectStudentsForeachArray" resultType="org.example.pojo.Student">
select * from student
<if test="array != null and array.length > 0">
where id in
<foreach collection="array" open="(" close=")" separator="," item="item">
#{item}
</foreach>
</if>
</select>

<!--遍历泛型-->
<select id="selectStudentsForeachList2" resultType="org.example.pojo.Student">
select * from student
<if test="list != null and list.size > 0">
where id in
<foreach collection="list" open="(" close=")" separator="," item="item">
#{item.id}
</foreach>
</if>
</select>
原理

说明样例:<foreach collection="list" open="(" close=")" separator="," item="item">

  • open,close,参数为简单的参数拼接
  • collection会利用ognl类库来实现参数的获取,array标识获取数组类型,list标识获取集合类型
  • separator会通过装饰着PrefixedContext来扩展实现DynamicContext的功能,来逐步为后续sqlNode拼接上述样例中的“,”
  • 由于参数集是以map集合来实现的,item会用于设定索引,来存储遍历出的每个对象;儿后续子叶sqlNode中会通过DynamicContext获取到这个map并从中获取value值来拼接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class ForEachSqlNode implements SqlNode {
public static final String ITEM_PREFIX = "__frch_";
//表达式解析器
private final ExpressionEvaluator evaluator;
//集合解析器:用于解析传参中的集合类型如list为集合,array为数组
private final String collectionExpression;
//内部的子叶sqlNode
private final SqlNode contents;
//封装传参<foreach collection="list" open="(" close=")" separator="," item="item">
private final String open;
private final String close;
private final String separator;
private final String item;
private final String index;
private final Configuration configuration;

@Override
public boolean apply(DynamicContext context) {
//获取参数集合
Map<String, Object> bindings = context.getBindings();
//通过Ognl类库获取请求参数集合,根据collectionExpression表达式来判断集合和数组
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
//拼接open参数,这里的案例是拼接“(”
applyOpen(context);
int i = 0;
//遍历参数集合或数组中
for (Object o : iterable) {
DynamicContext oldContext = context;
//为 PrefixedContext工具类添加separator参数,样例中的separator是“,”,后续sqlNode中会继续利用PrefixedContext来辅助实现参数的拼接
if (first || separator == null) {
context = new PrefixedContext(context, "");
} else {
context = new PrefixedContext(context, separator);
}
int uniqueNumber = context.getUniqueNumber();
// Issue #709
//根据请求参数类型来绑定参数集,参数集以map的形式存储在DynamicContext中,以便后续SqlNode通过${}或#{}来从参数集中获取参数;mybatis的参数集是mybatis实现ognl语法的核销,以后的文档中会分析mybatis参数的获取传递以及ognl的利用实现
if (o instanceof Map.Entry) {
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
}
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
first = !((PrefixedContext) context).isPrefixApplied();
}
context = oldContext;
i++;
}
applyClose(context);
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}

//DynamicContext的装饰者实现,以便扩展参数集功能
private class PrefixedContext extends DynamicContext {...}

}

剩下的标签还有ChooseSqlNode,WhenSqlNode,OtherWiseSqlNode等,他们的原理与之前将的几个SqlNode相差无几,后面不多赘述;

SqlNode组合构成实现

启动类的初始化

  • mybatis的sqlNode构成实现启动类为XMLScriptBuilder,XMLScriptBuilder在初始化时会分别为context(解析xml中的sql)
    ,parameterType(与动态sql无关)赋值,以及初始化initNodeHandlerMap()方法,该方法用于初始化形成SqlNode组合的工具内部类们;该方法如下图

image-20201210105834529

启动类启动

  • parseScriptNode()方法是在初始化后被直接调用,也是启动类的启动方法,具体可查看XMLLanguageDriver类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public SqlSource parseScriptNode() {
//parseDynamicTags方法为具体sqlNode的封装方法,它返回封装后的sqlNode结果
MixedSqlNode rootSqlNode = this.parseDynamicTags(this.context);
SqlSource sqlSource = null;
//parseDynamicTags()在封装过程中判断是否有${}符号(及参数是否有以动态sql的形式注入) 注意:${}和#{}的区别
if (this.isDynamic) {
//如果有${}或则存储到动态SQL源中,如下以DynamicSqlSource来封装
//将来在Executer执行器执行sql参数注入时会通过DynamicSqlSource的getBoundSql方法来封装参数
sqlSource = new DynamicSqlSource(this.configuration, rootSqlNode);
} else {
//如果有没有${}则用RawSqlSource封装
sqlSource = new RawSqlSource(this.configuration, rootSqlNode, this.parameterType);
}

return (SqlSource)sqlSource;
}

封装SqlNode

  1. 启动方法的主要逻辑实现在parseDynamicTags()方法,如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
*该方法核心逻辑为for循环中的if,else判断;
*前置步骤是从node节点中获取子节点,注意,子节点并不包括孙子节点
**/
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
//当子节点Node是Text文本时,新建TextSqlNode封装文本
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
//根据text文本中是否有${}来判断是否是动态sql,如#{}则不是动态sql
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
//当子节点Node为Node时,调用HandleNode方法处理这个子节点
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
//根据节点名称获取特定的handler,这些handler在这个Builder类的构造函数中初始化
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
//传入集合容器contents,child子节点
handler.handleNode(child, contents);
isDynamic = true;
}
}
//返回这个容器list
return new MixedSqlNode(contents);
}
  1. 不同handler会有特有的自定义封装初始化以及特定的handleNode(child, contents)方法的操作,但大体流程一致
    1. 再次调用parseDynamicTags()方法封装这个子节点child内部子节点,递归的方式;并返回一个容器即MixedSqlNode,这个容器则是这个树状分支的分支即组合模式的component
    2. 解析Node标签中的属性如test等
    3. 新建特定的SqlNode(不同Handler处理不同的SqlNode)将获取的component分支以及Node标签属性传入它们的构造函数
    4. 将这个封装好的子叶SqlNode加入contents

DynamicContext类

DynamicContext用于封装请求参数,即我们查询数据库的实体类或基本数据类型等,该参数会以Object类型以DefaultSqlSession->
Exector->DynamicContix的流程传入;

而它最终应用于先前的各种SqlNode的apply方法中,以达到各种参数的获取,表达式的实现等;

下面对DynamicContext进行解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

public class DynamicContext {

public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";
//注册存取器,这个一个OGNL类库的功能,如下案例,它注册了一个ContextAccessor存取器,当OGNL解析表达式解析出ContextMap类时,则会用存取器的get方法来获取数据,达到自定义的作用
static {
OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
}

private final ContextMap bindings;
private final StringBuilder sqlBuilder = new StringBuilder();
private int uniqueNumber = 0;

public DynamicContext(Configuration configuration, Object parameterObject) {
/*
*若参数类型不是map类型,则将参数存入ContextMap的MetaObject成员中
*contextMap是存储参数的容器,它分别用继承自HashMap的map空间和自身的MetaObject成员来存储请求参数
*/
if (parameterObject != null && !(parameterObject instanceof Map)) {
//初始化metaObject
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}

static class ContextMap extends HashMap<String, Object> {
//MetaObject是mybatis通过反射封装的实体工具类,
//它封装了请求参数对象的反射方法,类等信息以便后续通过反射获取get方法获取参数类的值
private MetaObject parameterMetaObject;
public ContextMap(MetaObject parameterMetaObject) {
this.parameterMetaObject = parameterMetaObject;
}

@Override
public Object get(Object key) {
String strKey = (String) key;
if (super.containsKey(strKey)) {
return super.get(strKey);
}

if (parameterMetaObject != null) {
// issue #61 do not modify the context when reading
return parameterMetaObject.getValue(strKey);
}

return null;
}
}

static class ContextAccessor implements PropertyAccessor {

@Override
public Object getProperty(Map context, Object target, Object name)
throws OgnlException {
Map map = (Map) target;
Object result = map.get(name);
if (map.containsKey(name) || result != null) {
return result;
}
Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
if (parameterObject instanceof Map) {
return ((Map)parameterObject).get(name);
}
return null;
}
}
}

使用流程:

  1. 在映射参数之前,程序会初始化DynamicContext,并在构造函数中,为其成员ContextMap注入MetaObject;MetaObject封装请求参数param对象相关的反射信息
  2. 程序通过组合模式的树状结构,逐层向下调用apply方法,并传入DynamicContext
  3. 当有SqlNode需要映射#{}符号中的参数时,会调用对应的TokenParser方法从Ognl表达式中取值;
  4. 由于我们先前注册过ContextAccessor存取器,因此会Ognl表达式会优先通过存取器从ContextMap取值
  5. 最后通过DynamicContext的appendSql方法来拼接对应的#{}符号(这个符号会在特定方法中别替换,具体看GenericTokenParser.parse(
    text)方法),拼接替换的sql都会存在成员变量sqlBuilder中
  6. 此外DynamicContext也是实现动态sql的核心,实际上拼接映射参数和动态同时进行,例如TextSqlNode负责拼接#{}参数,而ifSqlNode负责动态sql根据不同的sqlNode实现不同的功能;

Sql封装

Sql封装分为两部分,分别为表达式封装,参数封装,以及Sql封装

  • 表达式封装,参考下一节“表达式#{}操作”
  • Sql封装,参考上一节“动态Mapper分析”以及”DynamicContext类“
  • 封装容器为SqlSource

SqlSource最终返回BoundSql,而里面的成员数据也均赋值于BoundSql,BoundSql参与后续的Sql语句执行

SqlSource分析

实际上在XMLStatementBuilder篇章就讲了SqlSource的部分功能,它用于封装已成树状结构的SqlNode;

SqlSource分为三种DynamicSqlSource,RawSqlSource,StaticSqlSource,下面逐一分析

  • *SqlSource有两个主要成员,初始化构造函数,和getBoundSql()方法
    • 以上三种sqlSource的初始化即是封装,封装过程在mapper配置加载阶段
    • getBoundSql()方法在执行sql操作时被调用

DynamicSqlSource

顾名思义,当xml中的sql语句为动态sql时,默认用DynamicSqlSource封装数据,(sql中有${}也用该方案封装)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class DynamicSqlSource implements SqlSource {

private final Configuration configuration;
private final SqlNode rootSqlNode;
//在配置加载阶段执行
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}


@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
//解析动态sql
rootSqlNode.apply(context);
//表达式封装工具类初始化,用于表达式封装,将请求中的#{}标识替换为?,并生成parameterMapping
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
//返回StaticSqlSource
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//为bondSql注入请求参数
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}

}

RawSqlSource

当mapper文件里的sql语句时文本时则使用RawSqlSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class RawSqlSource implements SqlSource {

private final SqlSource sqlSource;
//初始化步骤1
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
//初始化步骤3
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
//调用SqlSourceBuilder表达式封装工具类封装默认配置
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
//此处返回的是staticSqlsource
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
//初始化步骤2 初始化DynamicContext执行动态sql拼接
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(configuration, null);
rootSqlNode.apply(context);
return context.getSql();
}
//调用staticSqlsource的getBoundSql新建boundSql并注入parameterObject
@Override
public BoundSql getBoundSql(Object parameterObject) {
return sqlSource.getBoundSql(parameterObject);
}

}

StaticSqlSource

已完成封装的sqlSource,以上两种最终都会封装到staticSqlSource中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StaticSqlSource implements SqlSource {

private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Configuration configuration;

public StaticSqlSource(Configuration configuration, String sql) {
this(configuration, sql, null);
}

public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.configuration = configuration;
}

@Override
//注入请求参数
public BoundSql getBoundSql(Object parameterObject) {
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}

}

总结

  1. 无论是DynamicSqlSource还是RawSqlSource,最终都会转化为StaticSqlSource执行getBoundSql方法获取boundSql;
  2. DynamicSqlSource和RawSqlSource差异
    • DynamicSqlSource动态sql部分在请求数据库时执行,而RawSqlSource的动态sql部分在mybatis组件加载时执行,因此DynamicSqlSource有性能损失

表达式#{}操作

  1. 表达式封装位于SqlSourceBuilder类中,它会在Executor执行器执行时被调用
  2. SqlSourceBuilder实际只是作为封装的启动类或初始化类,其主要操作通过成员内部类ParameterMappingTokenHandler执行
  3. 它返回一个StaticSqlSource

表达式解析(ParameterExpression)

  • 已知Mybatis中通过#{表达式}来插入请求的参数,一般情况下我们仅仅插入单一的字符串值作为请求参数,但实际上mybatis还支持更多表达式功能;如下

    • #{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
      :JavaType,JdbcType,和TypeHandle顾名思义用于指定java类型,jdbc类型和类型处理器(类型处理器需要自行实现)
    • #{height,javaType=double,jdbcType=NUMERIC,numericScale=2}: numericScale用于指定小数点后面几位
  • ParameterExpression时表达式的解析类同事也是解析后的数据封装类,它继承了HashMap以key-Value的形式存解析出来的表达式

  • 表达式解析类通过ParameterExpression.java解析,具体分析如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
package org.learn.mybatis;//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import org.junit.Test;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

public class ParameterExpression extends HashMap<String, String> {
private static final long serialVersionUID = -2417552199605158680L;

public ParameterExpression() {

}
// public ParameterExpression(String expression) throws Exception {
// this.parse(expression);
// }

@Test
/**
* 解析表达式案例
*/
public void testParamExpression() throws Exception {
String s = " middleInitial, mode=OUT, jdbcType=STRUCT, jdbcTypeName=MY_TYPE, resultMap=departmentResultMap";
this.parse(s);
Set<Entry<String, String>> entries = this.entrySet();
Iterator<Entry<String, String>> iterator = entries.iterator();
while (iterator.hasNext()) {
Entry<String, String> next = iterator.next();
System.out.println("key=" + next.getKey() + ",value=" + next.getValue());
}
}

/**
* 表达式解析启动类
*/
private void parse(String expression) throws Exception {
int p = this.skipWS(expression, 0);
if (expression.charAt(p) == '(') {
this.expression(expression, p + 1);
} else {
this.property(expression, p);
}

}

private void expression(String expression, int left) throws Exception {
int match = 1;

int right;
for (right = left + 1; match > 0; ++right) {
if (expression.charAt(right) == ')') {
--match;
} else if (expression.charAt(right) == '(') {
++match;
}
}

this.put("expression", expression.substring(left, right - 1));
this.jdbcTypeOpt(expression, right);
}

/**
*从left左边界快开始,向
* @param expression 表达式
* @param left 左边界
* @throws Exception
*/
private void property(String expression, int left) throws Exception {
if (left < expression.length()) {
//找到有边界
int right = this.skipUntil(expression, left, ",:");
//截取propoty并插值
this.put("property", this.trimmedStr(expression, left, right));
this.jdbcTypeOpt(expression, right);
}

}

/**
* 识别空格分开的表达式,当有一个空格时,
* 返回这个空格位置,
* 以便后续截取空格前的表达式
* @param expression
* @param p
* @return
*/
private int skipWS(String expression, int p) {
for (int i = p; i < expression.length(); ++i) {
if (expression.charAt(i) > ' ') {
return i;
}
}

return expression.length();
}

/**
* 寻找endChars在表达式中的位置,
* 用于找到一个表达式的右边界
* 返回右边界位置
* @param expression 表达式
* @param p 查询起始位置
* @param endChars 需要找的字符
* @return
*/
private int skipUntil(String expression, int p, String endChars) {
for (int i = p; i < expression.length(); ++i) {
char c = expression.charAt(i);
if (endChars.indexOf(c) > -1) {
return i;
}
}

return expression.length();
}

private void jdbcTypeOpt(String expression, int p) throws Exception {
p = this.skipWS(expression, p);
if (p < expression.length()) {
if (expression.charAt(p) == ':') {
this.jdbcType(expression, p + 1);
} else {
if (expression.charAt(p) != ',') {
throw new Exception("Parsing error in {" + expression + "} in position " + p);
}

this.option(expression, p + 1);
}
}

}

/**
* 当有jdbcType属性时,插入这个属性
* @param expression
* @param p
* @throws Exception
*/
private void jdbcType(String expression, int p) throws Exception {
int left = this.skipWS(expression, p);
int right = this.skipUntil(expression, left, ",");
if (right > left) {
this.put("jdbcType", this.trimmedStr(expression, left, right));
this.option(expression, right + 1);
} else {
throw new Exception("Parsing error in {" + expression + "} in position " + p);
}
}

/**
* 通过递归的方式识别“=”两别的key和value,将表达式中的值插入map
* @param expression
* @param p
*/
private void option(String expression, int p) {
int left = this.skipWS(expression, p);
if (left < expression.length()) {
int right = this.skipUntil(expression, left, "=");
String name = this.trimmedStr(expression, left, right);
left = right + 1;
right = this.skipUntil(expression, left, ",");
String value = this.trimmedStr(expression, left, right);
this.put(name, value);
this.option(expression, right + 1);
}

}

/**
* 截取字符串,从star到end
* 倘若star或者end有空格,则向前或向后移一位
* 便于保证截出来的值不带空格
* @param str
* @param start
* @param end
* @return
*/
private String trimmedStr(String str, int start, int end) {
while (str.charAt(start) <= ' ') {
++start;
}

while (str.charAt(end - 1) <= ' ') {
--end;
}

return start >= end ? "" : str.substring(start, end);
}
}

表达式封装(ParameterMappingTokenHandler)

表达式识别

表达式识别通过工具类GenericTokenParser来实现,该工具类会根据参数识别需要的表达式,并自动调用handler中的handleToken()
方法处理表达式

image-20210129144106148

表达式封装

该方法会将表达式替换为?以便后续sql注入,具体的封装在buildParameterMapping()中

image-20210129145643124

方法具体内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
private ParameterMapping buildParameterMapping(String content) {
//上一节已分析过,会返回表达式所有内容的map集合
Map<String, String> propertiesMap = parseParameterMapping(content);
//注意,这个property是表达式的值即#{value},不是参数变量里面的值
String property = propertiesMap.get("property");
Class<?> propertyType;
//该步骤尝试从各种方案中获取 参数的类型
if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
propertyType = metaParameters.getGetterType(property);
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
propertyType = Object.class;
} else {
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
if (metaClass.hasGetter(property)) {
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
}
//初始化ParameterMapping,通过它的内部类构造者builder封装数据,这里的propertyType参数将用于指定“TypeHandler”
//从集合遍历通过if-else方法块逐步封装
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
Class<?> javaType = propertyType;
String typeHandlerAlias = null;
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
} else if ("property".equals(name)) {
// Do Nothing
} else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + PARAMETER_PROPERTIES);
}
}
//当我们实现类型转换器时,也从这里加入
if (typeHandlerAlias != null) {
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
return builder.build();
}

注意:ParameterMapping从始至终中只存放了查询表达式’#{value}‘,类型,类型转换器等特性,并未封装请求参数的值

Sql执行

通过mybatis主架构得知,mybatis的具体语句执行需要通过执行器Executor语句执行

以查询为例,以下是SimpleExecutor中调用查询方法的代码,可见已经传入了执行sql所需要的所有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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();
//1.初始化StatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//2.生成jdbc的Statement对象
stmt = prepareStatement(handler, ms.getStatementLog());
//5.执行statement
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
//3.方法可见是通过StatementHandler利用Connection对象生成
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
//4.此步骤为statement参数赋值,即通过jdbc函数,为sql中的?赋值
handler.parameterize(stmt);
return stmt;
}

可见sql的执行过程在handler类的辅助下变得简单,接下来具体分析这个handler类

StatementHandler(statement辅助类)

初始化

StatementHandler初始化通过Configuration对象的newStatementHandler()实现

1
2
3
4
5
6
7
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
//初始化一个RoutingStatementHandler
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
//拦截器相关逻辑
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}

可见核心是RoutingStatementHandler类的初始化

RoutingStatementHandler

观察这个类不难发现其实他是一个任务委派者,而真正执行的是下述代码中的三种Handler。

  1. SimpleStatementHandler
  2. PreparedStatementHandler
  3. CallableStatementHandler

委派模式的核心是成员变量delegate(delegete将指向具体的委派者),并且委派者同被委派者们都实现了StatementHandler接口,因此需要实现接口中的方法,而这些方法实际的实现都是通过delegate来调用对应的同名方法;具体参考下例中的委派案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//成员变量delegate
private final StatementHandler delegate;

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
//通过StatementType判断需要实现的StatementHandler,mybatsi默认使用PreparedStatementHandler,可以在xml中自行定义各种Handler
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}
}
//委派案例
public ParameterHandler getParameterHandler() {
return delegate.getParameterHandler();
}

PreparedStatementHandler

根据上述SimpleExecutor的doQuary()方法及后续分析得知,jdbc的api调用分为一下几个阶段,通过以下代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @author Clinton Begin
*/
public class PreparedStatementHandler extends BaseStatementHandler {
//1.调用父类构造函数初始化
public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
}
//4.调用statement执行sql语句,并通过resultSetHandler进行结果集映射
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
//2.通过connector和sql初始化PreparedStatement对象
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
//sql可以从之前封装的boundSql中获取
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
return connection.prepareStatement(sql);
} else {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
}
}
//3. 调用parameterHandler未statement中的?分别插入值
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
}

上述代码有两个疑问

parameterHandler和resultSetHandler这两个处理器来自何处?

它们在PreparedStatementHandler的父类BaseStatementHandler构造函数中处生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
this.configuration = mappedStatement.getConfiguration();
this.executor = executor;
this.mappedStatement = mappedStatement;
this.rowBounds = rowBounds;

this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
this.objectFactory = configuration.getObjectFactory();

if (boundSql == null) { // issue #435, get the key before calculating the statement
generateKeys(parameterObject);
boundSql = mappedStatement.getBoundSql(parameterObject);
}

this.boundSql = boundSql;
//初始化参数处理器 提供了boundSql(内涵parameterMapping)和parameterObject(请求参数对象)
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
//初始化结果集处理器
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}

ParameterHandler

初始化

ParameterHandler默认只有一种实现DefaultParameterHandler,它在configuration对象的方法中创建

1
2
3
4
5
6
7
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
//初始化
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
//拦截器
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}

注入参数

核心方法分析如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
//1.获取parameterMappings
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
//2.遍历parameterMappings然后解析parameterMapping,该类在ParameterMappingTokenHandler处已分析过
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
//3.通过xml中的#{value}表达式获取parameterObject中的对象
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 {
//4.mybatis默认用自己实现的表达式解析类MetaObject来实现,它的实现目的类似OGNL的API,通过分析OGNL表达式来获取对象
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
//5.获取TypeHandler,TypeHandler通过
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {

typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}

可以看到ParameterHandler仅仅是通过#{value}表达式获取到了实际请求参数中的值,并解析了ParameterMapping中配置
,如jdbc类型等;而实际为Statement插入请求参数则需要TypeHandler;

TypeHandler

mybatis默认为这个接口实现了N多种实现类(实现了大部分传统数据类型),并初始化了它们,但实际用哪种需要用户指定。例如在表达式中写入
#{value,javaType="int"};当用户未指定时则默认使用实现类UnknownTypeHandler,指定流程在ParameterMapping对象构造时完成;

TypeHandler和它的子类基于模板方法模式实现

BaseTypeHandler(模板方法类)

TypeHandler有一个抽象类BaseTypeHandler,它实现了setParameter方法,该方法将作为模板成为TypeHandler的该方法的唯一实现;相应的模板方法中自然会调用元素方法(由具体的TypeHandler实现类实现,而这些元素方法根据不的实现类实现了不同的功能,如:本例中根据情况对不同类型的请求参数为Statement对象赋值);以上是对模板方法模式的描述

模板方法代码如下,仅仅只有几个判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
if (jdbcType == null) {
throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
}
try {
ps.setNull(i, jdbcType.TYPE_CODE);
} catch (SQLException e) {
throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
+ "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
+ "Cause: " + e, e);
}
} else {
try {
//参数部位null执行该方法
setNonNullParameter(ps, i, parameter, jdbcType);
} catch (Exception e) {
throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
+ "Try setting a different JdbcType for this parameter or a different configuration property. "
+ "Cause: " + e, e);
}
}
}

UnknownTypeHandler(默认Handler)

作为默认实现,当不知道或没有请求参数类型时,使用该Handler;它实际是一个路由,用来动态发现传入的数据是什么类型

有一个核心变量typeHandlerRegistrySupplier 保存了所有基本类型Handler的map集合(实际这些集合存储在TypeHandlerRegistry类中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class UnknownTypeHandler extends BaseTypeHandler<Object> {

private static final ObjectTypeHandler OBJECT_TYPE_HANDLER = new ObjectTypeHandler();
// TODO Rename to 'configuration' after removing the 'configuration' property(deprecated property) on parent class
private final Configuration config;
private final Supplier<TypeHandlerRegistry> typeHandlerRegistrySupplier;
public UnknownTypeHandler(Configuration configuration) {
this.config = configuration;
this.typeHandlerRegistrySupplier = configuration::getTypeHandlerRegistry;
}
//通过setNonNullParameter()启动查询
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
throws SQLException {
//动态发现
TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
//用发现的Handler的setParameter方法为statement插入参数和类型
handler.setParameter(ps, i, parameter, jdbcType);
}
//动态发现方法
private TypeHandler<?> resolveTypeHandler(Object parameter, JdbcType jdbcType) {
TypeHandler<?> handler;
if (parameter == null) {
handler = OBJECT_TYPE_HANDLER;
} else {
//获取TypeHandlerRegistry对象,并调用getTypeHandler获取handler
handler = typeHandlerRegistrySupplier.get().getTypeHandler(parameter.getClass(), jdbcType);
// check if handler is null (issue #270)
if (handler == null || handler instanceof UnknownTypeHandler) {
handler = OBJECT_TYPE_HANDLER;
}
}
return handler;
}

篇幅限制不介绍TypeHandlerRegistry类和getTypeHandler()方法了,简单说明一下

  • TypeHandlerRegistry注册了所有的TypeHandler并通过Map集合管理,key为对象Type类型,value为
  • getTypeHandler()具体操作分以下几步
  1. 根据要注入的parameter对象的反射类型,从集合中获取对应的jdbcTypeHandler的map(注意,是map)
  2. 如果有jdbc类型设置则从这个map中获取,如果没有则根据要注入的parameter对象的类型从map中获取

下述代码是实际handler执行的statement参数的设置代码

1
2
3
4
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, parameter);
}

辅助类

表达式识别(GenericTokenParser)

表达式工具类只有一个方法,大致逻辑有三步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class GenericTokenParser {

private final String openToken;
private final String closeToken;
private final TokenHandler handler;

public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}

public String parse(String text) {
//通过openToken和closeToken识别text中所有的表达式
// for 识别出来的表达式
// 调用handler的handleToken(表达式)方法处理识别出来的表达式
}
}

表达式调用逻辑可参考ParameterMappingTokenHandler

参数处理类(TypeHandler)

TypeHandler用于辅助注入请求参数并指定jdbc的type类型

类型注册中心(TypeHandlerRegistry)

  • 初始化

TypeHandlerRegistry跟随configuration对象生成,作为它的成员变量伴随configuration终生

  • 主要成员

private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
:基于两个map实现,由于可能发生一个java类型对应多个jdbc类型,如在mybatis中字符串可以转化为varchar,clob,char…等类型,因此需要第二个map集合用来映射可能带jdbc类型

  • 主要方法:注册handler

向typeHandlerMap集合放置数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
//查询是否已有相关javatype对应map数据
Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
//没有则新建map
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
}
//向子map存入jdbcType和handler
map.put(jdbcType, handler);
//向父map存入javatype和子map
typeHandlerMap.put(javaType, map);
}
allTypeHandlersMap.put(handler.getClass(), handler);
}

mybatis
https://andrewjiao.github.io/2019/12/21/mybatis/
作者
Andrew_Jiao
发布于
2019年12月21日
许可协议