MyBatis源码学习(二)
七、MyBatis 映射体系
7.1、MetaObject 反射工具类
1、简介
MetaObject 是 Mybatis 框架用于访问对象属性的工具类,底层实现为 java 的反射基础,通过它可以简化代码、不需要 try/catch 各种 reflect 异常
- 目前只支持 JavaBean、Collection、Map 三种类型对象访问,也可以自定义其他类型。
- MyBatis 中 MetaObject 的应用十分广泛,这个类位于 org.apache.ibatis.reflection 包下。
- MetaObject 几乎没有引用 MyBatis 的其他组件,是 MyBatis 中较为独立的工具类,在日后开发中如果需要使用类似的功能,可以考虑直接引用。
2、用法展示
- 创建一个 Blog 类和一个 User 类用于测试
- User 类
1 |
|
- Blog 类
1 |
|
- 创建测试方法进行测试
- 可以为对象属性中的值赋值(为 Blog 中的 User 对象属性中的 name 属性赋值)
- 当类中的对象属性为空时,会自动创建该属性对象(自动创建 Blog 中的 User 对象)
- 可以直接操作属性
- 自动查找属性名,支持下划线转驼峰命名
- 支持基于索引访问数组元素
1 |
|
查看结果
3、用法总结
- 查找属性
忽略大小写,支持驼峰、支持子属性
- 获取属性值
- 基于 . 获取子属性值,如 user.name
- 基于索引获取列表值 user[1].userId
- 基于 key 获取 map 值, map[key] ,如 resultMap[red]
- 设置属性
- 可以设置子属性值
- 支持自动创建子属性(必须带有空参构造方法,且不为集合)
7.2、MetaObject 底层结构
1、MetaObject 底层结构
在 MetaObject 工具类中同样使用了装饰器模式,查看 MetaObject 源码
可以看到, MetaObject 中存在一个 ObjectWrapper 类型的属性,这个 ObjectWrapper 是一个接口, MyBatis 提供了诸如 BaseWrapper (抽象类)、BeanWrapper、CollectionWrapper 和 MapWrapper 的类来实现这个接口,我们重点关注一下 BeanWrapper
- MetaObject 中 getValue 和 setValue 的源码展示
1 | public class MetaObject { |
2、BeanWrapper
和 MetaObject 不同的是,这个类只能操作当前对象的属性,不能操作属性之中的属性,它拥有的功能有
- 查找属性
MetaObject 中查找属性的具体实现, MetaObject 实现了对 BeanWrapper 的装饰
- 获取属性值
获取当前对象的属性
- 设置属性
设置当前对象的属性
- 创建属性
基于默认方法构造属性值
- 源码
1 | public class BeanWrapper extends BaseWrapper { |
在 MetaObject 工具类中,底层使用反射获取类属性与方法,而 BeanWrapper 中的 MetaClass 对象封装了反射相关逻辑,也就是说:BeanWrapper 实现了对 MetaClass 的包装,这也是装饰者模式的体现
3、MetaClass
使用反射基于属性名方法名获取方法与属性,封装了反射相关逻辑,它支持子属性获取。
- 查看源码
可以看到,MetaClass 虽然封装了与反射相关的逻辑,但还不是直接执行反射的组件,真正执行反射相关逻辑的是 MetaClass 中的 Reflector(反射器) 组件对象。
- 不支持子属性获取
- 装饰者
1 | public class MetaClass { |
7.3、ResultMap 结果集映射
1、介绍
MyBatis 中的结果集映射,实际上就是将 Java Bean 中的属性和**表中的 column **形成一一对应
2、ResultMapping
一个 ResultMap 对应多个 ResultMapping ,如 constructor 、id 、 result 、association 和 collection 标签都是 ResultMapping 的表现形式
7.4、自动映射
实现自动映射的条件
- 列名和属性名同时存在,忽略大小写和驼峰
- 当前列未手动设置映射
- 属性类别存在 TypeHandler
- 开启 autoMapping (默认开启)
7.5、MyBatis 解决循环依赖
1、问题提出
我们假设这样的一个情况,每一个博客对象中都对应多个评论,如果此时每个评论又关联一个博客对象,此时如果评论关联的博客对象与自己的所属的博客对象是同一个,那么是否会发生死循环?
不会,MyBatis 通过延迟加载来解决循环依赖问题
2、问题解决
MyBatis 执行结果映射的流程如下
- 填充属性
将一些简单的值填充到对应对象属性中
- 获取嵌套查询值
- 执行准备
准备参数
获取 MappedStatement
获取动态 SQL
创建缓存 key
- 判断是否存在一级缓存
如果存在一级缓存,那么获取到一级缓存中的值后,进行延迟装载,即获得对象值后不在第一时间把值填充到属性中。
- 是否开启懒加载?
如果开启了懒加载,那么进行懒加载,否则直接实时加载
3、查看源码
解决嵌套查询中循环依赖问题的代码在
org.apache.ibatis.executor.resultset.DefaultResultSetHandler
中该方法对应上面图中的 获取嵌套查询值
1 | private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) |
八、动态 SQL 解析
MyBatis 的另外一个核心功能便是动态 SQL,在了解 MyBatis 的动态 SQL 之前,我们先来了解一下关于动态 SQL 的一些类和接口
动态 SQL 就是在每次执行 SQL 时,基于预先编写的脚本和参数动态地构建可执行地 SQL 语句
将 XML 文件解析为 sql
8.1、相关类与接口介绍
1、SqlNode 接口
简单理解就是 xml 文件中的每个标签,如 if 、 for 、 set 等,每个 xml 标签都会解析成对应的 SqlNode 对象
1 | public interface SqlNode { |
- apply 方法
SqlNode 接口中定义的唯一方法,该方法会根据用户传入的实参,解析该 SqlNode 所记录的动态 SQL 节点,并调用 DynamicContext.appendSql() 方法将解析后的SQL片段追加到 DynamicContext.sqlBuilder 中保存。
当 SQL 节点下的所有SqlNode完成解析后,就可以从DynamicContext中获取一条动态生成的完整的SQL语句
2、SqlSource 接口
SQL 源接口,代表从 xml 文件或注解映射的sql内容,主要就是用于创建BoundSql,有实现类DynamicSqlSource(动态Sql源),StaticSqlSource(静态Sql源)等
1 | public interface SqlSource { BoundSql getBoundSql(Object parameterObject);} |
3、BoundSql
这个类是封装 MyBatis 最终产生 SQL 的类,包括 SQL 语句,参数和参数源数据等属性
1 | public class BoundSql { |
8.2、OGNL 表达式
1、概述
OGNL是Object Graphic Navigation Language(对象图导航语言)的缩写
它是一种功能强大的表达式语言,通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。这样可以更好的取得数据。
作用是降低对数据层访问的难度,它拥有类型转换、访问对象方法、操作集合对象等功能。
2、作用说明
- 条件断言
在 MyBatis 的 Mapper 文件中经常出现以下的判断
1 | <if test="field != null and field != ''"> |
在上面的写法中,OGNL 主要用于做条件断言,即判断 test=”field != null and field != ‘’ 的对错,从而决定是否进行拼接
- 四则运算
还有一些表达式用来赋值或者增强属性。经常用来做模糊搜索的
bind
标签:
1 | <bind name="nameLike" value="'%'+ name + '%'"/> |
这里的
value
也属于 OGNL 表达式e1+e2
,字符串是拼接,数字的话就是加法运算
- 类的内置方法
其实Mybatis的
Mapper.xml
中还可以使用对象的内置方法,比如我们需要判断一个java.util.Collection
集合是否为空,可以这么写
1 | <if test="collection!=null and collection.size()> 0"> and some_col = #{some_val}</if> |
这里就使用了对象的内置方法
Collection.size()
。我们还可以调用自定义对象
CollectionUtils
的静态方法来判断集合是否为空
1 | <if test="@org.apache.commons.collections4.CollectionUtils@isNotEmpty(collection)"> and some_col = #{some_val}</if> |
这里需要使用全限定类名
8.3、SqlSource 解析流程
SqlSource 解析 Mapper 文件,就是将 SqlSource 对象转换为 BoundSql 的过程,BoundSql 对象中包含可以交由 JDBC 执行的 SQL 、参数映射及参数
1、需要进行编译的脚本片段
对于拥有逻辑判断的一些脚本片段,在每次执行时我们都需要进行编译,这是因为每次都需要使用参数进行逻辑判断,这类脚本使用 DynamicSqlSource 进行解析
1 | <if test="title!=null"> ...</if> |
2、不需要进行编译的脚本片段
对于一些普通的脚本片段,在执行时只需要编译一次即可,这是因为不用每次都使用参数进行解析,使用 RowSqlSource 进行解析
3、编译后说明
就效率而言,RawSqlSource 的解析效率要高于 DynamicSqlSource ,这是因为不需要使用参数进行动态解析,无论是 DynamicSqlSource 还是 RawSqlSource ,每次解析完后都会将获取的 结果放在一个 StaticSqlSource 对象中。
8.4、动态脚本的解析流程
1、DynamicContext
DynamicContext从名字可以看出,它使动态sql解析过程中的上下文,其主要作用有两个:
- 将动态sql解析之后的sql片段保存下来
- 持有解析过程中使用到的参数
2、解析流程
一个 DynamicSqlSource 中包含多个 SqlNode 对象,以下图为例
在上图中,
select...
对应一个 SqlNode 对象,where 标签对应一个 SqlNode 对象,if 标签,if 标签所包围的值也都可以各自看为一个 SqlNode ,多个 SqlNode 节点组成一个语法树, DynamicSqlSource 就是为了去解析语法树中的各个节点。
参数
DynamicContext
的作用就是为各个动态sql节点实现类(诸如ChooseSqlNode
等)提供进行判断的上下信息.确保判断等操作的完整实现.
8.5、SqlNode 语法树结构
SqlNode 有非常多的实现子类,如下图
- MixedSqlNode
包含多个子 SqlNode
- IfSqlNode
顾名思义,就是 if 标签对应的 SqlNode,下同
判断标签中的表达式是否为真,如果是便执行 DynamicContext 对象的 apply 方法
1 | public boolean apply(DynamicContext context) { // 首先使用ognl表达式来判断<if>标签test属性的执行结果是否是true if (evaluator.evaluateBoolean(test, context.getBindings())) { // 然后解析子节点的内容,将解析完的sql放到DynamicContext中 contents.apply(context); return true; } return false;} |
- ForEachSqlNode
- TrimSqlNode
- WhereSqlNode
- StaticTextSqlNode
静态文本脚本对应的 SqlNode
- TextSqlNode
表达式文本脚本对应的 SqlNode,例如 select * from ${tablename}
TextSqlNode 的作用是用来解析sql中包含${}的内容,将${}替换成实际的参数
1、解析根节点 RootSqlNode
仍然以 8.4 的脚本为例,此时的根节点为一个 MixedSqlNode
使用 DynamicContext ,先执行根节点,也就是 MixedSqlNode 节点的逻辑,然后依次执行其子逻辑,每次执行都需要将结果追加到 DynamicContext 中,当执行到 IfSqlNode 节点的逻辑时,如果 if 标签中的判断为真,那么执行 if 节点下的逻辑,然后将结果追加到 DynamicContext 中。
2、逐行进行解析
- 第一行:
select * from users
为一个 StaticTextSqlNode - 第二行:
为一个 WhereSqlNode
WhereSqlNode 中又包含一个 MixedSqlNode ,对应 where 标签中的三个 if 标签
- 第三行:
为一个 IfSqlNode
注意,由于 if 标签中又可以写其他标签 ,那么 if 标签中包含的应该是一个 MixedSqlNode 节点,
- 第四行:and id = #{id}
一个 StaticText 节点
3、SqlNode 节点层级
上面 xml 文件中的 SQL 脚本转换为 SqlNode 后可以表示为
九、MyBatis 插件
9.1、MyBatis 插件体系
1、四大组件
MyBatis 有四个十分重要的组件,分别是执行器、JDBC 处理器、参数处理器和结果集处理器
2、 SQL 执行流程回顾
以查询为例,一条 SQL 的执行需要经过以下三步,具体可以分为4点
- 预处理
在这一步中,我们需要创建 Statement 对象(JDBC执行器参与)并设置参数(参数处理器参与)
- 执行 SQL 语句
在经过预处理及设置参数后,将 SQL 语句交由执行器执行
将执行完后的结果交由结果集处理器进行处理,读取结果。
- 关闭
9.2、基于插件实现自动分页
1、功能特性
- 易用性
不需要其他配置,参数中只需要带上 page 即可,page 需要尽可能简单
- 不对使用场景作假设
适配多种情况的业务,不限制用户使用方式,不影响其他正常业务
- 友好性
2、分页插件拦截入口分析
在实现分页插件时,我们需要做两件事
- 查询总记录数
- 改变原有的查询语句
分页插件的拦截入口是 StatementHandler
3、为什么拦截入口不是执行器
- 从上面的分析可知,在开启二级缓存的情况下,执行器的层级关系如下
如果拦截入口是执行器,那么我们假设一个分页执行器,这个执行器位于 CachingExecutor 上方,这个执行器需要做两件事情,即查询总记录数并重写 SQL ,但如果这次查询已经命中缓存,那便不需要再去做查询记录数与重写 SQL 的工作,故分页插件的拦截入口不应该设计在执行器中。
4、分页插件执行流程
使用 PageStatementHandler 对原有的 StatementHandler 进行装饰,在原有的基础上添加分页逻辑
9.3、简单分页插件实现
1、设计一个类 Page
这个类拥有三个属性
- total
总记录数
- size
每页大小
- index
页码,从 1 开始
1 | class Page { private int total; private int size; private int index; /** * 计算偏移量 * @return */ public int getOffset() { return size * (index - 1); }} |
2、定义一个 PagePlugin 类,这个类需要实现 Interceptor 接口
在类上添加 @Intercepts 注解,在 Intercepts 注解中使用 @Signature 指定要拦截的类与方法
- 使用 @Signature 中的 type 属性指定要拦截的类
这里我们需要拦截 StatementHandler 的一系列实现类,所以直接拦截接口
- 使用 @Signature 中的 method 属性指定要拦截的方法
这里我们要拦截 StatementHandler 中的 prepare 方法
- 使用 @Signature 中的 args 属性指定要拦截的方法的参数类型列表
配合上面的 method 名可以唯一确定一个方法
1 | public class PagePlugin implements Interceptor { |
3、实现 Interceptor 接口的 intercept 方法
1 | Object intercept(Invocation invocation) throws Throwable; |
- 查看 Invocation 类
1 | public class Invocation { |
我们要在 intercept 方法中实现分页逻辑
1 |
|
十、MyBatis 中的设计模式
10.1、建造者模式
1、模式介绍
Builder模式的定义是 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。,它属于创建类模式,一般来说,如果一个对象的构建比较复杂,超出了构造函数所能包含的范围,就可以使用工厂模式和Builder模式,相对于工厂模式会产出一个完整的产品,Builder应用于更加复杂的对象的构建,甚至只会构建产品的一个部分。
2、设计体现
例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;
10.2、工厂模式
1、模式介绍
简单工厂模式 (Simple Factory Pattern) :又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。
在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
2、设计体系
例如SqlSessionFactory、ObjectFactory、MapperProxyFactory
在 SqlSessionFactory 中,可以通过传入不同的 Executor 类型从而获取不同的 SqlSession 对象
1 | public interface SqlSessionFactory { |
10.3、单例模式
1、模式介绍
单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
单例模式的要点有三个:
- 某个类只能有一个实例;
- 它必须自行创建这个实例;
- 三是它必须自行向整个系统提供这个实例。
单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。
2、体现
在 MyBatis 中有两个地方运用到单例模式,分别是 ErrorContext 和 LogFactory
10.4、代理模式
1、模式介绍
代理模式 (Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用,它是一种对象结构型模式。
代理模式包含如下角色:
- Subject: 抽象主题角色
- Proxy: 代理主题角色
- RealSubject: 真实主题角色
2、体现
代理模式可以认为是Mybatis的核心使用的模式,正是由于这个模式,我们只需要编写
Mapper.java
接口,不需要实现,由Mybatis后台帮我们完成具体SQL的执行。当我们使用 getMapper 方法时,会调用
mapperRegistry.getMapper
方法,而该方法又会调用mapperProxyFactory.newInstance(sqlSession)
来生成一个具体的代理:
1 | /** * @author Lasse Voss */public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public Class<T> getMapperInterface() { return mapperInterface; } public Map<Method, MapperMethod> getMethodCache() { return methodCache; } protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } } |
- 先通过 newInstance(SqlSession sqlsession) 方法得到一个 MapperProxy 对象
- 然后调用
T newInstance(MapperProxy<T> mapperProxy)
生成代理对象然后返回。
查看 MapperProxy 代码
1 | public class MapperProxy<T> implements InvocationHandler, Serializable { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }} |
非常典型的,该
MapperProxy
类实现了InvocationHandler
接口,并且实现了该接口的invoke
方法。通过这种方式,我们只需要编写
Mapper.java
接口类,当真正执行一个Mapper
接口的时候,就会转发给MapperProxy.invoke
方法,而该方法则会调用后续的sqlSession.cud>executor.execute>prepareStatement
等一系列方法,完成SQL的执行和返回。
10.5、装饰器模式
1、模式介绍
装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility)**,就增加对象功能来说,装饰模式比生成子类实现更为灵活**。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。
2、体现
在 MyBatis 的 Executor 组件中,BaseExecutor 完成一级缓存相关功能,而将与数据库交互的功能交给子类进行,即 BaseExecutor 对拥有访问数据库功能的子类做了一层装饰,这层装饰让其可以使用一级缓存。
- 查看 BaseExecutor 的源码,可以看到 BaseExecutor 中定义了一个 Executor 属性,这个属性指向它的实现子类
1 | public abstract class BaseExecutor implements Executor { |
- 查看它的 query 方法,可以看到它底层调用了子类的 doQuery 方法,故 BaseExecutor 只实现了对其实现子类的装饰,为其实现子类添加了可以访问一级缓存的功能
doQuery 中调用了 queryFromDataBase 方法
queryFromDataBase 方法中调用了子类的实现方法
10.6、责任链模式
1、模式定义
责任链(Chain of Responsibility)模式的定义:为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
责任链模式也叫职责链模式。
在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,请求会自动进行传递。所以责任链将请求的发送者和请求的处理者解耦了。
2、体现
在 MyBatis 的二级缓存中,当有请求需要获取二级缓存时, MyBatis 会将许多 Cache 组件连成一条链,然后用于处理请求,这些 Cache 组件的功能各不一致,如
- SynchronizedCache 用于处理线程同步
- LoggingCahe 用于记录缓存命中率
- LRUCache 用于淘汰缓存
- BlockingCache 用于防止穿透
- PerpetualCache 用于缓存的内存存储等等
这些 Cache 组件一个或多个组合起来,形成一条处理链,就可以对请求进行处理,每一层的 Cache 组件将自己所能处理的任务处理完后,就将自己处理不了的请求交给链条的下一个 Cache 组件处理。
10.7、组合模式
1、模式介绍
组合模式组合多个对象形成树形结构以表示“整体-部分”的结构层次。
组合模式对单个对象(叶子对象)和组合对象(组合对象)具有一致性,它将对象组织到树结构中,可以用来描述整体与部分的关系。
同时它也模糊了简单元素(叶子对象)和复杂元素(容器对象)的概念,使得客户能够像处理简单元素一样来处理复杂元素,从而使客户程序能够与复杂元素的内部结构解耦。
在使用组合模式中需要注意一点也是组合模式最关键的地方:叶子对象和组合对象实现相同的接口。这就是组合模式能够将叶子节点和对象节点进行一致处理的原因。
2、体现
MyBatis 中一系列 SqlNode 接口的实现类就是组合模式的体现,这些类全部都是先统一的接口 SqlNode ,在经过解析后,会将动态 SQL 转换为 SqlNode 树
10.8、模板模式
1、模式介绍
模板方法模式是所有模式中最为常见的几个模式之一,是基于继承的代码复用的基本技术。
模板方法模式需要开发抽象类和具体子类的设计师之间的协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。
代表这些具体逻辑步骤的方法称做基本方法(primitive method);
而将这些基本方法汇总起来的方法叫做模板方法(template method),这个设计模式的名字就是从此而来。
模板类定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
2、体现
MyBatis 中的执行器就是这种模式的体现,先编写一个 BaseExecutor 实现大部分方法,然后将真正与数据库进行交互的 doQuery 和 doUpdate 方法定义为抽象方法,让它的子类根据需要去覆盖这些方法,真正与数据库打交道。
1 | public abstract class BaseExecutor implements Executor { |
10.9、适配器模式
1、模式介绍
适配器模式(Adapter Pattern) :**将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)**。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
2、体现
MyBatis 的 logging 包中有一个 Log 接口,这个接口定义了 Mybatis 直接使用的日志方法,而 MyBatis 提供了多种框架日志的实现,最终实现了所有外部日志框架到 MyBatis 日志包的适配
1 | /** |
以 Log4jImpl 实现类来说,这个类拥有一个 org.apache.log4j.Logger 的实例,然后所有的日志方法都委托这个实例来完成
1 | public class Log4jImpl implements Log { |
10.10、迭代器模式
1、模式介绍
迭代器(Iterator)模式,又叫做游标(Cursor)模式。
GOF给出的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。
2、体现
MyBatis 的
PropertyTokenizer
是property包中的重量级类,该类会被reflection包中其他的类频繁的引用到。这个类实现了Iterator
接口,在使用时经常被用到的是Iterator
接口中的hasNext
这个函数。
1 | public class PropertyTokenizer implements Iterator<PropertyTokenizer> { |