MyBatis 核心机制与实战:从动态 SQL 到缓存、拦截器及 Spring Boot 集成

发布时间:2026/9/17 19:19:03
MyBatis 核心机制与实战:从动态 SQL 到缓存、拦截器及 Spring Boot 集成 还记得没框架的年代我用 JDBC 写一个分页查询Connection、PreparedStatement、ResultSet 三件套轮着来动辄几十行样板代码SQL 还拼得心惊胆战。后来切到 MyBatis第一感受是“终于有人把我整天重复的脏活累活接管了”。一直到 2026 年MyBatis 依然是国内 Java 后端持久层的中坚力量哪怕 Spring Boot 全家桶里 JPA 也有一席之地但论 SQL 可控性、灵活性、团队落地成本MyBatis 这套“半自动”设计至今依然很能打。这篇东西适合三类人看刚学完 Java 基础、准备上手项目开发的初学者简历上写了 MyBatis 但实际只会复制粘贴想系统补一补的人以及马上要面试、想快速梳理高频考点和框架原理的求职者。我会从框架解决什么问题讲起再到配置、动态 SQL、批量操作、缓存、拦截器最后收在面试高频考点和避坑经验上。内容不追求把源码每一行都过一遍但会把核心机制讲透让你看完既能上手写代码也能跟面试官聊出深度。1. 先搞清楚MyBatis 到底解决什么问题1.1 从 JDBC 的痛点说起没有对比就没有伤害先回顾一下原始 JDBC 写数据库操作有多难受。假设你要查用户表里面的一条数据至少要经历这些步骤加载驱动、获取 Connection、创建 PreparedStatement、逐个 set 参数、执行 executeQuery、遍历 ResultSet 手动取值、关闭 ResultSet、关闭 Statement、关闭 Connection中间还得 try-catch 处理 SQLException。这还没算上把查询结果转成 Java 对象那一步——你要是封装过 ResultSet 映射一定记得那种“一个字段一个字段 getString/getInt再手动 new 对象塞进去”的机械感。这种写法有几个致命问题样板代码太多业务逻辑被淹没在异常处理和资源关闭里。SQL 和 Java 代码强耦合SQL 一改就得重新编译发布。结果集映射全靠手写字段一多就是体力活。连接管理稍不留神就泄漏线上连接池被打爆的坑十有八九是这里出来的。后来出现的各种持久层框架本质上都是在解决这几件事简化连接和事务管理、把 SQL 从 Java 代码里解放出来、自动完成结果集映射。1.2 MyBatis 的定位与核心设计MyBatis 的前身是 Apache 的 iBatis2010 年迁移到 Google Code 并改名 MyBatis现在的主版本是 3.5.x在 GitHub 上持续维护。它的定位是“半自动 ORM 框架”不同于 Hibernate 那种把实体类和数据库表完全映射、你基本不用写 SQL 的全自动方案MyBatis 把 SQL 编写的控制权完全交给你框架只负责参数绑定、SQL 执行、结果映射这些脏活。为什么“半自动”反而受欢迎核心原因是复杂查询场景下SQL 的可控性比“全自动生成的 SQL”重要得多。Hibernate 生成的 SQL 遇到多表关联、子查询、复杂分组时往往不够直观优化起来还要理解 ORM 的生成逻辑。而 MyBatis 的 SQL 就写在 XML 或注解里DBA 拿过去就能看、能改、能优化这对国内很多“SQL 重度依赖”的业务系统来说是决定性优势。它有几个核心组成一定要记住SqlSessionFactory全局唯一的会话工厂解析配置文件后创建 SqlSession。SqlSession数据库会话对象提供增删改查接口底层封装了 Executor 和事务。Mapper 接口你定义的 Java 接口MyBatis 会通过动态代理生成实现类。映射文件XMLSQL 定义的地方包含增删改查、动态 SQL、结果映射规则。1.3 一个请求的执行流程看一段 MyBatis 源码是快速建立整体认知的好办法不用全读只抓主线。一次完整的查询请求大概是这样流转的SqlSessionFactoryBuilder.build() - 解析 mybatis-config.xml 和 mapper.xml把每个 SQL 封装成 MappedStatement - 生成 DefaultSqlSession - 调用 mapper 接口方法 - 通过 JDK 动态代理进入 MapperProxy - 找到对应的 MappedStatement - Executor 执行器负责 SQL 整体调度 - StatementHandler 创建 PreparedStatement 并设置参数 - ParameterHandler 处理参数绑定 - ResultSetHandler 把结果集映射成对象这个流程里你会发现MyBatis 的扩展点全部藏在这几个核心对象里。看懂它之后后面学拦截器、二级缓存、分页插件都会顺理成章。2. 从零跑通一个 MyBatis 项目2.1 Maven 依赖与配置文件用纯 Maven 工程起步先不引入 Spring Boot这样能把框架本身最原始的运行机制看清楚。需要三个依赖MyBatis 本体、MySQL 驱动、日志实现推荐 logback 或 log4j2调试必备。dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.16/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency dependency groupIdch.qos.logback/groupId artifactIdlogback-classic/artifactId version1.4.14/version /dependency然后是核心配置文件 mybatis-config.xml。这里要理解每个配置项的作用而不是盲目照抄?xml version1.0 encodingUTF-8 ? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN https://mybatis.org/dtd/mybatis-3-config.dtd configuration properties resourcedb.properties/ settings setting namemapUnderscoreToCamelCase valuetrue/ setting namelogImpl valueSLF4J/ /settings typeAliases package namecom.example.entity/ /typeAliases environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver value${db.driver}/ property nameurl value${db.url}/ property nameusername value${db.username}/ property namepassword value${db.password}/ /dataSource /environment /environments mappers package namecom.example.mapper/ /mappers /configuration几个点值得说明第一mapUnderscoreToCamelCase 开启后数据库的 user_name 字段能自动映射到 userName 属性省去一大堆 resultMap 声明。第二logImpl 决定 SQL 日志打印方式用 SLF4J 对接统一日志框架最稳妥。第三POOLED 是连接池类型生产环境一般用 Druid 或 HikariCP但框架自带的 POOLED 足够学习用。第四mappers 里的包扫描配置配合接口上的 Mapper 注解或 XML 文件可以省掉逐个注册的麻烦。配置文件的路径问题也常坑新手src/main/resources 目录下编译时会把 XML 原样打到 classpath所以如果你的 Mapper 接口在 com.example.mapper 包下XML 映射文件也放在 resources/com/example/mapper 目录下才能被正确扫描到。2.2 POJO 和 Mapper 接口假设有一张 user 表字段有 id、username、password、create_time。实体类不贴了就一个标准 POJO。Mapper 接口定义如下public interface UserMapper { User findById(Long id); ListUser findAll(); int insert(User user); }对应的 UserMapper.xml?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN https://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.mapper.UserMapper select idfindById resultTypeUser SELECT id, username, password, create_time FROM user WHERE id #{id} /select select idfindAll resultTypeUser SELECT id, username, password, create_time FROM user /select insert idinsert parameterTypeUser INSERT INTO user(username, password, create_time) VALUES(#{username}, #{password}, #{createTime}) /insert /mappernamespace 必须写接口的全限定名select 标签的 id 必须和接口方法名完全一致。这是 Mapper 代理模式的绑定依据一个不匹配就报绑定错误。resultType 这里能直接写 User是因为配置里做了 typeAliases 包扫描否则要写 com.example.entity.User 全限定名。这里插一句XML 里选择性用 parameterType 其实不是必须的MyBatis 能根据接口方法的参数推断出来。多写没问题但别写错写错了反而报错。2.3 运行时初始化与调用写一个测试类把流程跑起来public class MyBatisDemo { public static void main(String[] args) throws IOException { String resource mybatis-config.xml; InputStream inputStream Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory new SqlSessionFactoryBuilder().build(inputStream); try (SqlSession session sqlSessionFactory.openSession(true)) { UserMapper userMapper session.getMapper(UserMapper.class); User user userMapper.findById(1L); System.out.println(user); } } }注意 openSession(true) 表示自动提交事务false 或默认情况下需要手动 session.commit()否则插入、更新操作不会真正落库。这也是新手最常踩的坑代码跑完没报错但数据库里就是没有数据原因大概率是没提交事务。SqlSession 使用完毕需要 closetry-with-resources 是最省心的方式。在真实项目中这一步由 Spring 框架管理但在纯 MyBatis 应用中忘记关 SqlSession 就是连接池被耗尽的元凶。2.4 配置文件里那些“默认值”别乱动有两设置项我建议从一开始就加上能省掉很多后期排查时间mapUnderscoreToCamelCase 上面提过了绝大多数现代项目都在用。jdbcTypeForNull 默认是 OTHER在 MySQL 下插入 null 值有时会报“无效的列类型”改成 NULL 可以规避大部分问题。setting namejdbcTypeForNull valueNULL/另外如果你用 Spring Boot mybatis-spring-boot-starter这些配置可以直接写在 application.yml 里比如mybatis: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl type-aliases-package: com.example.entity mapper-locations: classpath:mapper/*.xml注意这里的 log-impl 对应控制台直接输出 SQL 的方式适合开发调试生产环境建议切回 SLF4J。3. 动态 SQL复杂查询的利器含批量操作3.1 动态 SQL 标签速览动态 SQL 可以说是 MyBatis 的灵魂能力之一。以前拼 SQL 靠字符串连接条件一多代码就乱成粥。MyBatis 提供了一套基于 OGNL 表达式的标签让你在 XML 里就能完成条件判断和循环拼接。最常用的几个标签if最简单的条件判断。choose / when / otherwise相当于 Java 的 switch多个分支只选一个。where自动处理 WHERE 关键字去掉多余的 AND 或 OR。set动态更新时自动处理 SET 关键字去掉最后多余的逗号。foreach循环遍历集合用于 IN 查询或批量插入。trim更灵活的前后缀处理where 和 set 本质上都是它的特化。写一个典型的多条件查询select idsearchUsers resultTypeUser SELECT id, username, create_time FROM user where if testusername ! null and username ! AND username LIKE CONCAT(%, #{username}, %) /if if testcreateTime ! null AND create_time gt; #{createTime} /if /where ORDER BY create_time DESC /select使用 where 标签后如果所有条件都不成立生成的 SQL 就没有 WHERE 子句如果第一个条件成立它会自动去掉开头的 AND。这里要特别注意 XML 特殊字符大于号可以直接写但小于号必须写成lt;或者用![CDATA[ ]]包起来否则 XML 解析直接报错。3.2 对应热词“mybatis switch”——choose/when/otherwise 怎么用如果你有一类“多条件取其一”的查询需求用多个 if 容易互相干扰。比如搜索接口里前端可能传 name、status、dateRange 中的任意一项有优先级顺序这时就适合用 chooseselect idqueryByCondition resultTypeOrder SELECT * FROM order WHERE deleted 0 choose when testorderNo ! null and orderNo ! AND order_no #{orderNo} /when when teststatus ! null AND status #{status} /when when teststartTime ! null AND create_time gt; #{startTime} /when otherwise AND create_time gt; DATE_SUB(NOW(), INTERVAL 7 DAY) /otherwise /choose /select这样就能保证“只走一个分支”而不是多个条件叠加。真实业务里做高级搜索时这个标签的使用频率非常高。同时它可以搭配bind标签复用表达式比如在 if 里反复写name ! null and name ! 很啰嗦可以用bind namenameValid valuename ! null and name ! /简化但实际项目中为了可读性我倾向于直接写全。3.3 批量操作的三种写法及性能对比批量操作在真实项目中非常多批量导入用户、批量更新商品状态、批量插入订单明细。MyBatis 写批量操作一般有三种方案性能和使用场景各不相同。方案一foreach 批量拼 INSERT。这种写法最直观一条 INSERT 语句带多个 VALUESinsert idbatchInsert INSERT INTO user(username, password, create_time) VALUES foreach collectionlist itemitem separator, (#{item.username}, #{item.password}, #{item.createTime}) /foreach /insert这种 SQL 在数据库端只需要编译一次性能很好是实际开发中用得最多的方式。但要注意如果列表很大生成的 SQL 可能超出数据库或网络允许的最大报文长度MySQL 需要关注 max_allowed_packet 配置。一般单批控制在 500 到 1000 条以内比较稳妥。方案二循环单条 insert。接口循环调用单条插入每条都是独立的 SQL 和事务交互性能最差唯一优点是代码最简单。在数据量小、几秒跑完的场景能接受数据量一上来就被否了。方案三ExecutorType.BATCH。这是官方提供的批处理模式通过 BatchExecutor 在同一个 PreparedStatement 上累积参数最后统一提交减少网络往返。用法是try (SqlSession session sqlSessionFactory.openSession(ExecutorType.BATCH, false)) { UserMapper mapper session.getMapper(UserMapper.class); for (User user : userList) { mapper.insert(user); } session.commit(); }这个方案的坑在于BatchExecutor 的 update 操作不会立即执行而是把 SQL 攒着只有调用 flushStatements 或 commit/close 时才真正刷到数据库。如果你在批处理中途执行查询可能会查不到刚才插入的数据而且如果一条语句在批量执行中出错很难判断具体是哪条数据的问题。所以批量模式更适合“数据量大、容错要求不高的导入任务”比如从 Excel 导入几万条数据。补充一个 MyBatis-Plus 的情况IService.saveBatch 底层其实也是循环分批执行批量插入默认每 1000 条一批。所以别以为框架帮你做了“魔法”本质还是你手动写 foreach 批量 SQL 的那套逻辑只是封装了批量大小控制。3.4 对应热词“mybatis 单个数字字符比较”——一个能让你调半天的坑你在 XML 里写过这种判断吗if teststatus 1如果 status 是 String 类型这个判断可能和你预期的完全不一样。问题出在 OGNL 表达式的类型推断上在 Java 的 OGNL 里单个引号内的字符1会被解析成 char 类型而 char 在比较运算时会自动转成对应的 Unicode 数值。也就是说status 1在 OGNL 眼里可能是“字符串和数字 49 比较”结果往往是 false或者更隐蔽地出现奇怪的匹配。我在项目里就碰到过前端传了 status1代码里 判断死活不进去查了半天数据都正常最后发现是单引号引起的类型问题。解决办法有两个!-- 方式一用双引号包字符串但这在 XML 属性里比较别扭 -- if teststatus 1 !-- 方式二显式转成字符串推荐 -- if teststatus 1.toString()类似的坑也会出现在单个字符和数字比较上。写动态 SQL 条件时凡是做值比较尽量把类型统一字符串就用双引号或调用 toString数字就直接写数字别用单引号去碰运气。4. 缓存机制与性能优化4.1 对应热词“mybatis缓存”——一级缓存默认开启但容易踩坑MyBatis 的缓存分两级这个面试必问。一级缓存是 SqlSession 级别的默认开启你不需要任何配置就能用。它的作用范围是一次会话内同一个 SqlSession 执行两次完全相同的查询相同 SQL、相同参数第二次直接返回缓存结果不再查数据库。逻辑上很美好但实际使用中会发现一级缓存经常“没生效”尤其在 Spring 集成后。根本原因是一级缓存的生命周期绑定 SqlSession而 Spring 管理的 SqlSession 很可能在一个事务范围内只存在很短的时间或者每个 Mapper 操作都通过 SqlSessionTemplate 重新获取会话。如果你在 Service 里连续调用两次 userMapper.findById(1)这两个调用可能走了不同的 SqlSession一级缓存自然对不上。还有一个机制级细节任何 insert、update、delete 操作都会清空当前 SqlSession 的一级缓存这是为了保证数据一致性。所以哪怕是同一个 SqlSession先查一次再更新一条无关记录再查同一条数据第二次查询也不会走缓存。一级缓存设计得这么克制是有道理的它避免了很多分布式环境下的脏数据问题。但代价是你想靠它在真实项目里显著扛住热点查询基本指望不上。于是就有了二级缓存。4.2 二级缓存namespace 级别的双刃剑二级缓存的作用范围是 Mapper 的 namespace 级别也就是同一个 Mapper 下的所有查询可以共享缓存跨 SqlSession 也生效。开启方式很简单在 XML 映射文件里加一行cache/这一行会启用当前 Mapper 的二级缓存缓存对象需要实现 Serializable 接口。官方还允许你通过各种参数定制eviction 指定淘汰算法默认 LRU、flushInterval 指定刷新间隔、size 指定最大缓存对象数量、readOnly 指定是否只读。听起来不错但二级缓存有非常经典的“脏数据”风险假设 UserMapper 查出来一个 User 对象放进缓存整个命名空间都缓存了如果另一个 Mapper比如 OrderMapper 的统计 SQL关联了 user 表并修改了数据UserMapper 是感知不到变化的。更极端的是多表 join 查询缓存的数据可能被其他 Mapper 的更新“悄悄地”弄脏。所以官方文档才会强调如果多个 Mapper 操作了同一张表建议使用cache-ref namespace.../让它们共享同一个缓存区域否则就干脆别开。我的经验是二级缓存不适合用在数据更新频繁的业务表上适合那种“读多写少、数据基本不变”的场景比如数据字典、省市区县、系统配置表。对于一般业务数据优先考虑用 Redis 做应用层缓存可控性要高得多。面试被问到“为什么不用 MyBatis 二级缓存”这几点就是最佳答案。4.3 打印 SQL 与日志配置热点词“mybatis配置打印”、“mybatis log plugin”开发调试时看不到 SQL基本等于盲写。MyBatis 打印 SQL 有几种方式从轻到重最轻量的是在配置里设 logImpl 为 StdOutImplsetting namelogImpl valueorg.apache.ibatis.logging.stdout.StdOutImpl/Spring Boot 环境对应的是mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这个模式直接把 SQL 和参数打印到控制台零依赖缺点是格式比较粗糙。如果你的项目用了 logback/log4j2更推荐把 Mapper 接口所在包的日志级别设为 DEBUG这样能看到带参数、带耗时、带结果条数的完整日志而且打印格式和项目统一logging: level: com.example.mapper: debug对应热词里的“mybatis log plugin”是 IDEA 里的一个插件它能把 MyBatis 打印的参数占位符?自动替换成实际参数值生成可以直接复制到数据库客户端执行的完整 SQL。调试复杂动态 SQL 的时候特别有用不用手动一个个替换问号了。注意一点这个插件本质是解析日志输出不是真正拦截 SQL所以必须开启日志打印后它才能生效。5. 拦截器机制理解 MyBatis 插件化设计5.1 四大对象与代理原理聊到 MyBatis 的扩展机制就绕不开拦截器Interceptor。MyBatis 允许你在 SQL 执行链路中插入自定义逻辑常见应用场景包括分页插件PageHelper、SQL 日志追踪、数据权限过滤、公共字段自动填充、读写分离路由、慢 SQL 统计。拦截器能拦截的对象只有四个对应执行 SQL 的四大核心组件Executor调度执行器负责增删改查的入口和一级缓存维护。StatementHandler语句处理器负责 SQL 语句准备、参数绑定、执行。ParameterHandler参数处理器负责把 Java 参数设置到 PreparedStatement。ResultSetHandler结果集处理器负责把 ResultSet 映射成 Java 对象。MyBatis 拦截器底层就是 JDK 动态代理拦截器包装了目标对象。你在配置中声明的每个拦截器都会生成一个新的代理层。当调用目标方法时会依次经过所有拦截器的 intercept 方法最后才调用真实逻辑。所以多个拦截器像洋葱一样层层包着这一个理解透了再看 PageHelper 原理就很清晰。5.2 手写一个耗时统计拦截器光看理论容易飘直接写一个实用性很高的拦截器统计每个查询的执行耗时打印 SQL 对应的 MappedStatement id。这个在性能排查时非常有用。Intercepts({ Signature( type StatementHandler.class, method query, args {Statement.class, ResultHandler.class} ) }) public class SqlTimingInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { long start System.currentTimeMillis(); try { return invocation.proceed(); } finally { long cost System.currentTimeMillis() - start; StatementHandler handler (StatementHandler) invocation.getTarget(); BoundSql boundSql handler.getBoundSql(); String sql boundSql.getSql().replaceAll(\\s, ); System.out.println([SQL-TIMING] cost ms - sql); } } Override public Object plugin(Object target) { return Plugin.wrap(target, this); } }注意几个细节Signature 里的 type 必须是四大对象之一method 必须是该对象真实存在的方法args 参数类型顺序要和目标方法签名完全一致否则拦截不上而且不报错容易让人摸不着头脑。Plugin.wrap 是框架提供的代理封装方法它会把 target 包装成代理对象只有命中签名的方法才走拦截器逻辑。注册方式是在 mybatis-config.xml 里plugins plugin interceptorcom.example.interceptor.SqlTimingInterceptor/ /pluginsSpring Boot 里则是注册一个 ConfigurationCustomizer Bean。5.3 分页插件 PageHelper 的原理和注意点PageHelper 是 MyBatis 生态里最流行的分页插件使用起来简单到你只需要在查询前调用 PageHelper.startPage(pageNum, pageSize)接着查询的返回结果会自动带上分页信息。它的核心原理其实就是一个 Executor 拦截器startPage 方法把分页参数存到本地线程变量ThreadLocal里然后拦截器拦截 Executor 的 query 方法先判断当前线程变量里有没有分页参数如果有就从原 SQL 生成 count 查询执行一遍再改写原 SQL 加上数据库方言对应的分页语句MySQL 的 LIMITOracle 的 ROWNUM 等最后执行并返回。整个过程对业务代码完全透明。使用上有几个坑我提示一下startPage 只对紧接着的下一条查询生效查完一定要清除 ThreadLocalPageHelper 的 PageInterceptor 在 query 后会自动清理但如果你手动捕获了异常要留意分页线程变量是否残留。另外startPage 所在的方法如果后续调用了两次查询第二次查询也会受影响因为分页参数还在线程变量里。为了避免这种问题最好遵循“startPage 和查询放在同一行或紧邻位置”的团队规范。6. 面试高频考点与避坑指南6.1 必考第一题#{} 和 ${} 的区别MyBatis 面试如果只问一道题大概率是这个。两者的核心区别在于预编译和字符串替换#{}在预编译阶段被解析成 JDBC 的占位符?然后通过 PreparedStatement 的 set 方法设置参数值。这个过程是安全的能有效防止 SQL 注入。${}直接做字符串拼接把参数原样替换到 SQL 中。如果参数来自外部输入就会产生 SQL 注入风险。一个经典场景order by 排序字段和表名/列名这些数据库对象名不能用 #{}。因为预编译的占位符只能用于值不能用于表名、列名、排序方向。这种场景只能用 ${}但必须先做白名单校验。我一般会在代码层先判断排序字段是否在允许的集合内再拼接到 SQL绝对不会直接透传前端传参。6.2 resultType 和 resultMap从对象映射到一对一/一对多resultType 是“自动映射”只要数据库列名和 JavaBean 属性名能对应上配合驼峰转换就能用。resultMap 是“显式映射”适合处理字段名不一致、一对一关联、一对多集合这些复杂场景。resultMap idOrderWithUserMap typeOrder id propertyid columnid/ result propertyorderNo columnorder_no/ association propertyuser javaTypeUser id propertyid columnuser_id/ result propertyusername columnusername/ /association /resultMapassociation 处理“一对一”collection 处理“一对多”。面试官问“MyBatis 的一对多怎么写”你要能顺手说出两种方式嵌套结果映射一次 join 查出来用 resultMap 嵌套映射和嵌套查询SQL 分开查延迟加载。嵌套查询虽然写起来简单但有 N1 查询的性能隐患我记得有一次就是图省事用了嵌套查询结果列表页一次性查了上百条 SQL后来改成嵌套结果 join 才压下去。6.3 高频面试题速查表下面这些题目是我做技术面试官时经常问的也和你提到热词里“mybatis面试题”高度重合每个都值得自己动手验证一下再背问题回答要点MyBatis 和 JDBC 相比省了什么省去连接管理、结果集手动映射、参数手动绑定动态 SQL 让 SQL 按条件拼接不再痛苦SqlSessionFactory 是线程安全的吗是。全局一个实例即可SqlSession 非线程安全使用时每个线程独立获取Mapper 接口没有实现类为什么能调用MyBatis 用 JDK 动态代理生成了接口的代理实现调用方法名匹配到 XML 或注解里的 SQL一级缓存什么时候失效SqlSession 关闭执行增删改查询条件或 SQL 不同手动 clearCache二级缓存脏数据怎么避免多表操作时使用 cache-ref 统一缓存区或用 Redis或干脆不开启怎么实现批量插入foreach 拼 VALUES 列表或 ExecutorType.BATCH控制单批数量MyBatis 怎么防止 SQL 注入值比较一律用 #{}${} 只允许在白名单场景出现MyBatis-Plus 和 MyBatis 什么关系MyBatis-Plus 是增强工具包基于 MyBatis 做了通用 CRUD、条件构造器、分页插件等增强底层核心仍然是 MyBatis6.4 实战中容易踩的坑多看几遍能省半天调试时间第一位是 XML 放错位置。Maven 项目里如果 XML 放在 src/main/java 下默认不会被编译到 classpath运行时就报“Invalid bound statement”。常规解法是放在 resources 目录下与接口路径相同的位置或者在 pom.xml 里加 resources 配置把 xml 也纳入编译。第二位是方法重载问题。Mapper 接口里不要写同名方法即使参数不同也不行。MyBatis 是根据“接口全限定名 方法名”绑定 SQL 的方法重载会让绑定冲突报错信息还特别隐晦。第三位是参数传递。单个参数可以直接用 #{} 取任意名字多个参数必须用 Param 指定名字否则只能用 param1、param2 这种默认名。我在 review 代码时看到过同事不写 Param 导致 SQL 里 #{id} 映射不上报 binding 异常一行 Param 能解决的事别省。第四位是 XML 特殊字符转义。SQL 里有小于号、大于号、不等于号时一定要转义或用 CDATA。别问问就是我在 WHERE 里写create_time now()翻过车。7. 从入门到实战学习路线与 Spring Boot 集成要点7.1 学习路线建议如果你刚接触 MyBatis我的建议是按照四个阶段来走第一阶段不依赖 Spring纯 MyBatis 跑通 CRUD把配置文件、SqlSession、Mapper 机制搞明白。第二阶段掌握动态 SQL 的常见标签自己写一个多条件搜索、批量插入的 demo感受 SQL 拼接的解放感。第三阶段把 MyBatis 放进 Spring Boot 里理解 mybatis-spring-boot-starter 做了哪些封装学会用 MapperScan、配多数据源。第四阶段看源码、写插件把 Executor、StatementHandler、拦截器这些内部对象看清楚能自己实现分页或日志拦截器。尤其第一阶段不要跳过。很多人直接上 Spring Boot 的 starter出了问题只知道报错信息带“Invalid bound statement”但完全不知道是 XML 路径错了还是 namespace 错了。回到纯 MyBatis 环境里跑一遍你会对这些底层机制有肌肉记忆。7.2 对应热词“Spring和MyBatis集成”——Spring Boot 里的关键配置Spring Boot 集成 MyBatis 目前标准方案是引入dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency注意版本对应关系如果你的 Spring Boot 是 3.x需要 MyBatis starter 的 3.0.x 版本Spring Boot 2.x 对应 starter 的 2.3.x 左右。版本对不上可能直接启动失败。启动类加 MapperScan 指定 Mapper 接口所在包或者每个 Mapper 接口加 Mapper 注解两个方式二选一即可。然后 application.yml 里配置 mapper-locations 指向 XML 文件位置常见写法是mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true事务方面Spring Boot 里直接用 Transactional 包住 Service 方法底层通过 Spring 的事务同步管理器接管 SqlSession 的提交/回滚所以你不必也不该手动调用 sqlSession.commit() 或 close()框架会统一处理。控制器里拿到 SqlSession 手动操作属于抗框架的用法不建议。多数据源是另一个热点但配置起来稍麻烦你要给每个数据源分别定义 SqlSessionFactory 和 SqlSessionTemplate用 MapperScan 指定不同的 basePackages 和对应的 factory 类。核心点在于MyBatis 的 SqlSessionFactory 是绑定具体数据源的一个工程里多数据源本质上是多个 SqlSessionFactory 并存各扫各的 Mapper 包。7.3 对应热词“mybatis源码”——值得精读的三个地方源码深度阅读不需要全部铺开我建议优先读三个核心点。第一个是 Configuration 类它是 MyBatis 的中央仓库MappedStatement、ResultMap、缓存、拦截器等全部注册在这里。看懂它你就知道 MyBatis 启动时到底把这些配置都藏哪了。第二个是 Executor 的实现链BaseExecutor 管一级缓存和查询流程SimpleExecutor 直接执行 JDBCBatchExecutor 做批量攒批ReuseExecutor 复用 Statement。第三个是 MapperProxy 的 invoke 方法它解释了为什么调用接口方法就能触发 SQL 执行核心是 MapperMethod它把接口方法调用拆成 SqlCommand增删改查类型和 MethodSignature参数映射规则。阅读顺序也按上面来。先看 Configuration再看 MapperProxy最后看 Executor 及拦截器链。用 debug 模式跑一次简单查询在关键方法打断点比翻十篇文章都管用。这内容写到这里核心部分已经全过了一遍。最后再啰嗦两句个人经验MyBatis 这东西背再多面试题都不如自己搭一个项目玩一遍。我碰到过太多人把“缓存失效、批量插入报错、XML 绑定失败”这些坑背得滚瓜烂熟真到写代码时依然一踩一个准。你只要把上面那些 demo 老老实实跑一遍再故意改错几个配置观察报错长什么样脑子里对 MyBatis 的理解就会立起来。真到了 2026 年这个节点技术栈更迭再快持久层这块 MyBatis 生态的地位短期不会动摇早点把它的原理吃透走哪都不慌。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询