
Spring Boot MyBatis这套组合几乎已经成了国内Java后端的“默认选项”。我最近这大半年把手上一个老项目从SSH架构整体迁移到了Spring Boot 2.7 MyBatis 3.5中途翻了小半套源码也踩了不少文档没写明白的坑。群里问到最多的几个高频问题翻来覆去就是分页插件怎么配、二级缓存什么时候生效、Update执行慢该从哪查、Oracle时间字段映射又是一堆幺蛾子。索性把从项目初始化到故障排查的完整过程整理成文尽量把每一步的“为什么”也讲透而不是只甩一段能跑的配置。这套东西看起来简单本质上却是两套成熟框架的深度整合。Spring Boot负责自动装配和生命周期管理MyBatis负责SQL与Java对象的映射和持久化。真正的问题几乎都出在“边界”上——配置项谁生效、插件按什么顺序拦截、缓存作用域到底到哪一层。搞懂这些边界才算真正会用。1. 项目选型与整体设计为什么是MyBatis Spring Boot1.1 Spring Boot为什么是MyBatis的最佳宿主很多人觉得用Spring Boot无非是省了写XML配置的时间其实它对MyBatis的价值远不止这一点。MyBatis自己是个纯粹的持久层框架它需要外部容器来管理数据源、事务、Mapper接口的注册和生命周期。在没有Spring Boot的时代你得手动创建SqlSessionFactory写一堆DataSource配置还要处理Mapper扫描的细节。Spring Boot用自动配置机制AutoConfiguration把这一步完全接管了。mybatis-spring-boot-starter在启动时会根据classpath里的依赖自动创建SqlSessionFactory和SqlSessionTemplate你只需要提供一个DataSource和一些基础配置。这意味着团队里任何一个新人都能在五分钟内跑通一个可用的数据库访问链路不需要理解底层全貌。这背后其实是Spring Boot“约定优于配置”的思想在起作用。它把复杂的Bean装配过程变成了“依赖引入少量配置”两步走让MyBatis的接入成本指数级下降。我见过不少老项目迁移到Spring Boot后把原来几百行的XML配置文件压缩成了几十行维护成本肉眼可见地下降。1.2 MyBatis与JPA、MyBatis Plus的选型对比选型的时候绕不开一个问题同样做持久层为什么选原生MyBatis而不是Spring Data JPA或者MyBatis Plus我经历过至少三个项目在这条路上来回摇摆说说实际感受。JPA的特点是面向对象建模对简单CRUD非常友好尤其是单表操作几乎不用写SQL。但它一遇到复杂查询、多表联查、动态条件这种“中国特色需求”就会逼你写JPQL或者原生SQL而这时候MyBatis的优势就出来了——SQL完全可控动态SQL是原生能力性能调优时可以精确控制每一条语句。MyBatis Plus是在原生MyBatis之上做增强的内置了通用CRUD、条件构造器、分页插件等能力开发效率确实高。但我的建议是如果你刚接触这套技术栈先从原生MyBatis入手把原理吃透再引入Plus就非常顺畅。直接上手Plus容易产生“什么都能调用但不知道为什么”的空洞感一旦遇到Plus解决不了的场景回退到原生写法就会很被动。从项目长期维护的角度看团队如果以老练的SQL工程师为主原生MyBatis会让代码审查非常舒服因为每条SQL都是透明的。这也是我们最终选择原生MyBatis的原因。1.3 从零初始化项目的两条路径创建Spring Boot项目我推荐两条路线。第一条是IDEA自带初始化器直接选择Spring Initializr勾选依赖时添加Spring Web、MySQL Driver生成后自己加一个mybatis-spring-boot-starter。第二条是直接在Spring官网的start.spring.io页面生成导入IDEA后补齐MyBatis相关依赖。注意如果使用了IDEA的Spring Initializr生成的项目默认构建工具是Maven建议保留默认。Gradle虽然更灵活但国内团队Maven的普及度更高遇到依赖冲突时查询资料也更方便。实际搭建时我的标准动作还会额外做三件事设置Java版本为项目团队统一版本、把groupId和artifactId改成公司规范、在pom里预置一个统一的dependencyManagement。这些小细节在新项目刚起步时不显眼但等依赖多了再回头改就很麻烦。2. 环境搭建与基础配置拆解2.1 Maven依赖引入starter、驱动与连接池我当前的稳定组合是Spring Boot 2.7.18 MyBatis 3.5.x mybatis-spring-boot-starter 2.3.x。为什么要强调组合版本匹配因为我见过太多人直接把starter升到最新版结果Spring Boot版本没跟上启动直接报映射器绑定异常。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency注意一个细节新版MySQL驱动坐标已经从mysql-connector-java改成了mysql-connector-jSpring Boot 2.7.18里你不需要手动指定版本它会被Spring Boot的依赖管理自动锁定。如果你用的是旧坐标也建议统一升级到新坐标避免后续出现驱动兼容问题。连接池我直接用HikariCP它是Spring Boot的默认选型没有额外引入的必要。实际压测中HikariCP在低延迟场景的表现确实比DBCP2和C3P0好而且配置简单几乎不需要调优就能满足绝大多数业务场景。2.2 application.yml配置项逐行解析这是最容易被“复制粘贴”但完全没搞懂的一部分。我按照实际项目配置逐行说明包括经常被忽略的随机端口配置和优雅关闭。server: port: ${PORT:8080} shutdown: graceful spring: application: name: demo-boot datasource: url: jdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver hikari: minimum-idle: 5 maximum-pool-size: 15 idle-timeout: 30000 connection-timeout: 30000 pool-name: DemoHikariPool mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.demo.entity configuration: map-underscore-to-camel-case: true cache-enabled: trueURL里的参数每项都有讲究useUnicode和characterEncoding保证UTF-8存取serverTimezone解决MySQL 8.0以上版本的时间时区问题不设这个参数很容易出现时间比实际早8小时allowPublicKeyRetrieval用在了非SSL连接时MySQL 8的认证流程中Google一圈抱怨无法连接的问题里有一半是这个参数缺失造成的。mapper-locations指定XML文件位置这是Mapper接口和XML映射文件关联的物理基础。map-underscore-to-camel-case: true允许你直接把数据库的下划线字段自动映射成驼峰Java属性比如user_name直接映射为userName省去大量resultMap手写量。cache-enabled先留个开关后面聊缓存会展开。还有一个容易被忽略的场景开发环境多实例调试时需要随机端口启动避免冲突。可以在配置里加server.port: ${PORT:0}启动时让Spring随机分配一个可用端口。但注意这要求你的应用逻辑里没有写死端口的地方否则会出现拿着随机端口去连接固定地址的诡异Bug。2.3 Mapper接口扫描与XML绑定机制MyBatis与Spring整合后Mapper接口本身没有实现类它是通过JDK动态代理生成代理对象然后注册到Spring容器的。你唯一要做的事就是在启动类或配置类上声明扫描范围SpringBootApplication MapperScan(com.demo.mapper) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }MapperScan的背后逻辑是Spring在启动时会扫描指定包下所有接口然后为每个接口生成一个MapperFactoryBean交给MyBatis的SqlSessionTemplate管理。这个过程有一个容易踩的坑——如果Mapper接口的包路径和XML文件的namespace没有严格对应启动时并不会立刻报错只有第一次调用这个Mapper方法时才会抛出“Invalid bound statement (not found)”异常。我排查这种问题时的固定动作是看三点第一mapper-locations路径是否真的能匹配到XML文件存储位置第二XML文件的namespace是否和接口全限定名完全一致第三接口方法名与XML中语句的id是否一致、参数类型是否匹配。这三处任何一处不一致都会出现运行时才发现的问题。2.4 打印SQL与配置文件实战开发阶段不打印SQL排查问题就跟瞎子摸象一样。MyBatis的SQL打印并不在MyBatis自身配置里而是通过日志框架实现。Spring Boot 2.x默认的日志门面是SLF4J底层实现是Logback所以最直接的方式是在application.yml里这样配logging: level: com.demo.mapper: debugcom.demo.mapper是你的Mapper接口所在包路径。一旦配置成debug级别MyBatis会通过该接口的日志对象打印完整的Preparing、Parameters和Rows这对排查参数绑定问题和SQL语法错误极其有用。补充一个进阶用法生产环境如果不想在配置文件中暴露SQL可以利用MyBatis拦截器单独实现SQL日志输出把参数用占位符替换后打印完整SQL。我写过一个小拦截器核心逻辑是在Executor的query/update方法上切入在preHandle处取出BoundSql然后遍历ParameterMapping拼接真实参数。这套做法的好处是可以控制开关、脱敏和格式化比纯日志级别的方案更灵活。3. 核心功能实战CURD、分页与缓存3.1 动态SQL是MyBatis的灵魂真正拉开MyBatis和其他ORM差距的时刻就是遇到复杂的动态查询条件时。比如一个用户列表查询姓名可能为空部门可能为null状态可能有值也可能要求查全部。用MyBatis的where加if几乎是标配解决方案select idlistUsersByCondition parameterTypemap resultTypeUserEntity SELECT * FROM t_user where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testdeptId ! null AND dept_id #{deptId} /if if teststatus ! null AND status #{status} /if /where ORDER BY create_time DESC /selectwhere标签很聪明它会在子标签有内容时自动生成WHERE关键词并且自动移除第一个AND/OR避免写出WHERE AND name ?这种非法SQL。同理set标签用于update语句自动处理SET后多余逗号的问题。动态SQL还包括choose、trim、foreach等标签。foreach用于批量操作最容易出错集合为null或空时如果不做保护生成出来的SQL会是IN ()直接报语法错误。我的习惯是在foreach外层套一个if testids ! null and ids.size() 0。3.2 分页插件PageHelper的配置与用法分页这个问题网上一下子就能搜到一堆文章但能说清原理的很少。PageHelper是一个基于MyBatis拦截器实现的分页插件它的核心逻辑是通过拦截Executor.query()方法在原有SQL上动态拼接LIMIT ? OFFSET ?。我用的是PageHelper 5.3.x配合Spring Boot的配置如下pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: true在代码中的用法分两种。传统方式是PageHelper.startPage(pageNum, pageSize); ListUserEntity list userMapper.selectUserList(); PageInfoUserEntity pageInfo new PageInfo(list);startPage之后的第一查询会被拦截并自动做分页处理注意必须是紧接着的第一条查询。这个“紧接着”是PageHelper使用中最容易犯错的地方中间但凡多一次查询分页就会作用到错误的SQL上。设计上也有点建议更推荐把startPage放在和查询方法紧邻的上一行中间不要穿插任何逻辑代码。另一种是MyBatis Plus风格的分页使用Page对象作为参数传入Mapper方法。两者的差别在于PageHelper是线程本地变量保存分页参数而Plus的分页是把Page实体直接作为参数传递后者在可读性和方法重入方面明显更优。如果团队规范允许引入MyBatis Plus建议用Plus的分页方式替代PageHelper能少很多由于调用时序引发的诡异问题。分页还有一个隐藏性能点count语句的生成。PageHelper默认会执行一条count查询统计总数如果你的主查询很复杂多表join、子查询count的效率可能很低。我的做法是给count查询单独写一个轻量统计SQL用PageHelper的countSuffix或手动改写尽量避开大表join。3.3 MyBatis缓存机制一级缓存与二级缓存MyBatis的缓存是面试高频点也是线上问题容易踩雷的地方。先说一级缓存它是SqlSession级别的默认开启且无法关闭。在一次数据库会话中如果连续执行两条完全相同的查询第二次会直接从缓存返回不经过数据库。这个机制在Spring整合环境下有一个容易被忽视的问题Spring管理的SqlSession默认每执行一次Mapper方法就会关闭并重建所以一级缓存实际作用域通常局限在单次方法调用内。只有在同一个方法中连续调两次相同查询才可能命中的一级缓存跨方法几乎不可能命中。因此想靠一级缓存提升性能效果微乎其微反而是长会话场景比如手动控制SqlSession的批处理需要注意缓存带来的数据一致性问题。二级缓存是Mapper级别的跨SqlSession共享默认关闭。开启方式是在Mapper XML中声明cache evictionLRU flushInterval60000 size512 readOnlyfalse/这里我想聊聊readOnly参数。如果设置成trueMyBatis会直接返回缓存对象的引用性能好但没有隔离设置成falseMyBatis会通过序列化复制对象后返回安全性好但有序列化性能开销。如果你的实体类没实现Serializable开启readOnlyfalse会直接报序列化错误。二级缓存最大的雷区是关联查询。一个多表join查询的结果缓存在一个Mapper里但另一张表的Mapper做了更新这个缓存不会自动失效。这就是为什么很多团队明令禁止在分布式环境下开启二级缓存——数据一致性问题很难保证。我的经验是单机、低频更新、不涉及join的表可以考虑二级缓存其他情况一律关闭。默认配置里我保持了cache-enabled: true但没有任何Mapper声明cache等同于所有Mapper都不启用二级缓存。3.4 XML文件的编写体验优化高亮、跳转与校验写MyBatis的XML映射文件很多人还停留在裸写XML的状态。IDEA里只要引入了MyBatis插件XML文件就能获得三个关键能力一是Mapper方法名和XML id之间的跳转二是写了错误SQL时的实时校验三是SQL语言高亮。XML高亮这件事看起来是锦上添花实际操作中被低估了。没有高亮时一个多条件嵌套的查询SQL写错一个标签排查可能花掉几十分钟。有了IDE支持标签不闭合、属性名拼错这类低级错误在保存瞬间就能看到。另外一个提升效率的技巧是把常用SQL片段拆成sql标签。比如所有查询都要用到的字段列表和关联条件定义成公共片段后用include引用既保证一致性又减少重复编写。对多表join的查询我会把连接条件单独抽出来这样后续新增筛选条件时不会动到join骨架。4. 常见问题与性能排查实录4.1 Update执行慢的场景与排查思路群里问“MyBatis Update 执行慢”的频率非常高。这个问题不能一概而论我拆成几类场景逐一说排查路径。第一类是单条update慢每次执行都在一秒以上甚至几秒。这种我优先怀疑锁竞争。用SHOW ENGINE INNODB STATUS查看当前事务锁等待大概率会看到某个行锁或者间隙锁卡住了。常见原因是事务A更新某行未提交事务B尝试更新同一行MyBatis本身只是执行SQL锁问题完全是数据库层的事先排查索引是否有匹配的更新条件。比如更新时where条件没有走索引就会导致行锁升级为表锁并发场景下极其致命。第二类是批量update慢。这类问题常见原因是批量语句在循环里逐条提交MyBatis默认的ExecutorType为SIMPLE每次执行都自动提交。我建议手动配置批量ExecutorTypemybatis: executor-type: batch或者使用SqlSessionTemplate的batch模式能减少网络往返和日志刷盘。实测一个5万条数据的批量更新从逐条执行的三四分钟压缩到二三十秒效果肉眼可见。第三类是update执行速度正常但整个接口响应慢。这种往往是事务过大导致连接池被占满比如一个事务里包含了大量查询和update持锁时间过长。排查方法是打印事务执行时间拆解每个步骤的耗时占比。我处理过一个案例update本身2ms但它所在的事务前面有个远程调用耗时800ms把事务边界一缩整体从900ms降到15ms。4.2 Oracle查询时间字段映射问题MySQL迁移到Oracle或者在两边同时使用的项目最容易出的问题就是时间字段映射。Oracle的DATE类型包含时分秒但JDBC驱动映射到Java的java.sql.Timestamp和java.util.Date时如果不显式处理容易丢失秒级精度或者格式错乱。我在配置里的处理方式是对时间字段在resultMap中显式指定jdbcType和javaTyperesult columnCREATE_TIME propertycreateTime jdbcTypeTIMESTAMP javaTypejava.util.Date/查询参数传时间时同样要在参数注解里指定ListOrderEntity listByTime(Param(startTime) DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss) Date startTime);还有一个细节Oracle的SYSDATE返回精度很高的时间存入的是秒级。如果你用#{createTime}传入一个LocalDateTime对象JDBC 4.2后游标映射问题会出现版本差异。稳妥做法是entity里统一用LocalDateTime在XML里避免直接比较字符串形式的日期而是比较Date类型参数。4.3 Spring Boot版本过高引发的兼容问题Spring Boot的版本节奏这两年很快每次大版本升级都伴随一堆兼容问题。我踩过一次最典型的项目用Spring Boot 3.0 MyBatis时发现mybatis-spring-boot-starter 2.x版本不兼容因为Spring Boot 3基于Jakarta EE 9包名从javax变成了jakarta老版本starter依赖的javax.sql完全失效。Spring Boot 3.0下必须使用mybatis-spring-boot-starter 3.0.3以上版本同时MyBatis本身也要求3.5.13以上。如果你还在用Spring Boot 2.3.x又要升到2.7.x同样要注意xstream、hikari等依赖的版本变化。我的升级策略是先升级Spring Boot小版本跑通基础用例之后再升MyBatis相关依赖最后再处理断崖式变更比如Java 17记录类替换Getter/Setter。还有一个常见情况是springboot版本太高导致配置文件失效。比如Spring Boot 2.4之后spring.profiles.active的写法被废掉了得改成spring.config.activate.on-profile。很多老配置直接搬过来会静默失效尤其要注意spring.datasource和mybatis这些配置项在新版本中是否有别名或被重新归类。4.4 数据库驱动连接参数与占位符的坑连接串里的每个参数都可能是性能瓶颈。有一个很隐蔽的参数是rewriteBatchedStatementstrue。如果不加这个参数MySQL JDBC驱动并不会真正把多条insert批量提交虽然代码里用的batch API但驱动还是在循环逐条执行。加上之后驱动会把批量SQL重写成多值insert性能差异可以达到十倍以上。我们接口从“慢到超时”优化到“毫秒级返回”关键就在这一行参数。另一个坑是useSSLfalse在一些高版本驱动上会导致警告刷屏但无碍功能。真正的坑在于连接串里的characterEncodingutf8MySQL 8.0默认使用utf8mb4如果你明写utf8表情符号存储时会报错或截断。改成utf8mb4同时确认数据库表字符集也是utf8mb4才敢保证内容安全。MyBatis的#{}和${}占位符区别就不用多说了${}直接字符串拼接存在SQL注入风险。但很多人忽略了一个点分页插件动态拼接SQL时有些场景会用到${}比如动态表名、动态排序列名。这种地方如果能用白名单校验就不要直接把用户参数放进去我习惯把所有排序列名在代码层做一次映射校验不匹配直接就拒绝请求。5. 面试题视角高频考点评析5.1 MyBatis核心原理与生命周期问题MyBatis的原理是面试必问的核心要能说清从Mapper接口到数据库执行的全链路。我的标准表述是Mapper接口通过MapperProxy动态代理生成代理对象调用方法时解析MapperMethodMapperMethod会根据SQL类型调用SqlSession的对应方法SqlSession内部通过Executor执行SQLExecutor负责一级缓存和二级缓存的逻辑最后通过StatementHandler和ParameterHandler完成JDBC层面的交互。一级缓存为什么是SqlSession级别、二级缓存为什么是Mapper级别、使用二级缓存时为什么需要序列化这几个问题都是高频考点。答案对应本文3.3节的内容。面试官如果继续追问“分布式环境下二级缓存有哪些隐患”你就可以展开缓存一致性和脏数据模型的问题把场景具体到实际项目。生命周期相关的问题也一样SqlSessionFactory是全局单例SqlSession是线程不安全的、每次请求都应新建Mapper是绑定在SqlSession基础上的线程安全代理。这三个对象的生命周期搞清楚了才不会被问倒。5.2 Spring Boot自动配置与MyBatis的联动Spring Boot面试题的经典套路是“说说自动配置原理”落地到MyBatis就是mybatis-spring-boot-starter在META-INF/spring.factories里声明了MybatisAutoConfiguration这个类用ConditionalOnClass和ConditionalOnBean判断是否需要执行然后通过SqlSessionFactoryBean创建SqlSessionFactory并注册MapperScannerConfigurer完成Mapper扫描。追问环节还会涉及“自定义starter怎么做”这时候要能说清楚自动配置类、条件注解、配置绑定ConfigurationProperties三者的协作关系。我建议直接看mybatis-spring-boot-starter源码它一千多行代码逻辑清晰是理解Spring Boot自动配置机制的最佳教材。还有一个接口逻辑需要注意Spring Boot 2.7里MybatisAutoConfiguration上标记了EnableConfigurationProperties(MybatisProperties.class)配置前缀是mybatis。我的yml里所有带mybatis.前缀的配置都会绑定到这个类上理解这一点就能解释为什么mybatis.configuration.*下的属性能直接映射到MyBatis全局Config。5.3 综合场景设计题的答题套路面试官喜欢给一个“高并发订单系统请设计数据访问层”的场景题。我的回答框架分三层第一层讲CRUD和事务边界如何处理批量插入和唯一索引冲突第二层讲缓存层级何时用Redis、何时用MyBatis二级缓存阐述各自的风险第三层讲分页和大数据量查询说明主键分页和LIMIT深分页的差异。直角深分页的问题一定要答出来LIMIT 100000, 20这种写法会先扫描前面十万行再丢弃性能极差。替代方案是记录上一页的最大主键用WHERE id ?加上ORDER BY id LIMIT 20。这套思路在MyBatis里实现特别自然把上一页游标作为参数传进来就行。能答到这一层说明你确实做过大规模数据的项目。6. 源码复盘与真实项目落地经验6.1 核心调用链SqlSession、Executor与MapperProxy调试MyBatis问题时花些时间跟一遍源码会有长远回报。我推荐的阅读路径是先看Configuration类里注册的MapperRegistry和InterceptorChain然后看MapperProxy的invoke方法接着跟到MapperMethod.execute。这层能搞清接口方法名和SQL语句怎么对应上。继续往下就是Executor体系。BaseExecutor负责一级缓存和获取连接SimpleExecutor/BatchExecutor/ReuseExecutor分别对应三种执行模式。一级缓存的key是CacheKey由statementId、SQL、参数和分页信息组成的。我记忆里踩过的一个坑就发生在这里跨Mapper执行同样SQL但statementId不同一级缓存不会共享这是很多人误以为一级缓存有问题的原因。二级缓存在CachingExecutor这一层它包裹着BaseExecutor。执行的顺序是先查CachingExecutor的二级缓存未命中再进入BaseExecutor查一级缓存。每次update操作都会清空相关Mapper的二级缓存但一级缓存是否清空取决于配置localCacheScope默认在同一个SqlSession下可能会保留旧值这在长事务里需要注意。6.2 实践中的项目部署与自动化项目开发完还要能稳定部署我目前的标准流程是Docker Compose编排Spring Boot容器和MySQL容器MyBatis配置通过环境变量动态注入。这时配置里的${PORT:8080}、${DB_PASSWORD}这类占位符的价值就体现出来了。同一个镜像在开发、测试、生产各环境通过环境变量覆盖配置完全不改代码。我维护过的一个迁移项目Dockerfile里的基础镜像是eclipse-temurin:17-jdk-alpine构建阶段用Maven先打包再拷贝jar包能有效缩小镜像体积。启动命令用了优雅关闭配置ENTRYPOINT [java,-Xms256m,-Xmx512m,-jar,/app/demo.jar]这里注意一点Java容器的内存参数不要只设置堆最好把容器总内存限制也交给Docker控制否则容易出现容器内Java进程不受控的情况。我在实际项目中用-XX:MaxRAMPercentage75.0替代固定堆内存让JVM根据容器限制自动调整堆大小线上表现稳定很多。6.3 团队协作与代码规范最后分享一个从经验里总结的团队协作建议。MyBatis最容易被忽视的是XML文件和Mapper接口的命名规范。我要求团队所有XML文件以实体名命名Mapper接口必须在文件名上就能看出对应关系比如UserMapper与UserMapper.xml必须一一对应。同时每个Mapper接口的类头注释里标明业务场景XML的SQL里对复杂语句加上注释说明业务意图和涉及的表。这样做的目的很朴素三个月后回来看代码的同事或者自己不用靠猜就能重建上下文。MyBatis的SQL是透明的这层透明不利用起来就太可惜了。还有一个小技巧如果项目长期由多人维护建议在CI流程里加入MyBatis SQL的静态检查。网上有现成的MyBatis SQL拦截插件在测试阶段做一次真实执行用EXPLAIN验证关键查询是否走了索引。我用这个方式拦截过好几条线上才会暴露的大表全扫描SQL省下的运维成本远超引入这层检查的成本。回到开头说的那个老项目迁移完成到现在跑了快一年最深的体会是MyBatis Spring Boot这套组合能力上限很高但大多数问题的根源不在框架本身而在使用者是否理解边界。缓存边界、分页插件时序边界、事务边界、版本兼容边界每一条边界都是一道关卡。把边界理清楚你会觉得这套工具是真的顺手而不是在网上搜一段配置粘贴了事。最后补一个我自己常用的辅助手段在本地开发时把logging.level设置成trace然后完整刷一遍查询接口观察SQL的执行次数。如果发现同一数据被查询了多次多半就是N1问题或者缓存没有正确利用这种肉眼排查的方式比任何工具都直接。做完这一步再去设计缓存策略方向基本上不会偏。