
纯 Spring 项目里把 MyBatis 接进来这件事我做过不止一次。每次带项目我都建议团队新人不要一上来就 Spring Boot mybatis-starter而是先用最原始的方式手动整合一遍。原因很简单只有亲手把 SqlSessionFactory 交给 Spring、把 Mapper 接口变成可注入的 Bean、把事务边界从 MyBatis 手里移交到 Spring 手里你才能真正理解那些 starter 到底替你做了什么。这篇文章就把这条手动整合的路完整走一遍从版本选型、依赖引入、配置拆解到 CRUD 跑通、常见报错排查全部给你。1. 整合本质Spring 容器和 MyBatis 之间到底交换了什么1.1 很多人对整合“会配但不懂”的根源早些年带团队的时候我见过不少这样的场景用 Spring Boot 的人顺手就加上了 mybatis-spring-boot-starter写一个接口、写一个 XML项目就活了。但一旦脱离 Spring Boot回到传统 SSM 项目或者公司要求手动管理依赖很多人就懵了——不知道 SqlSessionFactory 是怎么进 Spring 的不知道 Mapper 接口为什么能直接注入更不知道事务是怎么串起来的。这个问题的根源是 Spring Boot 的自动配置把整合细节全藏了。你看到的只是“加依赖、写接口”底层发生了什么一概不知。理解 Spring 整合 MyBatis本质上是搞清楚三件事SqlSessionFactory 如何被 Spring 创建和管理Mapper 接口如何变成可注入的 Bean数据库事务如何从 MyBatis 的 SqlSession 转移到 Spring 的事务管理器上。把这三条线理清任何 starter 版本你都能读懂它在干嘛出了问题也能顺着链路去定位。1.2 为什么不能直接在业务代码里 new SqlSessionFactory很多初学者第一反应是MyBatis 自己不是有 SqlSessionFactoryBuilder 吗我写个工具类 new 一个工厂再拿 SqlSession 去执行行不行技术上完全可行工程上非常不推荐。原因是 SqlSessionFactory 本身是重量级对象一个数据库对应一个就够了而且 SqlSession 不是线程安全的。如果每个线程都自己 new SqlSession连接管理、事务边界、资源释放全乱套。Spring 整合后这些事交给了容器SqlSessionFactory 以单例 Bean 的形式存在生命周期由容器管理每次数据库操作通过 SqlSessionTemplate由它按需获取和释放 SqlSession事务边界由 Spring 的 PlatformTransactionManager 统一控制MyBatis 的 SqlSession 会感知当前是否存在 Spring 事务决定是新建会话还是复用事务内会话。这个过程的载体就是 mybatis-spring 这个桥接包。它提供的 SqlSessionFactoryBean、MapperFactoryBean、SqlSessionTemplate 三样东西分别对上了上面三条线。1.3 整合后的整体链路记住这张图为了后续配置不迷路先给出一张我脑子里的对象链路图文字版Spring 容器持有 DataSource它是整个数据库连接的源头SqlSessionFactoryBean 接收 DataSource生产 MyBatis 的 SqlSessionFactoryMapperScannerConfigurer 扫描指定包下的接口为每个接口注册 MapperFactoryBeanMapperFactoryBean 依赖 SqlSessionFactory创建出 Mapper 接口的 JDK 动态代理DataSourceTransactionManager 也接收同一个 DataSource负责开启、提交、回滚事务业务代码注入 Mapper 接口实际拿到的是代理对象代理内部通过 SqlSessionTemplate 执行 SQL。记住这张图后面每个配置项你都知道它挂在哪条线上。这就是我建议任何人都至少手动整合一次的原因自动配置确实爽但出了问题你得知道去哪里定位。2. 环境与依赖版本矩阵是最容易被低估的坑2.1 技术选型与版本组合整合之前先把环境定下来。我用的组合如下也是目前兼容性最好、踩坑最少的经典组合组件版本说明JDK8 或 11Spring 5.3 最低要求 JDK8JDK11 也没问题Spring Framework5.3.x原生 Spring非 Spring BootMyBatis3.5.x当前主流 3.5.16mybatis-spring2.1.x与 MyBatis 3.5 配套MySQL8.0使用官方驱动 mysql-connector-j连接池Druid 1.2.x也可以换 HikariCP思路一致注意如果你准备用 Spring 6JDK 必须是 17 以上而且旧代码里的 javax.* 命名空间要全部迁移到 jakarta.*工程量大。老项目升级前一定要评估影响面不能只看依赖版本号。2.2 Maven 依赖清单因为项目是原生 Spring MyBatis没有 Boot 的依赖管理建议直接把以下依赖加到 pom.xml。这里有两个点特别容易踩我会重点说明properties spring.version5.3.39/spring.version mybatis.version3.5.16/mybatis.version mybatis.spring.version2.1.2/mybatis.spring.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis.spring.version}/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version scoperuntime/scope /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies第一点mybatis-spring 2.1.x 依赖 MyBatis 3.5。如果你接手的是老项目MyBatis 还是 3.4.x请把 mybatis-spring 换成 1.3.x否则启动时会直接抛 NoSuchMethodError。这类报错通常不是代码问题而是版本错位。第二点spring-jdbc 必须显式引入。它提供了 DataSourceTransactionManager、JdbcTemplate 等关键类spring-context 不会自动带上它。之前的项目就有同事漏引入运行期报 ClassNotFoundException: org.springframework.jdbc.datasource.DataSourceTransactionManager排查了半天才发现是依赖缺失。2.3 数据库准备整合过程需要一个能连的库。我用一张最小化的表后面所有演示都围绕它CREATE DATABASE IF NOT EXISTS demo DEFAULT CHARACTER SET utf8mb4; USE demo; CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_name VARCHAR(50) NOT NULL COMMENT 用户名, email VARCHAR(100), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO user(user_name, email) VALUES (zhangsan, zhangsanexample.com); INSERT INTO user(user_name, email) VALUES (lisi, lisiexample.com);注意表引擎用了 InnoDB这关系到后面第 5 节事务能否生效。MyISAM 不支持事务很多人辛辛苦苦配好了 Transactional结果回调滚没用最后发现是表引擎的问题。3. 配置逐项拆解数据源、工厂与扫描器三个关键角色3.1 数据源配置连接是整合的第一条命第一个 Bean 是 DataSource。我用 Druid 举例换成 HikariCP 思路完全一致只改类名即可。在 JavaConfig 里这样写Bean public DataSource dataSource() { DruidDataSource ds new DruidDataSource(); ds.setDriverClassName(com.mysql.cj.jdbc.Driver); ds.setUrl(jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai); ds.setUsername(root); ds.setPassword(你的密码); ds.setInitialSize(5); ds.setMaxActive(20); return ds; }URL 里的三个参数单独说一下useUnicodetruecharacterEncodingutf8解决中文乱码问题存进 MySQL 的中文能不能正确读写主要靠它serverTimezoneAsia/ShanghaiMySQL 8 驱动强制要求时区参数不写会报 “The server time zone value” 错误如果只在本机测试useSSLfalse 也建议加上避免 SSL 握手带来的噪音日志和额外耗时。这些参数看似小事但几乎每个新人第一次跑整合都会栽在时区或乱码上。3.2 SqlSessionFactoryBean把 MyBatis 原生配置收编进 Spring第二个 Bean 是关键中的关键。SqlSessionFactoryBean 是 FactoryBean 的一种Spring 容器里声明的是这个 Bean但通过 getBean(sqlSessionFactory) 拿到的其实是它内部生产的 SqlSessionFactory 对象。这是 Spring 经典的 FactoryBean 机制很多同学第一次看到 Bean 名和实际类型对不上容易绕晕。Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) throws IOException { SqlSessionFactoryBean factoryBean new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setTypeAliasesPackage(com.demo.entity); PathMatchingResourcePatternResolver resolver new PathMatchingResourcePatternResolver(); factoryBean.setMapperLocations(resolver.getResources(classpath:mapper/*.xml)); org.apache.ibatis.session.Configuration configuration new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class); factoryBean.setConfiguration(configuration); return factoryBean; }逐项解释setDataSource 必须有SqlSessionFactory 从它拿到连接setTypeAliasesPackage 指定实体类包名XML 里写 resultTypeUser 就能自动映射到 com.demo.entity.User不用写全限定名setMapperLocations 指定 XML 文件位置classpath:mapper/*.xml 表示 classpath 下 mapper 目录所有 XML。要注意Maven 默认只把 src/main/resources 下的文件当资源打包XML 放错位置会导致运行期找不到mapUnderscoreToCamelCase 开启后数据库字段 user_name 能自动映射到属性 userName省掉大量手写 resultMap 的重复劳动StdOutImpl 是控制台打印 SQL调试期强烈建议开上线前要换掉。这里还有个很容易犯的错同时用 setConfiguration 和 setConfigLocation。如果你有自己的 mybatis-config.xml这两个概念别混setConfigLocation 指向外部 mybatis-config.xmlsetConfiguration 直接塞一个 MyBatis Configuration 对象。两者同时设置时会互相覆盖结果不可预期。我的习惯是新项目完全用 setConfiguration不再保留外置 mybatis-config.xml老项目迁移才用 setConfigLocation保持原来 XML 不动平滑过渡。3.3 纯注解配置和 XML 配置怎么选为了照顾不同习惯的读者我把 XML 等价的配置也贴出来。本质上JavaConfig 和 XML 画的是同一张对象图context:component-scan base-packagecom.demo/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/demo?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value你的密码/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nametypeAliasesPackage valuecom.demo.entity/ property namemapperLocations valueclasspath:mapper/*.xml/ property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue/ /bean /property /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.demo.mapper/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/从零搭建的话我更推荐 JavaConfig。理由很简单Bean 之间的依赖在编译期部分可查重构时 IDE 能帮你定位引用XML 字符串配置只能等运行期报错再去排查。但如果你接的是老项目XML 是主流完全可以直接用。4. Mapper 注册的三种方式与底层代理逻辑4.1 最小可用方案逐个注册 MapperFactoryBean先说最朴素的方式。如果项目里只有一个两个 Mapper可以直接注册 MapperFactoryBean这也是理解 Mapper 注入机制的起点Bean public MapperFactoryBeanUserMapper userMapper(SqlSessionFactory sqlSessionFactory) { MapperFactoryBeanUserMapper factoryBean new MapperFactoryBean(); factoryBean.setMapperInterface(UserMapper.class); factoryBean.setSqlSessionFactory(sqlSessionFactory); return factoryBean; }MapperFactoryBean 实现的是 FactoryBean 接口它 getObject() 返回的不是工厂本身而是 UserMapper 的代理对象。代理的生成逻辑在 MapperProxy 中每次调用接口方法都会通过底层的 SqlSessionTemplate 去执行对应 namespace 下的 SQLnamespace 默认取接口的全限定名。这种方式的好处是显式、直观坏处也很明显——Mapper 一多Bean 定义多到没法看。所以它只适合教学和理解实际项目不会这么写。4.2 自动扫描注解和扫描器二选一项目里实际用的一定是自动扫描。XML 配置我上面已经写了 MapperScannerConfigurerJavaConfig 更推荐直接用注解一行搞定Configuration MapperScan(com.demo.mapper) public class AppConfig { ... }注意 MapperScan 在 mybatis-spring 包里路径是 org.mybatis.spring.annotation.MapperScan。它在底层做的事和 XML 里的 MapperScannerConfigurer 一样都是给扫描到的接口注册一批 MapperFactoryBean 的 BeanDefinition。用扫描方式时basePackage 的粒度有讲究。我习惯把 Mapper 接口单独放在一个包比如 com.demo.mapper不要和 service、entity 混在一起。原因有二扫描路径越精确越不容易误扫线上问题排查时一眼能定位到数据访问层。如果你想把接口散落在不同包也可以用逗号分隔写多个包。这里还有个隐藏规则MapperScan 和手动声明 MapperScannerConfigurer 不要同时用。两者功能一样同时配置可能造成重复扫描出现 Bean 定义冲突。4.3 接口与 XML 的绑定硬规则这里说三个绑定规则违反任何一个都会在不经意间翻车namespace 必须等于接口的全限定名比如 com.demo.mapper.UserMapperSQL 标签的 id 必须等于接口方法名且方法不能重载。MyBatis 不区分重载同一个接口里出现两个同名方法哪怕参数不同也会直接冲突方法返回值和参数类型要和 select 标签的 resultType/resultMap、parameterType 匹配。除了 XMLMyBatis 3.5 也支持用注解写 SQLpublic interface UserMapper { Select(select * from user where id #{id}) User findById(Long id); }我的实际经验是单表两三个字段的简单查询注解写起来很舒服一旦涉及多表 join、动态 where、批量更新XML 的可读性和维护性远超注解字符串。很多团队定规矩“一条 SQL 超过 10 行就必须进 XML”我深表赞同你可以参考。5. 事务衔接DataSourceTransactionManager 与 SqlSessionTemplate 的配合5.1 事务管理器的配置与同一个 DataSource 原则事务是整合里的重头戏也是最容易“配了但没生效”的部分。JavaConfig 里需要加EnableTransactionManagement public class AppConfig { Bean public DataSourceTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }这里有一个强制要求DataSourceTransactionManager 注入的 DataSource必须和 SqlSessionFactoryBean 用的是同一个 DataSource 实例。如果配置了两份不同的连接池事务管理器控制的那份连接和 MyBatis 用的那份连接不是一回事事务就形同虚设。一旦出现“明明配了 Transactional 但数据还是写进去了”的诡异问题第一嫌疑就是这里。5.2 SqlSessionTemplate 的线程安全与事务跟随机制早期有人自己封装 SqlSession 工具类每个线程 new 一个 SqlSession一个方法里连续查两次第二次可能因为第一次的 SqlSession 已关闭而失效。mybatis-spring 给出的解决方案是 SqlSessionTemplate。SqlSessionTemplate 内部维护了一个 SqlSessionProxy通过 JDK 动态代理拦截所有方法调用。每次调用它都会先判断当前线程有没有被 Spring 事务管理器同步的 SqlSession有直接复用事务内那个 SqlSession没有新开一个 SqlSession执行完立即关闭。这个“跟着事务走”的机制让 MyBatis 的 SqlSession 生命周期与 Spring 事务边界完全对齐。你所注入的 Mapper 接口代理最终也是落到这个逻辑上。顺带解释一个很多人困惑的现象非事务环境下同一个方法里两次调用 mapper.selectMyBatis 的一级缓存基本不起作用。原因就是每次调用都新建和销毁 SqlSession而一级缓存是 SqlSession 级别的。想要缓存命中就得在同一个事务里连续查询。这不是 Bug是 Spring 整合后的正常行为。5.3 事务配置检查清单把容易踩的点列成清单建议逐条对照检查数据源只有一份DataSourceTransactionManager 与 SqlSessionFactoryBean 复用同一个EnableTransactionManagement 或 tx:annotation-driven 必须存在于容器事务方法必须是 public并且不能通过对象内部自调用否则代理不生效数据库表引擎是 InnoDB不是 MyISAM异常默认只在 RuntimeException 和 Error 时回滚受检异常默认不回滚需要时用 Transactional(rollbackFor Exception.class)如果你在 Spring 容器外还配置了 MyBatis 自己的 transactionManager整合后建议删掉交给 Spring 统一管理。自调用问题值得单独强调A 类的 methodA() 调用同类 methodB()methodB 上有 Transactional此时事务不会生效因为调用发生在对象内部没经过 Spring 代理。解决办法是把 methodB 放到另一个 Bean 里。这个坑几乎每个团队都踩过。6. 从空项目到跑通 CRUD 的完整落地流程6.1 项目结构与完整配置下面是完整可运行的骨架照着建就行src/main/java/com/demo ├── App.java ├── config/AppConfig.java ├── entity/User.java ├── mapper/UserMapper.java └── service/UserService.java src/main/resources └── mapper/UserMapper.xmlAppConfig 就是把前面 3.1、3.2 和 5.1 节里的 Bean 方法全部放进一个类再加上 ComponentScan 和 MapperScan。为了避免你还要自己拼装我贴出完整类package com.demo.config; import com.alibaba.druid.pool.DruidDataSource; import org.apache.ibatis.session.Configuration; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; Configuration ComponentScan(com.demo.service) MapperScan(com.demo.mapper) EnableTransactionManagement public class AppConfig { Bean public DataSource dataSource() { DruidDataSource ds new DruidDataSource(); ds.setDriverClassName(com.mysql.cj.jdbc.Driver); ds.setUrl(jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai); ds.setUsername(root); ds.setPassword(你的密码); return ds; } Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setTypeAliasesPackage(com.demo.entity); PathMatchingResourcePatternResolver resolver new PathMatchingResourcePatternResolver(); factoryBean.setMapperLocations(resolver.getResources(classpath:mapper/*.xml)); Configuration configuration new Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class); factoryBean.setConfiguration(configuration); return factoryBean; } Bean public DataSourceTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }实体类就不过度展开了注意必须补全 getter/setter并加上无参构造package com.demo.entity; public class User { private Long id; private String userName; private String email; public User() {} public User(String userName, String email) { this.userName userName; this.email email; } // 省略 getter/setter请自行补全 }Mapper 接口package com.demo.mapper; import com.demo.entity.User; import java.util.List; public interface UserMapper { User findById(Long id); ListUser findAll(); int insert(User user); int update(User user); int deleteById(Long id); }Mapper XMLnamespace 必须和接口全限定名完全一致?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.demo.mapper.UserMapper insert idinsert parameterTypeUser useGeneratedKeystrue keyPropertyid insert into user(user_name, email) values (#{userName}, #{email}) /insert update idupdate parameterTypeUser update user set user_name #{userName}, email #{email} where id #{id} /update delete iddeleteById parameterTypelong delete from user where id #{id} /delete select idfindById parameterTypelong resultTypeUser select id, user_name, email from user where id #{id} /select select idfindAll resultTypeUser select id, user_name, email from user /select /mapper这里特意写了 useGeneratedKeystrue keyPropertyid插入后主键会回填到 User 对象的 id 字段。这个功能日常开发使用频率极高但不看文档的新人基本想不到。Service 层加事务演示package com.demo.service; import com.demo.entity.User; import com.demo.mapper.UserMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; Service public class UserService { private final UserMapper userMapper; public UserService(UserMapper userMapper) { this.userMapper userMapper; } public ListUser listAll() { return userMapper.findAll(); } public User getById(Long id) { return userMapper.findById(id); } Transactional public User createUser(String userName, String email) { User u new User(userName, email); userMapper.insert(u); return u; } Transactional public void updateUser(Long id, String userName, String email) { User u new User(userName, email); u.setId(id); userMapper.update(u); } Transactional public void deleteUser(Long id) { userMapper.deleteById(id); } Transactional(rollbackFor Exception.class) public void createWithRollback(String userName, String email) { User u new User(userName, email); userMapper.insert(u); throw new RuntimeException(模拟异常触发回滚); } }6.2 用 main 方法启动容器非 Spring Boot 启动姿势没有 Spring Boot启动入口只能是手动创建容器。如果你习惯写单元测试用 RunWith(SpringJUnit4ClassRunner.class) 搭配 ContextConfiguration(classes AppConfig.class) 也可以原理一样。这里用 main 方法演示更直观package com.demo; import com.demo.config.AppConfig; import com.demo.entity.User; import com.demo.service.UserService; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import java.util.List; public class App { public static void main(String[] args) { AnnotationConfigApplicationContext ctx new AnnotationConfigApplicationContext(AppConfig.class); UserService userService ctx.getBean(UserService.class); System.out.println(查询全部 userService.listAll()); User newUser userService.createUser(wangwu, wangwuexample.com); System.out.println(新增用户回填主键 newUser.getId()); System.out.println(新增后查询 userService.getById(newUser.getId())); userService.updateUser(newUser.getId(), wangwu_updated, newexample.com); System.out.println(更新后查询 userService.getById(newUser.getId())); userService.deleteUser(newUser.getId()); System.out.println(删除后查询 userService.getById(newUser.getId())); try { userService.createWithRollback(rollback_case, rollbackexample.com); } catch (RuntimeException e) { System.out.println(捕获异常 e.getMessage()); } System.out.println(回滚验证rollback_case 不应出现在下面列表中); ListUser users userService.listAll(); users.forEach(u - System.out.println(u.getId() - u.getUserName())); ctx.close(); } }如果配置全部正确控制台会打印 SQL 日志新增的 wangwu 能查到、更新能生效、删除后查不到、回滚用例不会出现在最终列表里。这几个现象同时出现就说明 Mapper 扫描、SQL 绑定、事务控制三件事全部生效了。6.3 验证逻辑每一行日志分别证明什么第一次跑通的时刻值得细看。开着 StdOutImpl控制台会出现这些关键输出Preparing: select id, user_name, email from user where id ?Parameters: 3(String)说明参数绑定正常 Total: 1说明查询有结果返回回滚验证时最终列表里没有 rollback_case说明事务回滚生效。如果哪一步输出缺失顺着这条证据链去定位比自己瞎猜快得多。这也是我特别推荐调试期开 SQL 日志的原因——每个功能点都有明确的可见信号而不是“感觉它工作了”。7. 整合后常见报错与我的排查清单7.1 Invalid bound statement (not found)最高频的运行错误这个错误出现时第一反应不是改代码而是按下面顺序排查打开 target/classes/mapper 目录确认 UserMapper.xml 是否真的打包进去了。Maven 默认把 src/main/resources 当资源目录如果 XML 误放在 src/main/java 下默认不会被打包除非额外配置。确认 mapperLocations 的路径和你实际放的路径一致。classpath:mapper/*.xml 对应的是 resources/mapper 下的 XML。打开 XML 看 namespace必须等于接口全限定名。看标签 id必须和接口方法名完全一致。看 basePackage / MapperScan 是否扫到了这个接口。判断方法启动时打开 DEBUG 日志观察 org.mybatis.spring.mapper.ClassPathMapperScanner 的输出正常会出现 Registering mapper interface com.demo.mapper.UserMapper 之类的日志没有就说明接口没被扫描到。还有一种情况是接口方法重载。前面说过MyBatis 不允许同一个接口里有同名方法即使参数不同也不可以。遇到这种需求把其中一个方法改名或者合并参数。7.2 事务不生效数据照样写库这个坑我在 5.3 节给了完整清单这里补充一个真实场景。之前一个同事的项目里新增数据总是回滚不掉我查到最后发现他的数据源在 XML 里配了一个事务管理器的 dataSource 又手动指定了另一个连接池 Bean。两者虽然指向同一个库但不是同一个实例MyBatis 用的连接和事务管理器控制的连接根本不是同一份事务自然无效。排查顺序建议在加了 Transactional 的方法里打断点看 TransactionSynchronizationManager.isActualTransactionActive() 是否返回 truefalse 说明事务压根没开启确认事务管理器用的 DataSource 和 sqlSessionFactory 用的是不是同一个 Bean再看异常类型是不是 RuntimeException。如果业务抛的是 IOException 这类受检异常默认不会触发回滚必须显式 rollbackFor。7.3 其它高频问题汇总最后把另外几个常见问题集中说明Property sqlSessionFactory or sqlSessionTemplate are required通常是 MapperFactoryBean 在缺少 SqlSessionFactory 的情况下被创建。检查是否同时用了 MapperScan 和手动注册 MapperFactoryBean两边重复会冲突只保留一种。启动报 The server time zone value is unrecognizedMySQL 8 驱动要求 URL 带 serverTimezone补上即可。中文乱码URL 补 characterEncodingutf8且建库建表统一用 utf8mb4。启动后出现大量 WARN: More than one configuration同时设置了 configLocation 和 configuration二选一。MyBatis 3.4.x 配 mybatis-spring 2.x 直接 NoSuchMethodError版本错位回到第 2 节表格对照修正。整合这种基础工程最有价值的往往不是“怎么配置对”而是“配置错了怎么快速定位”。我习惯把错误信息原文先复制下来判断它属于扫描类、绑定类、连接类还是事务类再按对应清单逐项排除。这比从头到尾看一遍配置高效得多。这是我多年来反复手动整合 Spring 和 MyBatis 沉淀下来的流程和避坑经验。如果你之前一直用 Spring Boot starter强烈建议照这篇文章自己搭一遍纯 Spring 版。跑通一次之后再看任何自动配置的源码都会有豁然开朗的感觉。