做后端这几年MyBatis和Spring这套组合几乎是绕不开的。从最早自己写JDBC工具类到后来用MyBatis框架再到现在Spring Boot里一行依赖搞定整个整合过程我踩了不少坑也终于把底层那点事理清楚了。网上讲整合的教程很多但大多是照着配置抄一遍至于SqlSessionFactory怎么被Spring管起来的、Mapper接口为什么能被注入、一级缓存二级缓存到底怎么回事很少有文章讲透。这篇我就从整合全流程出发把配置、源码原理、常见坑一次说清楚至少让你看完之后面试被问到MyBatis整合Spring不会再慌。这套流程适合谁刚学完SSM准备做项目的学生、写了两三年CRUD但对底层原理模糊的同学、以及想系统梳理MyBatis配置和缓存机制的后端开发。文章不会只贴配置每个关键点我都会解释为什么这么做以及实际运行时的表现。1. 整合前的准备工作版本搭配与依赖选型1.1 版本搭配是第一道坑很多新手第一次整合MyBatis和Spring失败十有八九是版本不匹配。这里的版本不是指MyBatis本身而是mybatis-spring这个桥接包。MyBatis官方并不直接依赖Spring两者之间靠mybatis-spring来桥接而这个桥接包的版本和Spring大版本有严格的对应关系。以实际项目为例Spring 5.x对应mybatis-spring 2.x系列Spring 6.x对应mybatis-spring 3.x系列。如果你用的是Spring Boot就别自己瞎配了直接用mybatis-spring-boot-starter它会自动带好相匹配的mybatis和mybatis-spring版本。Spring Boot 2.7及以下用2.3.x的starterSpring Boot 3.x用3.0.x的starter。这里有个额外提醒国内不少老项目还在用Spring Boot 2.3.x对应starter建议用mybatis-spring-boot-starter 2.3.1以上否则可能遇到一些兼容性补丁缺失的问题。我的建议是新项目直接用Spring Boot 3.x加MyBatis Starter 3.0.3这一套下载量最大、测试覆盖最广遇到问题也好搜。老项目如果升级Spring版本一定要同步升级mybatis-spring别只升一半不然启动时会出现各种诡异异常比如BeanCreationException或者ClassNotFoundException: org.mybatis.spring.SqlSessionFactoryBean。1.2 整合方式的三条路线MyBatis整合Spring主流路线有三条传统XML配置方式通过applicationContext.xml手动配置SqlSessionFactoryBean和MapperScannerConfigurer早期SSM项目最常见。Java Config方式把XML里的配置改成Configuration类本质逻辑一样只是换了一种写法。Spring Boot自动配置方式引入starter之后框架自动创建SqlSessionFactory和SqlSessionTemplate你只需要配置application.yml和数据源。三条路线底层原理完全一致区别只在于“谁来做组装”。我建议不管你用不用Spring Boot都要先把传统XML方式跑一遍因为那能让你清清楚楚看到每个Bean长什么样也就是能看清整合的本质。用Boot方式是省事但也容易让很多细节变成黑盒。本文我会先把原理讲清楚再给出传统XML和Boot两种落地配置这样你既能看懂底层也能上手干活。2. 整合背后的核心原理从DataSource到Mapper代理2.1 SqlSessionFactory是怎么被Spring收编的先回忆一下不用Spring时MyBatis怎么工作通过mybatis-config.xml构建SqlSessionFactory再用工厂打开一个SqlSession最后从Session里取Mapper执行SQL。每来一个请求你要手动开关Session非常啰嗦。Spring整合之后这些事全交给容器。核心入口是org.mybatis.spring.SqlSessionFactoryBean它实现了Spring的FactoryBean接口。FactoryBean的作用是容器里注册的是SqlSessionFactoryBean这个类但你调用getBean(sqlSessionFactory)时Spring会调用它的getObject()方法把真正构建好的SqlSessionFactory返回给你。这样MyBatis的工厂对象就成了Spring容器里的一个Bean可以被注入到任何地方。这里有个关键点SqlSessionFactoryBean持有DataSource引用也就是数据源由Spring管理MyBatis只负责用这个数据源创建Environment。所以整合之后事务管理器可以同时控制业务代码里的数据库操作和MyBatis的SQL操作这才是Spring统一事务的基础。2.2 Mapper接口为什么能被直接注入这是面试常客也是理解整合的钥匙。日常你写一个UserMapper接口方法定义好注解或XML里写好SQL然后在Service里Autowired直接注入。这背后靠的是JDK动态代理。实现上分两步MapperScannerConfigurer或注解MapperScan扫描指定包下所有接口。对每个接口Spring通过FactoryBean注册一个MapperFactoryBean实例。MapperFactoryBean的getObject()返回一个Proxy对象这个代理的InvocationHandler会把方法调用转发给SqlSession。也就是说注入的UserMapper其实是一个代理对象真正干活的是SqlSession。代理对象每次从SqlSession里拿到对应的MappedStatement执行SQL并做结果映射。我把这个过程用表格拆一下步骤参与对象作用扫描接口MapperScannerConfigurer注册MapperFactoryBean创建代理MapperFactoryBean JDK Proxy生成可注入的Mapper实现方法调用MapperProxy解析方法对应的MappedStatementSQL执行SqlSession Executor查询数据库并映射结果所以Mapper接口本身没有实现类你的Service注入的就是动态代理这个我在后面排查“为什么Mapper无法注入”时还会再用到。理解了这个你就明白为什么Mapper接口不能直接new必须通过扫描注册才能用。2.3 SqlSessionTemplate与线程绑定Spring整合MyBatis之后你执行的每个SQL最终都通过SqlSessionTemplate完成。它替换了原生MyBatis里的DefaultSqlSession因为DefaultSqlSession不是线程安全的里面保存了当前连接等状态。如果你在多个线程里共用同一个Session会出现连接串线、结果混乱的问题。SqlSessionTemplate通过动态代理和ThreadLocal把Session绑定到当前线程保证同一次数据库操作全程使用同一个连接事务提交或回滚后自动清理。这一点和Spring管理事务的方式天然契合事务管理器的连接也是绑定在当前线程上的MyBatis去拿连接时会优先复用事务中的同一个连接。很多人不知道的是SqlSessionTemplate还负责把MyBatis的异常翻译成Spring的DataAccessException。也就是说你在业务代码里不用捕获MyBatis的PersistenceException直接处理Spring统一的数据库异常即可这对上层代码的解耦很关键。3. mybatis-config.xml核心配置与SQL日志打印3.1 全局配置文件的标签到底有哪些用mybatis-config.xml是MyBatis的全局配置文件整合Spring后仍然可以保留只是数据源和事务交给Spring管理了配置文件里主要负责行为设置。标签不多但每个都有讲究settings全局行为开关比如mapUnderscoreToCamelCase、cacheEnabled、lazyLoadingEnabled。typeAliases给实体类起别名简化XML里的resultType写法。plugins注册拦截器比如分页插件PageHelper就是在这里配置。environments在Spring整合中通常不用因为数据源来自Spring容器配置了反而容易混淆。mappers在Spring整合中通常也不用Mapper扫描由Spring的扫描器负责。最常用、影响最大的就是settings。我一般在开发环境打开mapUnderscoreToCamelCase这样user_name自动映射到userName不用写一堆resultMap。同时会把logImpl设置成STDOUT_LOGGING方便在控制台看SQL。生产环境再调整成SLF4J让日志统一走项目日志框架。3.2 配置打印SQL的三种姿势调试SQL是日常开发最高频的需求网上搜“mybatis配置打印”能找到一堆帖子但很多帖子只给一种方案没说明白各自适用场景。我整理下我实际用过的三种方式方式一在mybatis-config.xml里设置setting namelogImpl valueSLF4J/然后通过日志框架输出。注意保证你的Mapper接口所在包日志级别是DEBUG。方式二Spring Boot项目中直接在application.yml里写logging: level: com.example.mapper: debug这种方式最简单也是我日常用得最多的不需要改MyBatis配置只要日志框架支持。方式三控制台直接输出设置logImplStdOutImpl适合临时排查生产环境不要用因为会输出到System.out不好统一管控。这里我特别推荐IntelliJ IDEA的MyBatis Log Free插件。它能把MyBatis日志里带?参数占位符的SQL和实际参数值拼起来直接输出一条可直接执行的完整SQL。调试动态SQL时这个插件能省下大量人工拼接参数的时间。注意这个插件依赖SQL日志输出所以你得先通过方式一或方式二把日志打开。4. 完整落地传统Spring XML整合与Spring Boot整合4.1 传统SSM项目的XML配置全流程如果不用Spring Boot传统整合需要三步配置。第一步配置数据源和事务管理器第二步配置SqlSessionFactoryBean第三步配置Mapper扫描。我直接给一份精简配置bean iddataSource classorg.apache.commons.dbcp2.BasicDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/test/ property nameusername valueroot/ property namepassword value123456/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.mapper/ /bean注意几个细节mapperLocations指向的是XML映射文件的位置如果你的SQL写在注解里可以省略configLocation指向全局配置文件basePackage是Mapper接口所在包。事务管理器中dataSource必须和SqlSessionFactoryBean里的是同一个数据源否则事务不生效。很多老项目还会在SqlSessionFactoryBean里加typeAliasesPackage设置实体类包名。这样XML的resultType可以直接写类名小写不用写全限定名。我个人建议新代码尽量写全限定名可读性更好IDE跳转也更方便。typeAliasesPackage适合那种实体特别多、想偷懒的项目。4.2 Spring Boot下的极简配置Spring Boot整合就简单太多了。引入依赖之后配置集中在application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/test?useUnicodetruecharacterEncodingutf8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true cache-enabled: true主启动类或任意配置类上加MapperScan(com.example.mapper)或者直接在Mapper接口上标Mapper。我建议用MapperScan好处是统一管理扫描路径不用在每个接口上都加注解。Boot方式下MyBatis的SqlSessionFactoryBean由mybatis-spring-boot-autoconfigure自动配置完成它会读取mybatis.*配置项并生成工厂。你可以注入SqlSessionFactory查看它已经为你准备好了所有该有的属性和插件配置。这段时间我用Boot 3写新项目这套配置基本就够了很少需要再手动调整。4.3 动态SQL中if标签使用心得顺便提一下热词中的“mybatis if标签语法”因为这个在整合项目中太难绕开了。简单记忆就是if testname ! null and name ! 注意XML中和要转义不等于号用!没问题但小于号要写成lt;。多条件组合时要配合where标签它能自动去掉第一个多余的AND。我见过不少同事在if里写list.size() 0结果运行报错。原因很简单XML里有时候没问题但必须转义。更稳妥的写法是用list.size() gt 0或者gt;。另外if判断字符串不等于空标准写法是testname ! null and name ! 千万不要用name ! 这种万一传进来是null直接空指针。SQL拼接最怕的条件就是动态条件建议把这些细节在项目规范里定清楚避免每个人踩一遍。5. 缓存机制深入一级缓存、二级缓存和Spring的关系5.1 一级缓存的生命周期MyBatis一级缓存默认开启也叫LocalCache作用域是一个SqlSession。同一个Session里执行相同SQL参数也相同第一次查询会查数据库第二次直接从缓存返回。整合Spring之后日常Service里你用的SqlSessionTemplate每次请求可能绑定不同的SqlSession。如果两次查询之间没有被同一个事务包裹它们使用的是不同SqlSession一级缓存就失效了。所以你在Controller里连续调两次Mapper查询其实每次都走了数据库别以为一级缓存能帮你扛。一级缓存失效的常见场景我列一下不同的SqlSession。查询之间执行了增删改操作MyBatis会清空缓存。手动调用了sqlSession.clearCache()。开启localCacheScopeSTATEMENT缓存只作用于单条语句。Spring整合模式下一级缓存真正发挥作用的地方是同一个事务方法内多次执行相同查询。这种情况缓存是生效的能省掉重复查询的开销。5.2 二级缓存配置与那些年踩过的序列化坑二级缓存作用域是namespace也就是一个Mapper接口范围内共用。要开启二级缓存需要两步在Mapper XML里加cache/标签并且对应的实体类实现Serializable接口。全局开关默认是打开的如果你用Boot的configuration.cache-enabled把它关了怎么加都白搭。二级缓存能带来的收益确实有但坑也很深。最常见的是分布式环境下多实例缓存不一致以及多表关联查询时命中了缓存但数据源表已经变了的脏读问题。举个例子OrderMapper和UserMapper关联查询订单和用户信息如果两个Mapper都开了二级缓存OrderMapper的查询缓存了用户数据但UserMapper更新了用户表其缓存会清掉自己的部分OrderMapper里那颗缓存不会自动清于是你查到的还是老数据。所以我一般建议单机低并发、配置数据这类不怎么变更的表可以开二级缓存核心业务表、多表频繁关联的表尽量别开。开了缓存就要接受一致性窗口的存在这个窗口期缓存数据是旧的。真要追求查询性能优先考虑Redis这类外部缓存可控性高得多这也是现在主流做法。5.3 Spring的缓存到底管不管MyBatis这里必须得澄清一个常被搞混的点。热词里出现的“spring三级缓存”其实和MyBatis缓存完全是两码事。Spring的三级缓存是IOC容器解决循环依赖的机制维护的是单例Bean的创建状态不是查询数据的缓存。你要是拿MyBatis缓存的问题去找Spring三级缓存的文章那肯定一头雾水。Spring本身也提供缓存抽象Cacheable注解但它只管方法层需要结合Redis或EhCache等缓存中间件实现。它的工作方式是拦截Service方法调用缓存方法的返回值。MyBatis的缓存是框架内部查询层的实现两者负责的层次完全不同。在整合项目里如果你觉得“已经加了CacheableMyBatis一级缓存还有必要管吗”答案是两个层面的事该关的关、该开的开互不干扰。6. 整合实战中的高频问题与排查技巧6.1 Mapper接口注入失败或找不到SQL这大概是整合后出现概率最高的问题。常见报错有NoSuchBeanDefinitionException、Invalid bound statement (not found)、BindingException。我的排查顺序基本固定先确认Mapper接口有没有被扫描到检查MapperScan包路径是否覆盖接口所在包。再确认Mapper XML里的namespace是否完全匹配接口全限定名。然后确认XML里每个操作的id是否和接口方法名一致。最后确认XML文件是否被打包到classpath也就是构建配置里有没有漏掉src/main/java下的xml文件。第4点很隐蔽Maven项目里默认只把src/main/resources的资源打进classpath。如果开发时把Mapper XML放在src/main/java下运行时会发现XML找不到。解决方式是在pom.xml里显式声明包含*.xml或者干脆把XML放回resources/mapper目录。我个人极力推荐后者目录规范一目了然不用折腾构建配置。6.2 事务不生效的排查清单Spring整合MyBatis后事务不生效我见过太多案例了。最常见的四个原因没有配置DataSourceTransactionManager或它的dataSource和MyBatis的数据源不是同一个对象。方法被private修饰或者同类内部方法调用自调用spring事务代理不触发。Transactional加非public方法上Spring默认不产生代理。使用EnableTransactionManagement开启事务管理时配置顺序不对导致代理创建失败。排查技巧就是看日志开启Spring事务管理日志logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManagerDEBUG看看事务有没有真正提交或回滚。如果管理器完全没有输出多半是事务管理器根本没参与进去。6.3 多数据源批量操作为什么只提交了一半热词里提到的“mybatis的saveorupdatebatch多数据源的问题”我也聊两句。这个问题本质不在MyBatis而在事务边界。当你在多数据源场景下做批量保存或批量更新事务管理器只能管住配置的那个数据源另一个数据源的操作就成了“无事务提交”。我之前遇到过一次订单库和日志库分属两个数据源批量保存订单成功但日志插入失败时订单回滚了日志却没回滚最终数据对不上。原因就是我用的DataSourceTransactionManager只绑定了订单库的连接日志库的每条插入都在自己的自动提交模式下完成。解决方向有三类一是使用分布式事务方案比如Atomikos或Seata适合强一致性场景二是把多数据源的操作拆成隔离的步骤比如先写主库异步写日志库允许短暂不一致三是谨慎使用Transactional明确事务边界只覆盖单数据源的操作。说实话多数据源一致性没有银弹必须结合业务容忍度来设计。6.4 缓存导致的数据错乱和排查实录最后说一个我实际排查过的案例。某次生产环境用户修改了昵称前端还是显示老昵称。一开始以为是前端缓存清了之后还在。后来排查数据库发现数据已经更新了但接口返回的老数据。最后定位到Mapper开了一级和二级缓存并且加载用户昵称的查询刚好走了二级缓存数据一直命中旧值。复现的思路是先关掉整个MyBatis的缓存开关cacheEnabledfalse问题消失说明是MyBatis缓存问题再逐个Mapper关闭二级缓存最终定位到具体namespace。那次之后就立了规矩对“更新频繁多环境共享”的数据坚决不用MyBatis二级缓存。类似这种排查思路我建议任何人遇到数据更新不回显的问题都按“先看事务是否提交、再看方法缓存、最后看MyBatis缓存”的顺序走一遍比盲目清Redis缓存高效得多。7. 整合路上的一点个人体会MyBatis整合Spring这件事说难不算难说简单也不简单。难的不是把配置跑通而是搞清楚每一层对象是怎么协作的DataSource谁提供、SqlSessionFactory谁创建、Mapper代理谁生成、事务边界画在哪里、缓存何时生效何时失效。这些弄明白了后面遇到配置报错、事务回滚、缓存脏读等问题你都可以不看日志就猜个八九不离十。我个人最深的体会是别迷信Spring Boot的自动配置前期一定要亲手用XML把整合流程走一遍哪怕最终项目用的是Boot。就像学开车自动挡开着舒服但手动挡能让你真正理解离合和换挡的逻辑。MyBatis整合Spring也算这一行的基本功花一天时间把原理吃透后面省下的排查时间远远不止一天。最后再分享一个小技巧整合完上线之前把mybatis.configuration.map-underscore-to-camel-case、log-impl、cache-enabled这三个配置写进项目的配置检查清单每换一次环境或升级一次版本都对一遍。很多线上问题其实就是这些小配置在环境切换时悄悄变了。
