Spring Mybatis细磕
前言
项目fork注释地址:https://github.com/nimbusking/mybatis-3
【备注】:
- MyBatis的源码注释非常emmm简单,如仔细研究的话,需要花点功夫。
- 项目注释里面,如看过的部分源码,会格式化代码(原始代码缩进2空格,看起来不是很舒服,原谅强迫症~)
一些概念
MyBatis的实现逻辑
- 在 MyBatis 的初始化过程中,会生成一个
Configuration
全局配置对象,里面包含了所有初始化过程中生成对象 - 根据
Configuration
创建一个SqlSessionFactory
对象,用于创建SqlSession
“会话” - 通过
SqlSession
可以获取到Mapper
接口对应的动态代理对象,去执行数据库的相关操作 - 动态代理对象执行数据库的操作,由
SqlSession
执行相应的方法,在他的内部调用Executor
执行器去执行数据库的相关操作 - 在
Executor
执行器中,会进行相应的处理,将数据库执行结果返回
MyBatis的缓存实现逻辑
MyBatis 提供了一级缓存和二级缓存
- 一级缓存
在 Executor 执行器(SimpleExecutor)中有一个 Cache 对象中,默认就是一个 HashMap 存储缓存数据,执行数据库查询操作前,如果在一级缓存中有对应的缓存数据,则直接返回,不会去访问数据库
默认的缓存区域为SESSION,表示开启一级缓存,可以设置为STATEMENT,执行完查询后会清空一级缓存,所有的数据库更新操作也会清空一级缓存- 缺陷:在多个 SqlSession 会话时,可能导致数据的不一致性,某一个 SqlSession 更新了数据而其他 SqlSession 无法获取到更新后的数据,出现数据不一致性,这种情况是不允许出现了,所以我们通常选择“关闭”一级缓存
- 缺陷:在多个 SqlSession 会话时,可能导致数据的不一致性,某一个 SqlSession 更新了数据而其他 SqlSession 无法获取到更新后的数据,出现数据不一致性,这种情况是不允许出现了,所以我们通常选择“关闭”一级缓存
- 二级缓存
在 Executor 执行器(CachingExecutor)中有一个 TransactionalCacheManager 对象中,可以在一定程度上解决的一级缓存中多个 SqlSession 会话可能会导致数据不一致的问题,就是将一个 XML 映射文件中定义的缓存对象放在全局对象中,对于同一个 Mapper 接口都是使用这个 Cache 对象,不管哪个 SqlSession 都是使用该 Cache 对象
执行数据库查询操作前,如果在二级缓存中有对应的缓存数据,则直接返回,没有的话则去一级缓存中获取,如果有对应的缓存数据,则直接返回,不会去访问数据库
默认全局开启,需要在每个 XML 映射文件中定义- 缺陷:对于不同的 XML 映射文件,如果某个的 XML 映射文件修改了相应的数据,其他的 XML 映射文件获取到的缓存数据就可能不是最新的,也出现了脏读的问题,当然你可以所有的 XML 映射文件都通过
<cache-ref />
来使用同一个 Cache 对象,不过这样太局限了,且缓存的数据仅仅是保存在了本地内存中,对于当前高并发的环境下是无法满足要求的,所以我们通常不使用MyBatis的缓存
- 缺陷:对于不同的 XML 映射文件,如果某个的 XML 映射文件修改了相应的数据,其他的 XML 映射文件获取到的缓存数据就可能不是最新的,也出现了脏读的问题,当然你可以所有的 XML 映射文件都通过
#{} 和 ${} 的区别
两者在 MyBatis 中都可以作为 SQL 的参数占位符,在处理方式上不同
- #{} :在解析 SQL 的时候会将其 替换成 ? 占位符,然后通过 JDBC 的
PreparedStatement
对象添加参数值,这里会进行预编译处理,可以有效的防止 SQL 注入,提高系统的安全性 - ${} :在 MyBatis 中带有该占位符的 SQL 片段会被解析成动态 SQL 语句,根据入参直接替换掉这个值,然后执行数据库相关操作,存在 SQL注入 的安全性问题
MyBatis中自定义标签的执行原理
MyBatis 提供了以下几种动态 SQL 的标签:<if />、<choose />、<when />、<otherwise />、<trim />、<where />、<set />、<foreach />、<bind />
大体的流程是:
在 MyBatis 的初始化过程中的解析 SQL 过程中,会将定义的一个 SQL 解析成一个个的 SqlNode 对象,当需要执行数据库查询前,需要根据入参对这些 SqlNode 对象进行解析,使用OGNL表达式计算出结果,然后根据结果拼接对应的 SQL 片段,以此完成动态 SQL 的功能。
简述Mapper接口的工作原理
在 MyBatis 的初始化过程中,每个一个 XML 映射文件中的<select />、<insert />、<update />、<delete />
标签,会被解析成一个 MappedStatement 对像,对应的 id 就是 XML 映射文件配置的 namespace+’.’+statementId,这个 id 跟 Mapper 接口中的方法进行关联
延伸的一个问题:同一个 Mapper 接口中为什么不能定义重载方法?
因为 Mapper 接口中的方法是通过 接口名称+’.’+方法名 去找到对应的 MappedStatement 对象,如果方法名相同,则对应的 MappedStatement 对象就是同一个,就存在问题了,所以同一个 Mapper 接口不能定义重载的方法
每个 Mapper 接口都会创建一个动态代理对象(JDK 动态代理),代理类会拦截接口的方法,找到对应的 MappedStatement 对象,然后执行数据库相关操作
执行流程如下:
在Spring中Mapper接口是如何被注入的?
通过 SqlSession 的 getMapper(Class<T> type)
方法,可以获取到 Mapper 接口的动态代理对象,那么在 Spring 中是如何将 Mapper 接口注入到其他 Spring Bean 中的呢?
在 MyBatis 的 MyBatis-Spring 集成 Spring 子项目中,通过实现 Spring 的 BeanDefinitionRegistryPostProcessor 接口,实现它的 postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) 方法,也就是在 Spring 完成 BeanDefinition 的初始化工作后,会将 Mapper 接口也解析成 BeanDefinition 对象注册到 registry 注册表中,并且会修改其 beanClass 为 MapperFactoryBean 类型,还添加了一个入参为 Mapper 接口的 Class 对象的名称
这样 Mapper 接口会对应一个 MapperFactoryBean 对象,由于这个对象实现了 FactoryBean 接口,实现它的 getObject() 方法,该方法会通过 SqlSession 的 getMapper(Class<T> type)
方法,返回该 Mapper 接口的动态代理对象,所以在 Spring Bean 中注入的 Mapper 接口时,调用其 getObeject() 方法,拿到的是 Mapper 接口的动态代理对象
Mybatis有哪些Executor执行器
- SimpleExecutor(默认):每执行一次数据库的操作,就创建一个 Statement 对象,用完立刻关闭 Statement 对象。
- ReuseExecutor:执行数据库的操作,以 SQL 作为 key 查找缓存的 Statement 对象,存在就使用,不存在就创建;用完后,不关闭 Statement 对象,而是放置于缓存
Map<String, Statement>
内,供下一次使用,就是重复使用 Statement 对象 - BatchExecutor:执行数据库更新操作(没有查询操作,因为 JDBC 批处理不支持查询操作),将所有 SQL 都添加到批处理中(通过 addBatch 方法),等待统一执行(使用 executeBatch 方法)。它缓存了多个 Statement 对象,每个 Statement 对象都是调用 addBatch 方法完毕后,等待一次执行 executeBatch 批处理。实际上,整个过程与 JDBC 批处理是相同。
- CachingExecutor:在上述的三个执行器之上,增加二级缓存的功能
MyBatis的延迟加载原理
MyBatis 获取到查询结果需要进行结果集映射,也就是将 JDBC 返回的结果集转换成我们需要的 Java 对象
在映射的过程中,如果存在嵌套子查询且需要延迟加载,则会为该返回结果的实例对象创建一个动态代理对象(Javassist),也就是说我们拿到的返回结果实际上是一个动态代理对象
这个动态代理对象中包含一个 ResultLoaderMap 对象,保存了需要延迟加载的属性和嵌套子查询的映射关系
当你调用了需要延迟加载的属性的 getter 方法时,会执行嵌套子查询,将结果设置到该对象中,然后将该延迟加载的属性从 ResultLoaderMap 中移除
- 如果你调用了该对象equals、clone、hashCode、toString某个方法(默认),会触发所有的延迟加载,然后全部移除
- 如果你调用了延迟加载的属性的 setter 方法,也会将将延迟加载的属性从 ResultLoaderMap 中移除
MyBatis的插件运行原理
MyBatis 的插件机制仅针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor
这 4 种接口中的方法进行增强处理
Mybatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandler 的 #invoke(…)方法。当然,只会拦截那些你指定需要拦截的方法
为什么仅支持这 4 种接口呢?
因为 MyBatis 仅在创建上述接口对象后,将所有的插件应用在该对象上面,也就是为该对象创建一个动态代理对象,然后返回
如何编写一个 MyBatis 插件?
让插件类实现 org.apache.ibatis.plugin.Interceptor
接口,还需要通过注解标注该插件的拦截点,然后在 MyBatis 的配置文件中添加的插件,例如:
1 |
|
Mybatis是如何进行分页的
Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非数据库分页
所以,实际场景下,不适合直接使用 MyBatis 原有的 RowBounds 对象进行分页。而是使用如下两种方案:
- 在 SQL 内直接书写带有数据库分页的参数来完成数据库分页功能
- 也可以使用分页插件来完成数据库分页。
这两者都是基于数据库分页,差别在于前者是工程师手动编写分页条件,后者是插件自动添加分页条件。
其中,分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义分页插件。在插件的拦截方法内,拦截待执行的 SQL ,然后重写 SQL ,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。
Mybatis如何处理include标签的
例如:如果 A 标签通过 include 引用了 B 标签的内容,请问,B 标签能否定义在 A 标签的后面,还是说必须定义在A标签的前面
虽然 Mybatis 解析 XML 映射文件是按照顺序解析的。但是,被引用的 B 标签依然可以定义在同一个 XML 映射文件的任何地方,Mybatis 都可以正确识别。也就是说,无需按照顺序,进行定义。
原理是,Mybatis 解析 A 标签,发现 A 标签引用了 B 标签,但是 B 标签尚未解析到,尚不存在,此时,Mybatis 会将 A 标签标记为未解析状态。然后,继续解析余下的标签,包含 B 标签,待所有标签解析完毕,Mybatis 会重新解析那些被标记为未解析的标签,此时再解析A标签时,B 标签已经存在,A 标签也就可以正常解析完成了(PS:跟IOC容器处理循环依赖很相似)。
MyBatis与Hibernate有哪些不同
按照用户的需求在有限的资源环境下只要能做出维护性、扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。简单总结如下:
- Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取。
- Mybatis 属于半自动 ORM 映射工具,在查询关联对象或关联集合对象时,需要手动编写 SQL 来完成。
Mybatis比IBatis比较大的几个改进是什么
- 有接口绑定,包括注解绑定 SQL 和 XML 绑定 SQL 。
- 动态 SQL 由原来的节点配置变成 OGNL 表达式。
- 在一对一或一对多的时候,引进了
<association />
标签,在一对多的时候,引入了<collection />
标签,不过都是在<resultMap />
里面配置
其它补充
MyBatis 工作原理总结
- 核心组件:
- SqlSessionFactory:全局单例,通过
mybatis-config.xml
和Mapper文件构建,生成SqlSession
。 - SqlSession:代表一次数据库会话,线程不安全,需确保每次请求独立创建。
- Executor:执行器,负责SQL调度(如
SimpleExecutor
、ReuseExecutor
、BatchExecutor
)。 - MapperProxy:动态代理对象,将Mapper接口方法映射到XML或注解的SQL语句。
- StatementHandler:处理JDBC Statement(预编译、参数设置)。
- ParameterHandler:将Java对象转换为SQL参数。
- ResultSetHandler:将ResultSet结果集转换为Java对象。
- 核心组件与初始化过程
- 配置文件加载:MyBatis 通过
mybatis-config.xml
加载全局配置(如数据源、类型处理器、插件等),并通过 Mapper XML 文件或注解定义 SQL 映射。 - SqlSessionFactory 构建:使用
SqlSessionFactoryBuilder
解析配置文件,生成SqlSessionFactory
,用于创建SqlSession
。 - SqlSession 生命周期:每个线程通过
SqlSessionFactory
创建SqlSession
实例,用于执行 SQL 操作。SqlSession
是非线程安全的,通常在一次请求或事务中使用后关闭。
- 动态代理与 SQL 执行流程
- Mapper 接口代理:MyBatis 通过 JDK 动态代理为 Mapper 接口生成代理对象,将接口方法调用转换为对应的 SQL 语句执行。
- 执行器(Executor):
SqlSession
内部通过Executor
执行 SQL,处理缓存(一级缓存)、参数绑定、结果映射等逻辑。 - 结果映射:通过
ResultMap
将 JDBCResultSet
转换为 Java 对象,支持自动映射或自定义映射规则。
- 核心流程步骤
- 加载配置:解析
mybatis-config.xml
和Mapper文件,构建Configuration
对象。 - 创建SqlSessionFactory:通过
SqlSessionFactoryBuilder
生成工厂实例。 - 创建SqlSession:通过工厂实例开启会话,内部生成
Executor
。 - 获取Mapper代理:通过
SqlSession.getMapper()
生成Mapper接口的代理对象。 - 执行SQL:代理对象调用方法,根据方法名关联SQL,通过
Executor
执行。 - 结果映射:通过
ResultSetHandler
将结果转换为POJO或集合。
MyBatis 与 Spring 集成原理
- Spring 管理 MyBatis 核心组件
- SqlSessionFactoryBean:Spring 通过
SqlSessionFactoryBean
替代原生SqlSessionFactoryBuilder
,将SqlSessionFactory
作为 Bean 注册到 IoC 容器,整合数据源和 MyBaticannerConfigurer 或 @MapperScan**:自动扫描指定包下的 Mapper 接口,生成代理 Bean 并注册到 Spring 容器,开发者可直接通过
@Autowired` 注入使用。 - 代理逻辑:调用 Mapper 方法时,代理对象通过
SqlSessionTemplate
执行 SQL,确保线程安全。
- SqlSessionTemplate 的作用
- 替代原生 SqlSession:
SqlSessionTemplate
是线程安全的,内部通过SqlSessionHolder
结合 Spring 事务管理,确保同一事务内共享同一个SqlSession
。 - 事务同步:与 Spring 事务管理器协同,在事务提交或回滚时自动关闭
SqlSession
。
- 事务管理整合
- 声明式事务:通过
@Transactional
注解,由 Spring 的DataSourceTransactionManager
管理事务。 - Connection 绑定:事务开启时,Spring 将数据库连接绑定到当前线程,MyBatis 操作复用该连接,保证事务原子性。
常见问题扩展
- Q:如何防止MyBatis的N+1查询问题?
- 【注】 N+1 查询问题:当查询主对象(如订单)时,触发 1 次主查询(获取 N 条数据),随后对每条主数据执行关联子查询(如查询每个订单的商品列表),导致总执行 1 + N 次 SQL,性能急剧下降。
<collection fetchType="eager">
,默认就是急加载)【一般使用联合查询就能解决了】 - Q:MyBatis如何支持多数据源?
A:配置多个SqlSessionFactory
,结合Spring的@Primary
和@Qualifier
注解区分。 - Q:XML和注解配置的优劣?
A:XML适合复杂SQL,注解适合简单场景,但可读性差。
使用手册
一个示例的mybatis-config.xml
1 |
|
配置动态数据源
基于 Spring 提供的 AbstractRoutingDataSource
,自己实现数据源的切换,下面就如何配置动态数据源提供一个简单的实现
例如:
1 | public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { |
整体架构
项目源码结构一览
MyBatis 的整体架构分为三层:基础支持层、核心处理层和接口层
每个模块分层介绍如下:
接口层
接口层,核心为 SqlSession 接口,该接口定义了 MyBatis 暴露给应用程序调用的 API,也就是上层应用与 MyBatis 交互的桥梁。接口层在接收到调用请求时,会调用核心处理层的相应模块来完成具体的数据库操作
核心处理层
核心处理层,实现了 MyBatis 的核心处理流程,包括 MyBatis 的初始化以及完成一次数据库操作的涉及的全部流程
基础支持层
- 解析器模块:解析文件,处理占位符
- 反射模块:对 Java 原生的反射进行良好的封装,进行了一系列的优化,提供更加简洁易用的 API 方便使用
- 异常模块:定义 MyBatis 自己的 Exception
- 数据源模块:提供相应的 DataSource 数据源实现,支持与第三方数据源的继承
- 事务模块:对数据库中的事务进行了抽象,提供事务接口的简单实现
- 缓存模块:提供一级缓存和二级缓存的支持
- 类型模块:提供别名机制,JDBC Type 和 Java Type 之间的转换
- IO模块:提供资源加载功能
- 日志模块:提供日志输出,支持集成第三方日志框架
- 注解模块:提供相关注解
- Binding模块:提供 Mapper 接口与 XML 映射文件进行关联的支持
基础支持层
解析器模块
主要包路径:org.apache.ibatis.parsing
主要功能:初始化时解析mybatis-config.xml配置文件、为处理动态SQL语句中占位符提供支持
这里面有三个重要的类,来分别看一下:
- org.apache.ibatis.parsing.XPathParser:基于Java XPath 解析器,用于解析MyBatis的mybatis-config.xml和**Mapper.xml等XML配置文件
- org.apache.ibatis.parsing.GenericTokenParser:通用的Token解析器
- org.apache.ibatis.parsing.PropertyParser:动态属性解析器
XPathParser类
代码不复杂,就不贴了,咱们就来看看总结的东西
这个类里面定义了几个属性:
类型 | 属性名 | 说明 |
---|---|---|
Document | document | XML文件被解析后生成对应的org.w3c.dom.Document 对象 |
boolean | validation | 是否校验XML文件,一般情况下为true |
EntityResolver | entityResolver | org.xml.sax.EntityResolver 对象,XML实体解析器,一般通过自定义的org.apache.ibatis.builder.xml.XMLMapperEntityResolver 从本地获取DTD文件解析 |
Properties | variables | 变量Properties对象,用来替换需要动态配置的属性值,例如我们在MyBatis的配置文件中使用变量将用户名密码放在另外一个配置文件中,那么这个配置会被解析到Properties对象用,用于替换XML文件中的动态值 |
XPath | xpath | javax.xml.xpath.XPath 对象,用于查询XML中的节点和元素 |
构造函数有很多,基本都相似,内部都是调用commonConstructor方法设置相关属性和createDocument方法为该XML文件创建一个Document对象
提供了一系列的eval*
方法,用于获取Document对象中的元素或者节点:
- eval相关元素的方法:根据表达式获取我们常用类型的元素的值,其中会基于variables调用PropertyParser的parse方法替换掉其中的动态值(如果存在),这就是MyBatis如何替换掉XML中的动态值实现的方式
- eval相关节点的方法:根据表达式获取到org.w3c.dom.Node节点对象,将其封装成自己定义的XNode对象,方便主要为了动态值的替换
PropertyParser类
org.apache.ibatis.parsing.PropertyParser
:动态属性解析器
这里面有一个parse方法
1 | public static String parse(String string, Properties variables) { |
其中:parse方法:创建VariableTokenHandler对象和GenericTokenParser对象,然后调用GenericTokenParser的parse方法替换其中的动态值
GenericTokenParser类
本质根据开始字符串和结束字符串解析出里面的表达式(例如${name}->name),然后通过TokenHandler进行解析处理
反射模块
将一个Entity实体类或者Map集合转换成MetaObject对象,该对象通过反射机制封装了各种简便的方法,使更加方便安全地操作该对象,创建过程:
- 通过Configuration全局配置对象的newMetaObject(Object object)方法创建,会传入DefaultObjectFactory、DefaultObjectWrapperFactory和DefaultReflectorFactory几个默认实现类
- 内部调用MetaObject的forObject静态方法,通过它的构造方法创建一个实例对象
- 在MetaObject的构造函数中,会根据Object对象的类型来创建ObjectWrapper对象
- 如果是创建BeanWrapper,则在其构造函数中,会再调用MetaClass的forClass方法创建MetaClass对象,也就是通过其构造函数创建一个实例对象
- 如果是MapWrapper,则直接复制给内部的
Map<String, Object> map
属性即可,其他集合对象类似 - 在MetaClass的构造函数中,会通过调用DefaultReflectorFactory的findForClass方法创建Reflector对象
- 在Reflector的构造函数中,通过反射机制解析该Class类,属性的set和get方法会被封装成MethodInvoker对象
数据源模块
MyBatis支持三种数据源配置,分别为UNPOOLED、POOLED和JNDI
。内部提供了两种数据源实现,分别是UnpooledDataSource
和PooledDataSource
。
在三种数据源配置中,**UNPOOLED
和 POOLED
是常用的两种配置。**
至于 JNDI,MyBatis 提供这种数据源的目的是为了让其能够运行在 EJB 或应用服务器等容器中,这一点官方文档中有所说明。由于 JNDI 数据源在日常开发中使用甚少,因此,本篇文章不打算分析 JNDI 数据源相关实现。大家若有兴趣,可自行分析。
MyBatis初始化
在MyBatis初始化过程中,大致会有以下几个步骤:
- 创建Configuration全局配置对象,会往TypeAliasRegistry别名注册中心添加Mybatis需要用到的相关类,并设置默认的语言驱动类为XMLLanguageDriver
- 加载mybatis-config.xml配置文件、Mapper接口中的注解信息和XML映射文件,解析后的配置信息会形成相应的对象并保存到Configuration全局配置对象中
- 构建DefaultSqlSessionFactory对象,通过它可以创建DefaultSqlSession对象,MyBatis中SqlSession的默认实现类
加载mybatis-config.xml
初始化入口在org.apache.ibatis.session.SqlSessionFactoryBuilder构造器中,因为需要通过mybatis-config.xml配置文件构建一个SqlSessionFactory工厂,用于创建SqlSession会话
主要涉及到以下几个类:
- org.apache.ibatis.session.SqlSessionFactoryBuilder:用于构建SqlSessionFactory工厂
- org.apache.ibatis.builder.xml.XMLConfigBuilder:根据配置文件进行解析,开始Mapper接口与XML映射文件的初始化,生成Configuration全局配置对象
- org.apache.ibatis.builder.xml.XMLMapperBuilder:继承BaseBuilder抽象类,用于解析XML映射文件内的标签
- org.apache.ibatis.session.Configuration:MyBatis的全局配置对象,保存所有的配置与初始化过程所产生的对象。上面解析mybatis-config.xml后的配置属性会存到这个类相关属性中去。
解析的过程就不用过多贴出来了,总结就是:
MyBatis会在SqlSessionFactoryBuilder构造器中根据mybatis-config.xml配置文件初始化Configuration全局配置对象,然后创建对应的DefaultSqlSessionFactory工厂
加载Mapper接口与映射文件
我们先看看一个示例的mapper文件
这个过程里面主要涉及下面一些类:
- org.apache.ibatis.builder.xml.XMLConfigBuilder:根据配置文件进行解析,开始Mapper接口与XML映射文件的初始化,生成Configuration全局配置对象
- org.apache.ibatis.binding.MapperRegistry:Mapper接口注册中心,将Mapper接口与其动态代理对象工厂进行保存,这里我们解析到的Mapper接口需要往其进行注册
- org.apache.ibatis.builder.annotation.MapperAnnotationBuilder:解析Mapper接口,主要是解析接口上面注解,其中加载XML映射文件内部会调用XMLMapperBuilder类进行解析
- org.apache.ibatis.builder.xml.XMLMapperBuilder:解析XML映射文件
- org.apache.ibatis.builder.xml.XMLStatementBuilder:解析XML映射文件中的Statement配置(
<select /> <update /> <delete /> <insert />
标签) - org.apache.ibatis.builder.MapperBuilderAssistant:Mapper构造器小助手,用于创建ResultMapping、ResultMap和MappedStatement对象
- org.apache.ibatis.mapping.ResultMapping:保存
<resultMap />
标签的子标签相关信息,也就是 Java Type 与 Jdbc Type 的映射信息 - org.apache.ibatis.mapping.ResultMap:保存了
<resultMap />
标签的配置信息以及子标签的所有信息 - org.apache.ibatis.mapping.MappedStatement:保存了解析
<select /> <update /> <delete /> <insert />
标签内的SQL语句所生成的所有信息
XMLConfigBuilder-解析入口
在org.apache.ibatis.builder.xml.XMLConfigBuilder中会解析mybatis-config.xml配置文件中的<mapper />
标签,调用其parse()->parseConfiguration(XNode root)->mappersElement(XNode parent)
方法,那么我们来看看这个方法,代码如下:
1 | private void mappersElement(XNode context) throws Exception { |
上述的configuration.addMappers(mapperPackage)
方法,底层是通过调用它的MapperRegistry的addMappers(String packageName)方法进行注册的。
看一下MapperRegistry中addMapper方法:
1 | // org.apache.ibatis.binding.MapperRegistry#addMapper |
注意上面第4步,通过new了一个MapperAnnotationBuilder
对象,完了调用parse方法解析。而这个对象就是解析该Mapper接口与对应XML映射文件的重要类。
MapperAnnotationBuilder类
MapperAnnotationBuilder:解析Mapper接口,主要是解析接口上面注解,加载XML文件会调用XMLMapperBuilder类进行解析
1 | // org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse |
上述loadXmlResource()方法中是通过,XMLMapperBuilder类来具体进行解析的,我们直接看看对应的parse方法
1 | public void parse() { |
这里面的执行步骤是:
- 根据Configuration全局配置判断当前XML映射文件是否已经加载过,例如resource为:xxx/xxx/xxx.xml
- 解析
<mapper />
节点,也就是解析整个的XML映射文件,在下面的configurationElement方法中讲解 - 标记该XML映射文件已经加载过,往Configuration全局配置添加该字段文件,例如添加:xxx/xxx/xxx.xml
- 绑定 Mapper 到该命名空间,避免在
MapperAnnotationBuilder#loadXmlResource
方法中重复加载该XML映射文件 - 解析待定的
<resultMap />、<cache-ref />
节点以及Statement
对象,因为我们配置的这些对象可能还依赖的其他对象,在解析的过程中这些依赖可能还没解析出来,导致这个对象解析失败,所以先保存在Configuration全局配置对象中,待整个XML映射文件解析完后,再遍历之前解析失败的对象进行初始化,这里就不做详细的讲述了,感兴趣的小伙伴可以看一下
我们接着上面,看几个特定的方法:
第2步的:configurationElement(parser.evalNode(“/mapper”))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24private void configurationElement(XNode context) {
try {
// <1> 获得 namespace 属性
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// <2> 解析 <cache-ref /> 节点
cacheRefElement(context.evalNode("cache-ref"));
// <3> 解析 <cache /> 节点
cacheElement(context.evalNode("cache"));
// 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// <4> 解析 <resultMap /> 节点
resultMapElements(context.evalNodes("/mapper/resultMap"));
// <5> 解析 <sql /> 节点们
sqlElement(context.evalNodes("/mapper/sql"));
// <6> 解析 <select /> <insert /> <update /> <delete /> 节点
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}这里面呢,就是处理mapper文件的具体步骤。对应的功能也非常清楚,我们就挑常见的继续看看。
resultMapElements(List<XNode> list)
方法用于解析<resultMap />
节点,最后会调用resultMapElement()
方法逐个解析生成ResultMap
对象
看看resultMapElements方法: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
52private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings,
Class<?> enclosingType) {
// 获取当前线程的上下文
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// <1> 获得 type 属性
String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType"))));
// 获得 type 对应的类
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
// 从 enclosingType Class 对象获取该 property 属性的 Class 对象
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
// 创建 ResultMapping 集合
List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
// 添加父 ResultMap 的 ResultMapping 集合
List<XNode> resultChildren = resultMapNode.getChildren();
// <2> 遍历 <resultMap /> 的子节点
for (XNode resultChild : resultChildren) {
if ("constructor".equals(resultChild.getName())) { // <2.1> 处理 <constructor /> 节点
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) { // <2.2> 处理 <discriminator /> 节点
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
// <2.3> 处理其它节点
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
// 为添加该 ResultMapping 添加一个 Id 标志
flags.add(ResultFlag.ID);
}
// 生成对应的 ResultMapping 对象
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
// 获得 id 属性,没有的话自动生成
String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
// 获得 extends 属性
String extend = resultMapNode.getStringAttribute("extends");
// 获得 autoMapping 属性
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
// <3> 创建 ResultMapResolver 对象,执行解析
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator,
resultMappings, autoMapping);
try {
// 处理 ResultMap 并添加到 Configuration 全局配置中
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}sqlElement方法
sqlElement(List<XNode> list)
方法用于解析所有的<sql />
节点,内部调用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class XMLMapperBuilder extends BaseBuilder {
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
// <1> 遍历所有 <sql /> 节点
for (XNode context : list) {
// <2> 获得 databaseId 属性
String databaseId = context.getStringAttribute("databaseId");
// <3> 获得完整的 id 属性
String id = context.getStringAttribute("id");
// 设置为 `${namespace}.${id}` 格式
id = builderAssistant.applyCurrentNamespace(id, false);
// <4> 判断 databaseId 是否匹配
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
// <5> 添加到 sqlFragments 中
sqlFragments.put(id, context);
}
}
}
}
XMLStatementBuilder类-Statement配置
XMLStatementBuilder:解析XML映射文件中的Statement配置
也就是解析<select /> <insert /> <update /> <delete />
节点,解析过程在parseStatementNode()方法中
1 | public void parseStatementNode() { |
SQL初始化过程
在解析<select /> <insert /> <update /> <delete />
节点的过程中,是如何解析SQL语句,如何实现动态SQL语句,最终会生成一个org.apache.ibatis.mapping.SqlSource
对象的,对于这烦琐且易出错的过程,我们来看看MyBatis如何实现的?
整体解析过程:
- 在XMLLanguageDriver语言驱动类中,通过XMLScriptBuilder对该到节点的内容进行解析,创建相应的SqlSource资源对象
- 在其解析的过程会根据不同的NodeHandler节点处理器对MyBatis自定义的标签(
<if /> <foreach />
等)进行处理,生成相应的SqlNode对象, - 最后将所有的SqlNode对象存放在MixedSqlNode中
其中解析的过程中会判断是否为动态的SQL语句,包含了MyBatis自定义的标签或者使用了${}都是动态的SQL语句,动态的SQL语句创建DynamicSqlSource对象,否则创建RawSqlSource对象
而通过SqlSource这个对象根据入参可以获取到对应的BoundSql对象,BoundSql对象中包含了数据库需要执行的SQL语句、ParameterMapping参数信息、入参对象和附加的参数(通过<bind />
标签生成的,或者<foreach />
标签中的集合的元素等等)
MyBatis-SQL执行过程
MyBatis中SQL执行的整体过程如下图所示:
在 SqlSession 中,会将执行 SQL 的过程交由Executor执行器去执行,过程大致如下:
- 通过DefaultSqlSessionFactory创建与数据库交互的 SqlSession “会话”,其内部会创建一个Executor执行器对象
- 然后Executor执行器通过StatementHandler创建对应的java.sql.Statement对象,并通过ParameterHandler设置参数,然后执行数据库相关操作
- 如果是数据库更新操作,则可能需要通过KeyGenerator先设置自增键,然后返回受影响的行数
- 如果是数据库查询操作,则需要将数据库返回的ResultSet结果集对象包装成ResultSetWrapper,然后通过DefaultResultSetHandler对结果集进行映射,最后返回 Java 对象
SqlSession会话与SQL执行入口
插件机制
MyBatis-Spring整合相关源码
Spring-Boot-Starter相关
常见的使用MyBatis遇到的错误
在使用 MyBatis 的过程中,开发者可能会遇到一些常见的错误。以下是这些错误的分类、原因分析以及解决方法:
1. 配置错误
1.1 数据源配置错误
- 错误现象:
- 数据库连接失败。
- 提示
Cannot get connection
或DataSource not configured
。
- 原因:
- 数据源配置不正确(如 URL、用户名、密码错误)。
- 数据库驱动未正确加载。
- 解决方法:
- 检查数据源配置(如
jdbc.url
、jdbc.username
、jdbc.password
)。 - 确保数据库驱动已正确添加到项目中。
- 检查数据源配置(如
1.2 MyBatis 配置文件错误
- 错误现象:
- 启动时提示
Invalid configuration
或Cannot find configuration file
。
- 启动时提示
- 原因:
mybatis-config.xml
文件路径错误或格式不正确。- XML 文件中有语法错误。
- 解决方法:
- 检查
mybatis-config.xml
文件路径是否正确。 - 使用 XML 校验工具检查文件格式。
- 检查
2. SQL 映射错误
2.1 SQL 语句错误
- 错误现象:
- 执行 SQL 时提示语法错误。
- 查询结果不符合预期。
- 原因:
- SQL 语句编写错误(如缺少逗号、关键字拼写错误)。
- 动态 SQL 拼接错误(如
<if>
标签使用不当)。
- 解决方法:
- 检查 SQL 语句的正确性。
- 使用日志打印最终执行的 SQL 语句(开启 MyBatis 的 SQL 日志)。
2.2 参数绑定错误
- 错误现象:
- 提示
Parameter not found
或BindingException
。
- 提示
- 原因:
- SQL 中的参数名与 Java 方法参数名不一致。
- 参数类型不匹配。
- 解决方法:
- 确保 SQL 中的参数名与方法参数名一致。
- 使用
@Param
注解显式指定参数名。
2.3 结果映射错误
- 错误现象:
- 查询结果无法映射到 Java 对象。
- 提示
ResultMap not found
或Unknown column
。
- 原因:
- 数据库字段名与 Java 对象属性名不一致。
resultMap
配置错误。
- 解决方法:
- 检查
resultMap
配置,确保字段名与属性名一致。 - 使用别名或
@Results
注解显式映射字段。
- 检查
3. 事务管理错误
3.1 事务未生效
- 错误现象:
- 数据库操作未回滚。
- 原因:
- 未配置 Spring 事务管理器。
- 事务注解
@Transactional
未正确使用。
- 解决方法:
- 配置
DataSourceTransactionManager
。 - 确保
@Transactional
注解添加到 Service 层方法上。
- 配置
3.2 事务传播行为错误
- 错误现象:
- 嵌套事务未按预期执行。
- 原因:
- 未正确配置事务传播行为(如
REQUIRED
、REQUIRES_NEW
)。
- 未正确配置事务传播行为(如
- 解决方法:
- 根据业务需求配置事务传播行为。
4. 动态 SQL 错误
4.1 动态 SQL 拼接错误
- 错误现象:
- 动态 SQL 拼接结果不符合预期。
- 原因:
<if>
、<choose>
等标签使用错误。- 参数为空时未正确处理。
- 解决方法:
- 检查动态 SQL 标签的使用。
- 使用
OGNL
表达式处理空值。
4.2 SQL 注入风险
- 错误现象:
- SQL 语句被恶意注入。
- 原因:
- 直接拼接用户输入的参数。
- 解决方法:
- 使用
#{}
代替${}
进行参数绑定。 - 避免直接拼接 SQL。
- 使用
5. 缓存错误
5.1 缓存未生效
- 错误现象:
- 查询结果未缓存。
- 原因:
- 未启用 MyBatis 二级缓存。
- 缓存配置错误。
- 解决方法:
- 在
mybatis-config.xml
中启用二级缓存。 - 在 Mapper 接口或 XML 文件中配置缓存。
- 在
5.2 缓存脏数据
- 错误现象:
- 查询结果与数据库不一致。
- 原因:
- 缓存未及时更新。
- 解决方法:
- 在更新操作后清除缓存。
- 配置缓存的刷新策略。
6. 其他常见错误
6.1 类型转换错误
- 错误现象:
- 提示
TypeHandler not found
或Cannot convert type
。
- 提示
- 原因:
- 数据库字段类型与 Java 类型不匹配。
- 解决方法:
- 使用合适的类型处理器(
TypeHandler
)。 - 在 SQL 中显式转换类型。
- 使用合适的类型处理器(
6.2 懒加载错误
- 错误现象:
- 提示
LazyInitializationException
。
- 提示
- 原因:
- 在 Session 关闭后尝试加载懒加载数据。
- 解决方法:
- 在 Session 关闭前加载数据。
- 使用
@Transactional
注解确保 Session 未关闭。
6.3 分页错误
- 错误现象:
- 分页查询结果不正确。
- 原因:
- 分页参数传递错误。
- 分页插件(如 PageHelper)配置错误。
- 解决方法:
- 检查分页参数是否正确传递。
- 确保分页插件配置正确。
总结
在使用 MyBatis 时,常见的错误主要集中在配置、SQL 映射、事务管理、动态 SQL 和缓存等方面。通过仔细检查配置、日志和代码逻辑,可以快速定位并解决这些问题。以下是一些通用的排查建议:
- 开启 MyBatis 的 SQL 日志,检查实际执行的 SQL。
- 使用单元测试验证 Mapper 方法。
- 确保 Spring 和 MyBatis 的版本兼容。
- 参考官方文档和社区资源解决问题。