☰
Spring整合MyBatis:从SqlSessionFactory到Mapper代理的机制与配置全解
2026/10/13 17:02:31 网站建设 项目流程

1. 别急着写代码:先弄懂Spring和MyBatis是怎么"接上头"的

说实话,做Java开发这些年,凡是拿"整合Spring+Mybatis"来问我的,十有八九不是不会写代码,而是不理解两个框架到底在哪一步、通过什么机制"接上头"的。配置文件抄来抄去能跑通,一旦换个版本、挪个包路径就原地炸锅,原因就在这里。

先搞清楚两个框架各自的定位。Spring是一个IoC容器和管理框架,管的是对象生命周期、依赖注入、事务和AOP;MyBatis是一个持久层框架,管的是SQL语句、参数绑定、结果集映射。这两者唯一的交集是"数据源"——Spring管理的数据源连接,MyBatis拿来执行SQL。整合理念上要解决三个问题:谁来创建和维护SqlSessionFactory、Mapper接口如何变成Spring容器里的Bean、事务由谁统一控制。

在原生MyBatis里,我们得自己动手:

String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream); try (SqlSession session = factory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); User user = mapper.selectById(1); session.commit(); }

这里每一步都在考验开发者的细心程度:SqlSession不是线程安全的,用完必须关闭,否则连接泄漏;事务要记得手动commit;每次调用都要重复这段模板代码。整合Spring之后,这些脏活全部被容器接管,Service层里只需要写:

@Service public class UserService { @Autowired private UserMapper userMapper; public User getUserById(Integer id) { return userMapper.selectById(id); } }

你会好奇,一个接口,Spring怎么能直接注入呢?答案在mybatis-spring这个桥接包里。它提供了几个关键类:SqlSessionFactoryBean负责把MyBatis的工厂类做成Spring Bean;MapperFactoryBean负责为每个Mapper接口生成代理对象;SqlSessionTemplate是线程安全的会话模板,为Mapper代理提供执行SQL的能力。平时注入的UserMapper,本质上就是这个代理对象,方法调用层层委托给SqlSessionTemplate,再落到SqlSession上。

这条"委托链"是整个整合逻辑的地基。后面所有报错,比如Invalid bound statement、No qualifying bean、事务不生效,追根溯源都能落到这条链的某一环上。所以我的建议是:配置可以抄,但这条链必须自己捋清楚。捋清楚之后再看配置,每一行都是有意义的,而不是咒语。

2. 依赖版本怎么选:mybatis-spring是整合成败的关键一环

整合的第一步不是写配置,是选依赖。很多翻车案例的根源不是配置写错了,而是mybatis-spring和Spring主版本不匹配。mybatis-spring是MyBatis官方出的适配包,专门用来把MyBatis组件挂到Spring容器里,它和Spring之间存在严格的版本配套关系。

Spring主版本mybatis-spring建议版本适用场景
Spring 4.x1.3.x老项目维护,JDK 7/8
Spring 5.x2.0.x ~ 2.1.x当前最常见的组合
Spring 6.x3.0.x配合JDK 17+的新项目

我搭传统XML整合工程时,惯用这套依赖组合(Maven坐标):

<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.31</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.31</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.16</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.1.2</version> </dependency> <!-- 数据库驱动和连接池,按实际环境替换 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.18</version> </dependency> </dependencies>

有几个点要特别提醒。第一,spring-jdbc不能省。DataSourceTransactionManager、JdbcTemplate都在这包里,没有它事务配置会在启动阶段直接报ClassNotFoundException。第二,如果项目同时引了多套日志实现,比如logback和log4j都出现在依赖树里,启动时会出现一堆日志绑定警告,虽然不一定崩,但会严重干扰排查视线,强烈建议用mvn dependency:tree提前理清。第三,如果用的是Spring Boot却又想参考这篇文章的XML整合方式,多数情况下官方starter已经替你封装好了,不需要再手动写SqlSessionFactoryBean,硬要混用反而容易出现Bean冲突。

版本选型这块,我给的实操结论是:先定Spring版本,再定mybatis-spring,最后定mybatis。顺序反了很容易掉进"单看每个依赖都新,合在一起互相不认"的坑。网上那些报NoSuchMethodError的整合问题,八成是mybatis-spring1.x硬塞进Spring 5项目导致的。

3. 核心配置逐段拆解:从数据源到Mapper注册

这一节我把一个能跑通的applicationContext.xml从头拆到尾。注意,这是传统Spring的XML整合方式,不含Spring Boot自动化配置。

3.1 数据源和事务管理器

数据源是所有数据库操作的起点,MyBatis的SqlSessionFactory、Spring的事务管理器都要依赖它。

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/ssm_demo?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>

这里有两个容易踩的细节。一是XML里的&必须写成&amp;,比如URL参数分隔符,直接写&会导致XML解析错误。二是MySQL 8以上驱动类名是com.mysql.cj.jdbc.Driver,老教程里的com.mysql.jdbc.Driver虽然兼容但会报警告,建议直接换新的。事务管理器必须和MyBatis最终使用的数据源是同一个对象,否则事务控制会"各管各的",后面第5节还会细说。

关于连接池,我习惯用Druid或HikariCP,两者各有适合的场景。Druid自带监控和慢SQL日志,排障方便;HikariCP更轻量,性能下限高。选哪个不是关键,关键是初始化参数要合理,比如initialSize、maxActive这种值如果保持默认,在高并发压测下很容易把连接池打满,报waiting for connection超时。

3.2 SqlSessionFactoryBean:接管MyBatis的工厂

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.example.entity"/> </bean>

SqlSessionFactoryBean是整合的中枢。它实现了Spring的FactoryBean接口,容器在启动阶段调用它的afterPropertiesSet()方法,把数据源、全局配置、XML映射位置等信息汇总,最终构建出一个真正的SqlSessionFactory,然后以sqlSessionFactory这个名字放进容器。

逐个属性说:dataSource不用解释,没有它MyBatis没有连接来源;configLocation指向MyBatis自己的全局配置文件,主要放settings、typeAliases、typeHandlers这些;mapperLocations声明SQL映射XML的存放路径,这里支持通配符,classpath:mapper/*.xml是最高频的写法;typeAliasesPackage可以让MyBatis自动扫描实体包生成类别名,这样resultType="User"就不用写全限定名com.example.entity.User了。

需要交代一个常见误解:用Spring整合后,mybatis-config.xml里不要再配置environments和dataSource。很多新手把原生MyBatis的配置整个搬过来,里面还带着<environments>标签。从机制上讲,SqlSessionFactoryBean在构建工厂时会用自己持有的数据源覆盖掉environments里的配置,但这些残留配置不仅误导人,而且有些版本下会引发奇怪的初始化顺序问题。我见过一个项目,前后端联调一切正常,一做单元测试就报数据源冲突,最后发现就是mybatis-config.xml里多留了一段environments。全局配置文件里保留settings、typeAliases这类纯MyBatis行为配置就够了。

如果要在SqlSessionFactory层面挂拦截器插件(比如分页插件、SQL性能分析插件),用plugins属性注册即可:

<property name="plugins"> <array> <bean class="com.example.interceptor.PaginationInterceptor"/> </array> </property>

3.3 MapperScannerConfigurer:让接口自动变成Bean

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.mapper"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean>

这一步的作用是扫描指定包下的Mapper接口,逐个用MapperFactoryBean把它们注册成Spring Bean。basePackage支持逗号分隔多包,也支持通配符。这里有一个很重要的写法细节:给MapperScannerConfigurer传工厂时,推荐用sqlSessionFactoryBeanName传字符串,不要用ref直接引用Bean。

这个坑的根源在于MapperScannerConfigurer本身是一个BeanDefinitionRegistryPostProcessor,它的执行时机在普通Bean实例化之前。如果这边用ref引用sqlSessionFactory,Spring在解析引用时会提前触发sqlSessionFactory的初始化,而它依赖的dataSource等Bean还没有完全就绪,容易出现"过早初始化"引发的连锁报错。用字符串名称则是在所有Bean定义处理完之后才去解析,安全得多。

3.4 mybatis-config.xml:只留行为配置

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="logImpl" value="SLF4J"/> </settings> </configuration>

mapUnderscoreToCamelCase这个开关我建议一定开。数据库字段习惯用下划线命名user_name,实体属性用驼峰userName,开了这个开关,MyBatis会自动完成映射,省掉大量resultMap。logImpl指定日志实现,调试SQL时非常有用,能把执行的SQL和参数都打出来。注意这里是logImpl不是log-impl,缩写拼写错也是新手常报的配置解析错误。

4. Mapper接口与XML映射:三种写法怎么选

配置层打通之后,剩下的就是业务侧的Mapper编写方式。这里有三条路线,很多人纠结选哪个,我按实际项目的经验分开说。

4.1 经典XML映射方式

Mapper接口只写方法签名:

public interface UserMapper { User selectById(@Param("id") Integer id); }

对应XML文件:

<mapper namespace="com.example.mapper.UserMapper"> <select id="selectById" resultType="User"> SELECT id, username, age FROM t_user WHERE id = #{id} </select> </mapper>

这里有三个铁律:namespace必须等于接口的全限定类名;id必须等于接口方法名;resultType或resultMap必须覆盖查询结果的映射。任何一条不满足,运行时都会报Invalid bound statement。

4.2 纯注解方式

@Select("SELECT id, username, age FROM t_user WHERE id = #{id}") User selectById(@Param("id") Integer id);

注解方式对单表简单CRUD非常友好,代码量少,且MapperScannerConfigurer照常扫描接口,不需要额外配置。但动态SQL一旦变复杂就难受了,比如那种"按条件拼接where"的查询,注解里得把整段动态SQL塞进<script>标签,可读性直线下降,IDE也没法给SQL做语法检查。我给团队定的规矩是:超过五行或者存在动态拼接的SQL,一律走XML。

4.3 混合方式与边界建议

实际项目里最常用的其实是第三种:同包共存。注解负责简单方法,XML负责复杂查询,MapperScannerConfigurer同时扫描两者,MyBatis在绑定逻辑上对两个来源一视同仁,只要namespace和id对得上,你甚至可以让XML覆盖注解实现。不过我不建议在真正上线的重要模块里这么玩,容易让接手的人崩溃——他在注解里改了半天,发现生效的是另一份XML。

参数绑定方面还有一个建议:显式加@Param。Java 8之后虽然可以用-parameters编译参数让MyBatis自动识别参数名,但很多项目压根没开这个编译选项,一旦忘了就会报Parameter 'xxx' not found。与其依赖编译环境,不如每个参数都写清楚@Param("xxx"),稳定、可读性好。

5. 声明式事务:@Transactional为什么偶尔不生效

数据源和Mapper都通了,事务往往是最后一道坎。很多人喜欢用@Transactional,但"注解没生效"是整合篇里提问率最高的话题。

5.1 Spring事务和MyBatis会话是怎么协作的

tx:annotation-driven开启后,Spring会用AOP给标注了@Transactional的Bean生成代理。方法调用进来时,代理会先从TransactionSynchronizationManager里拿到当前线程绑定的事务资源。MyBatis的SqlSessionTemplate在执行SQL前会检查:如果当前线程已经存在Spring事务,它就从事务同步管理器里拿已经绑定的SqlSession,而不是新开一个;事务提交或回滚时,连接上的commit()/rollback()由Spring统一控制。所以MyBatis的会话和Spring事务会话在底层是"同一条连接、同一个动作"。

5.2 四类"注解没生效"的场景

理解了链路,四类失效场景就很好排查了。

第一类,方法自调用。this.method()调用同一个类里另一个@Transactional方法,绕过的是Spring生成的代理对象,事务自然不生效。解决办法是把需要事务的调用放到另一个Bean里,或者通过ApplicationContext拿到代理再调用。

第二类,方法被private或final修饰。默认的代理方式对类做CGLIB增强时无法处理final方法,private方法本来就进不了代理链,事务配置会被静默忽略。

第三类,异常被吞掉或没配rollbackFor。默认情况下@Transactional只对RuntimeException和Error回滚,方法声明里抛出的受检异常不会触发回滚。稳妥的做法是:

@Transactional(rollbackFor = Exception.class) public void batchSave(List<User> users) { // 批量插入逻辑 }

同时要注意方法内部别加try { ... } catch (Exception e) { log.error(...); }这种把所有异常吞掉的写法——异常都没出方法边界,Spring根本不知道要回滚。

第四类,数据源和事务管理器不是同一个对象。比如SqlSessionFactoryBean里配的是A数据源,DataSourceTransactionManager里挂的是B数据源,事务管理器和MyBatis各开各的连接,回滚自然无从谈起。整合阶段务必确认两个配置里的dataSource引用指向同一个Bean。

5.3 传播行为选择的两个经验

默认的REQUIRED表示有事务就加入、没事务就新建,大多数业务都用这个。遇到"批量导入,前面数据不能因为最后一条失败就全回滚"的场景,可以用REQUIRES_NEW把每批操作挂起外层事务、独立提交。但要注意,传播行为同样受代理限制,自调用场景下REQUIRES_NEW也是无效的,这是老手也容易翻车的点。

6. 翻车实录:四类高频报错的完整排查链路

这一节我挑了四个我在群里见过最多、也亲自帮人排查过的报错,每个都按实际排查链路来讲,而不只是丢一个结论。

6.1 "Invalid bound statement (not found)"排查

这个报错的完整链路是:方法运行时,MyBatis要根据接口的全限定名和当前方法名去MapperRegistry里找注册过的语句,找不到就抛这个异常。报错本身只说明了"没找到",真正的问题在哪一环,得按顺序查。

第一步,确认namespace。接口是com.example.mapper.UserMapper,XML的namespace就必须一字不差地写成这个全限定名。我见过把包路径拼错的、多写少写一个字母的,这类错误IDE不报,只能自己核对。

第二步,确认XML文件真的在classes目录里。这里有个典型的Maven坑:XML如果放在src/main/java里(很多人为了方便把Mapper接口和XML放一起),Maven默认的resources配置并不会把.java目录下的xml拷贝进target/classes,运行期自然找不到。排查时直接去看target/classes/mapper下有没有文件。解决方案是在pom.xml里显式声明:

<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </build>

或者干脆把XML统一放到src/main/resources/mapper目录下,保守稳妥。

第三步,检查mapperLocations。路径写的是classpath:mapper/*.xml,实际文件和它对应不上,同样找不到。注意*通配符只匹配当前目录一层,如果XML在mapper/order/这类子包下,得用classpath*:mapper/**/*.xml。

第四步,回头看id和resultType。方法名和XML里的id不一致、返回类型写错,也会报同样的错误。很多时候前三步都查完了没问题,最后才发现是这里。

6.2 "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required"

这个报错的根因是:某个MapperFactoryBean在被实例化时,Spring没有找到它需要的会话工厂。最容易出现这个问题的场景是手动用<bean>逐个声明Mapper工厂,比如:

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="com.example.mapper.UserMapper"/> </bean>

这个配置漏掉了sqlSessionFactory属性,Spring只看到接口不知道上哪执行SQL,于是抛出上述异常。正确做法是补上属性引用,或者干脆放弃手写,统一用MapperScannerConfigurer。如果用了扫描器还报这个错,那大概率是basePackage指向的包下面根本不存在接口,或者接口放在了别的模块没引进来。

还有个容易被忽略的点:如果项目分成了父上下文和子上下文(比如SpringMVC项目常见的applicationContext.xml和spring-mvc.xml),Mapper的扫描配置必须放在加载优先级更高的父上下文中,否则子上下文中注入Mapper时会因为找不到代理对象而报No qualifying bean,这两个错误常常是一对孪生兄弟。

6.3 事务回滚"没生效"的排查链路

表现是:抛出异常后,数据依然写进了库。我排查的顺序是固定的。

第一,看表引擎。用SHOW TABLE STATUS LIKE 't_user'查看,Engine列不是InnoDB而是MyISAM的话,事务天然无效,因为MyISAM不支持事务,MySQL会把每一条SQL的提交当成独立操作。这招经常被忽略,但它是底层检查。

第二,看调用路径。方法是不是被别的Bean通过代理调用的?如果是在同一个类里this.xxx()调用的,先挪出去再说。如果方法是private的,改成public。

第三,看异常是否被吞掉。断点打在方法出口,观察异常到底有没有抛出方法边界。吞掉了就往throw new RuntimeException(e)改,别只打日志。

第四,看rollbackFor。受检异常默认不触发回滚,无脑加上rollbackFor = Exception.class是很多团队的标准配置。

第五,看事务管理器绑定关系。检查DataSourceTransactionManager的dataSource和SqlSessionFactoryBean的dataSource是不是同一个Bean。别看这一步简单,我经手的一个线上事故就是重构时新配了一个数据源Bean,事务管理器和MyBatis各拿各的,最后回滚全失效,数据错乱了一天才定位到。

6.4 Mapper Bean注入失败:"No qualifying bean of type ..."

这个报错在整合中通常会附带一堆候选信息。排查链路是:确认MapperScannerConfigurer的basePackage覆盖了Mapper接口所在的包;确认接口上没被误加@Component之类导致重复定义;确认这个注入动作发生在Spring容器能看到的上下文里。如果是Web项目,还要看看是不是两个上下文重复扫描导致Bean定义冲突,或者扫描配置压根没被加载。

这里给个小技巧:启动时临时开DEBUG日志,看容器到底注册了哪些Bean、每个Bean来自哪个配置,比对着配置猜快得多。

7. 最后补几句实操心得

整篇讲完,最后分享三个比较个人的经验。

第一,新项目整合时一定做一个"最小验证"。只配数据源、一个Mapper接口、一条查询,用JUnit跑通查询成功,再加事务测试,最后才引入Web层或者更复杂的业务。很多人一上来就搭SSM全家桶,报错时根本分不清是SpringMVC的问题还是MyBatis的问题,排查成本翻倍。

第二,我习惯在配置文件里始终开着mapUnderscoreToCamelCase,并且在调试期把SQL日志打开。这两个配置一开,绝大多数"字段值不对""SQL看不懂"的问题能直接暴露在日志里。上线前再根据安全要求决定日志级别。

第三,把配置文件和代码分开管理。数据源密码这类信息至少要走环境变量或者独立的properties文件,用PropertyPlaceholderConfigurer读取,别往工程里硬编码。整合这种基础工作,前期多花十分钟,后面省的是整个团队的时间。

关于"整合Spring+Mybatis",核心其实不在配置本身,而在你能不能把"数据源 → 会话工厂 → Mapper代理 → 事务同步"这条链路在心里画出来。画出来了,任何报错对你来说都只是链路中某个环节的具体故障而已。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询