ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Mybatis 一连串提问,被面试官吊打了!2万字详解

Mybatis 一连串提问,被面试官吊打了!2万字详解 写在前面为什么 MyBatis 总是面试重灾区很多 Java 同学都有过这样的经历项目里 MyBatis 用得很熟增删改查写得飞快结果一到面试面试官从「#{} 和 ${} 有什么区别」一路追问到「一级缓存为什么会读到脏数据」「Mapper 接口没有实现类为什么能注入成功」一环扣一环最后直接被打穿。原因其实很简单MyBatis 不是一个只会写 SQL 的封装层它背后有一套完整的“配置解析—代理生成—执行器调度—结果映射—缓存管理”的链路。这篇长文把这条链路从头到尾拆开按面试追问的逻辑串起来讲覆盖基础认知、源码原理、缓存、动态 SQL、插件、延迟加载、Spring 整合和高频题帮你把 MyBatis 建立成体系而不是零散地背几个答案。适用人群准备初中高级 Java 面试、正在准备校招/社招八股、想系统补 MyBatis 原理的同学。一、MyBatis 入门与整体认知1.1 先回答最经典的问题MyBatis 到底是什么MyBatis 是一款半自动化的持久层框架它帮助我们在 Java 对象和 SQL 之间建立映射关系。说它“半自动”是因为它对 SQL 的控制权留给了开发者我们可以自由编写 SQL、优化 SQL、调整索引策略框架只负责参数绑定、结果集转换、连接管理和缓存等工作。与之相对Hibernate 是全自动 ORM它在多数场景下可以由框架自动生成 SQL开发者对 SQL 的掌控力相对弱一些。MyBatis 的优势可以概括为四点SQL 可控复杂查询、报表统计、存储过程都能直接写 SQL性能优化空间大。学习成本低核心就是 XML 映射文件或注解入门非常快。与 JDBC 兼容性好底层仍然是 JDBC因此可以灵活使用连接池、事务等 JDBC 能力。生态成熟有 MyBatis-Spring、PageHelper、MyBatis-Plus、tk.mybatis 等大量配套组件。1.2 与 Hibernate、JPA 的对比要能说出取舍面试官经常问「MyBatis 和 Hibernate 怎么选」回答的重点不是“谁更好”而是根据业务特征做取舍。对比维度MyBatisHibernateSQL 生成方式开发者手写 SQL大部分场景框架自动生成自动程度半自动全自动数据库可移植性弱强依赖具体 SQL 方言强可通过方言适配多种数据库复杂查询性能调优灵活直接改 SQL需要深入理解 HQL/Criteria/N1 问题学习成本低高概念多、调优复杂适用场景互联网高并发、报表、遗留系统、SQL 复杂多变以对象模型为中心、快速 CRUD 的业务系统结论可以这样表达互联网业务通常要求 SQL 精确可控、方便 DBA 优化因此 MyBatis 更受欢迎传统企业级系统中如果对象关系映射简单Hibernate 能减少重复劳动。1.3 MyBatis 核心组件清单把下面这些组件记住以后再看源码就不会迷路SqlSessionFactory会话工厂负责创建 SqlSession整个应用通常一个实例就够。SqlSession一次数据库会话的入口封装了增删改查、提交、回滚等操作。Configuration全局配置对象保存 mybatis-config.xml 以及所有 Mapper 映射信息。MappedStatement封装一条 SQL 的完整信息包括 id、参数类型、结果类型、SQL 语句、缓存配置等。ExecutorSQL 执行器负责调度 StatementHandler、缓存、事务等。StatementHandler处理 JDBC Statement完成参数设置和 SQL 执行。ParameterHandler把 Java 参数转换为 JDBC 参数。ResultSetHandler把 JDBC 结果集转换为 Java 对象或集合。TypeHandler在 Java 类型和 JDBC 类型之间转换。MapperProxy为 Mapper 接口生成 JDK 动态代理把方法调用转交给 SqlSession。这十个组件之间的关系后面会随着执行流程逐步展开。二、MyBatis 核心源码流程面试官从这里开始挖2.1 从加载配置到创建 SqlSessionFactoryMyBatis 的使用入口是SqlSessionFactoryBuilder#build它会读取核心配置文件最终得到一个SqlSessionFactory。核心代码路径如下InputStream inputStream Resources.getResourceAsStream(mybatis-config.xml); SqlSessionFactory sqlSessionFactory new SqlSessionFactoryBuilder().build(inputStream);build方法内部主要做了三件事用XMLConfigBuilder解析 mybatis-config.xml构建出Configuration对象。通过Configuration扫描并解析所有 Mapper XML 或注解注册MappedStatement。用Configuration创建DefaultSqlSessionFactory。mybatis-config.xml 中的environments、settings、typeAliases、mappers等配置最后都落到Configuration对象中。也就是说Configuration 是 MyBatis 的“大管家”一个应用生命周期内通常只创建一个因为它内部包含大量 Map创建成本较高。2.2 Mapper 映射文件是怎么被解析的在XMLConfigBuilder解析到mappers节点时会通过XMLMapperBuilder逐个解析 Mapper XML。每个 Mapper XML 里的一条 SQL 节点例如select idselectById parameterTypelong resultTypecom.example.User select id, username, age from user where id #{id} /select会被解析成一个MappedStatement其中包含id命名空间 SQL id即com.example.UserMapper.selectById。sqlCommandType表示它是 SELECT。parameterMap/resultMap入参和出参映射信息。SqlSource原始 SQL 经过处理后得到的可执行 SQL 源动态 SQL 和静态 SQL 处理方式不同。缓存配置是否使用二级缓存、flushCache、useCache 等。最后这些MappedStatement会被放入Configuration.mappedStatements这个 Map 中key 就是全限定 SQL id。后续执行 SQL 时都是先根据 id 拿到MappedStatement再交给 Executor 执行。2.3 Mapper 接口没有实现类为什么能调用这是面试高频题。我们在 Service 里直接注入UserMapper而这个接口没有任何实现类为什么还能执行 SQL答案是JDK 动态代理 MapperProxy。当调用sqlSession.getMapper(UserMapper.class)或 Spring 扫描 Mapper 接口时MyBatis 会通过MapperProxyFactory为该接口创建代理对象UserMapper mapper sqlSession.getMapper(UserMapper.class); User user mapper.selectById(1L);而真正干活的是MapperProxy它实现了InvocationHandler接口。每次调用 Mapper 方法都会进入MapperProxy#invoke大致流程为如果不是Object类的方法如toString、hashCode就继续往下走。根据接口全限定名 方法名找到对应的MappedStatement。将方法参数与 SQL 绑定最终委派给SqlSession执行selectOne、selectList、insert、update、delete等方法。将执行结果返回给调用方。所以Mapper 接口能工作的本质是接口方法名 参数 返回类型与 MappedStatement 的约定一致Method 调用被代理成数据库操作。也正因如此Mapper 接口方法不允许重载因为映射 key 由「接口全限定名.方法名」构成重载会导致 key 冲突同时返回类型如果和实际查询结果不一致会在结果映射阶段抛异常。2.4 SqlSession 执行 SQL 的完整链路以selectById为例完整链路如下// 1. 调用 Mapper 接口方法 mapper.selectById(1L) // 2. 进入 MapperProxy MapperProxy.invoke() // 3. 委派给 SqlSession.selectOne DefaultSqlSession.selectOne() // 4. 从 Configuration 中取出 MappedStatement configuration.getMappedStatement(com.example.UserMapper.selectById) // 5. 交给 Executor 执行 executor.query(ms, parameter, rowBounds, resultHandler) // 6. Executor 调度 StatementHandler 完成 JDBC 操作 statementHandler.query(stmt, resultHandler) // 7. ResultSetHandler 处理结果集转换为 User 对象 resultSetHandler.handleResultSets(stmt)这条链路上有三个关键角色需要重点理解Executor、StatementHandler和ResultSetHandler面试官很容易顺着往下问。2.5 Executor 执行器的三种类型以及装饰器Executor 是 MyBatis 执行 SQL 的核心接口直接负责调度执行、缓存、事务。它有三种基础实现SimpleExecutor默认执行器每次执行 SQL 都会预编译一个新的 Statement。ReuseExecutor可复用执行器会缓存 Statement相同 SQL 能重复使用预编译结果。BatchExecutor批处理执行器用于批量更新场景需要手工 flush 提交。除了基础实现MyBatis 还使用了装饰器模式强化 Executor 能力CachingExecutor在二级缓存开启时包装基础 Executor优先查缓存。BaseExecutor基础执行器抽象类内部实现了一级缓存、事务、Statement 生命周期管理。这里的面试考点是一级缓存住在 BaseExecutor 里二级缓存住在 CachingExecutor 里。所以二级缓存开启时执行器结构是CachingExecutor负责二级缓存→ 包裹SimpleExecutor/ReuseExecutor/BatchExecutor继承 BaseExecutor负责一级缓存和真正的 JDBC 执行。2.6 StatementHandler 与四大组件的关系Executor 最终会调用StatementHandler执行数据库操作。StatementHandler 有三种实现对应 JDBC 的三种 StatementSimpleStatementHandler对应Statement不支持占位符。PreparedStatementHandler对应PreparedStatement支持#{} 参数预编译是默认实现。CallableStatementHandler对应CallableStatement用于调用存储过程。StatementHandler 内部依赖另外三个组件ParameterHandler负责把 Java 参数设置到PreparedStatement上。ResultSetHandler负责把数据库返回的 ResultSet 映射为 Java 对象。TypeHandler负责 Java 类型与 JDBC 类型之间的双向转换。如果面试官继续问TypeHandler可以举两个典型场景枚举类型存储、Json 字符串与对象互转。自定义 TypeHandler 只需要实现TypeHandler接口并在配置文件或注解中注册即可。三、参数处理、#{} 与 ${}、SQL 注入3.1 重点 #{} 和 ${} 的区别这几乎是 MyBatis 面试逃不掉的题目。回答时要抓住三个层次预编译、SQL 注入风险、使用场景。#{} 是预编译参数占位符。MyBatis 会把 SQL 中的#{} 替换成 ?然后把参数通过 PreparedStatement 的 setXxx 方法安全地绑定进去。数据库会先解析、编译 SQL再传入参数值因此参数不会改变 SQL 结构能有效防止 SQL 注入。${} 是字符串替换。MyBatis 会在 SQL 生成阶段直接把变量值拼接进 SQL 字符串中相当于字符串拼接。因为它发生在 SQL 编译之前恶意内容可以直接改变 SQL 结构存在 SQL 注入风险。举两个例子。假设表名需要动态传入必须使用${}因为表名和列名不能通过占位符预编译select idselectByTable resultTypemap select * from ${tableName} where id #{id} /select而普通参数应该坚决使用#{}select idselectByName resultTypecom.example.User select * from user where username #{username} /select如果用户传入username admin or 11使用#{}时会被当作普通字符串查询不会影响 SQL 结构使用${}时SQL 会被拼成where username admin or 11就会查到不该查的数据。3.2 什么时候必须用 ${}以及如何安全使用合法使用${}的场景主要有动态表名如按月分表的order_202501。动态字段名如排序字段、分组字段。动态排序方式如order by ${column} ${direction}。在这些场景下虽然无法完全使用#{}但必须做白名单校验不能让用户输入直接进入${}。四、缓存机制一级缓存为什么会有脏数据4.1 一级缓存的作用范围与失效条件MyBatis 的一级缓存是SqlSession级别的本地缓存默认开启。在同一个SqlSession中对相同 SQL 和相同参数执行查询时第二次不会再查数据库而是直接返回缓存结果。失效条件主要有执行了insert/update/delete等修改操作MyBatis 会清空一级缓存。SqlSession提交或回滚时会清空一级缓存。查询时设置flushCachetrue。缓存作用域是SqlSession关闭后缓存消失。4.2 一级缓存为什么会读到脏数据经典脏数据场景出现在两个SqlSession操作同一张表时UserMapper mapper1 session1.getMapper(UserMapper.class); UserMapper mapper2 session2.getMapper(UserMapper.class); User u1 mapper1.selectById(1L); // 查库并放入 session1 一级缓存 mapper2.updateAge(1L, 30); // session2 更新 age30但 session1 不知道 User u2 mapper1.selectById(1L); // session1 命中一级缓存读到旧 age这是面试常问的“一级缓存脏数据”问题一级缓存只在同一个 SqlSession 内生效无法感知其他会话的提交。解决办法通常是把查询和更新放在同一个 SqlSession 中或者业务上不要依赖一级缓存甚至通过flushCache立即清空。4.3 二级缓存的工作机制与使用建议二级缓存是Mapper命名空间级别的缓存需要显式开启在核心配置里设置cacheEnabledtrue并在 Mapper XML 中添加cache/。执行器结构会变成CachingExecutor包裹基础 Executor先查二级缓存再查一级缓存最后查库。使用二级缓存时要注意只有SqlSession提交或关闭后一级缓存才会同步到二级缓存。查询结果必须可序列化否则缓存写入可能失败。多表关联查询时一个命名空间更新、另一个命名空间缓存未失效容易读到陈旧数据生产环境要谨慎开启。五、动态 SQLif/where/foreach 的原理与易错点5.1 动态 SQL 的执行原理MyBatis 在解析 Mapper XML 时会把动态 SQL 节点解析为SqlNode树。运行期根据参数对象动态拼接 SQL拼完后再交给 JDBC 执行。常见标签有if、where、set、foreach、choose、trim。5.2 常见标签与易错点示例select idselectByCondition resultTypecom.example.User select id, username, age from user where if testusername ! null and username ! and username #{username} /if if testage ! null and age #{age} /if /where /select易错点if里不要用参数名以外的变量test表达式中的对象通常是参数对象属性复杂判断建议用_parameter或Param限定。where能自动去掉开头的 AND/OR但多个条件都为空时不会生成 where避免where关键字残留。foreach注意参数名数组默认arrayList 默认list接口用Param(ids)后应写collectionids。select idselectByIds resultTypecom.example.User select id, username from user where id in foreach collectionlist itemid open( separator, close) #{id} /foreach /select六、插件机制MyBatis 插件如何做分页与慢 SQL 监控6.1 插件原理拦截器与责任链MyBatis 插件基于Interceptor接口和 JDK 动态代理实现可以拦截Executor、ParameterHandler、ResultSetHandler、StatementHandler四类组件。插件通过Intercepts注解声明要拦截的类型和方法在初始化阶段包装目标对象形成类似责任链的调用链。6.2 简单慢 SQL 插件示例Intercepts({ Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class}) }) public class SlowSqlInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { long start System.currentTimeMillis(); Object result invocation.proceed(); long cost System.currentTimeMillis() - start; if (cost 1000) { System.out.println(慢 SQL 耗时: cost ms); } return result; } }注意StatementHandler可能需要从MetaObject中取出真实目标对象多个插件包装后要处理代理嵌套问题。七、延迟加载为什么有时查到的是空对象或代理对象7.1 延迟加载触发条件MyBatis 延迟加载默认关闭需要在配置中开启lazyLoadingEnabledtrue、aggressiveLazyLoadingfalse。当查询主对象时关联属性不会立即加载而是在真正调用该属性的 getter 时才触发第二次 SQL。其本质是ResultSetHandler用 CGLIB/Javassist 为关联对象生成代理。7.2 延迟加载的坑必须配置 aggressiveLazyLoadingfalse否则所有懒加载属性都会在首次访问任意属性时被加载。延迟加载依赖 SqlSession 存活如果 SqlSession 已关闭再访问懒加载属性会抛异常。序列化问题代理对象默认不能被 Jackson 序列化需要 DTO 转换或关闭延迟加载。八、Spring 整合 MyBatisMapper 扫描与事务8.1 Spring 如何扫描 Mapper 接口Spring 整合 MyBatis 时不使用sqlSession.getMapper手动获取而是通过MapperScannerConfigurer或MapperScan扫描指定包路径为每个 Mapper 接口注册MapperFactoryBean。Spring 会创建 Mapper 接口的代理对象并注入到 Service 中代理内部仍然调用 MyBatis 的SqlSession。Configuration MapperScan(com.example.mapper) public class MyBatisConfig { }8.2 事务管理与 SqlSession 生命周期在 Spring 托管事务下每个SqlSession与 Spring 事务的Connection绑定同一事务内多个 Mapper 操作会复用同一个SqlSession。这就是为什么 Spring 中经常不会再遇到一级缓存脏数据同一个事务内共享同一 SqlSession。注意Transactional声明式事务由 Spring 管理MyBatis 自身不负责事务提交和回滚。九、MyBatis 高频面试题速查与总结9.1 高频题速查表题目回答要点MyBatis 和 Hibernate 怎么选半自动 vs 全自动SQL 可控性业务取舍Mapper 接口没有实现类为什么能调用JDK 动态代理 MapperProxy#{} 和 ${} 的区别预编译占位 vs 字符串替换SQL 注入一级缓存和二级缓存作用域、脏数据、开启条件Executor 有哪几种Simple/Reuse/Batch CachingExecutor插件原理动态代理 拦截器 责任链延迟加载原理代理对象 懒查询如何防止 SQL 注入尽量 #{}必须 ${} 时白名单校验9.2 总结MyBatis 面试也遵循梯度先问#{} 和 ${}再问Mapper 代理随后往执行链路、缓存、插件、延迟加载深入。建议把配置文件解析、SqlSessionFactory 到 MappedStatement 这条主线默画一遍再结合上述章节补齐每个延申专题就能从“会写 CRUD”升级到“能讲清原理”。
返回列表