《JAVA面经实录》- MyBatis 框架面试题
《JAVA面经实录》- MyBatis 框架面试题
一、MyBatis 是什么?优缺点?
1.面试标准回答
MyBatis 是一款开源的半自动ORM框架,基于Java持久层技术,核心作用是简化JDBC开发,将SQL语句与Java代码解耦,通过XML或注解的方式配置SQL语句,实现Java对象与数据库表的映射(ORM),底层封装了JDBC,开发者无需手动处理数据库连接、Statement创建、结果集封装等重复工作,专注于SQL编写和业务逻辑实现。
MyBatis 不属于全自动化ORM框架(如Hibernate),它不自动生成SQL,而是由开发者手动编写SQL,兼顾了灵活性和开发效率,是目前企业中使用最广泛的持久层框架(常与Spring、Spring Boot整合)。
核心优点
-
灵活性高:SQL语句完全由开发者控制,可灵活优化SQL(如索引、关联查询优化),支持动态SQL、存储过程、自定义函数,适配复杂业务场景;
-
解耦性强:将SQL语句与Java代码分离(XML配置或注解),便于维护和修改,降低代码耦合度;
-
学习成本低:核心是SQL映射,熟悉SQL和Java即可快速上手,配置简单,源码相对简洁,易于理解;
-
轻量级框架:核心包体积小,无过多依赖,运行效率高,不占用过多系统资源;
-
支持多种映射方式:支持XML配置映射、注解映射,支持一对一、一对多、多对多关联映射;
-
扩展性强:支持插件机制(如分页插件PageHelper),可自定义拦截器,扩展MyBatis功能。
核心缺点
-
SQL编写工作量大:所有SQL语句需开发者手动编写,尤其是复杂关联查询,耗时较长;
-
数据库移植性差:SQL语句与数据库方言绑定(如MySQL的limit、Oracle的rownum),切换数据库时,需修改大量SQL语句;
-
无内置缓存(仅一级缓存):二级缓存需手动配置(如结合Redis、EHCache),缓存功能相对简单,需手动维护;
-
无对象状态管理:所有Java对象都是瞬时态或脱管态,修改对象后需手动调用update方法,或通过SqlSession提交,才会同步到数据库;
-
调试难度大:SQL语句编写错误时,调试起来相对繁琐,需结合日志排查问题。
2.面试补充
MyBatis 与 MyBatis-Plus 区别:MyBatis-Plus是MyBatis的增强工具,在MyBatis基础上封装了基础CRUD操作(BaseMapper),无需编写基础SQL,同时保留了MyBatis的灵活性,支持分页、条件构造器等功能,目前企业使用最广泛。
二、#{} 和 ${} 区别?为什么推荐 #{}?
1.面试标准回答
#{} 和 ${} 是MyBatis中两种核心的参数占位符,均用于在SQL语句中注入参数,但底层实现、安全性、适用场景有本质区别,这是面试高频必考题,核心区别如下:
|
对比维度 |
#{}(推荐使用) |
${}(谨慎使用) |
|---|---|---|
|
底层实现 |
采用预编译SQL方式,将#{}替换为?占位符,MyBatis会使用PreparedStatement执行SQL,参数通过setXXX()方法注入,避免SQL注入。 |
采用字符串拼接方式,直接将参数值拼接在SQL语句中,不进行预编译,参数值会直接替换${},存在SQL注入风险。 |
|
安全性 |
安全,可有效防止SQL注入(如参数为恶意SQL片段,会被当作普通字符串处理)。 |
不安全,存在SQL注入风险(如参数为 "1 or 1=1",拼接后会改变SQL逻辑)。 |
|
参数类型处理 |
自动处理参数类型(如字符串自动添加单引号,数字不添加),无需手动拼接引号。 |
不处理参数类型,需手动拼接引号(如字符串参数需手动加''),否则会报错。 |
|
适用场景 |
绝大多数场景,尤其是参数注入(如查询、新增、修改、删除的条件参数、字段值)。 |
仅适用于动态拼接SQL片段(如表名、字段名、排序方式),无法用#{}替代的场景。 |
|
示例 |
SQL:select * from user where id = #{id};参数id=1,最终执行SQL:select * from user where id = ?(预编译后注入参数1)。 |
SQL:select * from ${tableName} where id = ${id};参数tableName=user、id=1,最终执行SQL:select * from user where id = 1(字符串拼接)。 |
2.为什么推荐使用 #{}?
核心原因有两点,也是企业开发的核心要求:
-
安全性:#{} 采用预编译SQL方式,能有效防止SQL注入攻击,这是企业级应用中最关键的安全需求(如用户登录、数据查询等场景,避免恶意参数篡改SQL逻辑);
-
便捷性:#{} 自动处理参数类型,无需手动拼接引号、转换参数格式,减少SQL编写错误,提升开发效率。
3.实战避坑
1. 禁止用 ${} 处理用户输入的参数(如登录密码、查询条件),否则会引发SQL注入漏洞;
2. 若必须使用 ${}(如动态表名),需对参数进行严格校验(如白名单校验,确保参数是允许的表名、字段名),避免恶意参数注入;
3. 特殊场景:MyBatis中like查询,若用#{},需手动拼接%(如 like concat('%', #{name}, '%')),若用${}(不推荐),可直接写 like '%${name}%',但需注意安全。
三、MyBatis 一级缓存、二级缓存机制
1.面试标准回答
MyBatis 提供两级缓存机制,核心目的是减少数据库访问次数,提升查询性能,缓存的核心是将频繁查询的数据存入内存,后续查询直接从内存获取,避免重复发送SQL。两级缓存的作用范围、生命周期、管理方式各不相同,具体如下:
(1). 一级缓存(SqlSession 级缓存,默认开启,无法关闭)
-
作用范围:当前 SqlSession 内有效,仅在单个 SqlSession 中共享,SqlSession 关闭、提交或回滚后,一级缓存中的数据会被清空。
-
核心作用:缓存当前 SqlSession 中查询到的结果集,避免同一 SqlSession 内多次查询同一数据(如多次调用同一个 Mapper 方法,参数相同,仅第一次发送SQL)。
-
底层实现:基于 SqlSession 内置的 HashMap 集合,key 是由「MappedStatement 的 id + 查询参数 + 分页信息 + 绑定的 SQL 语句」组成的唯一标识,value 是查询结果集(Java 对象)。
-
核心特性:
-
一级缓存是 MyBatis 内置的,无需任何配置,默认开启;
-
当 SqlSession 执行 insert、update、delete 操作时,会自动清空一级缓存(避免缓存与数据库数据不一致);
-
同一 SqlSession 内,若查询参数、SQL 语句完全一致,会直接从缓存获取数据,不发送 SQL。
-
-
示例:
SqlSession session = sqlSessionFactory.openSession(); UserMapper mapper = session.getMapper(UserMapper.class); // 第一次查询,发送SQL,存入一级缓存 User user1 = mapper.selectById(1); // 第二次查询,参数相同,从一级缓存获取,不发送SQL User user2 = mapper.selectById(1); session.close(); // 关闭SqlSession,一级缓存清空
(2). 二级缓存(SqlSessionFactory 级缓存,默认关闭,需手动开启)
-
作用范围:整个应用程序,由 SqlSessionFactory 管理,所有 SqlSession 共享二级缓存中的数据,SqlSessionFactory 销毁后,二级缓存数据才会销毁。
-
核心作用:缓存频繁查询、修改较少的全局数据(如字典表、配置表),减少不同 SqlSession 查询同一数据时的数据库访问。
-
开启方式(三步):
-
第一步:在 MyBatis 核心配置文件(mybatis-config.xml)中开启二级缓存:
<settings> <setting name="cacheEnabled" value="true"/> <!-- 开启二级缓存,默认false --> </settings> -
第二步:在 Mapper 映射文件(如 UserMapper.xml)中配置二级缓存(标注该 Mapper 的缓存生效):
<mapper namespace="com.xxx.mapper.UserMapper"> <cache/> <!-- 开启当前Mapper的二级缓存,使用默认配置 --> <!-- 可选:自定义缓存配置(如缓存大小、过期时间) --> <cache size="1024" eviction="LRU" flushInterval="60000"/> </mapper> -
第三步:确保查询的实体类实现 Serializable 接口(二级缓存需要序列化对象,避免缓存时抛出异常):
public class User implements Serializable { // 实现Serializable接口 private Integer id; private String name; // getter/setter 省略 }
-
-
底层实现:默认使用 MyBatis 内置的 PerpetualCache(基于 HashMap),也可集成第三方缓存(如 Redis、EHCache),通过配置 cache-ref 或自定义缓存实现类替换默认缓存。
-
核心特性:
-
二级缓存依赖一级缓存:查询数据时,先查询一级缓存 → 再查询二级缓存 → 最后查询数据库;
-
当任何一个 SqlSession 执行 insert、update、delete 操作时,会自动清空当前 Mapper 的二级缓存(确保缓存与数据库一致性);
-
可通过 @CacheNamespace 注解(注解方式)替代 XML 中的 <cache/> 配置。
-
2.面试补充(高频追问)
(1). 一级缓存与二级缓存的关系:二级缓存的作用范围比一级缓存大,多个 SqlSession 可共享二级缓存;数据查询时,优先从一级缓存获取,一级缓存没有再从二级缓存获取,都没有才查询数据库;
(2). 二级缓存的禁用:可在单个查询标签中配置 useCache="false"(如 <select useCache="false">),禁止该查询使用二级缓存;
(3). 第三方缓存集成:MyBatis 支持集成 Redis、EHCache 等第三方缓存,只需实现 Cache 接口,配置自定义缓存类即可(如集成 Redis,需导入 mybatis-redis 依赖,配置 cache 标签的 type 属性为 Redis 缓存实现类)。
四、缓存失效场景有哪些?
(1).面试标准回答
MyBatis 一级缓存和二级缓存的失效场景,核心都是「缓存与数据库数据不一致」,MyBatis 会自动清空缓存,避免脏数据读取。以下是常见的缓存失效场景,分一级缓存和二级缓存分别说明,覆盖源码级细节和实战场景:
(一)、一级缓存失效场景(5种,核心是 SqlSession 状态变化或数据修改)
-
SqlSession 关闭后失效:一级缓存是 SqlSession 级的,SqlSession.close() 会清空当前 SqlSession 的一级缓存,再次获取 SqlSession 查询同一数据,会重新发送 SQL。
-
SqlSession 提交/回滚后失效:执行 session.commit() 或 session.rollback() 后,MyBatis 会自动清空一级缓存(避免提交/回滚后,缓存数据与数据库不一致)。
-
执行 insert、update、delete 操作后失效:无论是否提交事务,只要在当前 SqlSession 中执行了 insert、update、delete 操作,MyBatis 会立即清空一级缓存(因为这些操作会修改数据库数据,缓存数据已过时)。
-
查询参数或 SQL 语句不同:一级缓存的 key 由 MappedStatement id + 查询参数 + SQL 语句等组成,若参数不同、SQL 语句不同(如排序方式不同),会认为是不同的查询,不命中缓存,重新发送 SQL。
-
手动清空缓存:调用 sqlSession.clearCache() 方法,会手动清空当前 SqlSession 的一级缓存,后续查询需重新发送 SQL。
(二)、二级缓存失效场景(6种,核心是 Mapper 数据修改或缓存配置不当)
-
未开启二级缓存:未在 mybatis-config.xml 中配置 cacheEnabled="true",或未在 Mapper 映射文件中添加 <cache/> 标签,二级缓存不生效,自然不存在缓存失效。
-
实体类未实现 Serializable 接口:二级缓存需要序列化对象,若实体类未实现 Serializable,会抛出序列化异常,缓存无法正常存储,相当于失效。
-
执行 insert、update、delete 操作后失效:任何一个 SqlSession 执行了当前 Mapper 的 insert、update、delete 操作(无论是否提交),MyBatis 会自动清空该 Mapper 的二级缓存(确保缓存与数据库一致)。
-
查询标签配置 useCache="false":在单个查询标签中配置 useCache="false",表示该查询不使用二级缓存,每次查询都会发送 SQL,缓存失效。
-
缓存过期或达到缓存上限:二级缓存默认使用 LRU(最近最少使用)淘汰策略,当缓存数据达到配置的 size(如 size="1024"),会淘汰最少使用的数据;若配置了 flushInterval(过期时间),过期后缓存自动失效。
-
手动清空二级缓存:调用 sqlSessionFactory.getConfiguration().getCache(key).clear() 方法,可手动清空指定 Mapper 的二级缓存;或在 Mapper 映射文件中配置 <cache-ref> 导致缓存共享冲突,也可能引发失效。
(2).实战避坑
1. 避免在频繁修改的数据上使用二级缓存(如订单表、库存表),否则会导致缓存频繁失效,不仅无法提升性能,还会增加缓存维护成本;
2. 一级缓存失效的常见误区:认为同一 Mapper 方法、同一参数,不同 SqlSession 会命中一级缓存,实则一级缓存仅在单个 SqlSession 内有效;
3. 二级缓存失效排查:先检查缓存配置(cacheEnabled、<cache/>),再检查实体类是否序列化,最后排查是否有 insert/update/delete 操作触发缓存清空。
五、MyBatis 延迟加载原理
(1).面试标准回答
MyBatis 延迟加载(懒加载)是一种「按需加载数据」的机制,核心原理是“只有当程序需要使用关联对象的数据时,才发送 SQL 语句查询关联数据,而非查询主对象时就一次性加载所有关联数据”,目的是减少不必要的数据库访问,提升系统性能,底层基于动态代理和 ResultMap 配置实现。
MyBatis 延迟加载仅支持「关联查询」(一对一、一对多、多对多),默认不开启,需手动配置开启。
(一)、延迟加载的开启方式
需在 MyBatis 核心配置文件(mybatis-config.xml)中配置两个参数,开启全局延迟加载:
<setting>
<!-- 开启全局延迟加载,默认false(立即加载) -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 关闭“积极加载”,即按需加载,默认true(积极加载所有关联对象) -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
补充:也可在单个关联标签中配置 lazyLoadTriggerMethods,指定触发延迟加载的方法(默认是 equals、clone、hashCode、toString),避免误触发加载。
(二)、延迟加载核心原理(源码级)
MyBatis 延迟加载的核心是「动态代理」,结合 ResultMap 中的关联配置,具体流程如下:
-
开启延迟加载后,MyBatis 执行主查询(如查询 User)时,会通过 JDK 动态代理生成主对象(User)的代理对象,而非直接返回真实的 User 对象;
-
主查询仅加载主对象的基本属性(如 User 的 id、name),不加载关联对象(如 User 关联的 Order 列表),此时关联对象为 null(或空代理集合);
-
当程序首次访问主对象的关联属性(如 user.getOrders())时,代理对象会拦截该方法,判断关联对象是否已加载;
-
若未加载,代理对象会通过 SqlSession 发送关联查询的 SQL(如查询该 User 的所有 Order),加载关联数据,初始化关联对象;
-
若已加载,直接返回关联对象,不再发送 SQL;
-
若 SqlSession 已关闭,代理对象无法获取 SqlSession 发送 SQL,会抛出 LazyInitializationException(懒加载异常)。
核心源码类:org.apache.ibatis.executor.loader.ResultLoader(负责延迟加载数据)、org.apache.ibatis.executor.loader.ProxyFactory(生成动态代理对象)。
(三)、延迟加载的适用场景及注意事项
-
适用场景:关联对象数据不常使用、关联数据量较大的场景(如查询 User 时,很少需要访问其关联的 Order 列表),可减少主查询的 SQL 开销。
-
注意事项:
-
懒加载异常(LazyInitializationException):SqlSession 关闭后,访问关联对象会抛出该异常,解决方案:① 延长 SqlSession 生命周期(如 Web 项目中使用 OpenSessionInViewFilter);② 关闭延迟加载(针对该关联);③ 提前加载关联对象(如使用 fetchType="eager");
-
延迟加载仅支持关联查询,单个对象查询(如 getById)不支持延迟加载;
-
若关联对象必用,建议关闭延迟加载(fetchType="eager"),避免频繁触发关联查询,反而降低性能。
-
六、MyBatis 插件机制(Interceptor)
(1).面试标准回答
MyBatis 插件机制(Interceptor)是 MyBatis 核心扩展性机制,允许开发者在不修改 MyBatis 源码的前提下,通过自定义插件,拦截 MyBatis 执行流程中的核心方法,实现功能增强(如分页、日志记录、参数加密、SQL 改写)。
MyBatis 插件本质是「AOP 动态代理」,底层通过拦截器链(InterceptorChain)实现,仅能拦截 MyBatis 预设的 4 个核心接口的方法,无法拦截自定义方法。
(一)、插件可拦截的核心接口及方法(源码级)
MyBatis 仅允许拦截以下 4 个核心接口的方法,这是插件机制的核心限制,面试必答:
-
Executor:MyBatis 核心执行器,负责 SQL 执行的整个流程,可拦截的核心方法:query(查询)、update(新增/修改/删除)、commit(提交)、rollback(回滚);
-
StatementHandler:负责处理 Statement(PreparedStatement、Statement),可拦截的核心方法:prepare(创建 Statement)、parameterize(设置参数)、execute(执行 SQL);
-
ParameterHandler:负责处理 SQL 参数,可拦截的核心方法:setParameters(给 PreparedStatement 设置参数);
-
ResultSetHandler:负责处理查询结果集,可拦截的核心方法:handleResultSets(将结果集封装为 Java 对象)。
(二)、自定义插件的实现步骤(实战示例)
自定义 MyBatis 插件,需遵循 3 个步骤:实现 Interceptor 接口、添加 @Intercepts 注解、配置插件到 MyBatis 核心配置,以下是日志记录插件示例:
-
第一步:实现 Interceptor 接口,重写 3 个核心方法:
import org.apache.ibatis.plugin.*; import java.util.Properties; // @Intercepts:指定拦截的接口、方法、参数(核心注解) @Intercepts({ @Signature( type = StatementHandler.class, // 拦截的接口 method = "prepare", // 拦截的方法 args = {java.sql.Connection.class, Integer.class} // 拦截方法的参数类型 ) }) public class LogInterceptor implements Interceptor { // 核心方法:拦截目标方法,实现增强逻辑 @Override public Object intercept(Invocation invocation) throws Throwable { // 1. 获取目标对象(StatementHandler) StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); // 2. 增强逻辑:记录SQL语句 String sql = statementHandler.getBoundSql().getSql(); System.out.println("MyBatis 执行SQL:" + sql); // 3. 执行目标方法(放行,继续执行原逻辑) return invocation.proceed(); } // 方法:生成目标对象的代理对象(MyBatis 内部调用,无需手动实现) @Override public Object plugin(Object target) { // 调用 MyBatis 提供的 Plugin 工具类,生成代理对象 return Plugin.wrap(target, this); } // 方法:读取插件配置的属性(如在mybatis-config.xml中配置的参数) @Override public void setProperties(Properties properties) { // 示例:读取配置的参数(如日志级别) String logLevel = properties.getProperty("logLevel"); System.out.println("插件日志级别:" + logLevel); } } -
第二步:在 MyBatis 核心配置文件(mybatis-config.xml)中配置插件:
<plugins> <plugin interceptor="com.xxx.plugin.LogInterceptor"> <!-- 可选:配置插件属性,通过setProperties方法读取 --> <property name="logLevel" value="INFO"/> </plugin> </plugins> -
第三步:测试插件:执行 Mapper 方法,会自动拦截 StatementHandler 的 prepare 方法,打印执行的 SQL 语句,实现日志记录功能。
(三)、插件执行原理(源码级)
-
MyBatis 初始化时,会读取核心配置文件中的 <plugins> 标签,将所有自定义插件注册到 InterceptorChain(拦截器链)中;
-
当 MyBatis 执行 SQL 时,会为可拦截的核心接口(如 StatementHandler)创建动态代理对象(通过 Plugin.wrap() 方法);
-
当调用目标方法(如 prepare())时,会先触发代理对象的 intercept() 方法,执行自定义增强逻辑;
-
增强逻辑执行完成后,通过 invocation.proceed() 方法,放行到下一个拦截器(若有),最终执行目标方法的原逻辑;
-
多个插件会按配置顺序组成拦截器链,依次执行每个插件的增强逻辑。
(2).面试补充(高频追问)
1. 插件的优先级:配置在 <plugins> 标签中前面的插件,先执行(拦截器链按配置顺序执行);
2. 常见插件应用:分页插件(PageHelper,拦截 Executor 的 query 方法,改写 SQL 实现分页)、数据加密插件(拦截 ParameterHandler 的 setParameters 方法,加密参数)、日志插件(记录 SQL 执行日志);
3. 插件开发注意事项:① 仅能拦截 MyBatis 预设的 4 个接口,不可拦截自定义 Mapper 方法;② 增强逻辑不宜过于复杂,避免影响 SQL 执行性能;③ 注意插件的兼容性,不同 MyBatis 版本的接口方法可能有差异。
七、MyBatis 执行流程(SqlSession -> Executor -> StatementHandler)
(1).面试标准回答
MyBatis 执行 SQL 的核心流程,围绕「SqlSession → Executor → StatementHandler → Statement → ResultSetHandler」展开,从获取 SqlSession 到返回查询结果,共 7 个核心步骤,每个步骤对应 MyBatis 核心组件的作用,源码级解析如下:
核心执行流程(7步,结合源码)
-
步骤1:获取 SqlSessionFactory通过 MyBatis 核心配置文件(mybatis-config.xml),构建 SqlSessionFactory 对象,这是 MyBatis 的核心工厂类,负责创建 SqlSession。
// 构建 SqlSessionFactory String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);源码细节:SqlSessionFactoryBuilder 会解析配置文件,生成 Configuration 对象(MyBatis 核心配置),再创建 DefaultSqlSessionFactory(SqlSessionFactory 的默认实现)。 -
步骤2:获取 SqlSession通过 SqlSessionFactory 的 openSession() 方法,获取 SqlSession 对象,SqlSession 是 MyBatis 与数据库交互的核心会话对象,封装了数据库连接和执行 SQL 的方法。
// 获取 SqlSession(true表示自动提交事务,默认false) SqlSession session = sqlSessionFactory.openSession(true);源码细节:openSession() 方法会创建 Executor(执行器),再创建 DefaultSqlSession(SqlSession 的默认实现),将 Executor 注入 SqlSession。 -
步骤3:获取 Mapper 代理对象通过 SqlSession 的 getMapper() 方法,获取 Mapper 接口的动态代理对象(MyBatis 不要求 Mapper 有实现类,代理对象负责执行 SQL 逻辑)。
UserMapper userMapper = session.getMapper(UserMapper.class);源码细节:getMapper() 方法通过 MapperProxyFactory 生成 Mapper 代理对象(JDK 动态代理),代理对象会将方法调用转发给 Executor。 -
步骤4:Executor 执行 SQL 调度调用 Mapper 代理对象的方法(如 userMapper.selectById(1)),代理对象会将方法参数、SQL 信息(从 Mapper 配置中获取)传递给 Executor,Executor 是 MyBatis 的核心执行器,负责 SQL 执行的调度。源码细节:Executor 有三种实现:SimpleExecutor(默认,每次执行 SQL 都创建新的 Statement)、ReuseExecutor(复用 Statement)、BatchExecutor(批量执行 SQL),可通过配置指定。
-
步骤5:StatementHandler 处理 StatementExecutor 会创建 StatementHandler 对象,由 StatementHandler 负责创建 Statement(PreparedStatement 或 Statement)、设置 SQL 参数、执行 SQL。核心流程:① StatementHandler 调用 prepare() 方法,通过 Connection 创建 PreparedStatement;② 调用 ParameterHandler 的 setParameters() 方法,给 PreparedStatement 设置参数;③ 调用 execute() 方法,执行 SQL,获取 ResultSet。
-
步骤6:ResultSetHandler 处理结果集 SQL 执行完成后,StatementHandler 会获取 ResultSet,交给 ResultSetHandler 处理,ResultSetHandler 会将 ResultSet 封装为 Java 对象(根据 Mapper 配置的 resultType 或 resultMap)。源码细节:ResultSetHandler 会根据配置的映射关系,将结果集中的字段值映射到 Java 对象的属性中,支持一对一、一对多关联映射。
-
步骤7:关闭 SqlSessionSQL 执行完成后,关闭 SqlSession,释放数据库连接(若未配置连接池,会直接关闭连接;若配置连接池,会将连接归还到连接池)。
session.close();
核心组件关系总结
SqlSession(会话)→ Executor(执行器,调度核心)→ StatementHandler(处理 Statement,执行 SQL)→ ParameterHandler(设置参数)/ ResultSetHandler(处理结果集),各组件分工明确,协同完成 SQL 执行流程。
(2).面试补充
1. 执行流程中的缓存:Executor 会先查询一级缓存,缓存未命中再执行后续 SQL 流程,执行完成后将结果存入一级缓存;
2. 插件的介入:在 Executor、StatementHandler 等组件创建时,会通过插件机制生成代理对象,拦截对应的方法,实现功能增强;
3. 事务管理:SqlSession 中的事务由 Executor 管理,commit()、rollback() 方法会转发给 Executor,由 Executor 处理事务的提交和回滚。
八、ResultType 和 ResultMap 区别
(1).面试标准回答
ResultType 和 ResultMap 是 MyBatis 中两种核心的「结果集映射方式」,均用于将 SQL 执行后的 ResultSet 封装为 Java 对象,但适用场景、灵活性、配置方式有显著区别,面试常考两者的区别及选型,具体如下:
|
对比维度 |
ResultType |
ResultMap |
|---|---|---|
|
核心作用 |
指定结果集的封装类型(Java 实体类、基本类型、Map 等),要求 SQL 查询的字段名与 Java 对象的属性名完全一致(大小写不敏感,MyBatis 可配置驼峰映射)。 |
自定义结果集映射规则,解决「字段名与属性名不一致」问题,支持复杂关联映射(一对一、一对多、多对多),灵活性更高。 |
|
字段名与属性名映射 |
要求字段名与属性名一致(或通过驼峰映射配置,如数据库字段 user_name 对应 Java 属性 userName),无法自定义映射。 |
可自定义映射规则(通过 <result> 标签),如数据库字段 user_id 对应 Java 属性 id,无需字段名与属性名一致。 |
|
关联映射支持 |
不支持关联映射(一对一、一对多等),仅能封装单个实体类或简单类型。 |
支持复杂关联映射,通过 <association>(一对一)、<collection>(一对多)标签,实现关联对象的封装。 |
|
配置方式 |
直接在查询标签中配置 resultType 属性,无需额外配置(如 <select resultType="com.xxx.pojo.User">)。 |
需先在 Mapper 映射文件中定义 <resultMap> 标签,配置映射规则,再在查询标签中通过 resultMap 属性引用(如 <select resultMap="userResultMap">)。 |
|
适用场景 |
简单查询,字段名与 Java 对象属性名一致(或可通过驼峰映射匹配),无需关联映射(如查询单个实体、简单统计查询)。 |
复杂查询,字段名与属性名不一致,或需要关联映射(如一对一、一对多查询),需自定义映射规则的场景。 |
|
示例 |
|
|
(2).面试补充(高频追问)
1. 驼峰映射与 ResultType 的关系:MyBatis 可通过配置 <setting name="mapUnderscoreToCamelCase" value="true"/>,实现数据库字段(下划线命名,如 user_name)与 Java 属性(驼峰命名,如 userName)的自动映射,此时可使用 ResultType,无需手动配置 ResultMap;
2. ResultMap 的继承:可通过 <resultMap extends="父ResultMapId"> 实现 ResultMap 的继承,复用父 ResultMap 的映射规则,减少重复配置;
3. 选型建议:优先使用 ResultType(配置简单、效率高),当字段名与属性名不一致,或需要关联映射时,使用 ResultMap。
九、MyBatis 如何处理多表关联查询?
(1).面试标准回答
MyBatis 处理多表关联查询,核心是通过「ResultMap 自定义映射规则」,结合 <association>(一对一)、<collection>(一对多)、<association + 嵌套查询> 等方式,将多表查询的 ResultSet 封装为包含关联对象的 Java 实体类,支持一对一、一对多、多对多三种关联关系,具体实现方式如下(结合实战示例):
1. 一对一关联查询(如 User ↔ UserCard,一个用户对应一张身份证)
核心:使用 <association> 标签,在 ResultMap 中配置一对一关联,将关联表的字段映射到主实体类的关联属性中,分为「迫切连接(一次性加载)」和「嵌套查询(懒加载)」两种方式。
方式1:迫切连接(推荐,一次性加载主表+关联表数据)
<!-- 1. 定义ResultMap,配置一对一关联 -->
<resultMap id="userWithCardResultMap" type="com.xxx.pojo.User">
<id column="user_id" property="id"/>
<result column="user_name" property="name"/>
<!-- association:一对一关联,property是主实体的关联属性名,javaType是关联对象类型 -->
<association property="userCard" javaType="com.xxx.pojo.UserCard">
<id column="card_id" property="id"/>
<result column="card_no" property="cardNo"/>
<result column="create_time" property="createTime"/>
</association>
</resultMap>
<!-- 2. 多表连接查询,一次性加载所有数据 -->
<select id="selectUserWithCard" resultMap="userWithCardResultMap">
select u.user_id, u.user_name, c.card_id, c.card_no, c.create_time
from t_user u
left join t_user_card c on u.id = c.user_id
where u.id = #{id}
</select>
方式2:嵌套查询(懒加载,主表查询后,按需加载关联表数据)
<!-- 1. 主表ResultMap,配置嵌套查询 -->
<resultMap id="userResultMap" type="com.xxx.pojo.User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<!-- select:关联查询的Mapper方法,column:主表与关联表的关联字段 -->
<association
property="userCard"
javaType="com.xxx.pojo.UserCard"
select="com.xxx.mapper.UserCardMapper.selectByUserId"
column="id"
fetchType="lazy"> <!-- fetchType="lazy":开启懒加载 -->
</association>
</resultMap>
<!-- 2. 主表查询 -->
<select id="selectUserById" resultMap="userResultMap">
select id, name from t_user where id = #{id}
</select>
<!-- 3. 关联表查询(被嵌套调用) -->
<select id="selectByUserId" resultType="com.xxx.pojo.UserCard">
select id, card_no as cardNo, create_time as createTime from t_user_card where user_id = #{userId}
</select>
2. 一对多关联查询(如 User ↔ Order,一个用户对应多个订单)
核心:使用 <collection> 标签,在 ResultMap 中配置一对多关联,将关联表的多条数据映射到主实体类的集合属性中(如 List<Order> orders),同样支持迫切连接和嵌套查询。
实战示例(迫切连接)
<!-- 1. 定义ResultMap,配置一对多关联 -->
<resultMap id="userWithOrdersResultMap" type="com.xxx.pojo.User">
<id column="user_id" property="id"/>
<result column="user_name" property="name"/>
<!-- collection:一对多关联,property是集合属性名,ofType是集合中元素的类型 -->
<collection property="orders" ofType="com.xxx.pojo.Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="create_time" property="createTime"/>
</collection>
</resultMap>
<!-- 2. 多表连接查询 -->
<select id="selectUserWithOrders" resultMap="userWithOrdersResultMap">
select u.user_id, u.user_name, o.order_id, o.order_no, o.create_time
from t_user u
left join t_order o on u.id = o.user_id
where u.id = #{id}
</select>
3. 多对多关联查询(如 Student ↔ Course,多个学生对应多个课程)
核心:多对多关联需要一张中间表(如 t_student_course),存储两个表的主键关联关系;MyBatis 处理时,将多对多拆分为两个一对多关联,通过 <collection> 标签配置,同样支持迫切连接和嵌套查询。
实战示例(迫切连接)
核心前提:多对多关联必须依赖中间表,先明确3张核心表结构(实战落地必备,面试可补充):
-
t_student(学生表):id(INT,主键)、student_name(VARCHAR,学生姓名)、student_age(INT,学生年龄)
-
t_course(课程表):id(INT,主键)、course_name(VARCHAR,课程名称)、course_credit(INT,学分)、course_desc(VARCHAR,课程描述)
-
t_student_course(中间表):id(INT,可选自增主键)、student_id(INT,外键,关联t_student.id)、course_id(INT,外键,关联t_course.id)
补充:实体类定义(简化版,贴合实战,面试可直接提及):
XML映射文件配置(迫切连接,一次性加载学生及关联的所有课程,核心实战代码):
补充:Mapper接口定义(实战必备,面试可完整提及):
测试代码(简化版,贴合实战,面试可简述流程):
迫切连接核心说明(面试加分点):
1. 核心逻辑:多对多关联通过“主表→中间表→关联表”的左连接,一次性加载所有数据,仅发送1次SQL,效率高于嵌套查询;
2. 关键注意:ResultMap中collection标签的ofType必须指定为关联实体类(Course),而非中间表;表连接时必须通过中间表的外键关联两张主表;
3. 优势:无需多次发送SQL,减少数据库交互,适合关联数据量不大、需一次性加载所有关联信息的场景(如学生详情页展示所选课程)。
面试补充(高频追问)
1. 多表关联查询的性能优化:优先使用迫切连接(一次性加载),减少数据库交互;数据量较大时,结合分页插件(如 PageHelper),避免一次性加载过多数据;
2. 关联查询的字段冲突:多表查询时,需给表添加别名(如 s、sc、c),明确区分同名字段(如 id),避免映射错误;
3. 懒加载异常:嵌套查询开启懒加载后,若 SqlSession 关闭后访问关联对象,会抛出 LazyInitializationException,解决方案:延长 SqlSession 生命周期,或关闭该关联的懒加载。
十、MyBatis 分页实现方式,RowBounds vs PageHelper
(1).面试标准回答
MyBatis 本身未提供内置的分页功能,需通过手动实现或第三方插件实现分页,常用的两种方式是「RowBounds 分页」和「PageHelper 分页」,两者在实现原理、使用方式、性能上有显著区别,具体对比及实战示例如下:
(一)、RowBounds 分页(MyBatis 内置,基础分页方式)
RowBounds 是 MyBatis 内置的分页工具类,无需导入额外依赖,核心原理是「内存分页」—— 先查询出所有符合条件的数据,再在内存中截取指定范围的记录,本质是“假分页”。
1. 核心使用方式
2. 核心原理
RowBounds 通过拦截器(RowBoundsInterceptor),在 ResultSetHandler 处理结果集时,跳过 offset 条记录,只读取 limit 条记录,本质是“先查全量数据,再内存截取”,未减少数据库的查询压力。
3. 优缺点
-
优点:无需导入额外依赖,MyBatis 内置,配置简单,适合少量数据分页;
-
缺点:内存分页,大数据量场景下(如万级以上数据),会查询大量冗余数据,占用内存,性能极差,不适合生产环境。
(二)、PageHelper 分页(第三方插件,推荐生产使用)
PageHelper 是 MyBatis 最常用的分页插件,基于 MyBatis 插件机制实现,核心原理是「数据库分页」—— 自动拦截查询 SQL,在 SQL 末尾添加分页语句(如 MySQL 的 limit、Oracle 的 rownum),让数据库只返回分页范围内的记录,本质是“真分页”,性能优于 RowBounds。
1. 核心使用步骤(实战落地)
-
第一步:导入依赖(Maven) <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.3.2</version> </dependency>
-
第二步:配置 PageHelper 插件(MyBatis 核心配置文件) <plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <!-- 配置数据库方言,告诉PageHelper使用哪种数据库的分页语法 --> <property name="helperDialect" value="mysql"/> <!-- 开启合理化分页(避免页码小于1时查询第1页,大于最大页码时查询最后一页) --> <property name="reasonable" value="true"/> </plugin> </plugins>
-
第三步:代码中使用(两种常用方式) // 方式1:使用PageHelper.startPage()静态方法(推荐) SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); // 分页参数:第1页,每页5条 PageHelper.startPage(1, 5); // 后续的第一个查询方法,会自动被分页 List<User> userList = mapper.selectAll(); // 封装分页结果(获取总条数、总页数等信息) PageInfo<User> pageInfo = new PageInfo<User>(userList); System.out.println("总条数:" + pageInfo.getTotal()); System.out.println("总页数:" + pageInfo.getPages()); // 方式2:使用Page对象作为返回值 Page<User> selectUserByPage(@Param("age") Integer age); // 调用时自动分页 Page<User> page = mapper.selectUserByPage(18); System.out.println("当前页数据:" + page.getResult()); System.out.println("总条数:" + page.getTotal());
2. 核心原理
PageHelper 实现了 MyBatis 的 Interceptor 接口,拦截 Executor 的 query 方法,在执行 SQL 之前,根据配置的数据库方言,自动给 SQL 末尾添加分页语句(如 MySQL 会添加 “limit offset, limit”),让数据库只查询分页范围内的数据,减少数据传输和内存占用。
(三)、RowBounds vs PageHelper 核心对比
|
对比维度 |
RowBounds |
PageHelper |
|---|---|---|
|
分页类型 |
内存分页(假分页) |
数据库分页(真分页) |
|
实现原理 |
先查询全量数据,再在内存中截取指定范围记录 |
拦截 SQL,自动添加分页语句,数据库只返回分页数据 |
|
性能 |
大数据量下性能极差,占用内存多 |
性能优秀,减少数据库查询和数据传输压力 |
|
依赖 |
MyBatis 内置,无需额外依赖 |
需导入 PageHelper 依赖,配置插件 |
|
功能 |
仅支持基础分页,无总条数、总页数等信息 |
支持分页、排序、合理化分页,可获取总条数、总页数等详情 |
|
适用场景 |
少量数据分页、测试场景 |
生产环境、大数据量分页(推荐) |
(2).面试补充(高频追问)
1. PageHelper 分页的注意事项:PageHelper.startPage() 方法需在查询方法之前调用,且只对后续的第一个查询方法生效;
2. 数据库方言配置:若不配置 helperDialect,PageHelper 会自动检测数据库方言,但建议手动配置,避免检测错误;
3. 分页插件的扩展:除了 PageHelper,还有 MyBatis-Plus 内置的分页插件(PaginationInnerInterceptor),用法与 PageHelper 类似,更适合 MyBatis-Plus 项目。
十一、MyBatis 绑定接口 Mapper 原理(JDK 动态代理)
(1).面试标准回答
MyBatis 中,Mapper 接口(如 UserMapper)无需编写实现类,就能被程序调用并执行对应的 SQL 语句,核心原理是「JDK 动态代理」—— MyBatis 会为每个 Mapper 接口生成动态代理对象,代理对象将方法调用转发给 MyBatis 核心组件(Executor、StatementHandler 等),最终执行 SQL 并返回结果,具体原理和流程如下:
(一). 核心前提
Mapper 接口必须满足两个条件,才能被 MyBatis 识别并生成代理对象:
-
Mapper 接口的全路径(如 com.xxx.mapper.UserMapper),必须与对应的 Mapper XML 映射文件的 namespace 属性一致;
-
Mapper 接口中的方法名,必须与 Mapper XML 中 <select>、<insert> 等标签的 id 属性一致,方法参数、返回值需与 SQL 语句的参数、结果集类型匹配。
(二). 动态代理核心原理(源码级流程)
MyBatis 生成 Mapper 代理对象的核心类是 MapperProxyFactory,流程分为 4 步,本质是 JDK 动态代理的应用(JDK 动态代理基于接口,不依赖实现类,贴合 Mapper 接口无实现类的场景):
-
步骤1:初始化加载 Mapper 接口MyBatis 初始化时,会解析 Mapper 接口和对应的 XML 映射文件,将 Mapper 接口的信息(方法名、参数、返回值)封装为 MappedStatement 对象,存入 Configuration 配置中(MyBatis 核心配置类)。
-
步骤2:获取 Mapper 代理对象当程序调用 sqlSession.getMapper(Mapper.class) 方法时,MyBatis 会通过 MapperProxyFactory,创建 Mapper 接口的动态代理对象(Proxy 实例),代理对象的 InvocationHandler 是 MapperProxy(MyBatis 自定义的调用处理器)。// 源码核心逻辑(简化) public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // 获取 MapperProxyFactory MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } // 生成动态代理对象,传入 MapperProxy(调用处理器) return mapperProxyFactory.newInstance(sqlSession); }
-
步骤3:代理对象拦截方法调用当程序调用 Mapper 代理对象的方法(如 userMapper.selectById(1))时,会被 MapperProxy 的 invoke() 方法拦截(JDK 动态代理的核心,所有方法调用都会转发到 invoke 方法)。
-
步骤4:转发方法调用,执行 SQLMapperProxy 的 invoke() 方法会做以下操作,最终执行 SQL:// MapperProxy 的 invoke 方法核心逻辑(简化) @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 处理 Object 类的方法(如 toString、hashCode),直接执行 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } // 处理 Mapper 接口的方法,转发给 SqlSession 执行 return mapperMethod.execute(sqlSession, args); }
-
解析当前调用的方法(方法名、参数),从 Configuration 中获取对应的 MappedStatement(包含 SQL 语句、参数类型、结果类型等信息);
-
将方法调用转发给 SqlSession 的对应方法(如 selectOne、selectList),由 SqlSession 调用 Executor、StatementHandler 等组件,执行 SQL 并封装结果;
-
将 SQL 执行结果返回给调用者。
-
(三). 核心总结
MyBatis Mapper 接口绑定的本质是「JDK 动态代理」:
-
代理对象:MyBatis 为 Mapper 接口生成 Proxy 代理实例,无真实实现类;
-
调用处理器:MapperProxy,负责拦截 Mapper 方法调用,转发给 MyBatis 核心组件;
-
关键匹配:Mapper 接口与 XML 的 namespace、方法名与标签 id 必须一致,否则无法找到对应的 SQL 语句,抛出 BindingException 异常。
(2).面试补充(高频追问)
1. 为什么 MyBatis 用 JDK 动态代理,而非 CGLIB 动态代理?
答:因为 Mapper 是接口,JDK 动态代理本身就是基于接口实现的,无需依赖实现类,且轻量级、效率高;CGLIB 是基于类的动态代理,需要生成子类,而 Mapper 接口无实现类,无法使用 CGLIB(MyBatis 也支持 CGLIB 代理,可通过配置修改,但默认是 JDK 动态代理)。
2. Mapper 接口中可以定义重载方法吗?
答:不建议定义,MyBatis 是通过「方法名」匹配 XML 中的 SQL 语句,重载方法名相同,会导致 MyBatis 无法区分对应的 MappedStatement,抛出绑定异常;若需实现类似重载的功能,可通过 @Param 注解区分参数,或修改方法名。
3. 注解方式的 Mapper(如 @Select),绑定原理和 XML 方式一致吗?
答:一致,都是通过 JDK 动态代理生成代理对象,区别仅在于 SQL 语句的配置方式(注解直接配置在方法上,XML 配置在文件中),解析时都会封装为 MappedStatement,后续执行流程完全相同。
十二、MyBatis 事务管理方式
(1).面试标准回答
MyBatis 本身提供了事务管理机制,核心是「事务的提交、回滚、关闭」,底层依赖 JDBC 事务或 Spring 事务管理,支持两种事务管理方式:「JDBC 事务管理」和「Spring 事务管理」,其中 Spring 事务管理是企业开发中的主流方式,具体说明如下:
1. 核心前提(事务的四大特性)
MyBatis 事务遵循 ACID 特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),事务管理的核心目的是保证数据操作的一致性和完整性。
2. 方式一:JDBC 事务管理(MyBatis 原生,独立使用)
MyBatis 原生的事务管理,底层依赖 JDBC 的事务机制(java.sql.Connection),由 SqlSession 直接管理事务,核心是通过 Connection 的 commit()、rollback() 方法实现事务的提交和回滚,默认关闭自动提交。
核心使用方式(实战示例)
核心特点
-
底层依赖 JDBC Connection,事务的边界由 SqlSession 控制(一个 SqlSession 对应一个 Connection,一个事务);
-
默认不自动提交(autoCommit=false),需手动调用 commit() 提交事务,发生异常时调用 rollback() 回滚;
-
局限性:仅能在单个 SqlSession 内管理事务,无法实现跨 SqlSession 的事务管理,适合 MyBatis 独立使用(不整合 Spring)的简单场景。
3. 方式二:Spring 事务管理(主流,企业开发首选)
当 MyBatis 与 Spring 整合时,通常使用 Spring 事务管理,Spring 提供了声明式事务(注解方式)和编程式事务两种方式,核心是「Spring 接管事务管理」,MyBatis 不再负责事务,而是由 Spring 统一管理 Connection,实现跨 SqlSession、跨 Mapper 的事务控制。
方式2.1:声明式事务(注解方式,最常用)
通过 @Transactional 注解,快速实现事务管理,无需手动编写 commit、rollback 代码,Spring 自动完成事务的提交和回滚。
方式2.2:编程式事务(手动控制,适合复杂场景)
通过 Spring 提供的 TransactionTemplate,手动控制事务的提交、回滚,适合需要灵活控制事务边界的场景。
核心特点
-
Spring 接管事务,通过 DataSourceTransactionManager 管理 Connection,实现跨 SqlSession 的事务控制(多个 SqlSession 可共享同一个 Connection);
-
声明式事务(@Transactional)配置简单,无需手动控制事务,适合大多数场景;
-
支持事务隔离级别、传播行为的配置(如 @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)),灵活适配复杂业务场景;
-
核心优势:与 Spring 生态深度整合,支持分布式事务(结合 Spring Cloud Alibaba Seata 等组件)。
4. MyBatis 事务管理的核心注意事项
-
事务的边界:事务应作用于业务层(Service),而非 Dao 层(Mapper),避免单个 Dao 方法单独提交事务,导致事务不一致;
-
自动提交配置:MyBatis 原生 SqlSession 默认 autoCommit=false,Spring 事务管理中,默认会根据 @Transactional 注解自动控制提交和回滚;
-
异常回滚:Spring 声明式事务中,默认只回滚 RuntimeException 及其子类,若需回滚所有异常,需配置 rollbackFor = Exception.class;
-
事务隔离级别:MyBatis 支持 JDBC 的 4 种隔离级别(READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE),可通过配置或注解指定,默认隔离级别与数据库一致(MySQL 默认 REPEATABLE_READ)。
(2).面试补充(高频追问)
1. MyBatis 事务与 Spring 事务的关系?
答:MyBatis 原生事务是基于 JDBC 的简单事务管理,适合独立使用;当与 Spring 整合时,Spring 会接管事务管理,MyBatis 不再负责 Connection 的提交和回滚,而是由 Spring 的事务管理器统一控制,实现更灵活、更强大的事务功能。
2. 什么是事务传播行为?MyBatis 结合 Spring 时,常用的传播行为有哪些?
答:事务传播行为定义了多个事务方法嵌套调用时,事务的传播规则,常用的有:
-
REQUIRED(默认):如果当前有事务,就加入当前事务;如果没有事务,就创建一个新事务;
-
REQUIRES_NEW:无论当前是否有事务,都创建一个新事务,原有事务暂停;
-
SUPPORTS:如果当前有事务,就加入当前事务;如果没有事务,就以非事务方式执行。
3. MyBatis 事务中,SqlSession 与 Connection 的关系?
答:一个 SqlSession 对应一个 Connection,MyBatis 原生事务中,事务的边界与 SqlSession 一致(一个 SqlSession 内的所有操作,属于同一个事务);
Spring 事务管理中,Spring 会为同一个事务分配一个 Connection,多个 SqlSession 可共享该 Connection,确保事务一致性。
更多推荐


所有评论(0)