最近后台收到不少私信问的基本上都是同一个问题Spring Boot 和 MyBatis 到底该怎么搭、怎么用才不算白用。不管是刚毕业准备写简历项目的学生还是工作中被迫从 JPA 转过来的同事好像都在这个组合上卡过壳。今天就把我这些年用 Spring Boot MyBatis 做项目的经验一次性倒出来从环境搭建、核心配置、Mapper 写法到缓存机制、日志打印、常见坑位全部捋一遍。这篇东西不端着全是实操层面的东西你跟着操作就能跑起来。1. 为什么是 Spring Boot MyBatis而不是别的组合1.1 这个组合解决的到底是什么问题先说个场景。你写一个大学生就业推荐系统背后要查学生表、岗位表、投递记录表还得按专业匹配度、技能标签做筛选排序。这种需求如果用 JPA 硬写关联查询和动态 SQL 能把你逼疯。MyBatis 的好处就在于它把 SQL 的控制权完全还给你SQL 怎么写、怎么优化、怎么走索引全在你手里攥着不会被 ORM 的自动生成逻辑带偏。Spring Boot 的角色更简单粗暴把以前 Spring 那一堆 XML 配置、Bean 定义、数据源管理全自动收拾干净。你只需要引入 starter把数据源信息写进配置文件剩下的交给自动配置。这个组合的核心价值就是八个字配置极简SQL 可控。1.2 相比 JPA / Hibernate 的核心优势我做过的项目里但凡涉及多表关联、分组统计、复杂子查询的用 MyBatis 写起来都明显更顺手。JPA 那种对象导航式查询在小表单查时确实爽但一旦业务复杂起来生成的 SQL 性能不可控排查问题还得先去翻它生成的 SQL 日志。MyBatis 是半自动映射SQL 自己写结果集映射可以通过配置控制。这意味着你写的每一句 SQL 你心里都有数SQL 跑多快、能不能命中索引一眼就能看明白。用大白话说JPA 是给你请了个替你写 SQL 的助理MyBatis 是把 SQL 的笔递到你手里你自己写。1.3 适用场景和选型判断适合互联网项目、管理系统、报表统计、数据处理平台凡是 SQL 复杂度高的地方MyBatis 都是首选。适合团队里 DBA 和开发协作紧密的项目SQL 可以单独交给 DBA 审查优化。不适合纯 CRUD 的极简应用、原型验证类项目用 JPA 会更快。我自己判断选型的标准很简单如果这个项目的核心价值在业务逻辑而非数据查询我选 JPA如果核心价值在数据统计分析、复杂查询、报表我选 MyBatis。就业推荐系统这种数据关联复杂的项目明显属于后者。2. 环境准备与快速搭起一个可运行的项目2.1 版本选型不踩坑Spring Boot 和 MyBatis 的版本兼容问题坑过不少人。我常用的稳妥组合是Spring Boot 2.7.x mybatis-spring-boot-starter 2.3.x这俩搭配经过大量生产环境验证稳定得让人放心。如果你的项目是 Spring Boot 3.x那要特别注意jakarta 命名空间变了需要引入mybatis-spring-boot-starter 3.0.x版本。我第一次把项目从 2.x 升到 3.x 时直接复制旧依赖结果启动直接报ClassNotFoundException: javax.servlet查了半天才发现是命名空间问题。表格对比一下组件Spring Boot 2.xSpring Boot 3.xJava 版本8 / 1117命名空间javax.*jakarta.*MyBatis Starter2.3.x3.0.x数据库驱动mysql-connector-javacom.mysql.cj.jdbc.Driver2.2 Maven 依赖引入可直接抄dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency说下为什么加 Druid。MyBatis 本身不管理数据库连接池它需要一个 DataSource。Spring Boot 默认的 HikariCP 确实性能好但阿里 Druid 带监控页面可以实时看 SQL 执行情况、慢查询统计、活跃连接数。排查线上问题的时候那个监控页能救命。2.3 application.yml 配置详解server: port: 8080 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/job_recommend?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: root druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.job.entity configuration: map-underscore-to-camel-case: true cache-enabled: true lazy-loading-enabled: true aggressive-lazy-loading: false log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这几项配置每个都有说头。map-underscore-to-camel-case是必须开的否则数据库的user_name映射不到 Java 的userName属性上你会看到一堆 null 字段。lazy-loading-enabled和aggressive-lazy-loading这对参数要配合使用lazy-loading-enabled开启动态代理延迟加载aggressive-lazy-loading设置为 false 才能保证真正按需加载不然一个懒加载属性被访问其它懒加载属性也会跟着被加载白费功夫。log-impl配成StdOutImpl是开发阶段用的SQL 会直接打到控制台方便调试。生产环境建议换掉后面会讲。3. Mapper 层的核心细节与动态 SQL3.1 Mapper 与 MapperScan 的区别这俩注解的区别很多新手搞不明白。Mapper加在单个 Mapper 接口上告诉 Spring 这个接口要生成代理实现类并注册为 Bean。MapperScan加在启动类或配置类上指定一个包路径一次性扫描包下所有 Mapper 接口。SpringBootApplication MapperScan(com.example.job.mapper) public class JobApplication { public static void main(String[] args) { SpringApplication.run(JobApplication.class, args); } }日常开发我建议直接用MapperScan。项目一大了 Mapper 可能有几十个每个都加Mapper不仅累还容易漏。另外注意MapperScan扫描的包路径别写太宽扫到不是 Mapper 的接口会报错。3.2 #{} 和 ${} 的区别面试必考也是事故高发区#{}是预编译占位符MyBatis 会把它替换成?然后通过PreparedStatement传参天然防 SQL 注入。${}是字符串拼接直接把值拼进 SQL 里存在注入风险。!-- 安全写法 -- select idfindByName resultTypeStudent SELECT * FROM student WHERE name #{name} /select !-- 危险写法 -- select idfindByName resultTypeStudent SELECT * FROM student WHERE name ${name} /select那$ {}就完全不能用吗也不是。动态排序、动态表名这种场景只能用${}比如ORDER BY ${sortField}因为排序字段没法用占位符。但传入的值绝对不能来自用户直接输入必须经过白名单校验。3.3 动态 SQL 实战就业推荐系统的筛选查询拿就业推荐系统的核心功能举例——按条件筛选岗位。学生可能按城市筛、按薪资范围筛、按技能标签筛条件组合还不固定。动态 SQL 就是干这个的。select idsearchJobs resultTypecom.example.job.entity.Job SELECT * FROM job where if testcity ! null and city ! AND city #{city} /if if testminSalary ! null AND salary #{minSalary} /if if testmaxSalary ! null AND salary lt; #{maxSalary} /if if testskillTag ! null and skillTag ! AND skill_tags LIKE CONCAT(%, #{skillTag}, %) /if /where ORDER BY post_date DESC LIMIT #{offset}, #{pageSize} /selectwhere标签会自动处理掉第一个条件前面的AND这招比手动拼接WHERE 11优雅得多。另外注意salary lt; #{maxSalary}这里XML 里必须转义成lt;或者用![CDATA[ ]]包起来不然 XML 解析直接报错。3.4 批量插入的正确姿势批量插入毕业生数据时最坑的写法是在 Java 里循环调用单条 insert几千条数据能跑半天。正确姿势是 foreach 批量插入insert idbatchInsertStudents INSERT INTO student (name, major, graduation_year, phone) VALUES foreach collectionlist itemstudent separator, (#{student.name}, #{student.major}, #{student.graduationYear}, #{student.phone}) /foreach /insert这里有个 MySQL 连接参数的坑rewriteBatchedStatementstrue必须加到 JDBC URL 上否则 MySQL 驱动默认还是逐条执行批量语句性能提升不明显。这个参数很多人不知道加完之后批量插入速度那叫一个质的飞跃。4. MyBatis 缓存机制一级缓存、二级缓存与生产事故4.1 一级缓存MyBatis 一级缓存是 SqlSession 级别的默认开启没法关闭。同一个 SqlSession 里执行两次相同的查询第二次会直接走缓存不再查数据库。Spring Boot 整合 MyBatis 后默认 SqlSession 的生命周期由 Spring 管理每次 Mapper 方法调用都会开启新的 SqlSession除非配置了事务。这导致一级缓存基本处于每次查询都失效的状态。我见过有人在这个坑上栽过方法内部连续查两次相同数据以为走缓存很快结果每次都打数据库。需要注意一级缓存的有效范围仅限同一个 SqlSession。Spring 管理下只有加了Transactional的方法多个查询才共享同一个 SqlSession一级缓存才会真正生效。4.2 二级缓存二级缓存是 Mapper 级别的跨 SqlSession 共享。配置需要三处配合mybatis: configuration: cache-enabled: trueMapper XML 里加cache evictionLRU flushInterval60000 size512 readOnlytrue/实体类实现 Serializablepublic class Student implements Serializable { private static final long serialVersionUID 1L; // ... }eviction是回收策略LRU表示最近最少使用flushInterval是缓存刷新间隔单位毫秒size是缓存最大对象数readOnly为 true 时直接返回缓存对象的同一个实例性能好但外部修改会影响缓存数据false 时会序列化反序列化拷贝一份安全但开销大。4.3 缓存引发的事故案例我实际遇到过就业推荐系统里用户修改了个人信息提交后页面还是旧数据。排查半天发现问题出在二级缓存更新操作没有触发缓存刷新。原因很简单如果 Mapper XML 中定义了cache但更新操作的 SQL 和查询操作不在同一个 Mapper 命名空间或者 XML 没有对应的 update 语句MyBatis 就无法自动清空该命名空间下的缓存。MyBatis 的缓存清理机制是只要命名空间下有任一 insert/update/delete 执行就会清空该命名空间下的所有缓存。如果你有跨表更新的情况比如更新学生表的同时要清岗位推荐表的缓存必须手动调用SqlSession.clearCache()或者通过CacheNamespace配置关联。我的建议是业务复杂的项目二级缓存能不开就别开。数据库撑不住的时候先优化 SQL、加索引都还不行再考虑缓存中间件别在 MyBatis 二级缓存上死磕。真要开只给那些几乎不变的表开比如专业字典表、城市表。4.4 整合 Redis 替代二级缓存生产环境我更推荐把缓存从 MyBatis 里剥离出来用 Redis 自己做。逻辑控制更灵活不会出现上面那些隐性问题Service public class JobService { Autowired private StringRedisTemplate redisTemplate; Autowired private JobMapper jobMapper; private static final String JOB_CACHE_KEY job:detail:; public Job getJobDetail(Integer jobId) { String key JOB_CACHE_KEY jobId; String json redisTemplate.opsForValue().get(key); if (json ! null) { return JSON.parseObject(json, Job.class); } Job job jobMapper.selectById(jobId); redisTemplate.opsForValue().set(key, JSON.toJSONString(job), 30, TimeUnit.MINUTES); return job; } }这种方案的优点缓存过期时间你说了算数据一致性控制代码里看得见摸得着还不需要考虑 MyBatis 那套序列化约束。真遇到缓存穿透加个布隆过滤器或缓存空值就解决了。5. 打印 SQL、参数与结果集日志配置一次到位5.1 方案一mybatis.configuration.log-impl前面的 yml 配置已经写了这个方案的原理是让 MyBatis 直接输出 SQL 到控制台不走日志框架。简单粗暴开发环境够用。但有个问题无法按 logger 过滤日志量一大就闹心生产环境很容易刷屏。5.2 方案二mybatis-plus 风格的 log-impl无加号如果项目引入了 MyBatis-Plus可以用mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl效果和方案一一样只是配置前缀变了。这俩方案本质相同输出格式上 MyBatis-Plus 会带点额外信息。5.3 方案三Logback 配置指定 Mapper 日志级别生产推荐这是我最推荐的方案。不改 MyBatis 配置只在 logback-spring.xml 里加一行logger namecom.example.job.mapper levelDEBUG/这样配置的好处只把 Mapper 包下的日志级别提到 DEBUG 输出 SQL其它包的日志级别不受影响。生产环境排查问题时可以动态调这个 logger 的级别不用重新发版。配合日志采集系统SQL 日志可以单独归档复盘问题时非常方便。需要说明的是使用 Logback 输出 SQL 日志日志信息不像 StdOutImpl 那样直接打印 Preparing: xxx 到控制台而是以 DEBUG 级别输出格式形如 Preparing: SELECT * FROM student WHERE id? Parameters: 1(String)需要在日志配置中开启 MyBatis 日志实现为 Logback 支持。如果你之前把log-impl显式配成了StdOutImpl那 Logback 的 logger 配置就不会生效二者只能选一个。正确做法是生产环境用 Logback 方案去掉log-impl配置。5.4 打印 MyBatis 实际执行的 SQL 语句参数太多看不清怎么办有时候 SQL 参数十几个控制台输出格式混乱看不出哪个参数对应哪个占位符。这时候可以在 logback 配置里调整 patternpattern%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n/pattern消息部分完整输出参数和 SQL 分行显示 Parameters: xxxx单独一行。如果还嫌乱可以自己写一个 MyBatis Interceptor在执行前把 SQL 和参数拼成一条可执行语句直接打出来Intercepts({ Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class}) }) public class SqlLogInterceptor implements Interceptor { // 从 BoundSql 获取 SQL 和参数拼接后打印 }这里有个点要说明不同的 MyBatis 版本中 Interceptor 拦截的方法签名略有差异上面示例是 3.x 系列的标准写法。真要自己写拦截器先确认你用的版本。6. 常见问题与排查技巧实录6.1 提示 Invalid bound statement (not found)这个报错 90% 是 Mapper 接口和 XML 没对上。检查顺序XML 文件是否在mapper-locations配置的路径下。XML 的namespace是否和接口全限定名一致。select的id是否和接口方法名一致。Mapper 接口是否被扫描到启动类有没有MapperScan。我踩过一次最隐蔽的坑在 IDEA 里 XML 文件放在 src/main/java 目录下和 Mapper 接口同一个包结果打包后 target 目录里没有 XML。Maven 默认只把 resources 目录下的 XML 打进去。解决方法是 pom.xml 里加resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources或者老老实实把 XML 放在 src/main/resources/mapper/ 下一劳永逸。6.2 数据库查出来 LocalDateTime 字段丢失数据库字段是create_time实体属性是createTime配置了map-underscore-to-camel-case后还是 null首先确认 MySQL 驱动版本新版驱动对LocalDateTime支持没问题问题多半出在 SQL 里用了SELECT *而表里字段名和实体属性名映射对不上。解决办法要么 SQL 里显式写别名SELECT create_time AS createTime要么确保实体属性命名规范。这个问题的根因是 MyBatis 的自动驼峰映射只对列名含下划线生效如果列名本来就叫createtime没下划线那映射规则就失效了必须在 SQL 里手动写别名。6.3 分页插件 PageHelper 不生效使用 PageHelper 时最常见的错误是 startPage 和查询方法之间隔着别的 SQL 查询。记住一条铁律startPage 之后必须是紧跟的第一条查询语句。// 正确 PageHelper.startPage(pageNum, pageSize); ListStudent list studentMapper.selectAll(); // 错误 PageHelper.startPage(pageNum, pageSize); someOtherMapper.queryCount(); // 这条会被拦截分页信息就乱了 ListStudent list studentMapper.selectAll();另外 PageHelper 只对SELECT生效对存储过程、复杂子查询有可能分页结果不对这种场景建议自己写 LIMIT 分页别绕弯子。6.4 打印 SQL 配置不生效我见过一个项目log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl配了application.yml 里也配置了logging.level的 mapper 包路径为 DEBUG但控制台就是没 SQL。排查后发现是 logback-spring.xml 里设置了levelINFO覆盖了外部配置。搞定方法确认 logback 配置文件里的根 logger 级别。确认 mapper 包名是否写错一定要写到 Mapper 接口所在的那个包。确认是否配置了log-impl写了StdOutImpl之后 logging.level 就不会走日志框架了。6.5 数据库连接池连接耗尽报错Connection is not available, request timed out after 30000ms。原因是连接池 max-active 太小或者有连接泄漏。排查命令SHOW PROCESSLIST;看有没有大量 Sleep 状态的连接。如果有查代码里有没有查询操作忘了关闭连接或释放资源。Spring Boot 管理下 SqlSession 会自动关闭但如果你手动创建了 SqlSessionTemplate 或多数据源就可能出现泄漏。Druid 监控页/druid可以带你精准定位活跃连接数、执行频率最高的 SQL这个页面是排查数据库性能问题的神器。6.6 mybatis 面试常见问题速查表问题回答要点#{} 和 ${} 的区别预编译占位符 vs 字符串拼接前者防注入后者用于动态表名排序字段MyBatis 缓存机制一级缓存 SqlSession 级二级缓存 Mapper 级Spring 下注意生命周期MyBatis 和 MyBatis-Plus 区别Plus 增强 CRUD、分页插件、代码生成器但不改变 SQL 控制本质接口和 XML 怎么绑定namespace 对应接口全限定名id 对应方法名参数由接口方法签名传递Mapper 接口能重载吗不能id 必须唯一不允许重载如何实现多表关联查询resultMap 关联映射或 SQL 用 JOIN 后映射到自定义 DTOMyBatis 延迟加载配置 lazy-loading-enabledtrue嵌套查询 select 语句按需触发7. 实操经验总结与深入原理的一点点补充我在实际使用中的体会是这个组合一旦跑顺了是真的省心。从学生就业推荐系统到企业管理后台我做过不少项目数据层全是 MyBatis 一把梭。但省心建立在两个前提上一个是配置文件必须一次到位另一个是 SQL 必须自己心里有数。你说它有没有缺点有。XML 文件多了之后管理是麻烦SQL 没有编译期校验字段拼错了只能运行时发现。所以后来我基本都配合 MyBatis-Plus 一起用单表 CRUD 直接继承 BaseMapper复杂查询仍然手写 XML。既要效率又要控制力这不是二选一的事。Spring Boot 和 MyBatis 这套技术栈的生态太成熟了资料多、社区活跃、能查到的案例也多。关键是你得真正理解它背后的东西SqlSessionFactory 是怎么构建的、Mapper 代理是怎么生成的、缓存是怎么失效的。这些搞清楚之后遇到问题你不会慌——因为你清楚问题出在哪一层。最后送大家一句话框架只是手段SQL 才是王道。你把自己写的每条 SQL 都吃透了这组合就是你的武器不是你的负担。
阅读完成 · 觉得有帮助?