作为开发,Idea里跑Spring Boot项目突然冒出一行红色堆栈,Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required,我相信不少人第一次看到时都是一脸懵。这个报错几乎是MyBatis集成Spring时的“入门级社死现场”,无论你是刚把MyBatis加到项目里,还是老项目换了个依赖版本,都可能被它拦住。它真正的含义是:MyBatis在向Spring容器注册Mapper接口时,没有找到一个可用的SqlSessionFactory或者SqlSessionTemplate,导致它没法创建Mapper的代理对象。
这篇文章不打算只贴个“加个注解”的答案了事,我会把报错发生的原理、各类触发场景、排查方法、以及多个数据源下的修复方式全部掰开揉碎讲一遍,尽量让你看完之后不只能解决眼前的问题,还能真正理解MyBatis和Spring之间那套协作机制。无论你是刚入门的小白,还是被多数据源折腾过的老手,这篇文章都值得收藏。
1. 先搞清楚报错从哪来:源码倒推异常生成机制
1.1 异常真正的抛出位置
这个报错不是Spring容器随便抛出来的,它的源头在org.mybatis.spring.mapper.MapperFactoryBean这个类里。你去翻它的源码,会看到这样一个方法:
java复制protected void checkDaoConfig() {
super.checkDaoConfig();
if (this.sqlSessionFactory == null && this.sqlSessionTemplate == null) {
throw new BeanCreationException(
"Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
}
}
也就是说,Spring容器在初始化MapperFactoryBean的时候,会调用checkDaoConfig()做一次校验。如果此时这个Bean里面既没有SqlSessionFactory,也没有SqlSessionTemplate,那么直接抛出BeanCreationException,启动失败。
很多人在项目里看到的堆栈长这样:
code复制org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userMapper'
Caused by: org.springframework.beans.factory.BeanCreationException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required
注意Error creating bean with name 'userMapper'这句,它说明了是哪个Mapper接口在创建时出了问题。而这个userMapper对应的Bean,正是MapperFactoryBean生成的。
MapperFactoryBean的作用,是把一个Mapper接口变成一个Spring管理的Bean。当Spring容器需要注入UserMapper时,MapperFactoryBean必须先拿到一个SqlSessionFactory,然后才能用它创建出操作数据库的代理对象。如果工厂都没拿到,代理自然无从谈起。
1.2 容器初始化sequence:为什么报错在启动期
要理解为什么会在启动期就报错,需要知道MyBatis和Spring整合时,Bean的初始化流程大致分三步:
- Spring容器启动,扫描所有配置类和注解。
@MapperScan会把指定包下的所有接口都注册成MapperFactoryBean。 - 每个
MapperFactoryBean在创建时,需要注入SqlSessionFactory或SqlSessionTemplate两者之一。 - 如果容器里根本没有这两个Bean,或者因为某些原因无法注入,那么在第2步就会触发
checkDaoConfig()校验,直接抛异常。
这里的关键点在于,MapperFactoryBean依赖的是SqlSessionFactory类型的Bean。Spring如果发现容器里根本不存在这个类型的Bean,就会在依赖注入阶段失败,最终表现就是这个经典报错。
所以,只要出现这个异常,问题基本锁定在两点:要么容器里压根没有注册SqlSessionFactory;要么注册了,但MapperFactoryBean因为某些原因没有注入成功。
1.3 为什么会"两个属性都要":MapperFactoryBean的依赖设计逻辑
可能会有朋友问,为什么代码里是sqlSessionFactory == null && sqlSessionTemplate == null才报错,也就是说只要有一个不为空就行?这是MyBatis-Spring特意做的兼容设计。
在早期版本中,大家习惯直接用SqlSessionFactory来构建SqlSessionTemplate。后来为了简化配置,MyBatis-Spring允许直接在MapperFactoryBean里注入SqlSessionTemplate,让这个模版类自己持有真正的SqlSessionFactory。所以两个属性只要注入其中一个,Mapper的创建逻辑就能跑通。
这点在排查时很实用:如果你在代码里自定义了SqlSessionTemplate,但漏掉了SqlSessionFactory,报错可能不是这个,而是别的类型不匹配错误。而反过来,始终报“Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required”,说明两个都没有注入成功。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境与排查清单:按顺序自查最常见原因
2.1 第一类:依赖缺失或版本错乱
先自查一下Maven或Gradle依赖。要用MyBatis和Spring Boot整合,光有mybatis这个核心包是不够的,需要引入mybatis-spring-boot-starter。它会帮你把mybatis、mybatis-spring、spring-boot-autoconfigure等必要依赖都拉进来。
xml复制<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.2</version>
</dependency>
如果你的项目用的是Spring Boot 3.x,则要注意版本升级的问题。Spring Boot 3对应的是mybatis-spring-boot-starter 3.x版本,如果你还是引用2.x版本,很可能因为自动配置类不兼容导致SqlSessionFactory压根没注册。
我见过不少项目,在从Spring Boot 2.7升级到3.x时,只是机械地把父版本改了,却没同步换掉MyBatis Starter的版本,最后就是各种奇怪的报错,其中就包括这个“两个属性都需要”的异常。
另一个容易踩坑的点是,项目里同时引了mybatis-spring-boot-starter和mybatis-spring老版本。依赖冲突会导致自动配置部分生效、部分被覆盖,表现也是千奇百怪。
2.2 第二类:注解和扫描配置不对
最常见的原因其实是自己忘了加扫描注解。Spring Boot项目里,通常需要在启动类或配置类上添加:
java复制@MapperScan("com.example.project.mapper")
或者给每个Mapper接口加@Mapper注解。二选一就行,但不少人两个都没做,导致MyBatis的自动配置虽然创建了SqlSessionFactory,但容器根本不知道要去把哪些接口注册成Bean。这种情况下,如果你在@Service里直接注入了Mapper接口,Spring会在注入时报NoSuchBeanDefinitionException,而不是这里的Property ... are required。
那什么时候会触发这个Property报错呢?通常是你用了@MapperScan,但扫描到的Mapper接口无法获得SqlSessionFactory。比如你定义了一个MyBatisConfig类,但配置类本身没有生效;或者配置类里写了SqlSessionFactory的@Bean方法,但方法名不对,被Spring忽略掉了。
还有一种情况,你在@Configuration类里写了类似这样的代码:
java复制@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
这个方法确实会创建SqlSessionFactory,但如果factoryBean.getObject()抛异常,或者SqlSessionFactoryBean的setTypeAliasesPackage填错了包名,整个Bean创建过程失败,同样会导致Mapper初始化时找不到工厂。
2.3 第三类:SqlSessionFactory被"顶掉"了
Spring Boot的自动配置里,MybatisAutoConfiguration会通过@ConditionalOnMissingBean来判断是否需要自动创建SqlSessionFactory。如果你在项目里自定义了一个SqlSessionFactory的@Bean,那么自动配置就不会介入,改由你自己的方法负责创建。
问题往往出在这里:你在配置类里定义了sqlSessionFactory方法,但参数不是DataSource,或者方法内部用了错误的DataSource实例(比如多数据源环境下选错了),导致工厂虽然创建了,却连的是不该连的库,甚至创建失败。还有一种情况是,你的@Bean方法返回值类型写成了SqlSessionFactoryBean,而不是SqlSessionFactory:
java复制@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
return factoryBean;
}
这样Spring容器里注册的Bean类型是SqlSessionFactoryBean,而不是SqlSessionFactory。MapperFactoryBean要注入的是SqlSessionFactory类型,自然找不到。这种细节光看控制台报错很难发现,得去检查Bean定义才能看出来。
2.4 第四类:多数据源场景下指定错工厂
多数据源是另一个重灾区。项目配置了两个库,你为两个库各创建了一个SqlSessionFactory,但@MapperScan创建Mapper时没有区分哪个Mapper用哪个工厂,结果就是Spring不知道该给哪个Mapper注入哪个工厂。
具体来说,你的配置可能是这样的:
java复制@Configuration
public class DataSourceConfig {
@Bean(name = "primarySqlSessionFactory")
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) {
// ...
}
@Bean(name = "secondarySqlSessionFactory")
public SqlSessionFactory secondarySqlSessionFactory(@Qualifier("secondaryDataSource") DataSource dataSource) {
// ...
}
}
然后在启动类上写:
java复制@MapperScan("com.example.project.mapper")
这时候不只一个SqlSessionFactory类型的Bean,MapperFactoryBean如果不明确指定,就会在按类型注入时产生歧义,最终导致注入失败或注入错误。多数情况下,Spring会报NoUniqueBeanDefinitionException,但有些版本会退化成“找不到Bean”,直接抛出Property ... are required。
所以多数据源场景,必须在@MapperScan上指定sqlSessionTemplateRef或sqlSessionFactoryRef,让每个Mapper明确自己属于哪个工厂。
3. 分场景解决方案:从Spring Boot到传统XML配置
3.1 场景A:Spring Boot最简修复
如果你是标准的Spring Boot项目,且没有自定义任何MyBatis相关配置,那么报错大概率出在依赖版本或注解上。先把依赖捋顺,然后确认启动类上有@MapperScan注解。
java复制@SpringBootApplication
@MapperScan("com.example.project.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
这里有个容易忽略的点:@MapperScan("com.example.project.mapper")扫描的是Mapper接口包名,不要手滑写成实体类包名或者服务接口包名。如果Mapper接口分布在多个包,可以用数组一次性写清楚:
java复制@MapperScan({"com.example.project.mapper", "com.example.project.othermapper"})
改完之后,如果还报错,就去IDEA的Maven面板执行一次clean,再重新package。有时候本地仓库里残留了旧的SNAPSHOT包,也会导致依赖混乱。我处理过不少这类问题,最后都是靠重新完整构建解决的。
3.2 场景B:传统Spring + MyBatis XML配置
如果你是传统Spring项目,没有Spring Boot的自动配置帮忙,那事情就更依赖手动装配。你需要保证Spring的XML配置或@Configuration类里,按顺序定义以下内容:
DataSource:数据库连接池,比如Druid或HikariCP。SqlSessionFactory:把DataSource塞进SqlSessionFactoryBean,并指定Mapper XML文件位置。MapperScannerConfigurer或XML里的<mybatis:scan>标签:扫描Mapper接口。
用XML配置的方式通常是:
xml复制<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath*:mapper/*.xml"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.project.mapper"/>
</bean>
这个配置里有个特别容易踩的坑:如果sqlSessionFactory这个Bean没有在Spring容器里正确创建,MapperScannerConfigurer在扫描Mapper接口后,会让每个MapperFactoryBean尝试注入SqlSessionFactory,一旦找不到,就是那个经典报错。所以排查顺序应该是:先确认DataSource创建成功了,再确认SqlSessionFactory创建成功了,最后才去看Mapper的扫描。
3.3 场景C:自定义SqlSessionFactory时
很多人为了解决MyBatis驼峰映射、插件注册、类型处理器注册等问题,会自己定义一个SqlSessionFactory配置类。写法本身没问题,但要注意下面几点。
第一,@Bean方法返回类型一定要是SqlSessionFactory,不是SqlSessionFactoryBean:
java复制@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setTypeAliasesPackage("com.example.project.entity");
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/*.xml"));
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
factoryBean.setConfiguration(configuration);
return factoryBean.getObject();
}
第二,如果你的@Configuration类上还标了@EnableConfigurationProperties或@ConfigurationProperties,确认没有因为配置项缺失导致bean创建中断。
第三,如果你在配置类里同时用了@MapperScan,并且这个配置类不是主启动类,那要注意@MapperScan扫描到的Mapper接口,和自定义的SqlSessionFactory是否在同一个Spring上下文里。如果配置类没有被Spring Boot扫描到,那么一切白搭。可以检查一下配置类所在的包,是否在@SpringBootApplication的扫描范围之内。
3.4 场景D:多数据源里精细化注册SqlSessionTemplate
多数据源比单数据源复杂得多。我给一个我实际用过的方案,思路是每个数据源一套独立的SqlSessionFactory和SqlSessionTemplate,然后通过@MapperScan的sqlSessionTemplateRef把它们关联起来。
java复制@Configuration
public class MyBatisDataSourceConfig {
@Primary
@Bean(name = "primaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "secondaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean(name = "primarySqlSessionFactory")
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/primary/*.xml"));
return bean.getObject();
}
@Bean(name = "secondarySqlSessionFactory")
public SqlSessionFactory secondarySqlSessionFactory(@Qualifier("secondaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/secondary/*.xml"));
return bean.getObject();
}
}
然后在启动类或某个配置类上,分别指定两个扫描包:
java复制@MapperScan(basePackages = "com.example.project.mapper.primary",
sqlSessionTemplateRef = "primarySqlSessionTemplate")
@MapperScan(basePackages = "com.example.project.mapper.secondary",
sqlSessionTemplateRef = "secondarySqlSessionTemplate")
这里有一个容易被忽略的细节:如果你只为SqlSessionFactory定义了@Bean,而没有定义对应的SqlSessionTemplate,那@MapperScan的sqlSessionTemplateRef会找不到Bean。所以要么你统一用sqlSessionFactoryRef,两个@MapperScan各自指定SqlSessionFactory;要么老老实实为每个数据源再配一个SqlSessionTemplate。
我个人更推荐在@MapperScan里使用sqlSessionFactoryRef,因为少一层中间对象。但如果你在代码里其他地方直接注入了SqlSessionTemplate,那还是把它创建出来比较好。
4. 一个能跑通的项目配置示例
4.1 整体目录结构
这一节我给一个完整的示例,按照这个结构搭出来的项目,基本上不会再遇到这个报错。先看目录结构:
code复制src/main/java/com/example/project
├── Application.java
├── config
│ └── MyBatisConfig.java
├── controller
│ └── UserController.java
├── entity
│ └── User.java
├── mapper
│ └── UserMapper.java
└── service
├── UserService.java
└── impl
└── UserServiceImpl.java
src/main/resources
├── application.yml
└── mapper
└── UserMapper.xml
注意这个结构里,UserMapper接口放在com.example.project.mapper下,UserMapper.xml放在resources/mapper下。这种约定式布局可以避免很多配置错误。
4.2 核心代码与配置说明
首先是启动类:
java复制@SpringBootApplication
@MapperScan("com.example.project.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
然后是application.yml中的DataSource配置。这里以最常用的HikariCP为例(Spring Boot 2.x默认连接池):
yaml复制spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
maximum-pool-size: 10
minimum-idle: 5
接着是UserMapper接口:
java复制public interface UserMapper {
User selectById(Integer id);
}
以及对应的UserMapper.xml:
xml复制<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.project.mapper.UserMapper">
<select id="selectById" resultType="com.example.project.entity.User">
select * from `user` where id = #{id}
</select>
</mapper>
这个时候,如果你什么都不自定义,Spring Boot的MybatisAutoConfiguration会自动注册SqlSessionFactory,自动把resources/mapper下的XML解析进来,@MapperScan负责把接口变成Bean。整套流程是可以直接跑通的。
4.3 关键点串联:Spring Boot是怎么把它们串起来的
我们顺着Spring Boot自动配置的逻辑,一步步看它到底做了什么。
第一,MybatisAutoConfiguration探测到类路径下有SqlSessionFactory和SqlSessionFactoryBean,于是它开始自动注册一个SqlSessionFactory。
第二,这个SqlSessionFactory会使用容器里的DataSource,并读取mybatis.mapper-locations、mybatis.type-aliases-package等配置项。如果你在application.yml里设置了:
yaml复制mybatis:
mapper-locations: classpath*:mapper/*.xml
type-aliases-package: com.example.project.entity
那么SqlSessionFactory就会把这些XML和别名都注册进去。
第三,@MapperScan扫描到com.example.project.mapper下的UserMapper接口,为它生成MapperFactoryBean。MapperFactoryBean在创建时,会在容器里找SqlSessionFactory类型的Bean,因为存在且唯一,所以顺利注入。
这一步里有个很关键的判断逻辑:Spring在注入SqlSessionFactory时,是按类型查找的。如果容器里只有一个SqlSessionFactory,那么万事大吉;如果有两个,但没有标@Primary,就会产生冲突。你可以把SqlSessionFactory想象成一个插座,MapperFactoryBean就是充电器,插座越多,充电器越不知道该插哪个。
这个例子虽然简单,但把背后的机制捋顺了,再遇到报错时就不会手足无措了。
5. 常见问题排查实录与避坑经验
5.1 问题速查表
我在工作中遇到过不少变体,下面这张表总结了几个典型场景和对应的处理方式。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
启动即报Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required,且没有自定义任何MyBatis配置 |
漏加依赖、版本冲突、@MapperScan扫描到了空包 |
检查mybatis-spring-boot-starter版本,确认@MapperScan包路径 |
自定义了SqlSessionFactory后报错 |
@Bean返回类型是SqlSessionFactoryBean而不是SqlSessionFactory |
方法返回SqlSessionFactory,内部调用factoryBean.getObject() |
| 多数据源下报错 | 没有指定sqlSessionFactoryRef或sqlSessionTemplateRef |
在@MapperScan上明确指定对应工厂 |
项目里同时有多个SqlSessionFactory,Spring启动就异常 |
缺少@Primary标记 |
在主要的SqlSessionFactory或SqlSessionTemplate上加@Primary |
| 升级Spring Boot版本后报错 | Starter版本与新Spring Boot不兼容 | 同步升级mybatis-spring-boot-starter到对应大版本 |
| 传统XML项目里报错 | MapperScannerConfigurer先于SqlSessionFactory创建 |
检查XML中的Bean顺序,保证SqlSessionFactory定义在先 |
5.2 排查时我常用的三板斧
第一板斧:打开IDEA的Bean结构视图,或者直接看启动日志,确认SqlSessionFactory是否真的被注册了。你可以在Application.java启动类里临时加一个监听器,或者直接在下图位置打断点:
java复制@Bean
public ApplicationRunner runner(SqlSessionFactory sqlSessionFactory) {
return args -> System.out.println(sqlSessionFactory);
}
如果启动时这个ApplicationRunner里能拿到SqlSessionFactory,说明工厂没问题,问题多半出在Mapper扫描上。如果连SqlSessionFactory都拿不到,那就回到依赖和配置类上排查。
第二板斧:在启动时开启Spring的Debug日志,观察MapperFactoryBean的创建过程。在application.yml里加:
yaml复制logging:
level:
org.springframework.beans.factory: DEBUG
org.mybatis.spring: DEBUG
重点看日志里有没有类似Creating shared instance of singleton bean 'sqlSessionFactory'的语句,以及每个Mapper的Bean创建过程。这样能把问题定位到具体是哪个Bean没起来。
第三板斧:使用IDEA的Condition评估功能。在异常抛出端点处打断点,查看当前Spring容器里已经注册了哪些Bean,尤其是sqlSessionFactory、sqlSessionTemplate是否存在。IDEA的Watches里可以直接输入表达式去查,比如:
code复制context.getBean("sqlSessionFactory")
如果能查到Bean,就说明Spring容器里有这个对象,问题出在注入环节;如果抛NoSuchBeanDefinitionException,那问题就出在Bean注册环节。
5.3 说点实在的避坑技巧
第一,不要过度依赖@MapperScan的全包扫描。我见过一些项目,为了省事直接写@MapperScan("com.example"),结果把不该扫描的接口也注册了,启动慢不说,还容易因为某些接口不是Mapper而报奇怪错误。包路径越具体越好。
第二,如果项目里使用了@MapperScan,就不要再用@Mapper注解挨个标了。两套机制同时用虽然不冲突,但会让代码看起来混乱。而且@Mapper注解在Spring Boot + MyBatis Starter场景下,有时会引起部分Mapper重复注册的假象,虽然不影响运行,但排查问题时容易分心。
第三,版本选择要克制。mybatis-spring-boot-starter的大版本最好跟Spring Boot大版本保持一致,不要盲目追新。一些刚开始接触Spring Boot 3的朋友,引入MyBatis Starter时还是用2.3.x,结果打包能通过,启动就翻车,最后查来查去还是版本兼容问题。
第四,如果你始终找不到原因,可以把SqlSessionTemplate也显式定义出来。在@Configuration类里加个Bean,手动指定SqlSessionFactory:
java复制@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
这一步能强制把SqlSessionFactory和SqlSessionTemplate的依赖关系串起来,很多时候能让Spring的自动装配多一层保障。不过这算治标,真正原因还是要找到。
说实话,这个报错本身并不复杂,复杂的是它背后的触发链条太长,涉及依赖、配置、扫描、多数据源等多个环节。我花了不少篇幅把原理和排查思路展开,就是希望大家以后遇到问题时能按图索骥,而不是反复改配置碰运气。在我自己踩坑的过程中,最深的体会就是:遇到这种框架整合报错,先别急着搜答案,沉下心看一下容器里到底注册了哪些Bean,往往比盲改代码高效得多。
