MyBatis 核心面试题 100 道(对标阿里 P6 级别)
P6 级别面试重点在于源码级理解、架构设计思想、性能调优实战和故障排查能力。以下题目按难度梯度编排,覆盖从核心原理到源码深度的全维度考察。
一、核心概念与架构设计(10题)
第1题:MyBatis 是什么?与传统 JDBC 和全自动 ORM 在架构设计上有何本质不同?
MyBatis 是什么:
MyBatis 是一款优秀的持久层框架,一个半自动 ORM(对象关系映射)框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的工作,可以使用简单的 XML 或注解来配置和映射原生类型。
与传统 JDBC 的本质不同:
在传统 JDBC 方式中,每次操作数据库都要进行注册驱动、获取连接、执行 SQL 语句、将结果集转换为 Java 实体对象、释放连接等步骤,过程繁琐且产生大量模板式代码。MyBatis 内部封装了 JDBC,设置参数、获取结果集、转换为实体类这些操作都由框架内部实现,用户无需关心。
与全自动 ORM(Hibernate)的本质不同:
Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或关联集合对象时,可以根据对象关系模型直接获取。而 MyBatis 在查询关联对象或关联集合对象时,需要手动编写 SQL 来完成。
MyBatis 的设计哲学承认对象与关系之间的阻抗不匹配是不可避免的,选择将 SQL 的控制权交还给开发者。全自动 ORM 试图完全屏蔽数据库细节,让开发者以面向对象的方式操作数据。
第2题:MyBatis 是"半自动"ORM 框架,"半"在哪里?设计决策背后的权衡是什么?
"半"在哪里:
"半自动"体现在两个方面:
- SQL 需要手动编写:开发者必须自己编写 SQL 语句,框架不会自动生成
- 映射关系需手动配置:需要通过 XML 或注解指定数据库表和实体类之间的映射规则
设计决策背后的权衡:
维度 全自动 ORM(Hibernate) 半自动 ORM(MyBatis)
SQL 控制 框架自动生成,开发者无法精细控制 开发者完全控制,可针对性优化
灵活性 较弱,难以处理复杂查询和存储过程 极强,支持动态表名、存储过程等
性能调优 自动生成的 SQL 不一定最优 开发者可手写最优 SQL
开发效率 高(无需写 SQL) 相对较低(需手写 SQL 和配置)
这个设计决策的本质是:在 SQL 控制权和开发效率之间寻找平衡点------MyBatis 选择牺牲部分自动化带来的开发效率,换取对 SQL 的完全控制权和极致的性能调优能力。
第3题:MyBatis 的核心架构分为哪几层?各层职责边界如何划分?
MyBatis 的功能架构分为三层:
第一层:API 接口层
· 提供给外部使用的接口 API,开发人员通过这些本地 API 来操纵数据库
· 核心对象是 SqlSession,它是上层应用和 MyBatis 打交道的桥梁
· 接口层接收到调用请求后,会调用数据处理层来完成具体的数据处理
第二层:数据处理层(核心层)
· 负责具体的 SQL 查找、SQL 解析、SQL 执行和执行结果映射处理等
· 主要目的是根据调用的请求完成一次数据库操作
· 核心组件包括:Executor(执行器)、StatementHandler(语句处理器)、ParameterHandler(参数处理器)、ResultSetHandler(结果集处理器)
第三层:基础支撑层
· 负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理
· 为上层的数据处理层提供最基础的支撑
· 这些是共用组件,被抽取出来作为最基础的组件
整体架构实现了门面模式(Facade Pattern) ,SqlSession 相当于一个 Facade,内部聚合了核心处理层的各个组件,对外屏蔽了复杂的逻辑处理。
第4题:MyBatis 的核心组件有哪些?核心类图及协作关系?
核心组件列表:
组件 作用
SqlSessionFactoryBuilder 解析配置文件,创建 SqlSessionFactory,是 MyBatis 的入口类
SqlSessionFactory MyBatis 的核心接口,用于创建 SqlSession 对象
SqlSession 核心接口,用于执行 SQL、提交事务、关闭连接等
Executor 执行器接口,负责 SQL 执行和缓存维护
StatementHandler 语句处理器,处理 PreparedStatement 对象
ParameterHandler 参数处理器,将 Java 对象转换成 JDBC 参数
ResultSetHandler 结果集处理器,将结果集映射为 Java 对象
MappedStatement 描述 SQL 语句的元数据对象
Configuration 核心配置类,管理所有配置信息
核心协作关系:
SqlSessionFactoryBuilder
↓ (读取配置,build)
SqlSessionFactory
↓ (openSession)
SqlSession ──── 聚合 ────→ Configuration
↓ (调用)
Executor
↓ (调用)
StatementHandler ←── ParameterHandler
↓ (执行)
ResultSetHandler
SqlSession 的默认实现类是 DefaultSqlSession,它有两个必须配置的属性:Configuration 和 Executor。SqlSession 对数据库的操作都是通过 Executor 来完成。
第5题:Configuration 对象的作用?为什么设计为单例?
Configuration 的作用:
Configuration 是 MyBatis 的核心配置类,负责管理 MyBatis 的所有配置信息。它涵盖了环境配置、数据源配置、映射配置等,所有与 MyBatis 运行时相关的配置信息都被封装在 Configuration 类中。
具体包括:
· 数据库环境信息(environment)
· Mapper 接口映射器信息(mapperRegistry)
· 类型处理器信息(typeHandlerRegistry)
· MappedStatement 的缓存
· 全局配置参数(settings)
为什么设计为单例(实际是单实例,非严格单例模式):
- 配置的唯一性:一个 SqlSessionFactory 对应一份完整的配置,配置在应用启动时一次性加载完成,运行时不应该被修改
- 资源效率:配置解析是重量级操作(XML 解析、反射等),重复创建会造成巨大开销
- 线程安全:Configuration 是只读的(启动后不再修改),多线程共享是安全的
- 语义正确:每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的,一个工厂对应一份配置
第6题:MyBatis 的模块边界如何划分?五层能力模型分析?
MyBatis 的源码模块划分清晰,可从五层能力模型理解:
- 接口层(session 模块)
· 定义 MyBatis 暴露给应用程序调用的 API
· 核心是 SqlSession 接口及其实现
- 核心处理层(核心处理层)
· 解析映射文件中的动态 SQL 节点,形成数据库可执行的 SQL 语句
· 包括:executor、statement、mapping 等包
- 配置解析层(解析器模块)
· 解析 XML 配置文件,处理占位符
· 包括:parsing、builder、binding 等包
- 基础支撑层(基础支持层)
· 为上层提供基础能力
· 包括:日志(logging)、数据源(datasource)、缓存(cache)、反射(reflection)、类型转换(type)等模块
- 扩展层(插件模块)
· 提供插件机制,允许拦截 MyBatis 的执行流程
· 基于动态代理实现
各层之间是单向依赖关系:上层依赖下层,下层不依赖上层,保证了模块的松耦合和可维护性。
第7题:MyBatis 如何做到 SQL 与 Java 代码的解耦?利弊分析?
解耦方式:
- XML 配置文件分离:SQL 语句写在 Mapper XML 文件中,与 Java 代码物理隔离
- 注解方式:通过 @Select、@Insert 等注解,将 SQL 定义在接口方法上(轻量级解耦)
- 命名空间映射:通过 namespace + statementId 将接口方法与 SQL 语句关联
- 参数和结果映射:通过 parameterType 和 resultMap/resultType 实现 Java 对象与 SQL 参数的映射
优势:
· SQL 可见、可调、可优化
· SQL 集中管理,便于 DBA 审查和优化
· 修改 SQL 无需重新编译 Java 代码
· 支持动态 SQL,灵活性极高
劣势:
· SQL 与代码分离导致跳转不直观,加大了维护成本
· 需要维护额外的 XML 文件,增加了项目文件数量
· XML 中的 SQL 缺少编译期检查,错误只能在运行时发现
· 对不熟悉 SQL 的开发者不够友好
第8题:MyBatis vs Hibernate,什么场景选 MyBatis?判断依据?
对比维度 MyBatis Hibernate
框架类型 半自动 ORM(SQL 映射) 全功能 ORM(对象关系映射)
SQL 控制 完全控制,手写 SQL 自动生成,HQL 操作
学习曲线 相对平缓 陡峭
复杂查询 强(手写优化 SQL) 弱(HQL 表达能力有限)
对象模型 较弱 强(完整 ORM 支持)
选择 MyBatis 的场景:
- 需要精细控制 SQL:复杂联表查询、报表统计、数据仓库场景
- 性能要求极高:需要针对特定 SQL 进行极致优化
- 遗留数据库适配:表结构不规范,无法用标准 ORM 映射
- 需要存储过程:MyBatis 支持存储过程调用
- 团队 SQL 能力强:团队有资深 DBA 或 SQL 专家
选择 Hibernate 的场景:
- 业务逻辑复杂、领域模型丰富的项目
- 追求开发效率,CRUD 为主,不需要复杂 SQL
- 团队对 SQL 不熟悉,更擅长面向对象编程
判断依据核心:如果对 SQL 有完全控制的需求 → 选 MyBatis;如果追求开发效率和对象建模 → 选 Hibernate。
第9题:MyBatis 的日志模块设计?如何适配多种日志框架?
设计模式:适配器模式(Adapter Pattern)
MyBatis 的日志模块是典型的适配器模式应用。为了兼容第三方日志框架,MyBatis 使用了适配器模式,并且使用的是适配器模式中的对象适配器------被适配者作为适配器的属性存在。
核心设计:
- 定义统一接口:MyBatis 定义了 Log 接口,并指定了四种日志级别
- 适配器实现:为每个第三方日志框架(Log4j、Log4j2、SLF4J、Logback、java.util.logging 等)提供对应的适配器实现
- 工厂模式加载:LogFactory 作为适配器对象工厂,负责选择具体的日志适配器
适配流程:
· 各日志框架的接口各不相同
· MyBatis 通过 Log 接口定义了一套统一的日志接口供上层使用
· LogFactory 在启动时按优先级尝试加载各个日志框架的适配器
· 一旦某个适配器加载成功,就不再尝试其他适配器
这种设计使得 MyBatis 可以在不修改核心代码的情况下,灵活适配任何日志框架。
第10题:MyBatis 的数据源模块设计特点?支持哪些数据源类型?
设计特点:
- 工厂模式:数据源的创建是典型的使用工厂模式的场景。DataSourceFactory 接口定义了数据源工厂的规范
- 集成与自研并重:MyBatis 不但能集成第三方的数据源组件,自身也提供了数据源的实现
- DataSource 接口标准:常见的数据源组件都实现了 javax.sql.DataSource 接口
支持的数据源类型:
类型 说明
UNPOOLED 无连接池的基础实现,每次请求都创建新连接
POOLED 自带连接池的实现,有连接池功能
JNDI 通过 JNDI 查找数据源
扩展性:在实际开发中,一般会使用 MyBatis 集成第三方数据源组件,如 C3P0、DBCP、Druid 等。数据源模块的性能直接关系到整个数据持久层的性能。
二、启动与初始化流程(12题)
第二部分答案:启动与初始化流程(第11-22题)
第11题:MyBatis 完整启动流程全链路
完整链路(从配置文件到 SqlSessionFactory):
1. 应用程序调用 SqlSessionFactoryBuilder.build(inputStream)
2. XMLConfigBuilder 解析 mybatis-config.xml
├── 解析 properties 文件,加载外部配置
├── 解析 settings,设置全局配置参数
├── 解析 typeAliases,注册别名
├── 解析 typeHandlers,注册类型处理器
├── 解析 plugins,注册拦截器
├── 解析 environments,配置数据源和事务工厂
├── 解析 databaseIdProvider
└── 解析 mappers → 触发 Mapper 加载
3. 加载 Mapper 映射文件/注解 Mapper
├── XMLMapperBuilder 解析 Mapper XML
│ ├── 解析 cache-ref / cache(二级缓存)
│ ├── 解析 resultMap
│ ├── 解析 parameterMap
│ ├── 解析 sql(可复用的 SQL 片段)
│ └── 解析 select/insert/update/delete → 创建 MappedStatement
└── MapperAnnotationBuilder 解析注解 Mapper
4. 所有配置组装成 Configuration 对象
5. SqlSessionFactoryBuilder 创建 DefaultSqlSessionFactory
6. 返回 SqlSessionFactory 给应用程序
关键类与调用链:
java
// 入口
SqlSessionFactoryBuilder.build(Reader reader)
→ XMLConfigBuilder.parse()
→ parseConfiguration(根节点)
→ mapperElement() // 处理 <mappers>
→ XMLMapperBuilder.parse()
→ configurationElement() // 解析 mapper XML
→ buildStatementFromContext() // 构建 MappedStatement
→ build(Configuration) // new DefaultSqlSessionFactory(config)
第12题:XML 配置文件由哪个类解析?解析后的信息存储在哪里?
解析类:
· 主配置文件(mybatis-config.xml):由 XMLConfigBuilder 解析
· Mapper 映射文件(xxxMapper.xml):由 XMLMapperBuilder 解析
· Mapper 注解:由 MapperAnnotationBuilder 解析
解析后的信息存储:
所有解析后的信息最终都存储在 Configuration 对象中。这是一个全局的配置容器,核心存储结构包括:
存储字段 类型 存储内容
mappedStatements Map<String, MappedStatement> 所有 SQL 映射(key = namespace + "." + id)
resultMaps Map<String, ResultMap> 所有结果集映射配置
parameterMaps Map<String, ParameterMap> 所有参数映射配置(已不推荐使用)
sqlFragments Map<String, XNode> 所有可复用的 SQL 片段( 标签)
typeHandlerRegistry TypeHandlerRegistry 类型处理器注册表
typeAliasRegistry TypeAliasRegistry 类型别名注册表
mapperRegistry MapperRegistry Mapper 接口注册表
environment Environment 数据库环境(数据源 + 事务工厂)
cache Cache 二级缓存(全局级别,已废弃使用 PerpetualCache)
源码确认:
java
// XMLConfigBuilder.parseConfiguration()
private void parseConfiguration(XNode root) {
propertiesElement(root.evalNode("properties"));
settingsAsProperties(root.evalNode("settings"));
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
typeHandlerElement(root.evalNode("typeHandlers"));
environmentElement(root.evalNode("environments"));
mapperElement(root.evalNode("mappers")); // 触发 Mapper 解析
}
第13题:mybatis-config.xml 中 settings 的关键配置及影响
核心配置及作用:
配置项 可选值 作用 生产环境建议
cacheEnabled true/false 全局开启/关闭二级缓存 true(开启)
lazyLoadingEnabled true/false 全局开启/关闭延迟加载 true(开启,结合分页)
aggressiveLazyLoading true/false 当开启时,对任意方法的调用都会触发该对象所有延迟加载属性的加载;否则只触发按需加载 false(3.4.1+ 后默认 false)
multipleResultSetsEnabled true/false 是否允许单一语句返回多个结果集 true
useColumnLabel true/false 使用列标签代替列名 true
useGeneratedKeys true/false 是否允许 JDBC 自动生成主键 false(建议在具体语句上配置)
autoMappingBehavior NONE/PARTIAL/FULL 自动映射行为 PARTIAL(默认,推荐)
autoMappingUnknownColumnBehavior NONE/WARNING/FAILING 遇到未知列时的行为 WARNING(开发期)→ NONE(生产)
defaultExecutorType SIMPLE/REUSE/BATCH 默认执行器类型 SIMPLE(默认,REUSE 适合高并发,BATCH 适合批量)
defaultStatementTimeout 整数(秒) 默认查询超时时间 根据业务设置
defaultFetchSize 整数 默认 fetch size,影响 ResultSet 每次读取的记录数 500-1000(调优)
mapUnderscoreToCamelCase true/false 是否将下划线字段名自动映射为驼峰属性 强烈建议开启
logImpl SLF4J/LOG4J/LOG4J2/STDOUT_LOGGING/NO_LOGGING 日志实现 使用 SLF4J(通过 logback 或 log4j2)
localCacheScope SESSION/STATEMENT 一级缓存范围 SESSION(默认,开启事务内缓存);STATEMENT(只缓存当前语句)
jdbcTypeForNull 默认 NULL 为 null 的字段设置默认 JDBC 类型 某些数据库(如 Oracle)需要设为 OTHER
callSettersOnNulls true/false 是否在 set 时处理 null false(默认)
关键配置联动效应:
mapUnderscoreToCamelCase = true → 减少 80% 的 resultMap 配置
lazyLoadingEnabled = true + aggressiveLazyLoading = false → 实现真正的按需懒加载
defaultExecutorType = BATCH + 手动 flush → 批量插入性能提升 10~100 倍
第14题:Mapper XML 文件的解析流程 & XMLMapperBuilder 的作用
XMLMapperBuilder 的核心职责:
XMLMapperBuilder 是专门用于解析 Mapper XML 文件的构建器,它的作用是把 XML 中的配置转换成 Configuration 中的 Java 对象。
完整解析流程:
java
// 1. 入口:XMLMapperBuilder.parse()
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
// 2. 绑定 Mapper 接口
bindMapperForNamespace();
}
// 3. 处理未完成的 ResultMap(处理继承关系)
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
// 2. configurationElement() 解析配置节点
private void configurationElement(XNode context) {
String namespace = context.getStringAttribute("namespace");
builderAssistant.setCurrentNamespace(namespace);
// 解析 cache-ref → CacheRefResolver
// 解析 cache → 创建 Cache 对象
// 解析 parameterMap → ParameterMap(已废弃)
// 解析 resultMap → ResultMap(核心)
// 解析 sql → SqlSource 存储为 SQL 片段
// 解析 select|insert|update|delete → MappedStatement
buildStatementFromContext(...);
}
// 3. buildStatementFromContext() 构建 MappedStatement
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(...);
statementParser.parseStatementNode(); // 解析每个 SQL 标签
}
}
解析后的存储:
每个 、、、 标签最终都会被封装成一个 MappedStatement 对象,并存入 Configuration.mappedStatements 中,key = namespace + "." + id。
第15题:注解 Mapper 的解析 & 与 XML 解析的异同
注解 Mapper 的解析流程:
java
// 入口:MapperAnnotationBuilder.parse()
public void parse() {
Class<?> mapperInterface = assistant.getMapperType();
// 1. 解析接口上的注解(@CacheNamespace 等)
parseCache();
// 2. 解析接口上的 @SelectProvider、@UpdateProvider 等
// 3. 遍历接口中的每个方法
for (Method method : mapperInterface.getMethods()) {
if (!method.isBridge()) {
parseStatement(method); // 解析方法上的 SQL 注解
}
}
}
// parseStatement() 中解析各注解
private void parseStatement(Method method) {
// 判断方法上是否有 @Select/@Insert/@Update/@Delete
// 或 @SelectProvider/@InsertProvider 等
// 将注解中的 SQL 提取出来,构建 SqlSource
// 构建 MappedStatement 并注册
}
XMLMapperBuilder vs MapperAnnotationBuilder 的对比:
对比维度 XMLMapperBuilder MapperAnnotationBuilder
触发时机 解析 时 调用 Configuration.addMapper(Class) 时
输入来源 XML 文件(XNode 节点) Java 类 + Method 上的注解
SQL 获取 从 XML 标签的文本内容读取 从 @Select、@Insert 等注解的 value 读取
动态 SQL XML 标签(if/foreach/choose) 通过 @SelectProvider + ScriptRunner,或使用
重要区别(源码层面):
· XML 解析时,XMLMapperBuilder 直接调用 XMLStatementBuilder 解析 SQL 节点
· 注解解析时,MapperAnnotationBuilder 通过 SqlSourceBuilder 将注解字符串转为 SqlSource,如果注解中使用了
第16题:SqlSessionFactory 构建过程的设计模式 & 原因
使用的设计模式:建造者模式(Builder Pattern)
涉及类结构:
java
// Builder(建造者接口/类)
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
// 多个重载方法
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse()); // parse() 返回 Configuration
}
// 最终构建方法
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
// 产品(Product)
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
// ...
}
为什么采用建造者模式:
- 配置步骤复杂且可选:MyBatis 的配置涉及数据源、事务、插件、映射器、类型处理器等几十个可选配置项,建造者模式可以将复杂的构建过程拆分为清晰的步骤
- 构建与表示分离:XMLConfigBuilder 负责配置的解析,SqlSessionFactoryBuilder 负责组装,最终的 SqlSessionFactory 是稳定的、不可变的对象
- 多入口统一:建造者提供了多个重载的 build() 方法,支持从 XML 文件、Reader、字节流等不同输入源构建,对外提供统一的接口
- 保证产品一致性:SqlSessionFactory 一旦构建完成,其配置(Configuration)就是不可变的,避免了运行时被篡改
补充:MyBatis 中使用了多个建造者:
建造者 构建产品 说明
SqlSessionFactoryBuilder SqlSessionFactory 入口建造者
XMLConfigBuilder Configuration 解析主配置
XMLMapperBuilder Mapper 配置 解析 Mapper XML
XMLStatementBuilder MappedStatement 解析 SQL 语句
MapperAnnotationBuilder Mapper 配置(注解版) 解析注解配置
第17题:SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession 的关系与生命周期
三层关系:
SqlSessionFactoryBuilder
↓ (1次性使用,build后即可销毁)
SqlSessionFactory (单例,长期存在)
↓ (openSession 每次创建新实例)
SqlSession (每个线程独立,用完即关)
各层详细说明:
对象 作用 生命周期 线程安全 设计模式
SqlSessionFactoryBuilder 解析配置,创建工厂实例 方法级:build() 执行完毕后即可被 GC 否(不共享) Builder
SqlSessionFactory 创建 SqlSession,管理 Configuration 应用级:应用启动时创建,关闭时销毁(单例) 是(线程安全,Configuration 只读) Factory
SqlSession 执行 SQL,管理事务,维护一级缓存 请求级/线程级:每次数据库操作独立创建,用完必须 close() 否(每个线程独立实例) Facade
生命周期最佳实践:
java
// ✅ 正确示例
SqlSessionFactory factory = null;
try (SqlSession session = factory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1L);
session.commit();
} // 自动 close
// ❌ 错误示例:将 SqlSession 设为成员变量共享
public class UserService {
private SqlSession session; // ❌ 不能这样,线程不安全!
}
为什么 Session 必须 close:
SqlSession 持有数据库连接(Connection),如果不释放,会导致连接池耗尽。一级缓存在 Session 关闭时清空。
第18题:MyBatis 启动时的"预加载"工作 & 对运行时性能的影响
预加载工作清单:
- 解析所有 Mapper XML/注解 → 生成 MappedStatement 并注册
- 解析所有 ResultMap → 构建字段映射关系,预计算映射路径
- 解析所有 SQL 片段 → 将 标签解析为 SqlSource 存储
- 注册所有 TypeHandler → 建立 Java 类型 ↔ JDBC 类型的映射表
- 注册所有别名 → 构建类型别名缓存(如 long ↔ Long)
- 初始化二级缓存 → 创建缓存对象(如果配置了 cache)
- 加载 Mapper 接口 → 为每个 Mapper 接口创建 MapperProxyFactory
- 预编译静态 SQL(部分) → RawSqlSource 在启动时就完成 SQL 拼接
对运行时性能的影响:
影响维度 说明
启动耗时增加 大量 XML 解析和反射操作会增加启动时间(通常数百毫秒到数秒)
运行时性能提升 SQL 解析和映射关系都在启动时完成,运行时只需要查表执行,性能更高
内存占用增加 所有配置信息常驻内存,包括每个 MappedStatement、ResultMap、SqlSource 等
异常前置发现 SQL 语法错误、映射配置错误在启动时就暴露,避免运行时才发现
关键源码印证:
java
// XMLStatementBuilder.parseStatementNode()
public void parseStatementNode() {
// 解析 SQL
SqlSource sqlSource = createSqlSource(...);
// 构建 MappedStatement(启动时完成)
builderAssistant.addMappedStatement(...);
}
RawSqlSource 和 DynamicSqlSource 的区别:前者在启动时就完成 SQL 拼装(静态 SQL),后者在运行时动态构建(动态 SQL)。
第19题:namespace 的作用 & 为什么 Mapper 接口全限定名必须与 namespace 一致
namespace 的作用:
- 防止 SQL 语句 ID 冲突:不同 Mapper 中可以有相同的 statementId,通过 namespace 区分
- 绑定 Mapper 接口:将 XML 映射与 Java 接口关联起来
- 作用域隔离:作为 MappedStatement 缓存 Key 的一部分
为什么必须一致:
namespace 必须与 Mapper 接口的全限定名保持一致,这是 MyBatis 约定的绑定机制。
源码层面的原理:
java
// 1. XMLMapperBuilder.bindMapperForNamespace()
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
Class<?> boundType = Resources.classForName(namespace); // 反射加载接口
configuration.addMapper(boundType); // 注册到 MapperRegistry
}
// 2. MapperRegistry.addMapper()
public <T> void addMapper(Class<T> type) {
// 将接口与 MapperProxyFactory 绑定
knownMappers.put(type, new MapperProxyFactory<>(type));
// 解析注解(如果之前 XML 已解析,则跳过)
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
}
// 3. 调用时通过 namespace + id 定位 MappedStatement
String statementId = mapperInterface.getName() + "." + methodName;
MappedStatement ms = configuration.getMappedStatement(statementId);
总结:全限定名 = namespace = Configuration 中查找 MappedStatement 的 key 前缀。如果不一致,bindMapperForNamespace() 会因类加载失败而报错,或者即使加载了也无法将方法调用路由到正确的 SQL。
第20题:MappedStatement 是什么?承担什么职责?
MappedStatement 定义:
MappedStatement 是 MyBatis 中描述一个完整 SQL 映射节点的元数据对象,对应 Mapper XML 中的一个 、、 或 标签(或注解中对应的 SQL 方法)。
核心属性(部分):
java
public final class MappedStatement {
private String resource; // 资源路径
private Configuration configuration;
private String id; // namespace + "." + statementId
private Integer fetchSize; // 获取大小
private Integer timeout; // 超时时间
private StatementType statementType; // STATEMENT | PREPARED | CALLABLE
private ResultSetType resultSetType;
private SqlSource sqlSource; // SQL 源(包含动态 SQL 的解析逻辑)
private ParameterMap parameterMap; // 参数映射
private List<ResultMap> resultMaps; // 结果映射
private boolean flushCacheRequired; // 是否清空缓存
private boolean useCache; // 是否使用二级缓存
private boolean resultOrdered;
private SqlCommandType sqlCommandType; // SELECT | INSERT | UPDATE | DELETE
private KeyGenerator keyGenerator; // 主键生成器
private String[] keyProperties; // 主键属性名
private String databaseId; // 数据库标识
private Log statementLog; // SQL 日志
private LanguageDriver lang; // 语言驱动
private ResultMap resultMap; // 默认结果映射
}
核心职责:
- SQL 持有者:持有 SqlSource 对象,通过它可以获取最终可执行的 SQL 和参数
- 执行配置:提供 SQL 执行所需的所有配置(超时、fetchSize、Statement 类型等)
- 结果映射定义:持有 ResultMap 列表,告诉 ResultSetHandler 如何将结果集映射为 Java 对象
- 缓存策略:定义是否使用二级缓存、是否刷新缓存
- 主键生成:持有 KeyGenerator,负责自增主键的回填逻辑
在 SQL 执行中的角色:
MapperProxy → MapperMethod → Executor → StatementHandler
↓ 获取 MappedStatement
Configuration.getMappedStatement(id)
↓ 使用 MappedStatement
Executor 根据 MappedStatement.sqlSource 获取 BoundSql
StatementHandler 使用 MappedStatement 的配置创建 Statement
ResultSetHandler 使用 MappedStatement.resultMaps 映射结果
第21题:MyBatis 如何支持多数据源配置?SqlSessionFactory 如何管理?
多数据源支持原理:
MyBatis 本身不提供多数据源的自动管理机制,而是通过多个独立的 SqlSessionFactory 实例来实现多数据源。每个数据源对应一个独立的 Configuration 和 Environment。
配置方式(Spring Boot 场景):
java
@Configuration
public class DataSourceConfig {
@Bean("db1DataSource")
@ConfigurationProperties("spring.datasource.db1")
public DataSource db1DataSource() { return new HikariDataSource(); }
@Bean("db2DataSource")
@ConfigurationProperties("spring.datasource.db2")
public DataSource db2DataSource() { return new HikariDataSource(); }
@Bean("db1SqlSessionFactory")
public SqlSessionFactory db1SqlSessionFactory(
@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
// 指定 Mapper 位置(db1 的 Mapper 放在特定包下)
factory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/db1/**/*.xml"));
return factory.getObject();
}
@Bean("db2SqlSessionFactory")
public SqlSessionFactory db2SqlSessionFactory(
@Qualifier("db2DataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/db2/**/*.xml"));
return factory.getObject();
}
}
Manager 模式管理:
java
@Component
public class SqlSessionFactoryManager {
private final Map<String, SqlSessionFactory> factoryMap = new ConcurrentHashMap<>();
@Autowired
public void init(Map<String, SqlSessionFactory> factories) {
factoryMap.putAll(factories);
}
public SqlSessionFactory getFactory(String dbKey) {
return factoryMap.get(dbKey);
}
}
关键点:
- 每个数据源独立创建 SqlSessionFactory
- Mapper 按数据源分包/分路径
- 通过 @Qualifier 或自定义注解区分不同的 Factory
- 不能在不同数据源的 Mapper 中做跨库 JOIN(需在应用层处理)
第22题:SQL 来源(XML vs 注解)的优劣和使用场景
对比总览:
维度 XML 方式 注解方式
代码侵入性 无,SQL 在 XML 中 有,写在 Java 接口上
可维护性 集中管理,便于 DBA 审核 分散在各个接口中
动态 SQL 表达力 极强(if/foreach/choose/where/set) 弱,需用 Provider 类或
使用场景建议:
✅ 使用 XML 的场景:
· SQL 语句复杂,超过 5 行
· 包含复杂的动态 SQL(、、)
· 需要复杂的 ResultMap(多表关联、嵌套结果)
· 需要 DBA 或运维人员审核 SQL
· 团队人数多,需要 SQL 集中管理
✅ 使用注解的场景:
· 单表简单 CRUD
· SQL 语句很短(2-3 行)
· 不需要动态 SQL 或动态条件简单
· 快速原型开发,追求高开发效率
· 个人或小团队项目
推荐组合策略:
复杂 SQL → XML(推荐)
简单 CRUD → 注解(方便快捷)
混合使用 → 同一 Mapper 中可混合,但建议统一风格
🔔 注解动态 SQL 的替代方案:
java
// ❌ 不推荐:注解中写复杂动态 SQL(可读性极差)
@Select("<script>SELECT * FROM user WHERE 1=1" +
"<if test='name != null'> AND name = #{name}</if></script>")
// ✅ 推荐:使用 Provider(将 SQL 抽离到独立类)
@SelectProvider(type = UserSqlProvider.class, method = "findByCondition")
List<User> findByCondition(UserCondition condition);
源码证据: 无论 XML 还是注解,最终都构建成 MappedStatement 存入 Configuration,执行流程完全统一。这是 MyBatis 统一抽象能力的体现。
附:第二部分高频追问题
Q:启动时如果多个 Mapper 的 namespace + id 冲突会怎样?
A:Configuration.mappedStatements 是 StrictMap(继承自 HashMap),put 时会检查 key 是否已存在,如果存在且来源不同,会抛出 IncompleteElementException。同 namespace 下的同名 id 会覆盖,但日志会有 warning。
Q:注解和 XML 同时存在时谁优先?
A:XML 优先。MapperAnnotationBuilder 在解析时会检查 Configuration.hasStatement(statementId),如果已存在(被 XML 注册),则不覆盖。
Q:启动时未找到 Mapper XML 文件会怎样?
A:取决于 还是 方式。 找不到会直接抛 IOException; 扫描方式只扫描类路径下的资源,不会因缺失某个 XML 而失败。
三、SQL 执行全流程(15题)
第23题:从调用 Mapper 方法到返回结果的完整链路
全链路追踪(从应用层到 JDBC 层):
【应用层】
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1L);
↓
【动态代理层】MapperProxy.invoke()
→ 判断是否为 Object 方法(toString/hashCode/equals)
→ 非 Object 方法 → MapperMethod.execute()
↓
【方法路由层】MapperMethod.execute()
→ 根据 SQL 类型(SELECT/INSERT/UPDATE/DELETE)路由
→ 处理参数:MethodSignature.convertArgsToSqlCommandParam()
→ 调用 SqlSession 的对应方法(selectOne/insert/update/delete)
↓
【执行器层】DefaultSqlSession.selectList()
→ 从 Configuration 获取 MappedStatement
→ 通过 MappedStatement.getBoundSql() 获取 BoundSql(含 SQL + 参数映射)
→ 调用 Executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql)
↓
【缓存层】CachingExecutor(如果启用二级缓存)
→ 先查二级缓存 → 缓存命中直接返回
→ 缓存未命中 → 委托给 BaseExecutor
↓
【核心执行层】BaseExecutor.query()
→ 一级缓存查询(PerpetualCache)
→ 缓存命中返回结果
→ 缓存未命中 → doQuery()
↓
【语句准备层】BaseExecutor.doQuery()
→ StatementHandler.prepare() → 获取 Connection
→ StatementHandler.parameterize() → 设置参数
→ StatementHandler.query() → 执行 SQL
↓
【结果处理层】ResultSetHandler.handleResultSets()
→ 遍历 ResultSet
→ 通过 TypeHandler 读取列值
→ 根据 ResultMap 映射为 Java 对象
→ 处理嵌套映射(association/collection)
↓
返回结果 → 逐层返回至应用层
关键源码调用链(精简版):
java
MapperProxy.invoke()
→ MapperMethod.execute()
→ SqlSession.selectOne()
→ Configuration.getMappedStatement()
→ MappedStatement.getBoundSql() // 解析动态 SQL,生成 BoundSql
→ Executor.query(ms, ...)
→ CachingExecutor.query() // 二级缓存
→ BaseExecutor.query() // 一级缓存
→ BaseExecutor.doQuery()
→ StatementHandler.prepare()
→ StatementHandler.parameterize()
→ StatementHandler.query()
→ ResultSetHandler.handleResultSets()
第24题:Executor 有哪几种实现?区别和使用场景?
四种 Executor 实现:
实现类 类型 核心机制 适用场景
SimpleExecutor 默认 每次执行都创建新的 Statement,用完即关 普通 OLTP 场景,默认选择
ReuseExecutor 复用 将 Statement 缓存到 Map 中,key = SQL,重复使用 同一 SQL 反复执行的场景,减少 Statement 创建开销
BatchExecutor 批量 攒批执行(批量 addBatch + executeBatch) 批量插入/更新,大批量数据同步
CachingExecutor 装饰器 二级缓存装饰器,包装上述三种 需要二级缓存的场景(默认启用)
继承关系:
Executor (接口)
↑
BaseExecutor (抽象类)
↑ ↑ ↑
SimpleExecutor ReuseExecutor BatchExecutor
↑
CachingExecutor (装饰器模式,持有 Executor 引用)
选择依据(通过 defaultExecutorType 配置或 openSession() 指定):
java
// 编程方式指定
SqlSession session = factory.openSession(ExecutorType.SIMPLE); // 默认
SqlSession session = factory.openSession(ExecutorType.REUSE);
SqlSession session = factory.openSession(ExecutorType.BATCH);
// 全局配置(mybatis-config.xml)
<settings>
<setting name="defaultExecutorType" value="SIMPLE"/>
</settings>
第25题:三种 Executor 的源码级差异
- SimpleExecutor - 最基础的实现:
java
// 每次 doQuery 都创建新 Statement
public <E> List<E> doQuery(MappedStatement ms, Object parameter, ...) {
Statement stmt = null;
try {
Configuration config = ms.getConfiguration();
// 每次 new 一个 StatementHandler
StatementHandler handler = config.newStatementHandler(...);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt); // 立即关闭
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
特点:每次新建 Statement,执行完立即关闭,不做任何复用。
- ReuseExecutor - 复用 Statement:
java
public class ReuseExecutor extends BaseExecutor {
// 关键:Statement 缓存
private final Map<String, Statement> statementMap = new HashMap<>();
public <E> List<E> doQuery(MappedStatement ms, Object parameter, ...) {
Statement stmt = null;
try {
Configuration config = ms.getConfiguration();
StatementHandler handler = config.newStatementHandler(...);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
// 不关闭 Statement,而是放回缓存
// closeStatement(stmt); // 不调用!
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) {
String sql = handler.getBoundSql().getSql();
Statement stmt;
if (statementMap.containsKey(sql)) {
stmt = statementMap.get(sql);
// 需要重新设置超时时间等
handler.getParameterHandler().setParameters((PreparedStatement) stmt);
} else {
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
statementMap.put(sql, stmt);
}
handler.parameterize(stmt);
return stmt;
}
}
特点:SQL 作为 key 缓存 Statement,同一 SQL 复用 PreparedStatement,减少预编译开销。
- BatchExecutor - 批量执行:
java
public class BatchExecutor extends BaseExecutor {
// 存储批量语句列表
private final List<Statement> statementList = new ArrayList<>();
private final List<BatchResult> batchResultList = new ArrayList<>();
private String currentSql;
private MappedStatement currentMs;
public int doUpdate(MappedStatement ms, Object parameter) {
final Configuration config = ms.getConfiguration();
final StatementHandler handler = config.newStatementHandler(...);
final BoundSql boundSql = handler.getBoundSql();
final String sql = boundSql.getSql();
// 如果 SQL 发生变化,触发 executeBatch()
if (sql != null && !sql.equals(currentSql)) {
executeBatch(); // 刷新批次
}
currentSql = sql;
currentMs = ms;
Statement stmt;
if (statementList.isEmpty()) {
stmt = prepareStatement(handler, ms.getStatementLog());
} else {
stmt = statementList.get(statementList.size() - 1);
// 重新设置参数(追加到同一批次)
}
handler.parameterize(stmt);
((PreparedStatement) stmt).addBatch(); // 核心:addBatch
return BATCH_UPDATE_RETURN_VALUE; // 返回 -2147482646
}
public int doFlushStatements(boolean isRollback) {
List<BatchResult> results = new ArrayList<>();
for (Statement stmt : statementList) {
int[] updateCounts = ((PreparedStatement) stmt).executeBatch(); // 核心:executeBatch
// 处理结果
}
return results.stream().mapToInt(BatchResult::getUpdateCounts).sum();
}
}
特点:攒批后一次性 executeBatch(),减少网络往返。注意:返回值为负值(表示批处理结果),需要调用 flushStatements() 获取真实影响行数。
第26题:StatementHandler 的作用 & 与 Executor 的职责划分
StatementHandler 的作用:
StatementHandler 是 SQL 语句处理器,负责创建 Statement、设置参数、执行 SQL。它屏蔽了不同 Statement 类型(普通 Statement、PreparedStatement、CallableStatement)的差异。
StatementHandler 的三种实现:
实现类 对应 Statement 类型 使用场景
SimpleStatementHandler Statement 普通 SQL,不预编译
PreparedStatementHandler PreparedStatement 预编译 SQL(最常用)
CallableStatementHandler CallableStatement 存储过程调用
核心方法:
java
public interface StatementHandler {
// 1. 准备 Statement:获取 Connection,创建 Statement 对象
Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;
// 2. 参数化:将 Java 参数设置到 PreparedStatement 中(调用 ParameterHandler)
void parameterize(Statement statement) throws SQLException;
// 3. 执行查询
<E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;
// 4. 执行更新
int update(Statement statement) throws SQLException;
// 5. 批量操作
void batch(Statement statement) throws SQLException;
}
Executor 与 StatementHandler 的职责划分(核心区别):
维度 Executor StatementHandler
职责层级 执行器层(宏观调度) 语句处理器层(微观操作)
核心任务 缓存管理(一/二级)、事务管理、批量控制 创建 Statement、设置参数、执行 SQL、结果提取
关注点 SQL 执行的流程编排 SQL 执行的具体操作
与 JDBC 的关系 不直接操作 JDBC 直接操作 JDBC API
生命周期 全局/会话级 每次 SQL 执行创建
形象比喻: Executor 是指挥官(决定什么时候执行、是否走缓存、是否批量),StatementHandler 是一线士兵(真的去执行 SQL 语句)。
第27题:ParameterHandler 的作用 & 参数设置流程
ParameterHandler 的作用:
ParameterHandler 负责将 Java 对象参数转换为 JDBC PreparedStatement 的参数,并设置到 PreparedStatement 中。它处理了类型转换(通过 TypeHandler)和参数位置映射。
核心接口:
java
public interface ParameterHandler {
// 获取参数对象
Object getParameterObject();
// 核心方法:将参数设置到 PreparedStatement 中
void setParameters(PreparedStatement ps) throws SQLException;
}
参数设置完整流程:
java
// DefaultParameterHandler.setParameters() 源码核心逻辑
public void setParameters(PreparedStatement ps) {
// 1. 从 BoundSql 中获取参数映射列表(每个 #{} 对应一个参数映射)
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
// 2. 判断是普通参数还是输出参数(存储过程 OUT 参数)
if (parameterMapping.getMode() != ParameterMode.OUT) {
// 3. 从参数对象中提取值(通过 MetaObject 反射)
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
// 额外参数(如 _parameter、_databaseId 等)
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
// 简单类型直接取值
value = parameterObject;
} else {
// 复杂对象:通过 MetaObject 反射获取属性值
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 4. 获取 TypeHandler(根据 JavaType + JdbcType)
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
// 5. 通过 TypeHandler 设置参数
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
}
}
}
关键细节:
- parameterMappings 的顺序与 SQL 中 #{} 出现的顺序一致
- 每个 ParameterMapping 包含了属性名、JavaType、JdbcType、TypeHandler 等信息
- MetaObject 是 MyBatis 的反射工具,支持嵌套属性访问(如 user.address.city)
- 参数位置从 1 开始(JDBC 规范)
第28题:ResultSetHandler 如何将 ResultSet 映射为 Java 对象?
核心流程(DefaultResultSetHandler.handleResultSets()):
java
public List<Object> handleResultSets(Statement stmt) throws SQLException {
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
ResultSetWrapper rsw = new ResultSetWrapper(rs, configuration);
// 1. 获取第一个 ResultMap(可能有多个,对应多结果集)
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
// 2. 核心映射方法
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
resultSetCount++;
}
return collapseSingleResultList(multipleResults);
}
单行映射详细流程(handleRowValues()):
1. 从 ResultSetWrapper 获取列名和列元数据
2. 根据 ResultMap 构建映射关系
├── 普通属性映射:字段名 → 属性名
├── 复杂映射:association(一对一)、collection(一对多)
└── 鉴别器(discriminator):根据列值选择不同的 ResultMap
3. 创建目标对象(通过 ObjectFactory)
├── 若结果类型是 Map → 创建 HashMap
└── 若结果类型是 POJO → 反射调用构造函数实例化
4. 填充普通属性
├── 获取对应的 TypeHandler
└── typeHandler.getResult(rs, columnName) 读取值并转换
5. 处理嵌套映射(association/collection)
├── 嵌套结果(Nested Result):在一次查询中通过 JOIN 获取
│ └── 使用 NestedResultHandler 递归解析
└── 嵌套查询(Nested Select):触发延迟加载或额外查询
└── 创建代理对象(Javassist)用于懒加载
6. 返回完整对象
关键类协作:
ResultSetHandler.handleResultSets()
→ handleResultSet()
→ handleRowValues() // 处理所有行
→ handleRowValuesForSimpleResultMap() // 简单映射
→ handleRowValuesForNestedResultMap() // 嵌套映射(N+1 或 JOIN)
→ getRowValue()
→ createResultObject() // 对象工厂
→ applyAutomaticMappings() // 自动映射
→ applyPropertyMappings() // 显式映射(ResultMap)
→ getNestedQueryMappingValue() // 嵌套查询(懒加载)
自动映射 vs 显式映射:
· 自动映射:通过 mapUnderscoreToCamelCase + 反射,将列名直接映射到同名字段
· 显式映射:通过 配置,支持类型转换、嵌套、继承等复杂场景
第29题:TypeHandler 的工作原理 & 自定义实现
TypeHandler 的作用:
TypeHandler 是 MyBatis 中连接 Java 类型与 JDBC 类型的桥梁,负责:
· Java 参数 → JDBC 值(setParameter())
· JDBC 值 → Java 对象(getResult())
核心接口:
java
public interface TypeHandler<T> {
// 设置参数:Java → JDBC
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
// 获取结果:JDBC → Java(通过列名)
T getResult(ResultSet rs, String columnName) throws SQLException;
// 获取结果:JDBC → Java(通过列索引)
T getResult(ResultSet rs, int columnIndex) throws SQLException;
// 获取结果:JDBC → Java(存储过程)
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
TypeHandler 的注册与查找流程:
java
// 1. 启动时注册:TypeHandlerRegistry
typeHandlerRegistry.register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
typeHandlerRegistry.register(Long.class, JdbcType.BIGINT, new LongTypeHandler());
// 2. 运行时查找:根据 JavaType + JdbcType 查找对应的 TypeHandler
TypeHandler<?> handler = typeHandlerRegistry.getTypeHandler(javaType, jdbcType);
自定义 TypeHandler 实现步骤:
java
// 场景:JSON 字段在 MySQL 中存储为 TEXT,Java 中为 Map
@MappedTypes(Map.class) // 指定处理的 Java 类型
@MappedJdbcTypes(JdbcType.VARCHAR) // 指定处理的 JDBC 类型
public class JsonTypeHandler extends BaseTypeHandler<Map<String, Object>> {
private static final ObjectMapper mapper = new ObjectMapper();
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
Map<String, Object> parameter, JdbcType jdbcType)
throws SQLException {
try {
ps.setString(i, mapper.writeValueAsString(parameter));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
@Override
public Map<String, Object> getNullableResult(ResultSet rs, String columnName)
throws SQLException {
String json = rs.getString(columnName);
return parseJson(json);
}
@Override
public Map<String, Object> getNullableResult(ResultSet rs, int columnIndex)
throws SQLException {
String json = rs.getString(columnIndex);
return parseJson(json);
}
@Override
public Map<String, Object> getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
String json = cs.getString(columnIndex);
return parseJson(json);
}
private Map<String, Object> parseJson(String json) {
try {
return json == null ? null : mapper.readValue(json, Map.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
// 注册(mybatis-config.xml)
<typeHandlers>
<typeHandler handler="com.example.JsonTypeHandler"
javaType="java.util.Map"
jdbcType="VARCHAR"/>
</typeHandlers>
⚠️ P6 级别的追问: 如果同一个 JavaType 有多个 TypeHandler 注册,MyBatis 如何选择?通过 @MappedJdbcTypes 的 includeNullJdbcType 和泛型类型匹配,优先级是:精确匹配 > 泛型匹配 > 自动注册。
第30题:#{} 占位符的处理原理(源码深度剖析)
#{} 的本质: 生成 PreparedStatement 的占位符 ?,并通过参数绑定防止 SQL 注入。
处理全链路:
SQL 文本(含 #{})
↓
【解析阶段】XMLScriptBuilder.parseScriptNode()
→ 遍历 SQL 文本,识别 #{} 和 ${}
→ 生成 SqlNode 树(TextSqlNode / IfSqlNode / ForEachSqlNode / ...)
↓
【构建阶段】SqlSourceBuilder.parse()
→ 将 #{} 替换为 ? 占位符
→ 记录每个占位符对应的参数属性名、JavaType、JdbcType
→ 生成 ParameterMapping 列表
↓
【运行时阶段】BoundSql
→ 持有最终 SQL(带 ?) + ParameterMapping 列表 + 参数对象
↓
【参数设置阶段】DefaultParameterHandler.setParameters()
→ 遍历 ParameterMapping,从参数对象中提取值
→ 通过 TypeHandler 设置到 PreparedStatement 的对应位置
核心源码解析(SqlSourceBuilder.parse()):
java
// SqlSourceBuilder 中的解析逻辑
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 1. 使用 GenericTokenParser 解析 #{},Handler 为 ParameterMappingTokenHandler
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
// 2. 返回 StaticSqlSource(持有 SQL + ParameterMapping 列表)
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
// ParameterMappingTokenHandler 处理每个 #{}
private static class ParameterMappingTokenHandler extends TokenHandler {
public String handleToken(String content) {
// content 是 #{userId} 中的 "userId"
// 1. 解析参数属性名和类型
// 2. 构建 ParameterMapping
// 3. 返回 "?" 作为替换
parameterMappings.add(buildParameterMapping(content));
return "?";
}
}
#{} vs ${} 的源码级区别:
对比 #{} ${}
解析器 ParameterMappingTokenHandler → 替换为 ? VariableTokenHandler → 直接拼接字符串
处理时机 运行时(PreparedStatement 参数绑定) 解析时(直接替换到 SQL 中)
SQL 注入 安全(预编译) 不安全(直接拼接,存在注入风险)
适用场景 值传递(WHERE 条件、INSERT 值) 表名、列名、ORDER BY 等动态结构
性能 高(预编译 SQL 可复用) 低(每次需重新编译)
实现类 StaticSqlSource RawSqlSource(启动时解析完成)
源码级证明(${} 的处理):
java
// TextSqlNode 中处理 ${}
public String handleToken(String content) {
// content 是 ${tableName} 中的 "tableName"
Object value = OgnlCache.getValue(content, context);
return (value == null ? "" : value.toString()); // 直接返回字符串值,拼接进 SQL
}
第31题:MyBatis 获取数据库连接的时机和位置
获取连接的时机: 在 StatementHandler.prepare() 方法中,当 Executor 需要执行 SQL 时触发。
调用链路:
java
// 1. BaseExecutor.doQuery()
protected <E> List<E> doQuery(MappedStatement ms, Object parameter, ...) {
Statement stmt = null;
try {
Configuration config = ms.getConfiguration();
StatementHandler handler = config.newStatementHandler(...);
// 2. 准备 Statement → 此时获取连接
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
// 3. BaseExecutor.prepareStatement()
private Statement prepareStatement(StatementHandler handler, Log statementLog) {
Statement stmt;
// 4. 从当前事务中获取 Connection!
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
// 5. BaseExecutor.getConnection()
protected Connection getConnection(Log statementLog) throws SQLException {
// 从 Transaction 对象中获取 Connection
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
事务与连接的绑定关系:
java
// JdbcTransaction.getConnection()
public Connection getConnection() throws SQLException {
if (connection == null) {
// 第一次调用时从 DataSource 获取
connection = dataSource.getConnection();
if (autoCommit != null && !autoCommit) {
connection.setAutoCommit(false);
}
}
return connection; // 同一事务内复用同一个连接
}
关键点:
- 连接在第一次执行 SQL 时才真正获取(延迟获取),而不是 openSession() 时
- 同一 SqlSession 内的多次操作共用同一个 Connection(事务上下文)
- 连接由 Transaction 对象管理,Executor 不直接持有 Connection
第32题:一次查询创建了多少个对象?生命周期?
以 selectById(1L) 为例,对象创建清单:
序号 对象类型 创建时机 数量 生命周期
1 MapperProxy 调用 getMapper() 时 1 方法级(可缓存)
2 MapperMethod 第一次调用该方法时 1 方法级(缓存)
3 DefaultSqlSession openSession() 1 会话级
4 BoundSql getBoundSql() 1 方法级
5 ParameterMapping 列表 构建 BoundSql 时 N(#{} 个数) 方法级
6 SimpleExecutor / 其他 Executor openSession() 1 会话级
7 PreparedStatementHandler newStatementHandler() 1 方法级
8 PreparedStatement handler.prepare() 1 方法级(Reuse 可能复用)
9 DefaultParameterHandler 随 StatementHandler 创建 1 方法级
10 DefaultResultSetHandler 随 StatementHandler 创建 1 方法级
11 ResultSetWrapper 处理结果集时 1 方法级
12 目标 POJO 对象 ObjectFactory.create() 1(或 N 行) 方法级 → 返回给应用层
13 MetaObject(反射工具) 参数处理/结果映射时 若干 方法级
14 RowBounds(分页对象) 方法调用时 1 方法级
汇总:
· 会话级(Singleton): SqlSession、Executor、Configuration
· 方法级(每次创建): ~10-15 个对象(含内部临时对象)
· 结果集级: 每行结果对应一个 POJO 对象(由应用层持有)
性能影响:
· 方法级对象在 GC 压力较大的场景下会影响吞吐量
· 使用 ReuseExecutor 可减少 Statement 的创建频率
第33题:SqlSession 的线程安全性问题
结论:SqlSession 不是线程安全的!
原因分析:
- 持有数据库连接:SqlSession 通过 Transaction 持有 Connection,而 Connection 不是线程安全的(多线程共用会导致事务混乱、连接状态污染)
- 一级缓存:SqlSession 持有 Executor,Executor 中有一级缓存 PerpetualCache(Map 结构),多线程并发操作会导致缓存数据不一致
- 事务状态:SqlSession 维护了事务状态(dirty 标记),多线程修改会相互干扰
源码证据:
java
// BaseExecutor.query() 中一级缓存操作没有加锁
public <E> List<E> query(MappedStatement ms, Object parameter, ...) {
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
List<E> list = (List<E>) localCache.getObject(key); // 非线程安全的 Map
if (list != null) {
return list; // 可能返回脏数据
}
// ...
}
最佳实践:
java
// ✅ 正确:每个线程/请求创建独立的 SqlSession
public User selectUser(Long id) {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
return mapper.selectById(id);
} // 自动关闭
}
// ❌ 错误:将 SqlSession 设为成员变量共享
@Service
public class UserService {
private SqlSession session; // ❌ 线程不安全!
}
// ✅ 正确:Spring 管理下,每个 Mapper 是线程安全的代理对象
@Service
public class UserService {
@Autowired
private UserMapper userMapper; // ✅ 线程安全(MapperProxy 无状态)
}
为什么 Mapper 是线程安全的?
MapperProxy 是无状态的,每个方法调用都从当前线程的 SqlSession 中获取 MappedStatement 执行,不存在共享可变状态。
第34题:"延迟创建"机制的理解
MyBatis 中延迟创建(Lazy Initialization)的几个层面:
- 数据库连接的延迟获取:
java
// openSession() 时并不立即获取 Connection
SqlSession session = factory.openSession();
// Connection 的获取延迟到第一次执行 SQL 时
// 由 Transaction.getConnection() 首次调用触发
- 代理对象的延迟创建(懒加载):
ResultSetHandler 在处理 association 或 collection 时,如果配置了 select 属性(嵌套查询),不会立即加载关联数据,而是创建一个 Javassist 代理对象。
java
// DefaultResultSetHandler.createProxyForNestedQuery()
private Object createProxyForNestedQuery(...) {
// 创建代理对象,暂不执行查询
return ProxyFactory.createProxy(..., new LazyLoader(...));
}
- Mapper 方法的延迟解析:
MapperMethod 在第一次调用时才完成参数解析器的构建,而非启动时。
java
// MapperMethod 构造函数
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
// MethodSignature 中解析参数,延迟到第一次调用
}
- MappedStatement 的延迟加载(Incomplete 机制):
某些 ResultMap 引用了尚未解析的 ResultMap(forward reference),MyBatis 会记录为 incompleteResultMaps,在启动完成后再次尝试解析。
java
// XMLMapperBuilder.parse()
public void parse() {
configurationElement(parser.evalNode("/mapper"));
// 完成所有映射后,处理未完成的部分
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
第35题:doUpdate 和 doQuery 执行流程的异同
相同点(BaseExecutor 模板方法):
步骤 doQuery doUpdate
获取 MappedStatement ✓ ✓
获取 BoundSql ✓ ✓
创建 StatementHandler ✓ ✓
准备 Statement(获取连接) ✓ ✓
参数化(设置参数) ✓ ✓
缓存清理策略 ✓ ✓
刷新本地缓存 ✓(清空) ✓(清空)
核心差异:
对比维度 doQuery doUpdate
返回值 List(结果集) int(影响行数)
执行方法 StatementHandler.query() StatementHandler.update()
结果处理 需要 ResultSetHandler 映射 只需返回更新计数
二级缓存 先查缓存再执行,执行后存入缓存 执行后清空相关缓存(flushCache)
RowBounds 支持 支持(内存分页) 不支持
自动提交 不影响 执行后可能自动提交
批量处理 BatchExecutor 不处理查询的 batch BatchExecutor 的 update 会 addBatch
源码对比:
java
// BaseExecutor.doQuery()
protected <E> List<E> doQuery(...) {
Statement stmt = null;
try {
// 准备 Statement
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行查询 + 结果映射
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
// BaseExecutor.doUpdate()
protected int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration config = ms.getConfiguration();
StatementHandler handler = config.newStatementHandler(...);
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行更新,返回影响行数
return handler.update(stmt);
} finally {
closeStatement(stmt);
}
}
第36题:BatchExecutor 的攒批原理和触发条件
攒批原理(BatchExecutor 核心机制):
java
public class BatchExecutor extends BaseExecutor {
// 三个核心集合
private final List<Statement> statementList = new ArrayList<>();
private final List<BatchResult> batchResultList = new ArrayList<>();
private String currentSql; // 当前累积的 SQL
private MappedStatement currentMs; // 当前累积的 MappedStatement
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
final Configuration config = ms.getConfiguration();
final StatementHandler handler = config.newStatementHandler(...);
final BoundSql boundSql = handler.getBoundSql();
final String sql = boundSql.getSql();
// ⚠️ 关键逻辑:当 SQL 或 MappedStatement 发生变化时,触发执行
if (sql != null && (sql.equals(currentSql) && ms.equals(currentMs))) {
// 相同 SQL → 追加到当前批次
} else {
// SQL 变化 → 执行已有批次
executeBatch();
}
currentSql = sql;
currentMs = ms;
// 获取或创建 Statement
Statement stmt;
if (statementList.isEmpty()) {
stmt = prepareStatement(handler, ms.getStatementLog());
} else {
// 复用最后一个 Statement
stmt = statementList.get(statementList.size() - 1);
// 重置参数
handler.parameterize(stmt);
}
// 添加到批次
((PreparedStatement) stmt).addBatch();
return BATCH_UPDATE_RETURN_VALUE; // 固定返回 -2147482646
}
}
攒批触发条件(何时执行 executeBatch):
触发条件 说明
SQL 变化 下一条 SQL 与当前缓存的 SQL 不一致
MappedStatement 变化 不同方法的 SQL 切换
手动调用 flushStatements() 开发者主动刷新
事务提交 commit() 时会自动 flush
事务回滚 rollback() 时会清空批次(不执行)
SqlSession 关闭 close() 前自动 flush
缓存大小限制 无固定大小,只受 SQL 是否变化控制
批量插入性能分析:
java
// ❌ 无批量(10000 条,每条单独提交)
for (User user : users) {
mapper.insert(user); // 10000 次网络往返
}
// 耗时:~5000ms
// ✅ 批量(使用 BatchExecutor)
SqlSession session = factory.openSession(ExecutorType.BATCH, false);
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : users) {
mapper.insert(user); // addBatch 攒批
}
session.commit(); // 一次 executeBatch
// 耗时:~200ms(性能提升 25 倍)
⚠️ 重要注意事项:
· BatchExecutor 的 doUpdate 返回的是固定值 -2147482646(Statement.SUCCESS_NO_INFO),真实影响行数需调用 flushStatements() 获取
· 批量执行失败时,回滚是整个批次回滚,无法部分提交
· 与 ReuseExecutor 不同,BatchExecutor 不缓存查询结果
第37题:自增主键获取原理(useGeneratedKeys & keyProperty)
核心原理: MyBatis 通过 JDBC 的 Statement.getGeneratedKeys() API 获取数据库自动生成的主键,并回填到实体对象的指定属性中。
执行流程:
java
// 1. 配置(XML)
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (name, age) VALUES (#{name}, #{age})
</insert>
// 2. 源码执行流程
// ① 构建 MappedStatement 时,记录 useGeneratedKeys 和 keyProperty
// ② 执行 INSERT 时,调用 StatementHandler.update()
// → PreparedStatementHandler.update()
// → ps.executeUpdate() // 执行插入
// → 如果 useGeneratedKeys = true,调用 ps.getGeneratedKeys()
// ③ 获取 ResultSet 中生成的主键值
// ④ 通过 MetaObject 反射设置到实体对象的 keyProperty 中
核心源码(DefaultParameterHandler.setParameters() 中的主键回填逻辑):
java
// 主键回填实际上在 Executor 层完成
// BaseExecutor.update() 中调用
public int update(MappedStatement ms, Object parameter) throws SQLException {
// 执行前:清空缓存
// 执行 doUpdate
int result = doUpdate(ms, parameter);
// 如果 useGeneratedKeys 且 KeyGenerator 是 Jdbc3KeyGenerator
if (ms.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
// 回填主键到参数对象
((Jdbc3KeyGenerator) ms.getKeyGenerator()).processAfter(ms, ...);
}
return result;
}
// Jdbc3KeyGenerator.processAfter()
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
// 1. 获取生成的 Key
ResultSet rs = stmt.getGeneratedKeys();
// 2. 解析 keyProperty 列表(支持多个主键)
String[] keyProperties = ms.getKeyProperties();
// 3. 遍历 ResultSet,通过 MetaObject 设置值
while (rs.next()) {
for (String keyProperty : keyProperties) {
// 获取主键值
Object value = rs.getObject(1);
// 反射设置到参数对象
metaObject.setValue(keyProperty, value);
}
}
}
多种主键生成策略对比:
策略 配置方式 适用场景 原理
JDBC 自增主键 useGeneratedKeys="true" MySQL、SQL Server 自增列 JDBC getGeneratedKeys()
前置 order="BEFORE" Oracle 序列(插入前获取) 先执行查询获取序列值
后置 order="AFTER" 插入后获取自增值 插入后执行查询获取
UUID 主键 + order="BEFORE" 分布式系统 应用层生成 UUID
源码示例:
xml
<insert id="insertUser">
<selectKey keyProperty="id" order="BEFORE" resultType="long">
SELECT user_seq.nextval FROM DUAL
</selectKey>
INSERT INTO user (id, name) VALUES (#{id}, #{name})
</insert>
执行流程:
java
// SelectKeyGenerator 继承 KeyGenerator
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
// 执行 selectKey SQL,获取值
// 设置到参数对象中
}
public void processAfter(...) {
// 插入后获取主键(与 JDBC 自增类似)
}
P6 追问:多主键(复合主键)场景,keyProperty 可配置多个(逗号分隔),getGeneratedKeys() 会返回多列结果。
附:第三部分高频追问题
Q:一个 SqlSession 内执行多次相同查询,SQL 会执行几次?
A:默认为 1 次(一级缓存命中)。但如果两次查询之间执行了更新操作(INSERT/UPDATE/DELETE),一级缓存会被清空,第二次查询重新执行。
Q:PreparedStatement 在 MyBatis 中为什么要预编译?
A:预编译让数据库提前编译 SQL 并生成执行计划,后续只绑定参数即可执行,大幅提升性能。同时参数绑定隔离了 SQL 结构和数据,从根本上防范 SQL 注入。
Q:ResultHandler 的作用是什么?
A:ResultHandler 是结果集回调接口,允许在遍历 ResultSet 时自定义处理逻辑(如流式处理大数据量),避免一次性加载所有结果到内存。典型场景:大批量导出。
四、Mapper 动态代理机制(12题)
第38题:核心必考题 --- Mapper 接口没有实现类,为什么可以被调用?
核心原理:JDK 动态代理 + 接口绑定
Mapper 接口虽然没有实现类,但 MyBatis 在运行时通过 JDK 动态代理 为接口生成了一个代理对象。当调用接口方法时,实际调用的是代理对象的 invoke() 方法,由代理对象完成 SQL 的查找、执行和结果映射。
源码链路(从代理创建到方法调用):
java
// 1. 获取代理对象
UserMapper mapper = session.getMapper(UserMapper.class);
// → DefaultSqlSession.getMapper()
// → Configuration.getMapper()
// → MapperRegistry.getMapper()
// → MapperProxyFactory.newInstance(sqlSession)
// → Proxy.newProxyInstance(..., new MapperProxy(sqlSession))
// 2. 调用方法时
User user = mapper.selectById(1L);
// → MapperProxy.invoke()
// → 判断是否为 Object 方法(toString/hashCode/equals 走本地)
// → 缓存/创建 MapperMethod
// → mapperMethod.execute(sqlSession, args)
// → 根据 SQL 类型路由到 sqlSession.selectOne() / insert() / update() / delete()
// → 最终执行 JDBC 操作
核心组件:
组件 作用
MapperProxyFactory 创建 Mapper 代理对象的工厂,缓存 MapperProxy
MapperProxy 实现 InvocationHandler,拦截所有接口方法调用
MapperMethod 封装接口方法对应的 SQL 命令和参数签名,执行时通过它调用 SqlSession
关键代码(MapperProxy.invoke()):
java
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// Object 方法直接调用本地
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 缓存 MapperMethod
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 执行 SQL
return mapperMethod.execute(sqlSession, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
第39题:MyBatis 使用什么类型的动态代理?为什么?
使用类型:JDK 动态代理(java.lang.reflect.Proxy)
为什么选择 JDK 动态代理:
- Mapper 本身就是接口:JDK 动态代理要求被代理对象是接口,而 MyBatis 的 Mapper 恰好是接口,这是天然契合
- 无需引入第三方字节码库:JDK 动态代理是 Java 内置的,不需要像 CGLIB/Javassist 那样引入额外依赖
- 代码简洁、性能足够:JDK 动态代理在 Java 8+ 后性能已有大幅提升,满足持久层调用的性能要求
- 避免 CGLIB 的局限性:CGLIB 通过继承实现代理,无法代理 final 类和方法,而接口代理没有这个限制
与懒加载代理的区别:
· Mapper 代理:使用 JDK 动态代理(代理接口)
· 懒加载代理(延迟加载):使用 Javassist 或 CGLIB(代理实体类,因为实体类是普通类而非接口)
java
// MapperProxyFactory 源码印证
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
protected T newInstance(MapperProxy<T> mapperProxy) {
// 使用 JDK 动态代理
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[] { mapperInterface },
mapperProxy
);
}
}
第40题:MapperProxy 的创建时机 & 从 getMapper() 到代理对象的全链路
创建时机: MapperProxy 在调用 SqlSession.getMapper(Class) 时被创建。
完整链路追踪:
java
// 第1步:应用层调用
UserMapper mapper = session.getMapper(UserMapper.class);
// 第2步:DefaultSqlSession.getMapper()
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
// 第3步:Configuration.getMapper()
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
// 第4步:MapperRegistry.getMapper() → 核心!
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 从缓存 Map 中获取 MapperProxyFactory
final MapperProxyFactory<T> mapperProxyFactory =
(MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 使用工厂创建代理对象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance...", e);
}
}
// 第5步:MapperProxyFactory.newInstance()
public T newInstance(SqlSession sqlSession) {
// 创建 MapperProxy(InvocationHandler)
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
// 创建 JDK 动态代理对象
return newInstance(mapperProxy);
}
// 第6步:返回代理对象给应用
关键数据结构:
java
// MapperRegistry 中的核心缓存
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
// 启动时通过 addMapper() 注册
// key = UserMapper.class, value = new MapperProxyFactory<>(UserMapper.class)
第41题:每次调用 getMapper() 都会创建新的代理对象吗?
结论:是的,每次调用 getMapper() 都会创建一个新的代理对象(但工厂对象是缓存的)。
源码证明:
java
// MapperProxyFactory.newInstance(SqlSession)
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
// 每次 new 一个 MapperProxy → 每次 Proxy.newProxyInstance 产生新对象
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[] { mapperInterface },
mapperProxy
);
}
为什么要每次都创建新代理对象?
原因 说明
持有 SqlSession 每次调用都需要绑定当前线程的 SqlSession(事务上下文)
无状态设计 代理对象本身是无状态的,创建成本极低(轻量级)
避免共享问题 如果代理对象作为单例持有 SqlSession,在多线程环境会出问题
方法缓存复用 MapperProxy 中的 methodCache 是 ConcurrentHashMap,在工厂级别共享,不会因新建代理而丢失
在 Spring 环境下的特殊性:
Spring 整合后,UserMapper 被注入为 Spring Bean(单例),这个 Bean 实际上是一个 MapperFactoryBean 生成的代理。每次调用该 Bean 的方法时,会从 SqlSessionTemplate 中获取当前线程的 SqlSession,但底层仍会为每次方法调用创建新的 MapperProxy 吗?实际上,Spring 的 MapperProxy 在 MapperFactoryBean 中只创建一次,但每次方法调用时会通过 SqlSessionTemplate.getMapper() 获取代理,因此本质上每次方法调用还是会走到 getMapper(),而原生 MyBatis 每次 getMapper() 都创建新代理,但 Spring 的 MapperFactoryBean 会缓存最终的代理对象,只是代理对象内部的 sqlSession 通过 SqlSessionTemplate 动态获取当前线程的会话。不过严格来说,MapperProxy 实例本身通常也是单例,但它内部的 sqlSession 字段是动态代理的 SqlSessionTemplate,线程安全。
👉 结论差异:
· 原生 MyBatis:每次 getMapper() → 新代理对象
· Spring + MyBatis:每个 Mapper Bean 是单例(代理对象单例),但执行时通过 SqlSessionTemplate 保证线程安全
第42题:MapperMethod 的作用 & 如何将方法调用转换为 SQL 执行
MapperMethod 的作用:
MapperMethod 是 Mapper 接口方法与 SQL 执行之间的桥梁/适配器。它封装了一个接口方法的完整信息(SQL 类型、参数签名、返回值类型),并在 execute() 方法中将方法调用转换为对 SqlSession 的具体操作。
核心结构:
java
public class MapperMethod {
// 1. SQL 命令信息
private final SqlCommand command;
// 2. 方法签名信息
private final MethodSignature method;
// 内部类:封装 SQL 命令
public static class SqlCommand {
private final String name; // namespace + "." + methodName
private final SqlCommandType type; // SELECT | INSERT | UPDATE | DELETE
}
// 内部类:封装方法签名
public static class MethodSignature {
private final boolean returnsMany; // 是否返回集合
private final boolean returnsMap; // 是否返回 Map
private final boolean returnsVoid;
private final Class<?> returnType;
private final Integer resultHandlerIndex;
private final Integer rowBoundsIndex;
private final SortedMap<Integer, String> paramNameIndex; // 参数位置 → 参数名
}
}
execute() 方法路由逻辑:
java
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
// 参数转换 → sqlSession.insert()
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
// 根据返回类型路由到不同的 select 方法
if (method.returnsVoid() && method.hasResultHandler()) {
// 带 ResultHandler 的查询
executeWithResultHandler(sqlSession, args);
} else if (method.returnsMany()) {
// 返回集合 → selectList()
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
// 返回 Map → selectMap()
result = executeForMap(sqlSession, args);
} else {
// 返回单个对象 → selectOne()
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method...");
}
return result;
}
关键转换:参数数组 → 单一参数对象
MethodSignature.convertArgsToSqlCommandParam() 将方法参数数组转换为 MyBatis 能识别的参数对象:
· 无参数 → null
· 单个参数 → 返回该参数本身(除非有 @Param)
· 多个参数 → 封装为 Map<String, Object>(key = 参数名,value = 参数值)
第43题:Mapper 接口中方法是否可以重载?
结论:不可以!MyBatis 不支持 Mapper 接口中的方法重载。
原因分析:
- MappedStatement 的 Key 规则:MappedStatement 的缓存 key 是 namespace + "." + methodName(仅方法名,不包含参数类型)
- 启动时冲突检测:当解析第二个重载方法时,会尝试向 Configuration.mappedStatements 中 put 相同的 key,StrictMap 会检测到冲突并抛出异常
源码证据(StrictMap.put()):
java
// Configuration 中 mappedStatements 是 StrictMap 类型
protected static class StrictMap<V> extends HashMap<String, V> {
@Override
public V put(String key, V value) {
if (containsKey(key)) {
// 如果 key 已存在,抛出异常!
throw new IllegalArgumentException(
"Mapped Statements collection already contains value for key " + key
);
}
return super.put(key, value);
}
}
重载尝试报错:
java
// 假设在 UserMapper 中定义了两个同名方法
public interface UserMapper {
User selectById(Long id); // statementId = "com.example.UserMapper.selectById"
User selectById(Long id, String name); // ❌ 同样 key,启动报错!
}
💡 特例(注解方式):
在注解 Mapper 中,Java 编译器允许方法重载(参数不同),但在启动解析时,MapperAnnotationBuilder 会遍历所有方法,同样使用 namespace.methodName 构建 statementId,第二个重载方法会导致 StrictMap 冲突。
解决方案: 使用不同的方法名(如 selectById、selectByIdAndName)
第44题:Mapper 接口方法参数的解析 & @Param 原理
参数解析的核心类:ParamNameResolver
解析流程:
java
// 1. MapperMethod 构造时初始化 MethodSignature
public MethodSignature(Configuration configuration, Method method) {
// 委托给 ParamNameResolver 解析参数
this.paramNameResolver = new ParamNameResolver(configuration, method);
}
// 2. ParamNameResolver 构造函数
public ParamNameResolver(Configuration config, Method method) {
// 获取方法的所有参数类型
Class<?>[] paramTypes = method.getParameterTypes();
// 获取参数的注解(@Param 等)
Annotation[][] paramAnnotations = method.getParameterAnnotations();
// 遍历每个参数
for (int i = 0; i < paramTypes.length; i++) {
// 是否被 @Param 标记
boolean hasParamAnnotation = false;
for (Annotation annotation : paramAnnotations[i]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
// 记录参数名(@Param 的 value)
names.put(i, ((Param) annotation).value());
}
}
// 没有 @Param 的情况
if (!hasParamAnnotation) {
// 使用参数实际名称(需要 JDK 8+ -parameters 编译参数)
// 或使用默认名称 arg0, arg1 / param1, param2
if (config.isUseActualParamName() && JDK.IS_AT_LEAST_JAVA_8) {
// 通过反射获取参数名(依赖 -parameters)
names.put(i, paramNames[i]);
} else {
// 默认:使用 arg0, arg1...
names.put(i, "arg" + i);
}
}
}
}
@Param 的原理:
- 命名覆盖:@Param("userId") 告诉 MyBatis 使用 "userId" 作为该参数在 Map 中的 key
- 强制命名:即使只有一个参数,使用 @Param 也能赋予有意义的名称
- 多参数场景:所有参数被封装到 Map<String, Object> 中,@Param 的值作为 key
参数转换流程(convertArgsToSqlCommandParam()):
java
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
}
// 单个参数且无 @Param → 直接返回参数值
if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
}
// 多个参数或有 @Param → 封装为 Map
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// 同时添加 param1, param2... 作为备选 key
param.put("param" + (i + 1), args[entry.getKey()]);
}
return param;
}
示例:
java
// 接口方法
User select(@Param("id") Long userId, @Param("name") String userName);
// 调用时传入 (1L, "Tom")
// 最终参数 Map = { "id": 1L, "param1": 1L, "name": "Tom", "param2": "Tom" }
// SQL 中可以使用 #{id} 或 #{param1}
第45题:如何根据 namespace + methodName 找到 MappedStatement?
查找流程(在 Configuration.getMappedStatement(String id) 中):
java
// 1. MapperMethod 中的 SqlCommand 构造
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
// 拼接 statementId = 接口全限定名 + "." + 方法名
String statementName = mapperInterface.getName() + "." + method.getName();
// 2. 从 Configuration 中获取 MappedStatement
this.name = statementName;
MappedStatement ms = configuration.getMappedStatement(statementName);
this.type = ms.getSqlCommandType();
}
// 3. Configuration.getMappedStatement()
public MappedStatement getMappedStatement(String id) {
return getMappedStatement(id, true);
}
public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
// 如果存在未解析完成的 Statement,先尝试解析
if (validateIncompleteStatements) {
buildAllStatements();
}
// 从 StrictMap 中查找
return mappedStatements.get(id);
}
StrictMap 的查找逻辑(支持唯一性校验):
java
// StrictMap 继承了 HashMap,put 时校验唯一性
public class StrictMap<V> extends HashMap<String, V> {
@Override
public V get(Object key) {
V value = super.get(key);
// 如果找不到,尝试添加 namespace 前缀再找(兼容旧版本)
if (value == null) {
throw new BindingException("Invalid bound statement (not found): " + key);
}
return value;
}
}
关键结论:
· statementId = Mapper接口全限定名 + . + 方法名(与 XML namespace 完全一致)
· 查找是 O(1) 的 HashMap 查找,性能极高
· 如果查找不到,抛出 BindingException
第46题:Mapper 接口方法没有对应 SQL 映射会发生什么?
异常阶段:在第一次调用该方法时抛出 BindingException
详细分析:
- 启动阶段不会报错:MyBatis 在启动时只会检查 Mapper 接口是否注册(MapperRegistry.addMapper()),但不会校验每个方法是否都有对应的 SQL 映射
- 第一次调用阶段抛出异常:
· 调用 mapper.method() 时,MapperProxy 触发 MapperMethod 构造
· SqlCommand 构造时调用 configuration.getMappedStatement(statementId)
· 找不到对应的 MappedStatement,抛出 BindingException
异常信息示例:
org.apache.ibatis.binding.BindingException:
Invalid bound statement (not found): com.example.mapper.UserMapper.selectByEmail
为什么会这样(源码):
java
// MapperMethod.SqlCommand 构造器
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
String statementName = mapperInterface.getName() + "." + method.getName();
MappedStatement ms = null;
try {
ms = configuration.getMappedStatement(statementName);
} catch (Exception e) {
// 不在此处抛出,而是通过 hasMappedStatement 检查
}
if (ms == null) {
// 检查是否在 Incomplete 中
if (configuration.hasIncompleteStatement(statementName)) {
// 标记为未完成,后续尝试解析
} else {
// 最终抛出
throw new BindingException("Invalid bound statement (not found): " + statementName);
}
}
}
排查步骤(P6 常见故障场景):
- 检查 XML 中 namespace 是否与接口全限定名一致
- 检查 XML 中 id 是否与方法名一致
- 检查 Mapper XML 是否被正确扫描到(mapperLocations 配置)
- 检查 Mapper 接口是否在同一个包下或被 @MapperScan 扫描
- 检查 Maven/Gradle 编译后 XML 是否被复制到 classpath(resources 目录问题)
第47题:MyBatis 的 Mapper 动态代理 vs Spring AOP 动态代理的异同
共同点:
· 都基于 JDK 动态代理 或 CGLIB 实现
· 都使用 InvocationHandler / MethodInterceptor 拦截方法调用
· 都实现了横切关注点的分离(MyBatis:SQL 执行;Spring AOP:日志、事务、权限等)
核心差异对比:
维度 MyBatis Mapper 代理 Spring AOP 代理
代理目标 Mapper 接口(必须为接口) 可以是接口或类(JDK 动态代理 / CGLIB)
代理目的 将接口方法调用转换为 SQL 执行 在方法执行前后织入增强逻辑(如事务、日志)
代理创建 MapperProxyFactory 手动创建 ProxyFactory / AopProxy 自动创建
调用链 方法调用 → SQL 执行 → 返回结果 方法调用 → 拦截器链 → 目标方法 → 返回结果
缓存机制 内部有 methodCache(方法缓存) 无特定缓存(但 Spring 有 AOP 代理缓存)
与 Spring 整合 通过 MapperScannerConfigurer 将 Mapper 代理注册为 Bean 通过 @EnableAspectJAutoProxy 开启自动代理
嵌套代理 通常不被其他代理包装(但可被 AOP 代理再包装) 可以多层嵌套(多个切面)
在 Spring 中两者的关系:
Spring 容器中的 UserMapper Bean
↓
实际是 MyBatis 的 MapperProxy 对象(实现了 UserMapper 接口)
↓
如果该 Bean 被 AOP 切面(如 @Transactional)拦截
↓
Spring AOP 会再包装一层代理(JDK 动态代理或 CGLIB)
↓
最终调用链:AOP 代理 → MapperProxy → SQL 执行
⚠️ 常见陷阱: 当 MyBatis Mapper 被 Spring AOP 代理后,如果使用 @Transactional,事务切面会在 MapperProxy 外层,事务的开启和提交由 Spring 管理,而 SqlSession 的获取由 SqlSessionTemplate 管理,两者协同工作。
第48题:为什么 MyBatis 要求 Mapper 接口不能有实现类?如果有会怎样?
核心原因:MyBatis 的绑定机制是基于接口代理的
- 设计理念冲突:
· MyBatis 通过 MapperProxy 拦截接口方法调用,将方法名转换为 statementId 查找 SQL
· 如果提供了实现类,调用实现类的方法将绕过 MapperProxy,无法触发 SQL 执行逻辑
· 实现类中无法定义 SQL 映射(没有意义),且会与代理逻辑产生歧义 - Spring 整合场景:即使提供了实现类并被 Spring 管理,MyBatis 的 MapperFactoryBean 仍会生成代理对象覆盖(或冲突),导致无法确定是使用代理还是实现类
- Batis 语义约定:MyBatis 的设计哲学就是"接口即映射",接口方法名即 SQL 标识,提供实现类会打破这种约定
如果提供了实现类(实验):
java
public interface UserMapper {
User selectById(Long id);
}
// ❌ 尝试提供实现类
@Component
public class UserMapperImpl implements UserMapper {
@Override
public User selectById(Long id) {
System.out.println("This will never be called!");
return null;
}
}
· 纯 MyBatis 环境:getMapper() 直接返回 JDK 动态代理对象,实现类被完全忽略,永远不会被调用
· Spring + MyBatis 环境:如果同时有 @Component 和 @MapperScan,会导致 Bean 冲突或代理覆盖,最终注入的是代理对象而非实现类
源码层面:
java
// MapperRegistry.addMapper()
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) { // 必须是接口
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known...");
}
knownMappers.put(type, new MapperProxyFactory<>(type));
}
}
// 如果不是接口,直接忽略!不会注册为 Mapper
结论: MyBatis 根本不支持 Mapper 实现类的注册,提供实现类只会带来混淆和资源浪费。请严格遵守"接口 + XML/注解"的开发模式。
第49题:MapperRegistry 的设计 & 如何管理所有 Mapper 接口
MapperRegistry 的核心职责:
MapperRegistry 是 Mapper 接口的注册中心,负责管理所有 Mapper 接口与 MapperProxyFactory 的映射关系,并提供 getMapper() 方法创建代理对象。
核心数据结构:
java
public class MapperRegistry {
// 1. 核心缓存:接口 → 代理工厂
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
// 2. Configuration 引用
private final Configuration config;
// 3. 用于处理未知 Mapper 的错误提示
private final Map<Class<?>, MapperProxyFactory<?>> knownMappersUnmodifiable;
}
核心方法:
java
// 1. 注册 Mapper(启动时调用)
public <T> void addMapper(Class<T> type) {
// 校验:必须是接口
if (!type.isInterface()) {
return;
}
// 校验:是否已注册
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known...");
}
// 是否加载完成(防止循环依赖)
boolean loadCompleted = false;
try {
// 创建 MapperProxyFactory 并存入 Map
knownMappers.put(type, new MapperProxyFactory<>(type));
// 解析接口上的注解(@CacheNamespace 等)
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
// 2. 获取 Mapper(运行时调用)
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory =
(MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known...");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance...", e);
}
}
设计特点:
特点 说明
工厂模式 缓存的是 MapperProxyFactory,而不是代理对象本身(避免持有 SqlSession)
单例工厂 每个 Mapper 接口对应一个 MapperProxyFactory 实例(单例)
线程安全 knownMappers 在启动后只读,无需额外同步;getMapper 只读操作
延迟绑定 Mapper 注册在启动时完成,但代理对象在第一次 getMapper() 时才创建
校验机制 注册时校验是否为接口,确保类型安全
加载流程(Spring 整合时的入口):
java
// 1. MyBatis 原生方式
configuration.addMapper(UserMapper.class);
// 2. Spring 扫描方式(MapperScannerConfigurer)
// 扫描指定包下的所有接口 → 逐个调用 configuration.addMapper()
// 将 Mapper 代理注册为 Spring Bean
💡 P6 追问:为什么 Map 中存的是 Factory 而不是 MapperProxy?
答:因为 MapperProxy 需要持有 SqlSession,而 SqlSession 是线程不安全的且每个线程/请求都不同。如果缓存代理对象,会导致 SqlSession 被固定,多线程共享会出问题。而 MapperProxyFactory 是无状态的,每次通过它 newInstance(sqlSession) 传入当前会话,保证了线程安全。这也是享元模式的体现------共享工厂,不共享产品。
附:第四部分高频追问题
Q:MyBatis 的 MapperProxy 是线程安全的吗?
A:MapperProxy 是无状态的(除了 methodCache 是 ConcurrentHashMap),因此是线程安全的。但传入的 SqlSession 不是线程安全的,所以在原生 MyBatis 中不应共享 MapperProxy 实例。而在 Spring 中,MapperProxy 持有的 SqlSessionTemplate 是线程安全的,所以可以共享。
Q:如果两个 Mapper 接口有相同的方法名,会冲突吗?
A:不会。MappedStatement 的 key 是 namespace + "." + methodName,不同 Mapper 的 namespace 不同,所以可以共存。
Q:@Mapper 注解和 @MapperScan 的关系?
A:@Mapper 标记单个 Mapper 接口,@MapperScan 批量扫描指定包下的所有 @Mapper 接口(Spring Boot 方式)。在原生 MyBatis 中,两者都不需要,通过 configuration.addMapper() 注册。
五、动态 SQL 与 OGNL(8题)
第50题:动态 SQL 的实现原理(SqlNode + OGNL)
核心架构:组合模式(Composite Pattern) + OGNL 表达式求值
MyBatis 将 XML 中的动态 SQL 解析为一棵 SqlNode 抽象语法树。运行时通过 OGNL 对表达式求值,决定是否拼接相应 SQL 片段。
核心接口与实现类:
java
public interface SqlNode {
// 根据上下文参数,动态生成 SQL 片段
boolean apply(DynamicContext context);
}
实现类 作用
StaticTextSqlNode 静态文本,直接追加
MixedSqlNode 容器节点,遍历子节点逐个 apply
IfSqlNode 对应 ,OGNL 表达式为 true 时 apply 子节点
ChooseSqlNode 对应 ,类似 switch-case
ForEachSqlNode 对应 ,循环拼接 SQL
WhereSqlNode / SetSqlNode 智能处理 WHERE / SET 子句(去除多余的 AND/OR 或逗号)
TrimSqlNode 通用的前后缀裁剪节点(where 和 set 的底层实现)
VarDeclSqlNode 对应 ,声明 OGNL 变量
运行时流程:
java
// 1. 启动时解析 XML → 构建 SqlNode 树(XMLScriptBuilder)
// 2. 运行时调用 SqlSource.getBoundSql()
DynamicContext context = new DynamicContext(configuration, parameterObject);
sqlNode.apply(context); // 递归遍历树,拼接 SQL
String sql = context.getSql(); // 获取最终 SQL
第51题:动态 SQL 标签及适用场景
标签 作用 适用场景
条件判断,true 时拼接 SQL 动态 WHERE 条件、动态 SET 字段
// 多分支选择(switch-case) 多个互斥条件,优先级依次判断
包裹条件,自动去除首个 AND/OR 动态查询条件,避免 WHERE 1=1 写法
包裹更新字段,自动去除末尾逗号 动态 UPDATE
自定义前后缀裁剪(where/set 的通用版) 复杂 SQL 片段裁剪
遍历集合,生成 IN 子句或批量 INSERT 批量操作、动态 IN 查询
创建 OGNL 变量 模糊查询拼接 %、复杂表达式复用
/ 定义和引用可复用的 SQL 片段 公共查询字段、公共 WHERE 条件
第52题:OGNL 在 MyBatis 中的角色与集成
角色:表达式引擎
OGNL(Object-Graph Navigation Language)负责:
- 条件判断: 中的 test 表达式
- 参数取值:#{user.name} 通过 OGNL 从参数对象中取属性值
- 循环变量:
- 变量绑定:
集成方式:
java
// OgnlCache 封装了 OGNL 静态方法调用
public static Object getValue(String expression, Object root) {
return Ognl.getValue(parseExpression(expression), root);
}
· 启动时预编译 OGNL 表达式(parseExpression 缓存),运行时直接求值
· 参数对象作为 OGNL 的 root 对象,可以直接访问其属性
· _parameter、_databaseId 等作为额外参数注入
第53题: 标签中 test 表达式的解析与执行
源码链路:
java
// XMLScriptBuilder.parseDynamicTags() 解析 <if>
public SqlNode parseDynamicTags(XNode node) {
// 1. 创建 IfSqlNode
String test = node.getStringAttribute("test");
// 2. 解析子节点内容
List<SqlNode> contents = parseDynamicTags(node);
// 3. 返回 IfSqlNode(持有 OGNL 表达式 + 子节点列表)
return new IfSqlNode(contents, test);
}
// IfSqlNode.apply()
public boolean apply(DynamicContext context) {
// 调用 OGNL 求值
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// true → 拼接子节点 SQL
for (SqlNode node : contents) {
node.apply(context);
}
return true;
}
return false;
}
// ExpressionEvaluator.evaluateBoolean()
public boolean evaluateBoolean(String expression, Object root) {
Object value = OgnlCache.getValue(expression, root);
if (value instanceof Boolean) return (Boolean) value;
if (value instanceof Number) return ((Number) value).doubleValue() > 0;
return value != null;
}
关键点:
· OGNL 表达式已预编译缓存(OgnlCache 内部有 ConcurrentHashMap)
· 参数为 null 时直接返回 false(不会 NPE)
第54题: 在批量操作中的使用与注意事项
批量插入示例:
xml
<insert id="batchInsert">
INSERT INTO user (name, age) VALUES
<foreach collection="list" item="u" separator=",">
(#{u.name}, #{u.age})
</foreach>
</insert>
注意事项:
事项 说明
SQL 长度限制 数据库对 SQL 长度有限制(如 MySQL max_allowed_packet),大批量需分批处理
collection 取值 参数为 List 用 list,数组用 array,Map 用 key 名,建议加 @Param
separator 用法 批量 INSERT 用 ,;IN 查询用 , 配合 open="(" close=")"
空集合处理 配合 判断,避免生成 IN () 语法错误
性能对比 foreach 批量 INSERT 比 Java 循环单条快 10~100 倍(减少网络往返)
第55题:动态 SQL 的解析时机(启动时 vs 运行时)
重要区分:两阶段解析
阶段 时机 工作内容 产物
第一阶段(语法解析) 启动时 读取 XML,将标签解析为 SqlNode 对象树 SqlSource(内含 SqlNode 树)
第二阶段(渲染拼接) 运行时(每次执行) 调用 SqlNode.apply(),传入参数进行 OGNL 求值,拼接 SQL BoundSql(含最终 SQL + 参数映射)
源码印证:
java
// 启动时:XMLScriptBuilder.parseScriptNode()
public SqlSource parseScriptNode() {
// 解析动态标签,生成 SqlNode 树
SqlNode root = parseDynamicTags(node);
// 判断是否为纯静态 SQL
if (isDynamic) {
return new DynamicSqlSource(configuration, root); // 运行时每次拼接
} else {
return new RawSqlSource(configuration, root, parameterType); // 启动时已拼接
}
}
// 运行时:DynamicSqlSource.getBoundSql()
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 遍历 SqlNode 树,拼接 SQL
rootSqlNode.apply(context);
// 解析 #{} 为 ?,生成 ParameterMapping
SqlSource sqlSourceParser = sqlSourceParser.parse(context.getSql(), ...);
return sqlSourceParser.getBoundSql(parameterObject);
}
结论:XML 标签结构在启动时解析为对象树(重量级,只做一次),但具体 SQL 文本在每次执行时动态渲染。
第56题:OGNL 表达式中的字符串比较为什么用单引号?
原因:OGNL 的语法解析规则
· OGNL 表达式被解析时,双引号在 OGNL 语境中有特殊含义(如表示字符字面量,或与外部 XML 属性双引号冲突)
· 在 XML 属性值 test="name == 'Tom'" 中,外层已是双引号,内层若再使用双引号会产生解析歧义(XML 层面会提前闭合属性)
· OGNL 要求字符串字面量使用单引号作为标准语法
xml
<!-- ✅ 正确 -->
<if test='name == "Tom"'> <!-- 外层单引号,内层双引号也可 -->
<if test="name == 'Tom'"> <!-- 外层双引号,内层单引号 -->
<!-- ❌ 错误 -->
<if test="name == "Tom""> <!-- XML 解析器会将 "Tom" 中的双引号误认为属性结束 -->
最佳实践:XML 属性统一使用双引号,表达式内统一使用单引号。
第57题:动态 SQL 的调试和追踪手段
手段 方法 说明
日志配置 logImpl=STDOUT_LOGGING 或 logback 设置 level=DEBUG 打印 SQL 及参数(PreparedStatement 的 debug 日志)
MyBatis 插件 自定义 Interceptor 拦截 StatementHandler 在 prepare 前打印完整 SQL(含已绑定参数)
断点调试 在 DynamicSqlNode.apply() / BoundSql.getSql() 打断点 观察 SqlNode 树遍历过程和最终生成的 SQL
P6Spy / Druid SQL 监控 使用第三方 SQL 拦截工具 输出完整可执行的 SQL,无需手动拼接
日志级别细调 org.apache.ibatis.logging.jdbc.BaseJdbcLogger DEBUG 级别会逐条输出参数绑定过程
推荐组合:
xml
<!-- mybatis-config.xml -->
<settings>
<setting name="logImpl" value="SLF4J"/>
</settings>
yaml
# application.yml(Spring Boot)
logging.level.com.example.mapper=DEBUG
logging.level.org.apache.ibatis=DEBUG
生产环境不建议开启 SQL 详细日志(大量 IO 影响性能),可在压测/调试时临时开启。
六、结果集映射(8题)
第58题:结果集封装方式及映射形式
MyBatis 通过 ResultSetHandler(默认实现 DefaultResultSetHandler)将 JDBC ResultSet 映射为 Java 对象。核心流程:遍历 ResultSet → 取列值 → 通过 TypeHandler 转换 → 填充目标对象。
映射形式(三种):
映射方式 配置方式 适用场景
自动映射 无需配置,靠字段名/列名匹配(可开启驼峰转换) 列名与属性名一致或符合驼峰规则
结果类型(resultType) 指定返回类型,由 MyBatis 自动完成映射 简单对象、单表查询
结果映射(resultMap) 显式配置 复杂关联、嵌套、字段名不一致、类型转换
优先级:resultMap > resultType(同时配置时 resultMap 生效)。
第59题:ResultMap 核心设计 & 嵌套映射处理
核心设计:
ResultMap 是结果映射的元数据对象,包含:
· id:映射唯一标识
· type:映射的目标 Java 类型
· mappedColumns / mappedProperties:字段与属性的对应关系
· idArgMappings / constructorMappings:构造函数映射
· propertyMappings:普通属性映射列表
· hasNestedQueries / hasNestedResultMaps:是否包含嵌套映射
嵌套映射处理机制:
嵌套类型 配置元素 底层处理
嵌套查询(Nested Select) association select="..." 执行额外 SQL(可能导致 N+1),支持懒加载代理
嵌套结果(Nested Result) association resultMap="..." / columnPrefix 通过 JOIN 一次性查出,ResultSetHandler 按行分组构建对象
源码入口:DefaultResultSetHandler.handleRowValuesForNestedResultMap() ------ 通过比较 和关联对象的 ID 列来判断是否属于同一对象,实现一对多/多对一的聚合。
第60题:字段名与属性名不一致的解决方案
方案 实现 优劣
驼峰转换 最简洁,适用于标准下划线→驼峰,覆盖 80% 场景
SQL 别名 SELECT user_name as userName FROM user 灵活,但每处查询都需写别名,重复劳动
resultMap 显式映射 最强大,支持类型转换、嵌套,配置稍繁琐
注解 @Results @Results(@Result(column="user_name", property="userName")) 注解版 resultMap,作用域小,不推荐用于复杂场景
P6 建议:优先开启驼峰转换,复杂映射使用 XML resultMap(可复用、可维护)。
第61题:association 与 collection 的区别
对比
映射关系 一对一 / 多对一 一对多
Java 类型 单个对象 集合(List/Set/Array)
属性配置 javaType(目标对象类型) ofType(集合中元素类型)
嵌套查询 返回单条记录的查询 返回多条记录的查询
示例:
xml
<!-- 一对一:每个用户有一个地址 -->
<association property="address" javaType="Address" column="address_id" select="selectAddress"/>
<!-- 一对多:每个用户有多个订单 -->
<collection property="orders" ofType="Order" column="id" select="selectOrdersByUserId"/>
第62题:嵌套查询 vs 嵌套结果的底层原理与性能
对比 嵌套查询(Nested Select) 嵌套结果(Nested Result)
SQL 次数 1 + N(主表 1 次 + 每条关联数据 1 次) 1 次(JOIN 联表)
实现方式 通过 执行额外 SQL 通过 JOIN + 映射
懒加载支持 ✅ 支持(Javassist/CGLIB 代理) ❌ 不支持(数据已在结果集中)
网络开销 大(多次数据库往返) 小(单次往返)
SQL 复杂度 简单 JOIN 复杂,可能产生数据冗余
适用场景 关联数据不常用、需要懒加载 关联数据频繁使用、要求高性能
性能差异量化: 主表 1000 条,每条关联 1 个子对象 → 嵌套查询执行 1001 次 SQL,嵌套结果执行 1 次 SQL(性能差距巨大)。
第63题:N+1 查询问题 & 发现与解决
定义: 执行 1 次主查询获取 N 条记录,随后对每条记录执行 1 次关联查询,总查询次数 = 1 + N。
发现方法:
· 开启 SQL 日志(DEBUG 级别),观察执行次数
· 监控数据库连接/慢查询日志
· 使用 MyBatis 插件统计 SQL 调用次数
解决方案:
方案 实施 本质
改成嵌套结果(JOIN) 修改 XML,使用 resultMap 嵌套 association/collection,一次 JOIN 查询 推荐
开启懒加载 将 N 次查询延迟到真正需要时
应用层批处理 先查询主表 ID 列表,再用 IN 一次性查出关联数据,应用层组装 对嵌套查询的优化
使用 @Fetch / 缓存 二级缓存减少重复查询 兜底手段
第64题:MyBatis 关联映射支持(1-1、1-N、N-N)
技术手段: association + collection 组合。
关系 映射方式 配置要点
一对一(1-1) 主表有外键指向附表
一对多(1-N) 主表外键在多方
多对一(N-1) 同 1-1,站在多方看
多对多(N-N) 两个 或 组合 需要中间表,通常分两次查询或 JOIN 中间表+目标表
多对多示例:
xml
<!-- 用户 ↔ 角色(多对多),通过中间表 user_role -->
<resultMap id="userWithRoles" type="User">
<id property="id" column="user_id"/>
<collection property="roles" ofType="Role" column="user_id"
select="selectRolesByUserId"/>
</resultMap>
<!-- selectRolesByUserId: SELECT r.* FROM role r JOIN user_role ur ON r.id=ur.role_id WHERE ur.user_id = #{userId} -->
第65题:自动映射(auto-mapping)规则 & 失效场景
规则:
配置值 行为
NONE 关闭自动映射,必须显式配置 resultMap
PARTIAL(默认) 对非嵌套的 resultMap 自动映射(不处理 association/collection 内部)
FULL 对所有 resultMap(含嵌套)都自动映射,但需谨慎(可能覆盖显式配置)
匹配规则:
· 列名与属性名 大小写不敏感 匹配
· 开启 mapUnderscoreToCamelCase 后,下划线自动转驼峰
· 支持别名(AS)
自动映射失效场景:
场景 原因
列名与属性名完全不相同 无法匹配
使用了 association/collection 且 autoMapping="false"(显式关闭) 显式配置覆盖
嵌套结果(嵌套对象内部属性未显式配置且父 resultMap 未开启 FULL) PARTIAL 不处理嵌套
属性类型与列类型无法自动转换(如 String → Date) 需要 TypeHandler 或显式映射
属性名是 Java 关键字(如 abstract) 反射无法写入,需显式指定
构造方法注入(无 setter) 需使用
P6 建议:
· 开发环境使用 PARTIAL + 驼峰转换
· 生产环境保持 PARTIAL,避免 FULL 带来的意外覆盖
· 关键查询使用显式 resultMap,不依赖自动映射(便于维护和性能优化)
七、延迟加载(懒加载)机制(7题)
第66题:延迟加载的底层实现原理
核心技术:代理模式(Proxy Pattern)+ 字节码增强
MyBatis 通过为关联对象(association/collection)创建代理对象来实现懒加载。代理对象中持有执行查询所需的所有信息(MappedStatement、参数、SqlSession 等),当代理对象的 getter 方法被首次调用时,才触发真正的数据库查询,加载数据后替换代理对象。
核心类协作:
java
// 代理工厂:JavassistProxyFactory / CglibProxyFactory
// 代理逻辑:LazyLoader (实现了 InvocationHandler / MethodHandler)
// 1. 创建代理(DefaultResultSetHandler.createProxyForNestedQuery)
public Object createProxyForNestedQuery(ResultSetWrapper rsw, ResultMap resultMap, ...) {
// 组装延迟加载所需的参数
LazyLoader lazyLoader = new LazyLoader(...);
// 通过 ProxyFactory 创建代理对象
return proxyFactory.createProxy(target, lazyLoader, configuration, objectFactory);
}
// 2. 触发加载(LazyLoader.invoke / intercept)
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 如果是触发加载的方法(如 getXxx)
if (isPropertyToLoad(method.getName())) {
// 执行真正的查询,加载数据
load(proxy, method, args);
}
return method.invoke(target, args);
}
第67题:懒加载为什么不用 JDK 动态代理,而选 Javassist?
核心原因:代理目标不同
对比项 JDK 动态代理 Javassist / CGLIB
代理目标 必须是接口 可以是普通类
懒加载场景 关联对象是 POJO(普通类),不是接口 ✅ 直接代理 POJO 类
依赖 JDK 内置 需引入第三方字节码库
为什么 MyBatis 选 Javassist(默认)而非 CGLIB?
· MyBatis 3.x 后默认使用 JavassistProxyFactory,因 Javassist 更轻量、API 更简洁
· CGLIB 作为备选(可通过 切换)
· 两者底层都是字节码增强(生成目标类的子类,重写 getter 方法)
源码证据(Configuration 默认值):
java
public class Configuration {
protected ProxyFactory proxyFactory = new JavassistProxyFactory();
// 可替换为 CglibProxyFactory
}
第68题:lazyLoadingEnabled 与 aggressiveLazyLoading 的区别
配置 默认值 作用
lazyLoadingEnabled false 全局总开关:开启后,所有 association/collection 默认懒加载;关闭则立即加载所有关联数据
aggressiveLazyLoading false(3.4.1+) 触发策略:true 时,任意方法(包括 equals、hashCode、toString)调用都会触发所有懒加载属性的加载;false 时,仅当调用该属性的 getter 时才加载该属性
⚠️ 重要提示:aggressiveLazyLoading 在 3.4.1 版本后默认改为 false,生产环境强烈建议保持 false,避免触发不可控的加载行为导致性能问题。
第69题:延迟加载的完整执行流程
1. 主查询执行(用户表查询)
→ ResultSetHandler 处理结果集
→ 发现 address 字段配置了懒加载(<association select="...">)
→ 不执行关联查询,创建一个 Javassist 代理对象填充到 address 属性
→ 返回 User 对象(address 是代理,未加载真实数据)
2. 应用层调用 user.getAddress().getCity()
→ 代理对象的 getter 被拦截(LazyLoader / Javassist 增强逻辑)
→ 检查该属性是否已加载(通过标识字段判断)
→ 未加载 → 触发 doLoad()
3. doLoad() 执行嵌套查询
→ 从代理对象中获取保存的 MappedStatement、参数、SqlSession
→ 通过 Executor 执行 SELECT * FROM address WHERE user_id = ?
→ ResultSetHandler 映射结果为 Address 对象
4. 结果回填
→ 将加载到的 Address 对象设置到 User 对象的 address 属性
→ 将代理对象替换为真实对象(或标记为已加载,后续直接返回)
→ 返回真实对象的 getCity() 结果
第70题:toString() 为什么会导致懒加载失效?如何避免?
原因:
java
// 实体类中常见的 toString()
public String toString() {
return "User{id=" + id + ", address=" + address.getCity() + "}";
}
// 调用 address.getCity() 会触发懒加载代理,导致额外执行 SQL
// 如果 aggressiveLazyLoading=true,toString() 本身就会触发所有懒加载
关键点:
· 日志打印、调试器展开对象、JSON 序列化(如 Jackson)都可能触发 getter,从而触发懒加载
· 若此时数据库连接已关闭(事务结束),会抛出 LazyLoadingException
避免方案:
方案 实施
toString() 中不访问懒加载字段 只包含基础字段,不调用关联对象的 getter
使用 DTO 替代实体类输出 在业务层转换为 DTO 后再序列化/打印
确保懒加载在事务内完成 使用 @Transactional 保证连接可用
关闭日志中的 SQL 输出 生产环境避免 DEBUG 日志频繁触发
使用 Jackson 注解 @JsonIgnore 忽略懒加载字段,或配置序列化模块处理代理
第71题:懒加载与二级缓存同时开启时会发生什么?为什么?
结论:懒加载与二级缓存同时使用可能导致代理对象被存入二级缓存,引发反序列化异常或数据不一致。
原因深度分析:
- 序列化要求:二级缓存默认要求对象实现 Serializable。懒加载代理对象(Javassist 生成)若未正确处理序列化,反序列化后会丢失代理上下文,导致 LazyLoadingException
- 缓存对象状态:如果对象被缓存时包含未加载的代理,反序列化后该代理无法绑定到原有的 SqlSession 和 Configuration,执行懒加载会失败
- 缓存命中后的行为:从二级缓存读取的对象是反序列化后的"干净"对象,其中的懒加载代理可能处于无效状态
MyBatis 的处理机制:
· 如果 resultMap 中配置了懒加载(fetchType="lazy"),默认不会将该对象的关联属性存入二级缓存
· 但若显式配置了 ,MyBatis 会尝试序列化整个对象,可能会在运行时抛出异常
最佳实践:不要将需要懒加载的复杂关联对象存入二级缓存。如需使用二级缓存,建议:
· 使用 fetchType="eager" 强制立即加载
· 在序列化前手动加载所有懒加载属性
· 使用 DTO 或专门的结果对象替代实体类进行缓存
第72题:Spring 事务中懒加载为什么会重复执行 SQL?如何解决?
问题现象:在 Spring 环境下,懒加载有时会执行多次关联查询,而不是预期的一次。
根本原因分析:
- 事务边界 + 连接关闭:
· 主查询在 @Transactional 方法内执行,关联对象的懒加载可能在事务外触发
· 事务外 SqlSession 已被关闭,MyBatis 会创建一个新的 SqlSession 执行查询
· 新 Session 的一级缓存是空的,所以每次懒加载都会重新执行 SQL - 一级缓存失效:
· Spring 的 SqlSessionTemplate 保证每次 getMapper 获取的代理绑定到当前线程的 SqlSession
· 但事务结束后,SqlSession 被清理,新的懒加载无法命中一级缓存 - 跨 Session 重复加载:如果同一个对象在多个独立的事务中被懒加载,每次都是新 Session,无法共享缓存
解决方案:
方案 说明 适用场景
确保懒加载在事务内完成 在 @Transactional 方法内提前调用 getter 触发加载 最推荐,确保同一 Session
使用 Open Session In View 延长 SqlSession 生命周期到视图渲染结束(Spring Web 环境) Web 应用,但需注意性能
使用 DTO 联表查询 放弃懒加载,用 JOIN 一次性查出所有数据 高性能场景,避免 N+1
二级缓存 将关联数据缓存到二级缓存,减少重复查询 读多写少场景
应用层手动组装 先用 IN 查询关联数据,再 set 到主对象中 复杂批量场景
Open Session In View 配置(Spring Boot):
yaml
spring:
jpa:
open-in-view: true # JPA 风格
# MyBatis 需自定义 Filter 或 Interceptor 保持 Session 开启
P6 核心建议:
· 严格区分事务边界与懒加载范围
· 生产环境推荐放弃懒加载,改用 JOIN + resultMap 嵌套结果,性能更可控,避免事务外加载的隐患
· 如确需懒加载,务必保证触发加载的代码在 @Transactional 包裹的方法内
八、缓存机制(一级缓存 + 二级缓存)(10题)
第73题:一级缓存的作用域与生命周期
· 作用域:SqlSession 级别(会话级)。由 BaseExecutor 中的 PerpetualCache 实例维护。
· 生命周期:
· 创建:SqlSession 打开时创建 Executor,随之创建一级缓存。
· 销毁:SqlSession 关闭(close())或提交/回滚(部分清空)时销毁/清空。
· 本质:一个简单的 HashMap(PerpetualCache),Key 为 CacheKey(由 StatementId + RowBounds + SQL + 参数值 组成),Value 为查询结果。
第74题:一级缓存什么时候被清空?
触发场景 机制
执行 UPDATE/INSERT/DELETE BaseExecutor.update() 中调用 clearLocalCache(),清空整个一级缓存(防止脏读)
手动调用 session.clearCache() 显式清空
SqlSession 提交/回滚 commit() / rollback() 时会清空缓存(commit 在 flush 后会清空)
SqlSession 关闭 close() 时缓存被丢弃
作用域设置为 STATEMENT ,每次查询后立即清空
第75题:Spring 整合环境下的一级缓存是否还有效?
结论:在同一个事务内有效;非事务下可能失效。
原理剖析:
Spring 通过 SqlSessionTemplate 管理 MyBatis。SqlSessionTemplate 内部使用 SpringManagedTransaction,并通过 SessionHolder 将同一个 SqlSession 绑定到当前线程(@Transactional 包裹时)。
· 有事务(@Transactional):整个事务共用一个 SqlSession → 一级缓存生效。
· 无事务:每次执行 Mapper 方法,SqlSessionTemplate 可能会从池中获取新的 SqlSession(或每次 close) → 一级缓存随 Session 销毁而失效。
源码依据:SqlSessionUtils.getSqlSession() 会尝试从 TransactionSynchronizationManager.getResource() 获取绑定的 Session,事务内必定命中同一个。
第76题:二级缓存的作用域与开启方式
· 作用域:Mapper 级别(Namespace 级别)。同一个 Namespace 下的所有 SqlSession 共享。
· 开启条件(必须同时满足):
- 全局开关:(默认 true)。
- Mapper 声明:在 Mapper XML 中添加 或在接口上加 @CacheNamespace。
- 具体语句启用:(默认 true,可选择性关闭)。
- 实体类序列化:POJO 需实现 Serializable(因为缓存可能跨 Session 序列化)。
第77题:二级缓存底层实现 & 序列化要求
底层机制:
二级缓存采用装饰器模式(Decorator Pattern)。基础实现是 PerpetualCache(HashMap),通过层层装饰添加功能:
PerpetualCache ← LruCache(淘汰策略)← SynchronizedCache(线程安全)← SerializedCache(序列化)← LoggingCache(日志)← TransactionalCache(事务性缓存,延迟提交)。
为什么要求 Serializable:
SerializedCache 装饰器会将存入缓存的对象进行序列化(字节数组),取出时反序列化。目的是深拷贝,防止对象引用被后续修改污染缓存数据。如果不序列化,直接存储引用,外部修改对象会导致缓存数据变化。
源码入口:CacheBuilder 构建缓存时,若配置了 readWrite=true(默认),会添加 SerializedCache。
第78题:一级缓存与二级缓存的查询顺序
执行顺序:二级缓存 → 一级缓存 → 数据库(DB)
源码链路(CachingExecutor.query()):
java
// 1. 先查二级缓存
Object cached = cache.getObject(cacheKey);
if (cached != null) return cached;
// 2. 二级未命中 → 委托给 BaseExecutor(执行一级缓存与 DB 查询)
List<E> list = delegate.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
// BaseExecutor.query() 内部
// 3. 查一级缓存 localCache
list = localCache.getObject(cacheKey);
if (list != null) return list;
// 4. 一级未命中 → doQuery() 查数据库
list = doQuery(...);
localCache.putObject(cacheKey, list); // 存入一级
第79题:二级缓存命中率监控
MyBatis 内置了缓存命中率统计,通过 Cache 对象的 getHitRatio() 获取。
开启监控方式:
方式一:配置日志
,会输出类似:
Cache Hit Ratio com.example.mapper.UserMapper: 0.8
方式二:编程获取
java
Cache cache = configuration.getCache("com.example.mapper.UserMapper");
double hitRatio = cache.getHitRatio(); // 需通过装饰器链获取底层统计数据
优化原则:命中率低于 0.7 时,需检查缓存配置、过期策略或考虑是否适合使用二级缓存。
第80题:缓存的设计缺陷与局限性(尤其分布式)
缺陷/局限性 说明
分布式脏读(一致性问题) 二级缓存是应用本地缓存(JVM 内存),不同节点间无法感知对方的更新。节点 A 更新数据后只清空自己的缓存,节点 B 仍返回旧数据(脏读)
事务性缓存粒度粗 TransactionalCache 在事务提交时才真正写入缓存,隔离级别处理简单,高并发下易覆盖
更新即清空全量 执行 UPDATE 会清空整个 Namespace 的所有缓存,而非精确失效,命中率骤降
缓存穿透/雪崩 无内置防穿透机制,热点 Key 失效可能导致 DB 压力骤增
P6 解决方案建议:
· 分布式环境建议关闭 MyBatis 二级缓存,改用 Redis(Spring Cache + Redis) 或 Memcached 等分布式缓存中间件。
· 如必须使用,可自定义 Cache 接口实现(如 RedisCache),将数据存于 Redis,实现跨节点共享。
第81题:自定义缓存实现(Cache 接口)
步骤:
- 实现 org.apache.ibatis.cache.Cache 接口:
java
public class RedisCache implements Cache {
private final String id;
private final Jedis jedis = new Jedis("localhost", 6379);
public RedisCache(String id) { this.id = id; }
@Override
public String getId() { return id; }
@Override
public void putObject(Object key, Object value) {
jedis.set(KeyUtil.serialize(key), SerializeUtil.serialize(value));
jedis.expire(key.toString(), 3600);
}
@Override
public Object getObject(Object key) {
byte[] bytes = jedis.get(KeyUtil.serialize(key));
return SerializeUtil.deserialize(bytes);
}
@Override
public void removeObject(Object key) { jedis.del(key.toString()); }
@Override
public void clear() { jedis.flushDB(); }
@Override
public int getSize() { return jedis.dbSize().intValue(); }
}
- 配置使用:
第82题:更新操作时缓存失效机制(源码级)
更新操作(UPDATE/INSERT/DELETE)触发缓存失效的源码链路:
java
// 1. CachingExecutor.update()
public int update(MappedStatement ms, Object parameter) {
// 先执行清理
flushCacheIfRequired(ms);
// 执行更新
int result = delegate.update(ms, parameter);
// 再次清理(执行后清理)
flushCacheIfRequired(ms);
return result;
}
// 2. flushCacheIfRequired()
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
// 清空该 Namespace 的整个二级缓存(!)
cache.clear();
}
}
// 3. BaseExecutor.update() 内部同时清空一级缓存
public int update(MappedStatement ms, Object parameter) {
clearLocalCache(); // 清空一级缓存
return doUpdate(ms, parameter);
}
核心要点:
· 默认情况下,INSERT/UPDATE/DELETE 的 flushCache 属性为 true。
· 失效粒度极粗:直接调用 Cache.clear() 清空整个 Namespace 的所有缓存条目,而非精确移除特定 Key。
· 这就是为什么高频更新场景下,二级缓存命中率极低的根本原因。
九、插件机制(拦截器)(8题)
第九部分答案:插件机制(拦截器)(第83-90题)
第83题:插件机制的设计目标与使用场景
设计目标:在 MyBatis 的核心执行链路(四大对象方法调用)上提供低侵入式的扩展能力,允许开发者在不修改源码的情况下拦截、增强核心行为。
底层本质:责任链模式(Chain of Responsibility)+ 动态代理(JDK Proxy)。
使用场景:
场景 拦截目标
分页插件(如 PageHelper) 拦截 Executor.query(),改写 SQL 拼接 LIMIT
性能监控 拦截 StatementHandler,记录 SQL 执行耗时
SQL 审计/日志 拦截 StatementHandler.prepare(),打印完整 SQL
数据脱敏/加密 拦截 ResultSetHandler,对特定字段脱敏
读写分离 拦截 Executor,根据 SQL 类型切换数据源
第84题:分页插件的原理与 SQL 重写
拦截点:Executor.query() 或 StatementHandler.prepare()(PageHelper 实际拦截 Executor.query)。
核心流程:
- 拦截 Executor.query(MappedStatement, parameter, RowBounds, ResultHandler)。
- 解析参数中的分页参数(页码、每页大小)。
- 通过 Dialect 方言(MySQL、Oracle、PostgreSQL)生成 COUNT 查询 SQL,获取总记录数。
- 改写原始 SQL,拼接 LIMIT offset, size(MySQL)或 ROWNUM(Oracle)。
- 将改写后的 BoundSql 通过反射替换到 MappedStatement 中(或构造新的 MappedStatement),执行查询。
- 封装为 Page 对象返回(包含数据列表 + 总条数)。
关键源码逻辑(简化):
java
// 拦截 Executor.query
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取原始 BoundSql
BoundSql boundSql = getBoundSql(invocation);
String sql = boundSql.getSql();
// 2. 拼接 COUNT 与 分页 LIMIT
String countSql = dialect.getCountSql(sql);
String pageSql = dialect.getPageSql(sql, offset, limit);
// 3. 反射修改 MappedStatement 中的 SqlSource
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
// ... 通过工具类替换为新的 SqlSource
return invocation.proceed();
}
第85题:四大拦截对象及各自的拦截作用
MyBatis 允许拦截 Executor、StatementHandler、ParameterHandler、ResultSetHandler。
拦截对象 典型拦截方法 可以做什么
Executor query(), update(), commit(), rollback() 全局缓存控制、读写分离、分页、SQL 执行前后埋点
StatementHandler prepare(), parameterize(), query(), update() SQL 重写、预编译前修改 SQL、设置超时时间
ParameterHandler setParameters() 参数加密、日志记录参数值、替换 null 的 JdbcType
ResultSetHandler handleResultSets() 结果解密、数据脱敏、结果集二次处理(如转为 Map)
拦截配置(注解)示例:
java
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
第86题:插件的底层实现原理(源码深度剖析)
原理:在四大对象被创建时(Configuration.newXxx() 方法),MyBatis 会调用 InterceptorChain.pluginAll(target),使用JDK 动态代理层层包装目标对象。
源码链路:
java
// 1. Configuration.newExecutor()
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
Executor executor = new SimpleExecutor(...);
// ...
// 关键:插件拦截链
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
// 2. InterceptorChain.pluginAll()
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target); // 层层包裹
}
return target;
}
// 3. Plugin.wrap()(默认实现)
public static Object wrap(Object target, Interceptor interceptor) {
// 判断目标类是否实现了被拦截的接口
if (matches(target, interceptor)) {
// JDK 动态代理!InvocationHandler 是 Plugin 自身
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new Plugin(target, interceptor));
}
return target;
}
Plugin.invoke() 执行逻辑:
java
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (signatureMap.containsKey(target.getClass())) {
// 匹配到拦截方法 → 调用 interceptor.intercept()
return interceptor.intercept(new Invocation(target, method, args));
}
// 未匹配 → 直接反射调用目标
return method.invoke(target, args);
}
第87题:多个插件的执行顺序(@Intercepts 和 @Signature 的作用)
执行顺序:
· 配置顺序决定:MyBatis 按 mybatis-config.xml 中 的配置顺序加载插件。
· 包装顺序:后配置的在外层。例如配置 在前, 在后,创建目标对象时:target = A.plugin(target) → target = B.plugin(target),最终 B 包裹在 A 外层(B 先执行 intercept,再调用 A)。
· 责任链传递:每个插件的 intercept() 中必须调用 invocation.proceed(),否则中断执行。
注解作用:
· @Intercepts:标记该类是一个插件(拦截器)。
· @Signature:精确定位要拦截的目标对象、方法名和参数类型(用于匹配 signatureMap)。只有参数类型完全匹配,代理才会触发 intercept,否则直接放行。
示例匹配:
java
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
第88题:自定义插件的完整实现步骤
Step 1:实现 Interceptor 接口。
java
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class SqlCostInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
System.out.println("SQL cost: " + cost + "ms");
}
}
@Override
public Object plugin(Object target) {
// 交给 Plugin 工具类处理
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 读取配置参数,如慢查询阈值
}
}
Step 2:配置到 mybatis-config.xml:
xml
<plugins>
<plugin interceptor="com.example.SqlCostInterceptor">
<property name="threshold" value="1000"/>
</plugin>
</plugins>
Spring Boot 整合(@Component + 无需 XML,或在 MybatisCustomizer 中添加)。
第89题:插件机制的性能开销与不当使用的风险
性能开销来源:
- 反射调用开销:method.invoke(target, args) 比直接调用慢(虽然 JVM 已优化,但依旧存在)。
- 代理链过长:多个插件层层嵌套,每层调用都有额外的栈帧开销。
- 对象创建开销:每次代理调用都会创建 Invocation 对象。
- 内存占用:代理类元数据永久代/元空间占用增加。
不当使用带来的问题:
· 分页插件误伤:拦截了 ResultHandler 或 update 方法导致分页逻辑错误。
· 代理死循环:若在 intercept 中再次调用目标对象的同一个方法且未正确区分,可能触发递归。
· 线程阻塞:监控插件内执行同步 IO(如打印大日志)会导致 SQL 执行阻塞。
· 修改 BoundSql 时的脏数据:直接修改 BoundSql.sql 时不更新 ParameterMappings,导致参数越界。
P6 建议:插件中避免重计算、避免 IO 阻塞、尽量使用缓存(如 OGNL 表达式预编译)。
第90题:为什么只能拦截四大对象,不能拦截任意方法?
根本原因:MyBatis 设计时仅对这四类核心接口提供了扩展点。
源码证据:
· Configuration 在创建 Executor、StatementHandler、ParameterHandler、ResultSetHandler 时,显式调用了 interceptorChain.pluginAll(target)。
· MapperProxy(接口代理)在 invoke 中并未调用 InterceptorChain,因此 Mapper 接口本身的调用无法被插件拦截。
java
// Configuration.newStatementHandler()
public StatementHandler newStatementHandler(...) {
StatementHandler statementHandler = new RoutingStatementHandler(...);
// 仅此处包装,意味着只有 StatementHandler 可被拦截
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
更深层原因:MyBatis 采用接口隔离原则,将 SQL 执行的各个环节抽象为这四大接口,只开放它们作为扩展点。拦截器本质是对接口实现类的代理,而非对任意类的字节码增强(非 AOP 切面织入)。
如果你需要拦截 Mapper 方法,Spring AOP 可以在 Service 层拦截,或使用 MyBatis 的 MapperProxy 结合自定义 InvocationHandler(但无法通过 MyBatis 插件机制实现)。
十、事务管理(6题)
第十部分答案:事务管理(第91-96题)
第91题:MyBatis 事务管理的实现(Transaction 接口)
核心接口:Transaction(位于 org.apache.ibatis.transaction)
职责:抽象数据库事务的核心操作,屏蔽不同事务实现(JDBC / MANAGED / Spring 托管)的差异。
java
public interface Transaction {
Connection getConnection() throws SQLException; // 获取连接
void commit() throws SQLException; // 提交
void rollback() throws SQLException; // 回滚
void close() throws SQLException; // 关闭
Integer getTimeout() throws SQLException; // 获取超时
}
核心实现类:
实现类 说明
JdbcTransaction 直接使用 JDBC Connection 的 commit/rollback(最常用)
ManagedTransaction 事务托管给外部容器(如 Spring),MyBatis 不主动提交/回滚
SpringManagedTransaction Spring 整合专用,由 Spring 事务管理器控制
创建入口:Environment.getTransactionFactory().newTransaction(dataSource, isolationLevel, autoCommit)。
第92题:JDBC 与 MANAGED 两种事务管理类型的源码级差异
对比维度 JDBC 事务(JdbcTransaction) MANAGED 事务(ManagedTransaction)
连接获取 从 DataSource.getConnection() 获取 从 DataSource.getConnection() 获取(或外部注入)
提交行为 调用 connection.commit() 空实现(不做任何操作,交给容器)
回滚行为 调用 connection.rollback() 空实现(不做任何操作,交给容器)
关闭行为 调用 connection.close() 若 closeConnection 为 true 则关闭,否则空实现
适用场景 独立 MyBatis 应用 J2EE 容器、Spring 整合(事务由外部管理)
源码核心差异(ManagedTransaction.commit()):
java
@Override
public void commit() throws SQLException {
// 空实现!既不提交也不回滚,全部交给容器
// 因为容器会在适当时机提交
}
而 JdbcTransaction.commit() 直接调用 connection.commit()。
配置方式:
xml
<transactionManager type="JDBC"/> <!-- 默认 -->
<transactionManager type="MANAGED"/>
第93题:MyBatis 原生事务与 Spring 声明式事务(@Transactional)的协同
协同机制:Spring 通过 SqlSessionTemplate 和 SpringManagedTransaction 接管事务控制。
核心原理:
- 事务管理接管:Spring 的 DataSourceTransactionManager 在 @Transactional 方法执行前,通过 TransactionSynchronizationManager 将当前 SqlSession 绑定到线程。
- MyBatis 行为变更:SqlSessionTemplate 使用 SpringManagedTransaction,其 commit()/rollback() 均为空实现,不干预事务提交。真正的提交/回滚由 DataSourceTransactionManager 在方法结束时执行。
- Session 绑定:同一事务内多次调用 Mapper,获取的是同一个 SqlSession(SessionHolder 缓存),一级缓存生效。
执行顺序:
@Transactional 开始
→ Spring 开启事务,绑定 SqlSession 到线程
→ Mapper 方法调用 → SqlSessionTemplate 从线程获取 Session
→ 执行 SQL (autoCommit=false)
→ @Transactional 方法正常结束
→ DataSourceTransactionManager 提交 Connection.commit()
关键:SqlSession 不再由业务代码手动 commit()/close(),全部委托给 Spring 管理。
第94题:批量插入中手动控制事务提交为何能大幅提升性能?
性能瓶颈分析:
· 自动提交模式(autoCommit=true):每条 INSERT 都是一个独立事务,每次执行 executeUpdate() 后立即提交,产生大量磁盘 fsync I/O 和网络往返。
· 手动提交(批量):多条 INSERT 在一个事务内累积,最后一次性提交。减少事务日志刷盘次数(日志从 N 次减为 1 次),大幅提升吞吐量。
性能提升量级:
· 10000 条数据:自动提交 ~5000ms,手动提交(BatchExecutor + 手动 commit)~200ms,提升 20-30 倍。
正确写法:
java
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
try {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : users) {
mapper.insert(user);
}
session.commit(); // 一次性提交
} catch (Exception e) {
session.rollback();
throw e;
} finally {
session.close();
}
两个关键参数:
· ExecutorType.BATCH:启用 BatchExecutor 攒批。
· autoCommit=false(第二个参数):禁止自动提交,避免每条都刷盘。
第95题:事务隔离级别的设置与传递
设置方式:
-
全局配置(mybatis-config.xml):
xml<environment id="dev"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <!-- 隔离级别在数据源连接池中设置 --> <property name="defaultTransactionIsolationLevel" value="READ_COMMITTED"/> </dataSource> </environment> -
编程方式(openSession):
java// 显式指定隔离级别 SqlSession session = factory.openSession(TransactionIsolationLevel.READ_COMMITTED); -
Spring 声明式:
java@Transactional(isolation = Isolation.READ_COMMITTED)
传递机制:
· 原生 MyBatis:openSession(level) → 创建 Transaction 对象时,将隔离级别存入 Transaction,在 getConnection() 时调用 connection.setTransactionIsolation(level) 设置。
· Spring 整合:由 DataSourceTransactionManager 负责设置隔离级别(通过 DataSourceUtils.prepareConnectionForTransaction),MyBatis 的 SpringManagedTransaction 不干预。
关键点:隔离级别在获取 Connection 时设置,若 Connection 已被获取过,后续修改无效(需确保在事务开启前完成)。
第96题:MyBatis 事务管理与 Spring 事务传播机制的整合原理
整合本质:MyBatis 的 SqlSession 与 Spring 的 TransactionSynchronizationManager 深度绑定,通过 SqlSessionHolder 实现事务传播。
核心机制:
- 资源绑定:每个数据源在 TransactionSynchronizationManager 中维护一个 Map<DataSource, SqlSessionHolder>。SqlSessionHolder 持有当前线程的 SqlSession 和事务同步状态。
- 传播行为实现:
· PROPAGATION_REQUIRED(默认):SqlSessionUtils.getSqlSession() 从线程绑定中查找已有 Session,若有则复用(同事务);若无则创建新 Session 并绑定。
· PROPAGATION_REQUIRES_NEW:挂起当前 Session,创建全新的 SqlSession(开启新事务),执行完毕后再恢复。
· PROPAGATION_SUPPORTS:有事务则加入,无则使用非事务方式(每次都获取新 Session,一级缓存失效)。
· PROPAGATION_NOT_SUPPORTED:暂停当前事务,以非事务方式执行。 - 事务同步回调:SqlSessionSynchronization 注册到 TransactionSynchronizationManager,在事务提交/回滚时触发 SqlSession.commit()/rollback()。
源码入口:
java
// SqlSessionUtils.getSqlSession()
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ...) {
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
if (holder != null && holder.isSynchronizedWithTransaction()) {
return holder.getSqlSession(); // 复用当前事务的 Session
}
// 否则创建新 Session,并注册到 SynchronizationManager
SqlSession session = sessionFactory.openSession(...);
registerSessionHolder(...);
return session;
}
关键结论:
· Spring + MyBatis 环境下事务传播由 Spring 管理,MyBatis 只负责提供 Session 资源。
· 事务传播的关键是 SqlSession 的生命周期管理:同一个事务内共享 Session → 一级缓存有效;不同事务/传播级别,Session 独立创建 → 一级缓存失效。
· 实际开发中,@Transactional 的 propagation 属性完全生效,MyBatis 的 TransactionFactory 在整合后被 SpringManagedTransactionFactory 替代,原生事务行为被覆盖。
十一、与 Spring 整合(4题)
第97题:Spring 整合 MyBatis 的原理 & SqlSessionFactoryBean 的作用
整合本质:Spring 通过 SqlSessionFactoryBean 接管 MyBatis 的初始化过程,将 MyBatis 的核心对象(SqlSessionFactory、SqlSession、Mapper 代理)作为 Spring Bean 管理,并整合 Spring 的事务管理机制。
SqlSessionFactoryBean 的核心作用:
SqlSessionFactoryBean 实现了 Spring 的 FactoryBean 和 InitializingBean 接口,负责:
- 解析配置:读取 mybatis-config.xml 或直接设置数据源、映射文件位置、类型别名等
- 构建 SqlSessionFactory:调用 SqlSessionFactoryBuilder.build() 创建单例 SqlSessionFactory
- 注入 Spring 容器:将构建好的 SqlSessionFactory 注册为 Spring Bean
java
// 核心源码逻辑(SqlSessionFactoryBean.afterPropertiesSet() / buildSqlSessionFactory())
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
protected SqlSessionFactory buildSqlSessionFactory() {
Configuration configuration = new Configuration();
// 1. 应用 settings 配置(如驼峰转换、缓存开关等)
if (this.configurationProperties != null) {
configuration.setVariables(this.configurationProperties);
}
// 2. 设置数据源和事务工厂(关键:使用 SpringManagedTransactionFactory)
configuration.setEnvironment(new Environment(
this.environment,
new SpringManagedTransactionFactory(),
this.dataSource
));
// 3. 注册类型别名、类型处理器
// 4. 扫描并加载 Mapper XML 文件(setMapperLocations)
for (Resource resource : this.mapperLocations) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(
resource.getInputStream(),
configuration,
resource.toString(),
configuration.getSqlFragments()
);
mapperParser.parse();
}
// 5. 返回 SqlSessionFactory
return new DefaultSqlSessionFactory(configuration);
}
关键配置项:
java
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
// 外部配置文件位置
factory.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
// Mapper XML 位置(支持通配符)
factory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/**/*.xml"));
// 类型别名包扫描
factory.setTypeAliasesPackage("com.example.entity");
// 外部属性覆盖
factory.setConfigurationProperties(properties);
return factory;
}
第98题:Mapper 接口在 Spring 中的扫描、注册与实例化
核心机制:Spring 通过 MapperScannerConfigurer(或 @MapperScan 注解)扫描指定包下的 Mapper 接口,将每个 Mapper 接口注册为 Spring Bean Definition(MapperFactoryBean),最终由 MapperFactoryBean 生成代理对象注入容器。
完整流程图:
@MapperScan("com.example.mapper")
↓
MapperScannerConfigurer.postProcessBeanDefinitionRegistry()
↓ (扫描包路径,过滤 @Mapper 注解或接口)
ClassPathMapperScanner.doScan()
↓ (将每个 Mapper 接口转换为 BeanDefinition)
BeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(MapperFactoryBean.class); // 关键!不是接口本身
definition.getPropertyValues().add("mapperInterface", mapperInterface);
↓ (设置自动注入)
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
↓
BeanDefinitionRegistry.postProcessBeanFactory()
↓
Spring 容器实例化 Bean → MapperFactoryBean.getObject()
↓
MapperFactoryBean.getObject() 内部调用 sqlSession.getMapper(mapperInterface)
↓
返回 MapperProxy 代理对象 → 注入到 Service 层
为什么注册的是 MapperFactoryBean 而不是 Mapper 接口本身?
因为 Mapper 是接口,无法直接实例化。MapperFactoryBean 是一个 FactoryBean,它的 getObject() 方法负责创建 Mapper 的代理对象(MapperProxy)。
java
// MapperFactoryBean.getObject()
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
关键类职责:
类 职责
@MapperScan 启用扫描(注解方式)
MapperScannerConfigurer 扫描包,生成 BeanDefinition(XML 配置方式)
ClassPathMapperScanner 实际的扫描逻辑,过滤接口,改写 BeanDefinition
MapperFactoryBean 每个 Mapper 对应的 FactoryBean,负责创建代理对象
第99题:Spring 中注入的 Mapper 接口到底是什么对象?
结论:Spring 注入的 Mapper 接口是一个 JDK 动态代理对象(MapperProxy 的实例),其内部持有 SqlSessionTemplate,通过 SqlSessionTemplate 间接调用 MyBatis 的 SQL 执行逻辑。
对象结构:
java
// 实际注入的对象
Proxy.newProxyInstance(
classLoader,
new Class[]{UserMapper.class}, // 实现了 Mapper 接口
new MapperProxy(...) // InvocationHandler
)
关键区别(对比原生 MyBatis):
对比维度 原生 MyBatis Spring 整合
代理创建 调用 session.getMapper() 时创建 Spring 容器启动时创建(Bean 实例化阶段)
持有的 SqlSession 直接持有 SqlSession 持有 SqlSessionTemplate(线程安全)
生命周期 由业务代码管理 由 Spring 容器管理(单例)
线程安全 代理对象无状态(但持有的 Session 不安全) 线程安全(SqlSessionTemplate 内部处理线程绑定)
Spring 中 Mapper Bean 的完整调用链:
Service 调用 userMapper.selectById()
↓ (注入的是 MapperProxy 代理对象)
MapperProxy.invoke()
↓ (MapperProxy 内部持有 SqlSessionTemplate)
MapperMethod.execute(sqlSessionTemplate, args)
↓ (SqlSessionTemplate 实现了 SqlSession 接口)
SqlSessionTemplate.selectOne()
→ SqlSessionTemplate.getSqlSession() // 从线程绑定中获取当前 Session
↓ (存在事务时返回绑定的 Session,否则新建)
→ DefaultSqlSession.selectOne()
↓ (同原生 MyBatis 执行流程)
→ 返回结果
第100题:Spring 环境下 SqlSession 的生命周期管理(为什么不需要手动关闭)
核心答案:Spring 通过 SqlSessionTemplate 和 SessionHolder 机制,自动管理 SqlSession 的生命周期。其生命周期绑定于当前线程的事务范围或单次方法调用,在合适的时机自动提交/回滚和关闭,因此无需手动 close()。
生命周期管理机制:
- 非事务场景:
· 每次 Mapper 方法调用时,SqlSessionTemplate.getSqlSession() 从 SqlSessionFactory 获取一个新的 SqlSession(实际上通过 SqlSessionUtils 获取,可能会从池中借用)。
· 方法执行完毕后,自动调用 SqlSessionUtils.closeSqlSession() 关闭 Session(返回连接池)。
· 一级缓存每次方法调用独立,不跨方法共享。 - 事务场景(@Transactional):
· 事务开启时,Spring 将 SqlSession 绑定到当前线程的 TransactionSynchronizationManager(通过 SessionHolder)。
· 同一事务内的所有 Mapper 操作复用同一个 SqlSession,一级缓存生效。
· 事务提交/回滚时,Spring 自动调用 SqlSession.commit() / rollback()。
· 事务结束后,自动 close() 并解绑线程资源。
源码逻辑(简化):
java
// SqlSessionTemplate 内部实现
public <T> T execute(SqlSessionCallback<T> action) {
// 关键:从当前线程获取或创建 SqlSession
SqlSession sqlSession = getSqlSession(sessionFactory, executorType, exceptionTranslator);
try {
// 执行数据库操作
return action.doInSqlSession(sqlSession);
} finally {
// 关键:自动关闭 SqlSession(事务内不会真关闭,只减少引用计数)
closeSqlSession(sqlSession, sessionFactory);
}
}
为什么不需要手动 close()?
原因 说明
模板模式封装 SqlSessionTemplate 在 execute() 方法的 finally 块中自动关闭 Session,保证资源释放
事务同步 事务内 Session 通过 SessionHolder 管理,事务结束后自动清理
连接池归还 close() 实际是归还连接到池中,不是真正的物理关闭
Spring 生命周期 不推荐手动 close(),否则会破坏 SqlSessionTemplate 的状态管理,导致事务内 Session 被提前释放
⚠️ 常见误区和代码规范:
java
// ✅ 正确:Spring 环境无需手动关闭
@Service
public class UserService {
@Autowired
private UserMapper userMapper; // 直接注入 Mapper,无需关注 Session
@Transactional
public void save() {
userMapper.insert(user); // Session 由 Spring 自动管理
}
}
// ❌ 错误:不要在 Spring 中手动创建和关闭 SqlSession(会绕过事务管理)
@Autowired
private SqlSessionFactory sqlSessionFactory;
public void wrongMethod() {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.insert(user);
session.commit();
} // 这么做会脱离 Spring 事务管理,一级缓存和事务传播均失效!
}
P6 总结要点:
· Spring 整合的核心价值在于将 MyBatis 的无状态对象(Mapper 代理)做成单例 Bean,并将有状态的 SqlSession 委托给 Spring 管理
· SqlSessionTemplate 是线程安全的,内部使用 ThreadLocal 绑定 Session
· 关闭资源由 Spring 自动处理,开发者只需关注业务逻辑
· 一级缓存在 Spring 事务环境下才真正有效,非事务环境下每次方法调用都是独立的 Session