在IDEA里开发Spring Boot项目,遇到Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required这个报错,几乎每个和MyBatis打过交道的人都绕不过去。这个报错信息本身很短,但背后藏着的可能性非常杂:依赖没引对、自动配置没生效、扫描路径写错、多数据源漏配了Bean……我以前第一次遇到时,盯着控制台看了快半小时,硬是没搞清楚问题出在哪。
这篇文章我会把我自己排查这个报错的过程完整复现一遍,从报错现场、底层原理,到分步骤的排查链路,再到几个高频踩坑场景的对照修复。如果你也正好卡在这,建议按照文章的顺序走一遍,大概率几分钟就能定位到根因。
1. 报错现场复盘:堆栈里真正值得看的几行内容
先说这个报错长什么样。项目启动时,控制台刷出一大片红色日志,常见的有两种形态。
第一种是Spring Boot启动框架级别的失败:
*************************** APPLICATION FAILED TO START *************************** Description: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required第二种更隐蔽一些,会先抛一个BeanCreationException,里面裹着真正的异常:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userMapper' defined in file [xxx/UserMapper.class]: Instantiation of bean failed; ... Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required很多人习惯盯着第一行看,其实没用。第一种形态是Spring Boot用FailureAnalyzer包装后的结果,信息被压缩得就剩下那一句;第二种形态的Caused by才是真正值得分析的线索。
我在IDEA里排查这个报错时有个习惯:先把IDEA底部控制台的日志模式切换成"按时间线显示",然后把日志拉到Caused by之后的三四行,看它是在创建哪个Bean时报的错。如果Caused by前面紧跟着Error creating bean with name 'xxxMapper',说明问题出在某个Mapper接口的注册环节;如果紧跟着的是xxxConfiguration,那基本可以断定是某个配置类里的Bean定义出了问题。
这个报错还有一个让人迷惑的点:它可能发生在MybatisAutoConfiguration里,也可能发生在MapperScannerConfigurer扫描之后。同样是这一句话,一个是因为SqlSessionFactory本身没创建,另一个是因为Mapper扫描器创建了MapperBean但拿不到工厂。这两种情况排查方向完全不同,所以第一步永远是看堆栈,而不是直接搜这句话。
2. 报错原理拆解:为什么Spring会向MapperBean要这两个属性
既然要排查,就得知道这个异常是从哪抛出来的。它不在Spring容器核心代码里,而是在MyBatis和Spring的桥接包mybatis-spring里,具体位置是MapperFactoryBean这个类。
MapperFactoryBean实现了Spring的FactoryBean和InitializingBean两个接口。Spring在创建Bean时,如果发现它实现了InitializingBean,就会在属性赋值完成后调用afterPropertiesSet()方法。这个方法里有一段关键校验:
public abstract class SqlSessionDaoSupport implements InitializingBean { private SqlSessionTemplate sqlSessionTemplate; public void afterPropertiesSet() { this.sqlSessionTemplate = createSqlSessionTemplate(this.sqlSessionFactory); } }真正的校验还得看MapperFactoryBean的父类SqlSessionDaoSupport之前的逻辑。简单说,在创建Mapper代理对象之前,Spring必须拿到一个SqlSessionFactory或者SqlSessionTemplate,这两个属性至少要有其中一个。如果两个都是空,就会抛出这段异常:
if (this.sqlSessionFactory == null && this.sqlSessionTemplate == null) { throw new IllegalArgumentException("Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required"); }我把这个过程翻译成人话:Spring容器收到指令去创建userMapper这个Bean,而userMapper本质上是一个动态代理。生成代理需要一把"钥匙"去连接数据库的SqlSession,这把钥匙就是SqlSessionFactory。结果Spring找了一圈发现,容器里根本没有SqlSessionFactory,也没有现成的SqlSessionTemplate,于是只能罢工。
那正常情况下这东西从哪来?如果用的是mybatis-spring-boot-starter,Spring Boot的自动配置类MybatisAutoConfiguration会在启动时自动注册两个关键的Bean:一个是SqlSessionFactory,一个是SqlSessionTemplate。换句话说,只要自动配置正常生效,MapperBean永远不会缺钥匙。
所以,这个报错的本质往往不是MyBatis本身的问题,而是"自动配置失效"或"Bean定义被意外覆盖"导致的连锁反应。我排查时脑子里始终绷着一根弦:不是去手动往容器里塞SqlSessionFactory,而是搞清楚为什么它没被自动注册进来。
3. 从依赖到配置的四步排查链路
3.1 第一步:确认引入的是不是mybatis-spring-boot-starter
这一步看起来低级,但真的有不少人栽在这。如果你的pom.xml里只有:
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.16</version> </dependency>那不好意思,MybatisAutoConfiguration是不会被激活的。因为自动配置类在mybatis-spring-boot-autoconfigure这个模块里,starter的作用只是把mybatis核心、mybatis-spring、autoconfigure打包到一起来触发Spring Boot的自动配置机制。
正确写法是:
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency>这里还要提醒一下版本适配。Spring Boot 2.x对应的是mybatis-spring-boot-starter2.x,Spring Boot 3.x对应的是3.x。如果用Spring Boot 3.2却塞一个2.3.2的starter,启动虽然不一定立刻报错,但后续非常容易触发各种Bean类型不匹配的问题。
3.2 第二步:检查@MapperScan的扫描路径
这个报错和扫描路径有很微妙的关系。如果@MapperScan写在启动类上,但basePackages写的是Mapper接口所在的包路径的父包或子包,一般没问题;但如果你写了精确到某个子包,而Mapper接口在另一个包下,Spring不会报"找不到接口",而是会静默跳过——因为Spring的类路径扫描本身就不报错。
我们的项目里就出过这么一件事:@MapperScan(basePackages = "com.demo.dao"),但Mapper接口实际上放在com.demo.mapper包下。启动时控制台很安静,结果一访问业务接口就报这个sqlSessionFactory的错。原因就是Mapper扫描器确实尝试创建了某个代理,但在生成代理时发现没有对应的Factory可用。
3.3 第三步:判断是否动了MyBatis自动配置的奶酪
Spring Boot的自动配置有一个特点:如果你在@Configuration类里手动定义了同类型的Bean,自动配置会通过@ConditionalOnMissingBean条件放弃注册。这本来是好事,给了你自定义的空间,但也很容易踩坑。
比如有人写了这样一个配置:
@Configuration public class MyBatisConfig { @Bean public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); return factoryBean; } }这段代码有个隐蔽问题:方法返回类型是SqlSessionFactoryBean。在Spring容器里,它注册的是一个FactoryBean,而自动配置需要的是SqlSessionFactory类型。两个Bean的类型不一样,MybatisAutoConfiguration发现容器里没有SqlSessionFactory,于是继续执行自己创建。结果你自定义的FactoryBean和自动配置创建的Factory产生了冲突,Mapper扫描时拿到的是错乱的Bean,最终一样报missing属性。
正确做法是让自定义配置和自动配置各自安好,或者直接用@MapperScan走扫描路线,而不是再手写FactoryBean。如果真的需要自定义SqlSessionFactory(比如加插件、加类型处理器),方法返回类型应该直接写SqlSessionFactory,并且让Spring Boot自动配置的逻辑承接你的Bean。
3.4 第四步:多数据源场景下的特殊雷区
如果你用了多个数据源,排查思路又要变一下。一个项目同时配置了主库、从库,每个数据源都需要各自的SqlSessionFactory或SqlSessionTemplate。常见错误是:主库配了工厂,从库只给了DataSource,没配工厂。Mapper扫描器把两个库的Mapper接口混在一个扫描路径里,结果生成代理时发现从库的Mapper没有对应的Factory,于是整锅炸掉。
这种情况的报错堆栈通常会指向一个具体的Mapper名字,比如Error creating bean with name 'db2UserMapper'。看到这个信号,基本可以断定是某条扫描链路缺了一条Factory的"分链"。修复方案很明确:要么把两个库的Mapper拆开放到不同路径,用两个@MapperScan分别绑定各自的SqlSessionTemplate:
@Configuration public class DataSourceConfig { @Primary @Bean(name = "sqlSessionTemplate") public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory factory) { return new SqlSessionTemplate(factory); } }要么给从库单独建一个SqlSessionFactory,并保证它也能被Spring扫描到。
4. 四个高频复现场景的修复对照
4.1 场景一:pom里只引了mybatis,没引starter
我见过不止一次这种配置:其他同事在项目中直接用原生mybatis写了一个工具,后来又引了一个Spring Boot的模块,两边一混合,pom里出现了两个MyBatis相关的依赖,但偏偏没有mybatis-spring-boot-starter。启动时项目不报依赖冲突,也不报缺包,就是到了创建MapperBean这一步突然冒出来这个错误。
这种场景下,修复特别简单。把org.mybatis那个纯依赖删掉,或者明确加上starter依赖。如果项目里确实需要原生的org.mybatis:mybatis(比如自己写了不依赖Spring的纯JDBC工具),那也没关系,让starter的版本覆盖它即可。一般情况下,mybatis-spring-boot-starter已经包含了mybatis和mybatis-spring,没必要重复声明。
4.2 场景二:自定义配置类被IDE自动优化成了private方法
这个坑很少有人提,但IDEA确实会给你"添乱"。当你在配置类里写@Bean方法时,IDEA的某些重构操作可能会把方法改成private。比如:
@Bean private SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); return bean.getObject(); }Spring对@Bean方法的要求是:非private、非final,并且要能被CGLIB代理。一旦方法变成private,Spring会认为这个方法不满足Bean注册条件,直接忽略它。结果就是容器里没有SqlSessionFactory,自动配置又发现不了它,最终触发同样的异常。
排查时要养成看方法修饰符的习惯,特别是当你发现"代码看起来完全正确"却报错时,先看@Bean方法是不是public。
4.3 场景三:@MapperScan路径写在了非配置类上
@MapperScan必须用在@Configuration类上才会被Spring处理。如果你不小心把它加在了普通Service类上,比如:
@Service @MapperScan("com.demo.mapper") public class UserService { }Spring扫描到这个注解时会创建MapperScannerConfigurer,但由于所在类不是配置类,这个注解不会被ConfigurationClassPostProcessor处理,扫描器等于没执行。然后Mapper接口没有注册成Bean,等到其他代码尝试注入Mapper时,要么报NoSuchBeanDefinitionException,要么在某种代理场景下报当前的missing属性错误。
我遇到过一个变种:@MapperScan放在启动类上没问题,但启动类本身用了@ComponentScan而且过滤规则把配置类排斥了。这种配置下来,启动日志里甚至不会出现MyBatis相关的任何输出,非常迷惑。
4.4 场景四:手动new了SqlSessionTemplate但没set工厂
有人为了追求"灵活",在配置类里自己new了一个SqlSessionTemplate:
@Bean public SqlSessionTemplate sqlSessionTemplate() { return new SqlSessionTemplate(null); }这代码能编译通过,因为SqlSessionTemplate构造器接受一个SqlSessionFactory参数,传null不报编译错误。等Spring初始化MapperBean时,把这个空壳template传进去,内部一执行就出问题,而且报的错误往往就是这个missing属性的变体。
正确做法是在构造时传入真实的SqlSessionFactory,或者干脆把FactoryBean返回给Spring,让容器来完成装配。
5. 排查过程中非常实用的两个调试手段
5.1 通过启动日志反查Bean注册情况
遇到这种报错,最怕的就是对着代码干瞪眼。我的做法是先把日志级别调起来看Bean注册的全过程。最简单的方式是在application.properties里加一句:
logging.level.org.springframework.boot.autoconfigure=DEBUG重启之后再刷新日志,重点搜两个关键词:MybatisAutoConfiguration和SqlSessionFactory。如果日志里能看到类似:
MybatisAutoConfiguration matched: - @ConditionalOnClass found required classes 'org.apache.ibatis...'说明自动配置类的激活条件全部满足,排除了一类问题。如果压根没有匹配记录,说明依赖或者类路径有问题。
5.2 用条件断点卡在异常抛出前
如果你用的是IDEA,这个方法特别适合定位问题链路。先在依赖包里找到SqlSessionDaoSupport.afterPropertiesSet()方法,在这行代码上打一个条件断点。条件可以写成this.sqlSessionFactory == null && this.sqlSessionTemplate == null。
这样每次启动到了一个MapperBean创建时,只要满足"两个属性都为空"的条件,IDEA就会停住。这时候切换到Debug窗口,展开调用栈,从下往上翻,你就能看到到底是谁创建了这个MapperBean——是@MapperScan扫描器,还是某个配置类手动注册的。这一步基本能把问题定位到具体代码行。
我用的另一个小技巧是直接在启动时给MybatisAutoConfiguration类加一个断点,看它是如何注册SqlSessionFactory的。这能直接判断是不是自定义配置把自动配置挤掉了。
5.3 临时写一个CommandLineRunner打印Bean清单
如果上面的断点太麻烦,还有一个更粗暴但很有效的方法:在启动类里临时加一段代码,把容器里所有名称带SqlSession的Bean打出来:
@Bean public CommandLineRunner printBeans(ApplicationContext ctx) { return args -> { String[] beanNames = ctx.getBeanNamesForType(SqlSessionFactory.class); System.out.println("SqlSessionFactory beans: " + Arrays.toString(beanNames)); String[] templateNames = ctx.getBeanNamesForType(SqlSessionTemplate.class); System.out.println("SqlSessionTemplate beans: " + Arrays.toString(templateNames)); }; }如果输出结果是空数组,那就说明容器里压根没有这两个Bean,问题聚焦在"为什么没有自动创建"。如果输出有Bean但启动仍然报错,那就是另一条链路的冲突了——比如多个SqlSessionFactory但MapperBean拿错了。
6. 这个报错修好之后,还有哪些连带问题要一起处理
报错修好后,不等于MyBatis就完全正常了。我在这边额外说三个自己碰到过的连带问题,避免你修完一个坑又掉进下一个。
第一是Mapper接口已经能被Spring管理,但运行时调用方法报Invalid bound statement (not found)。这种情况通常是XML文件路径没配置好,MyBatis扫描不到SQL语句。检查mybatis.mapper-locations配置是否正确,例如:
mybatis.mapper-locations=classpath:mapper/*.xml注意:如果用了多数据源,每个SqlSessionFactory都要分别设置自己的mapperLocations,只配一个全局的不够。
第二是事务不生效。当SqlSessionFactory被手动创建后,Spring的事务管理器需要通过DataSourceTransactionManager来协调事务。如果事务管理器创建的DataSource和你SqlSessionFactory里的不是同一个,事务就会静默失效。排查时先确认容器里是否只有一个数据源Bean,以及事务管理器是否引用了正确那个。
第三是懒加载问题。假如项目里配置了@MapperScan和自定义SqlSessionFactoryBean,但FactoryBean没有调用getObject()而是直接返回了工厂对象,那么在注入Mapper时会拿到一个错误的代理,表现就是"启动时不报错,一调用就炸"。这类问题在Spring 5.3版本之后尤其容易混淆,因为FactoryBean的早期解析行为有调整。
说实话,这个报错本身排查难度不算高,真正费时间的是搞不清楚问题到底出在自动配置失效、Bean冲突还是扫描路径错位。我的个人经验是:遇到它先别急着改代码,先去pom确认starter,再去启动日志确认自动配置是否匹配,最后才去看配置类。三步走完,大多数情况就水落石出了。