
1. Spring Boot中集成MyBatis究竟应该怎么选型先交代一下我的背景我从SSM时代就开始用MyBatis后来Spring Boot流行起来第一件事就是把老项目往Boot上迁移。最近几年带团队面试也经常被问到“Spring Boot里MyBatis到底怎么用”“MyBatis和JPA怎么选”所以这篇东西我打算按照我实际项目里的使用经验来写而不是把官方文档翻译一遍。如果你刚接触这套组合别慌看完你不仅能跑通一个最基础的增删改查还能理解Mapper接口为什么不需要写实现类、一级缓存和二级缓存什么时候会失效、TypeHandler到底在什么场景能救命。如果你是老手重点看第4节缓存机制和第6节问题排查这两个部分我踩过的坑比较多写出来的东西应该能帮大家少走弯路。先说结论Spring Boot MyBatis的组合在当前企业级Java后端开发里依然是“快速出活”的主力方案。Spring Boot解决了框架配置的繁琐MyBatis解决了SQL灵活控制的问题两者互补性很强。相比JPA那种“面向对象操作数据库”的思路MyBatis把SQL的执行权完全交还给你对于复杂查询、多表关联、报表统计这类场景控制力是JPA很难比拟的。2. 从零开始搭建一个可运行的Spring Boot MyBatis项目2.1 依赖引入的几个细节用Spring Boot集成MyBatis核心依赖不是mybatis本身而是官方提供的启动器mybatis-spring-boot-starter。我见过不少新手直接引入mybatis和mybatis-spring然后自己手工配置一堆Bean这在早期确实可行但既然用了Spring Boot就应该用它特有的自动配置机制来简化工作。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注意几个点第一版本号不写的话Spring Boot的依赖管理会自动帮你选一个匹配的版本但建议还是显式声明方便追踪。第二MySQL 8以上版本的驱动类名是com.mysql.cj.jdbc.Driver不是老的com.mysql.jdbc.Driver这个很多教程没更新照抄老配置会导致连接报错。第三如果你的项目是Spring Boot 3.x那么starter要用3.0对应版本因为底层做了Jakarta包名的迁移。2.2 application.yml里的核心配置配置文件是很多人容易图省事只写数据源其实MyBatis相关属性值得认真调一调。我习惯的配置模板长这样spring: datasource: url: jdbc:mysql://localhost:3306/test_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl逐一说明mapper-locations指定XML文件的位置。有人问为什么不写也能跑因为默认值是classpath:mapper/*.xml你只要保证XML都在这个目录下就行。我的习惯是显式写明防止以后调整目录结构时忘了改。type-aliases-package实体类的别名包开启后XML里写resultType可以直接用类名不用带全限定名。map-underscore-to-camel-case下划线转驼峰映射。数据库字段create_time自动映射到Java属性createTime不开启就等着字段全是null吧。log-impl控制台打印SQL。开发环境强烈建议打开线上则改成Slf4jImpl配合日志框架统一管理。提示configuration节点里的配置项实质上对应MyBatis的org.apache.ibatis.session.Configuration类的setter方法如果你对MyBatis原生配置项足够熟悉在这里配置的逻辑是一样的。2.3 最小可运行的代码骨架一个最简项目至少要包含实体类、Mapper接口、XML文件或注解SQL、Service层、Controller层。我直接贴核心代码。实体类public class User { private Long id; private String username; private String email; private Date createTime; // getter/setter 省略 }Mapper接口Mapper public interface UserMapper { User selectById(Param(id) Long id); ListUser selectAll(); int insert(User user); int update(User user); int deleteById(Long id); }XML文件放在resources/mapper/UserMapper.xml?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.demo.mapper.UserMapper select idselectById resultTypeUser select * from user where id #{id} /select select idselectAll resultTypeUser select * from user /select insert idinsert parameterTypeUser useGeneratedKeystrue keyPropertyid insert into user(username, email) values(#{username}, #{email}) /insert update idupdate update user set username #{username}, email #{email} where id #{id} /update delete iddeleteById delete from user where id #{id} /delete /mapper有人问Mapper接口不写实现类也能工作原理是什么这就要说到MyBatis给你生成的代理对象了。Spring Boot启动时MapperScan或Mapper注解会把接口扫描进来MyBatis通过MapperProxy为每个接口生成一个JDK动态代理。你调用接口方法时代理会根据方法名去namespace匹配对应的SQL语句执行并返回结果。所以Mapper接口只是一个约定真正的逻辑在XML和代理里。2.4 Service和Controller没什么特别的Service层就是普通Spring Bean注入Mapper即可Service public class UserService { Resource private UserMapper userMapper; public User getUserById(Long id) { return userMapper.selectById(id); } }Controller简单暴露接口这样整套请求链路就通了。到这里一个小而完整的项目已经能跑通注册、查询、修改、删除的基本功能对应很多热词里提到的“第1关项目整合 - springboot mybatis”和“第2关使用springboot mybatis实现一个最简单的注册功能”其实本质就是把上面这套骨架走一遍。3. 深入原理Spring Boot启动时MyBatis到底做了什么3.1 自动配置的装配链路很多人用MyBatis用得很溜但问到底层发生了什么就说不清了。这块其实是面试高频点也是排查问题的基础。我梳理一下启动时的关键动作。mybatis-spring-boot-starter里的MybatisAutoConfiguration是核心。它做了以下几件事第一构造SqlSessionFactory。SqlSessionFactory是MyBatis所有操作的入口它读取你配置的mapper-locations、type-aliases-package、configuration等属性解析XML里的SQL语句构建出完整的MappedStatement集合。第二注册SqlSessionTemplate。SqlSessionTemplate可以理解为线程安全的SqlSession代理。MyBatis原生使用的DefaultSqlSession不是线程安全的每个线程应该独立持有但在Spring管理下SqlSessionTemplate通过对SqlSession的代理和事务同步解决了这个问题。第三扫描Mapper接口。通过MapperScan或MapperSpring把这些接口注册为Bean。注意这时的Bean实际上是一个FactoryBean生产的是前面提到的代理对象。整个链路可以用一句话概括Spring Boot启动时解析你的配置生成SqlSessionFactory然后用工厂创建代理对象注入到使用Mapper的地方。理解了这条链路后面遇到“Mapper无法注入”“SQL语句找不到”“配置文件不生效”这类问题你排查起来就有方向了。3.2 MapperProxy与SQL执行过程每次你调用userMapper.selectById(1L)时具体发生了什么首先MapperProxy拦截到这次调用发现对应的MapperMethod已经缓存了方法的解析结果。MapperMethod里有SqlCommand它记录了这条SQL的类型是SELECT还是UPDATE以及对应的MappedStatement的id。接着通过SqlSessionTemplate获取SqlSession执行selectOne或update方法。执行时会通过参数处理器把Java对象转换成JDBC能识别的类型再交给数据库驱动执行。返回结果经过ResultSetHandler映射成实体对象但这步涉及的细节很多比如自动映射如何处理下划线、类型转换如何判断INTEGER到Long的转换等。这里面有一个非常实用的点MyBatis对参数的处理。#{}和${}的区别很多人云亦云。#{}使用的是PreparedStatement的占位符数据库会预编译能有效防SQL注入${}是字符串拼接直接把值拼进SQL存在注入风险。但在动态排序order by ${column}场景中排序字段不能作为占位符只能用${}这时候你就必须在业务层做白名单校验。我项目里通常维护一个允许排序的字段集合防止用户传入任意字符串。3.3 XML初始化与Configuration对象的角色之前列出的热词里出现了mybatis中xmlconfigbuilser、mybatis中基于xml配置的初始化工作原理这些引出了MyBatis底层初始化常谈的XMLConfigBuilder。它本质是负责把XML配置解析为Configuration对象。但要注意Spring Boot整合场景下大多数配置已经通过Spring Boot的配置项处理XMLConfigBuilder更多是MyBatis单独使用时的主角。我们在Spring Boot里遇到的摸错摸错通常还是去检查mybatis.configuration.*前缀的属性是否生效。有个值得养成的习惯在项目启动后打印一份SqlSessionFactory对象看看它内部的Configuration对象包含了哪些MappedStatement。写一个ApplicationRunner在启动完成后遍历Component public class SqlCheckRunner implements ApplicationRunner { Resource private SqlSessionFactory sqlSessionFactory; Override public void run(ApplicationArguments args) { Configuration configuration sqlSessionFactory.getConfiguration(); configuration.getMappedStatementNames().forEach(System.out::println); } }这个方法能帮你确认XML是否被成功解析、接口和Statement是否注册成功。如果XML有语法错误Boot启动时就会直接报错但如果是Statement命名空间不对错误可能要等到调用时才暴露。提前打印能少很多排查时间。4. MyBatis缓存机制一级缓存和二级缓存的实践与陷阱4.1 一级缓存为何“查不到最新数据”MyBatis的一级缓存默认是开启的作用范围是SqlSession。在Spring Boot里SqlSession的生命周期跟着事务走。如果你在一个不使用事务的Service方法里连续调用两次selectById(1L)由于每次调用都是在同一个SqlSessionTemplate里执行的如果中间没有update/insert/delete操作第二次查询会直接命中缓存不会真正访问数据库。这个特性有好有坏。好在同一个请求内相同参数的重复查询性能很高坏在如果你在同一事务里先查了数据然后其他线程修改了数据库你的事务里再查还是旧数据。这种情况往往让人摸不着头脑。排查思路是看日志如果第二条查询没有打印SQL语句说明它走了一级缓存。如果你希望强制刷新可以在Mapper方法上加flushCachetrue或者让查询方法带上Options(flushCache Options.FlushCachePolicy.TRUE)。不过我的建议是不要过度依赖一级缓存它的生命周期太短控制不了反而容易出问题。4.2 二级缓存根治你的“查询快”还是“脏数据”选择题二级缓存是跨SqlSession的作用范围是同一个namespace即同一个Mapper接口。默认不开启需要显式配置。配置方式分两步首先在Mapper XML中添加缓存声明mapper namespacecom.example.demo.mapper.UserMapper cache evictionLRU flushInterval60000 size512 readOnlytrue/ /mapper然后在需要缓存的查询语句上设置useCachetrue默认就是true。eviction选项有LRU、FIFO、SOFT、WEAK几种。实际项目我用得最多的是LRU即最近最少使用缓存满后淘汰最久没使用的数据。flushInterval设置缓存刷新间隔单位毫秒。size是缓存容量。readOnlytrue表示返回的缓存对象直接复用性能好但修改会影响缓存内容readOnlyfalse会做序列化拷贝安全但性能差。实体类要实现Serializable接口才能支持读写缓存。二级缓存最大的坑在于同一个namespace的insert/update/delete会自动清空缓存但如果你在别的Mapper里直接更新了这张表的数据比如UserLogMapper里通过联表更新了user表当前namespace的二级缓存根本感知不到就会返回脏数据。所以如果你的表被多个Mapper操作开二级缓存要非常谨慎。我通常只在数据很少变化、且只被单一Mapper访问的字典表上开启二级缓存业务表一律不开。4.3 缓存总结速查表项目一级缓存二级缓存默认状态开启关闭作用范围SqlSessionnamespace级别生命周期短事务或会话结束即失效长直到flushInterval或被清空冲突风险同事务内读到旧数据跨Mapper更新会被无视适用场景默认即可低频更新的字典表很多面试题会把一级缓存、二级缓存、cache-ref、CacheNamespace等等打包考你核心不是背概念而是说明清楚在Spring Boot的项目中缓存生效的边界在哪里——因为Spring Boot的事务管理方式决定了SqlSession的边界这个边界找对了缓存问题就解了一半。5. TypeHandler为什么它能让你少写几十行转换代码5.1 什么是TypeHandler何时需要自定义Java类型和JDBC类型之间不是天然一一对应的。比如数据库有一个json字段你希望映射到Java的MapString, Object或者你存了一个枚举类型希望用数字存进数据库但Java代码里还是枚举对象。这些场景默认的TypeHandler无法覆盖你就需要自定义。MyBatis里处理类型转换的组件就叫TypeHandler核心接口就两个方法setParameter把Java值转成JDBC值getResult把结果集的值转成Java值。最常见的自定义TypeHandler之一就是枚举映射。我举一个实际例子假设有订单状态枚举OrderStatus { PENDING, PAID, SHIPPED, COMPLETED }你希望在数据库里存一个int0/1/2/3Java里直接操作枚举。5.2 自定义枚举TypeHandler实例实现如下MappedTypes(OrderStatus.class) MappedJdbcTypes(JdbcType.INTEGER) public class OrderStatusTypeHandler extends BaseTypeHandlerOrderStatus { Override public void setNonNullParameter(PreparedStatement ps, int i, OrderStatus parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter.ordinal()); } Override public OrderStatus getNullableResult(ResultSet rs, String columnName) throws SQLException { int code rs.getInt(columnName); return OrderStatus.values()[code]; } Override public OrderStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException { int code rs.getInt(columnIndex); return OrderStatus.values()[code]; } Override public OrderStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { int code cs.getInt(columnIndex); return OrderStatus.values()[code]; } }注册方式有两种一种是在配置里指定mybatis: type-handlers-package: com.example.demo.handler另一种是在XML里显式指定resultMap idOrderResultMap typeOrder result columnstatus propertystatus typeHandlercom.example.demo.handler.OrderStatusTypeHandler/ /resultMap我个人的建议是能用type-handlers-package扫描就尽量扫描因为一旦某个实体字段忘了指定TypeHandler默认会按Integer处理大概率报错或不生效。扫描方式全局生效比较省心。5.3 工作中的实际收益用了TypeHandler之后Service层的代码干净很多。以前你要手动判断枚举的code值现在直接拿枚举对象做逻辑判断赋值也直接传枚举。持久层、业务层、展示层之间的类型是统一的维护成本直线下降。另外再分享一个复杂场景数据库存的JSON字符串想直接映射成Java的JSONObject。项目里我实现过一个JsonTypeHandler内部用的是Hutool或FastJSON解析字符串查询时把字符串parse成JSONObject插入时toJSONString。这套方案在配置表、扩展字段这类场景里非常实用替我省了不知道多少VO转换代码。6. 常见问题与排查技巧实录6.1 Mapper接口无法注入报错信息一般是Consider defining a bean of type xxxMapper in your configuration。原因和解决办法忘了在主类上使用MapperScan(com.example.mapper)或者Mapper接口上没标Mapper单纯检查一下扫描包的路径对不对别写错包名。有多个数据源时要指定不同的MapperScan和sqlSessionFactoryRef这块我改天单独写一篇。6.2 SQL参数条件不生效热词里有“mybatis条件不生效”这属于动态SQL常见问题。我列几个典型第一if testname ! null里判断的是JavaBean的属性不是数据库列名。你如果写testuser_name ! null这不成立因为实体里没有userName属性或字段名不匹配条件永远为false。第二传参是单个基础类型没有加Param。比如ListUser selectByName(String name)XML里写#{name}在低版本MyBatis里会报错找不到参数虽然新版本默认用param1这样也能取到但可读性太差。建议单参数也加Param(name)。第三if标签内部的条件写错了逻辑。最常见的莫过于有“查询全部”和“按条件查询”切换时忘记处理空值导致SQL语法错误。这里有个技巧用where标签替代手工写where 11它会自动移除前缀的WHERE或者多余的AND/OR。select idselectByCondition resultTypeUser select * from user where if testusername ! null and username ! and username like concat(%, #{username}, %) /if if testemail ! null and email #{email} /if /where /selectwhere标签会自动处理第一个and你不需要在SQL里写死where 11体验好很多。6.3 打印SQL却看不到参数值如果你配置了log-impl: StdOutImpl控制台会打印SQL语句但它打印的是带?占位符的SQL和参数列表参数值是在下一行以Parameters:开头的。新手容易以为自己SQL写错了其实只要看到Parameters: [1, zhangsan]这行就说明参数绑定成功。多提一嘴如果你想看完整的可执行SQL可以配置MyBatis的configuration.log-impl配合p6spy这类工具来实现不过我一般调试阶段用StdOut就够了线上用日志文件。6.4 版本冲突与Spring Boot 3.x迁移热词里提到了“springboot版本太高”和一些新版本适配问题。Spring Boot 3.0之后javax命名空间迁移到了jakarta很多老项目升级时Mapper接口本身没大问题但如果有代码直接用了javax.annotation.*、javax.sql.*编译期就会报错。另外MyBatis官方starter对Spring Boot 3的支持直到3.0.x版本才跟上强烈建议升级前先查一下兼容矩阵Spring Boot版本推荐starter版本2.x2.3.x3.x3.0.x3.23.0.4或3.0.56.5 JDBC连接配置的隐藏坑数据库URL里一定记得加serverTimezoneAsia/Shanghai否则MySQL 8以上版本连接时会因为默认时区问题抛异常。另外useSSLfalse建议显式写本地开发环境没有SSL证书时true会告警甚至报错。6.6 简单问题速查表现象常见原因建议动作实体属性全是null未开启下划线转驼峰map-underscore-to-camel-case: true控制台没有SQLlog-impl未配置配置StdOutImplMapper无法注入缺扫描或注解加MapperScan或Mapper动态SQL条件不生效条件判断属性名写错检查test中Java属性名批量插入太慢使用单条insert语句用foreach批量拼接或ExecutorType.BATCH7. 性能优化与最佳实践建议7.1 批量操作的正确打开方式很多初学者批量插入是循环调insert这会导致数千次网络往返和事务开销。正确做法有两种第一种XML里的foreach批量拼接insert idbatchInsert insert into user(username, email) values foreach collectionlist itemuser separator, (#{user.username}, #{user.email}) /foreach /insert注意foreach拼接的SQL字符串不能超过数据库的max_allowed_packet限制一般几百上千条没问题再多就需要分批了。第二种MyBatis内置的ExecutorType.BATCH。在Spring Boot里不用手工操作SqlSession重写一个事务模板即可相关代码网上都有完备方案。我项目里的经验是几百条以内用foreach几千条以上用BatchExecutor且必须配合事务否则BatchExecutor的SQL不会真正执行。7.2 善用MapperScan和Mapper的区别Mapper加在单个接口上MapperScan加在配置类或主类上批量扫描包。两者效果一样只是粒度不同。我建议在启动类上用MapperScan(com.example.mapper)这样每个Mapper接口不用重复标注代码干净些。如果是多数据源场景MapperScan还能指定对应的sqlSessionFactoryRef比单用Mapper灵活得多。7.3 设计Mapper层时XML好还是注解好这是个老生常谈但我的观点很明确复杂SQL用XML简单CRUD用注解。注解方式Select、Insert等优点是零配置、快速适合两三个字段的简单查询但只要SQL变长、涉及动态条件注解里的script标签写起来极其痛苦维护性也很差。XML方式虽然多了个文件但语法提示、格式化、版本管理都友好团队协作时更清晰。我甚至还见过有人把注解SQL写到几百行纯粹是给后人埋坑。7.4 监控SQL执行时间与慢SQL排查MyBatis不支持直接打印耗时但你可以通过MybatisInterceptor实现一个简单的MyBatis插件统计每个Mapper方法的执行时间。核心思路是实现Interceptor接口在invoke方法里记录方法调用的起止时间。这个插件对排查慢SQL特别有用尤其是接口响应时间突然变长时你能快速定位是数据库慢还是业务逻辑慢。关于插件有一个细节必须注意插件可以拦截Executor、ParameterHandler、ResultSetHandler、StatementHandler但不要随意拦截Executor执行之外的地方否则容易导致NPE。7.5 多数据源场景下的注意事项如果你的系统需要同时连多个数据库比如主从分离或者业务库加统计库MyBatis集成复杂度会立刻上升。你需要为每个数据源独立配置DataSource、SqlSessionFactory、MapperScan指向不同的Mapper包。很多踩坑的人往往忘了为每个数据源设置独立的transactionManager导致事务只对默认数据源生效。Spring Boot里推荐使用Primary标注主数据源再用Qualifier指定次数据源的Bean名称。这块不是本文主旋律但我还是提醒一句不要等项目上线了才想起来多数据源问题架构设计阶段就该把Mapper包按数据源边界拆分清楚。8. 我的近十年使用体检什么情况下我会弃用MyBatis刚才提到我一直在用MyBatis但不代表它适合所有项目。如果你遇到以下情况建议考虑其他方案第一你只需要极简单的CRUD且不需要复杂的动态SQL团队又不熟悉SQL调优用Spring Data JPA反而效率更高。JPA的自动建表能力在项目早期很舒服。第二你的项目以报表和数据仓库为主查询都是稀碎的外连接、窗口函数和超大结果集MyBatis虽然能写但逐条映射的开销不低这时候不如直接上Jooq或者干脆用SQL模板引擎配合JDBC。第三你的团队完全没有SQL功底写出来的SQL连索引都不会走那用MyBatis只会加速生产事故。但我个人体会是大多数业务系统、权限系统、工作流平台、后台管理系统MyBatis的灵活性和可控性恰好是最匹配的。你把SQL握在自己手里数据库索引设计和查询优化都有明确的发力点排查问题也直截了当——看SQL一眼基本就能判断是索引没走还是条件写错了。相比之下我在几个JPA项目里遇到“明明看着是同一个查询结果却多出一堆关联条件”的诡异问题时调试成本真心不低。最后再分享一个日常开发的小习惯每次新建Mapper或改动XML后先运行一遍项目里最小的测试用例确认SQL能正确解析而不是等到联调阶段才暴露。写这篇文章时我还在项目里保留了一个SqlCheckRunner的启动检查类作用就是打印所有已注册的Statement名称谁动过XML启动时立刻见分晓。这套组合拳打下来Spring Boot MyBatis的项目我基本没有遇到过部署后才发现的SQL配置问题希望对你们也有帮助。