每一次用Spring操作数据库时,这句异常早晚会出现在控制台上。No qualifying bean of type 'org.springframework.jdbc.core.JdbcTemplate' available,直译过来就是Spring容器在整个IoC仓库里翻了一遍,都没有找到JdbcTemplate类型的候选Bean。别急着往代码里堆@Bean,因为这句话背后往往挂着一连串问题:数据源有没有起来、自动配置有没有生效、组件扫描有没有扫到、多数据源场景下有没有出现同名竞争。我最早在某公司数据平台的报表工程里撞上它时,第一反应是查依赖,第二反应是翻配置文件,最后折腾了大半天才发现问题出在一个非常不起眼的小地方。这篇文章就把这些年的定位思路和修复方案一次讲清楚,对刚入门的Java开发者来说可以当排查手册,对老手来说也能回顾一遍Spring的Bean装配机制。
1. 先看现场:这个报错到底在说什么
1.1 异常信息的完整含义
先把整条异常信息拆开看,它不是一个简单的“找不到类”信息,而是一段包含容器语义的提示:
No qualifying bean of type 'org.springframework.jdbc.core.JdbcTemplate' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}几个关键点:
No qualifying bean表示容器中存在某种类型,但没有满足条件的候选Bean,注意是“没有”而不是“类型不匹配”。expected at least 1 bean which qualifies as autowire candidate的意思是Spring在注入点需要至少一个候选者,而它一个都没找到。- 最后的
Dependency annotations指出了注入方式,这里是@Autowired(required=true),也就是说这个依赖是强制的,容器找不到就直接抛异常,而不是容忍空值。
如果你的代码里是这样写的,那么恭喜你,已经踩进了最常见的触发场景:
@Service public class ReportService { @Autowired private JdbcTemplate jdbcTemplate; public List<Map<String, Object>> queryUserList() { return jdbcTemplate.queryForList("select * from t_user"); } }这里Spring要在注入ReportService时找到JdbcTemplate,而容器里没有注册过这个Bean,于是异常在启动阶段就直接爆出来了,时间点通常是在应用上下文刷新期间,也就是Spring扫描完所有配置类、准备完成依赖注入的那一步。
1.2 最容易踩坑的几个入场姿势
这个异常并不只出现在初学者身上,我见过好几种场景都会触发它:
- 手动创建的Spring项目,没有引入
spring-boot-starter-jdbc,只引入了spring-jdbc,于是自动配置没生效,JdbcTemplate当然不会凭空出现。 - 项目里存在多个
@Configuration类,但配置JdbcTemplate的那个类不在主启动类的扫描路径下,@Bean方法从未被执行。 - 使用
@Autowired注入了JdbcTemplate,但实际容器里只有DataSource,没有额外的JdbcTemplate Bean。 - 多数据源配置时,手动注入了多个
DataSource,但只给其中一个数据源配置了JdbcTemplate,在错误的位置注入了另一个。 - 单元测试里只加载了Service层,没有加载数据访问配置,导致测试上下文中根本没有JdbcTemplate。
所以说,这个错误的定位思路不能只盯着“缺一个Bean”这一层,还要理解Spring的Bean装配机制是怎么运作的,否则就容易修完一个坑又掉进另一个坑。
2. 核心原因拆解:基础设施为什么没有装进容器
2.1 表面上缺的是一个JdbcTemplate,实际缺的是一整条数据访问链路
JdbcTemplate本身不是一个自动实例化的类,它在Spring容器里通常依赖两个前提:
- 必须有
DataSource类型的Bean存在。 - 必须显式或自动创建一个JdbcTemplate实例,并把DataSource塞进去。
JdbcTemplate的构造器很简单,它就是拿着一个DataSource来干活:
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);如果你只配置了一个DataSource,却没有注册JdbcTemplate,那Spring不会“好心”地猜出你想要一个JdbcTemplate。它只知道你的Service里标了一个@Autowired private JdbcTemplate jdbcTemplate;,而容器里查无此物,于是抛错。
这就像你去食堂打饭,窗口师傅把菜都备好了,米饭也蒸上了,但取餐台上确实没有你要的那份套餐,你喊了半天“我要套餐A”,后厨没听到,自然不会变出一份套餐A给你。你需要做的就是明确告诉食堂:“请给这份菜配一份米饭。”对应到Spring里,就是注册一个JdbcTemplate Bean。
2.2 自动配置为什么没兜住
很多用Spring Boot的人会觉得奇怪:明明Spring Boot主打自动配置,为什么JdbcTemplate没有被自动创建?如果你用的是Spring Boot,并且没有做任何额外配置,那正常情况下自动配置是能兜住的。自动配置类JdbcTemplateAutoConfiguration会在类路径上有org.springframework.jdbc.core.JdbcTemplate,并且容器中存在唯一的DataSource时,自动创建一个JdbcTemplate Bean。
注意,这里有个大前提,就是自动配置的生效条件。条件包括:
- 容器中不存在同名Bean,即没有
@ConditionalOnMissingBean(JdbcTemplate.class),如果你自己定义了一个JdbcTemplate,自动配置就不再覆盖了。 - 类路径上必须包含
spring-jdbc,也就是你要引入相关依赖。 - 必须存在一个可用的
DataSource。
很多项目报这个错,根源是Spring Boot的自动配置没有满足条件。最常见的情况是pom.xml里引的是spring-boot-starter-web,但数据库访问相关的起步依赖并没有加进去。
也有些人用的是Spring Framework纯手动配置,没有Spring Boot概念,那当然不会自动创建JdbcTemplate。这种情况下,你要么在XML配置文件里定义JdbcTemplate,要么在@Configuration类里写@Bean方法,否则肯定报错。
2.3 组件扫描的“匪夷所思”问题
除了依赖与自动配置,还有一种隐蔽原因,就是组件扫描范围不正确。
假设你的项目结构是这样的:
com.example ├── application/Application.java ├── service/ReportService.java └── config/DbConfig.java启动类Application.java在com.example.application包下,默认扫描的是com.example.application这个包及其子包,而config.DbConfig和service.ReportService都可能在扫描范围之外。
结果就是:
ReportService没有被Spring管理,自然轮不到它去注入JdbcTemplate。DbConfig里的JdbcTemplate Bean也不会注册。
如果你在ReportService里注入JdbcTemplate,又把@ComponentScan的basePackage配错了位置,容器里就连Service都没有,报错就会先出在Service本身找不到上而不是JdbcTemplate。但有一种情况更迷惑:Service通过别的途径被扫描到了,而配置类没被扫描到,那么你看到的异常就是“找不到JdbcTemplate Bean”,而不是“找不到Service”。
这就是为什么我一直强调,遇到这种报错先别急着加@Bean,而是先理一下启动类到底扫描了哪些包,再看配置类有没有被加载。
3. 修复路径:从配置到代码一步步把Bean喂给Spring容器
3.1 方案A:JavaConfig显式注册JdbcTemplate
最直接的修复方式,就是在任意被扫描到的@Configuration类中显式声明JdbcTemplate。
@Configuration public class DatabaseConfig { @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } }这段代码的关键点在于方法参数DataSource dataSource。Spring在创建JdbcTemplate之前,会先从容器里找DataSource注入到方法参数里,然后再返回JdbcTemplate实例。
前提是你容器里得先有一个DataSource。如果DataSource也没有,那就要连数据源一起配置。
最简单的方式是使用Spring Boot的配置文件:
spring: datasource: url: jdbc:mysql://localhost:3306/report_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver只要引入了spring-boot-starter-jdbc和对应的数据库驱动,Spring Boot就会根据这些配置自动创建DataSource。这时你再加一个JdbcTemplate Bean就行,DataSource参数会由Spring注入。
如果你完全不依赖Spring Boot,而是在普通Spring项目里手动配置,那就用DriverManagerDataSource这类实现,注意它只适合测试环境,生产环境建议用连接池。
@Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/report_db"); dataSource.setUsername("root"); dataSource.setPassword("123456"); return dataSource; } @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); }这里有个实操细节:jdbcTemplate这个Bean方法名,Spring默认会把方法名当作Bean名,但类型是JdbcTemplate。如果你有两个同类型Bean,需要特别注意命名区分,否则就可能出现“不知道该注入哪一个”的冲突。
3.2 方案B:完全信赖自动配置,不写JdbcTemplate Bean
有些团队项目里并没有手动定义JdbcTemplate,但代码照样运行得很好,原因在于Spring Boot的自动配置已经创建了一个。我在第2节说过,自动配置的条件是容器中没有自定义的JdbcTemplate Bean。
如果你项目的依赖长这样:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>并且你也在配置文件中正确配置了数据源,那你完全不需要写JdbcTemplate的@Bean。这样反而更简洁,还能保证自动配置帮你处理一些默认行为。
问题恰恰在于,很多人不知道Spring Boot能自动创建JdbcTemplate,于是自己又定义了一个,这样就等于多写了一步。如果我的代码里已经显式定义了JdbcTemplate,再引入相关自动配置,自动配置会通过@ConditionalOnMissingBean放弃生效,我用的是自己的Bean。这个并不算错误,但有时会导致一些预料外的重复配置,尤其在多数据源场景下,容易让人犯迷糊。
所以修复路径B其实是:减少多余配置,让自动配置来干活。你需要做的只有两件事:
- 确认依赖引入无误。
- 确认数据源配置无误。
然后用@Autowired直接注入JdbcTemplate,就可以开始写数据访问逻辑了。
3.3 方案C:从数据源这个源头下手排查
如果加了JdbcTemplate Bean后还是报错,或者是报成了其他数据源异常,那问题很可能不在JdbcTemplate本身,而在于DataSource。
我把常见的数据源问题列个清单:
spring.datasource.url配置成了错误的数据库地址或端口,连接失败导致DataSource创建不了。- 数据库驱动没引入,比如你用的是MySQL 8,驱动类
com.mysql.cj.jdbc.Driver需要对应版本的依赖。 - 用户名或密码包含特殊字符,没有正确转义,解析失败。
- 配置了多个数据源来源,比如同时存在
application.yml和application.properties,两个文件里都定义了数据源,其中一份覆盖了另一份。
在这种情况里,就算你定义了JdbcTemplate的Bean,因为DataSource Bean创建失败,注入链还是断的。Spring会给你一串非常长的嵌套异常,JdbcTemplate相关的只是最外层,真正的根因在最底部。
遇到嵌套异常时,我一般习惯先翻最后的Caused by。例如:
Caused by: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class这才是关键。它说明数据源的driver没有判定出来,常见的解决办法是检查驱动的Maven坐标,以及driver-class-name配置是否正确。
3.4 多数据源场景下的修复方式
单数据源修起来很容易,但很多人踩坑最多的其实是多数据源。我在某电商中台系统里就配置过两个库,主库管订单,从库管商品。这时候,如果还是用自动配置那一套,Spring Boot会面临一个选择困难:你给了它多个DataSource,它不知道该用哪个来创建JdbcTemplate。
启动时会报出类似这样的异常:
Consider marking one of the beans as @Primary, updating the consumer configuration to accept the multiple beans, or using @Qualifier to identify the bean that should be consumed解决思路有两个。
第一个思路是给其中一个DataSource标上@Primary,那么自动配置会用这个主数据源创建JdbcTemplate。这适合主从分离但绝大多数场景都用主库的情况。
@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(); }然后你只需要在主要业务代码里直接注入那个默认的JdbcTemplate即可,次级数据源需要单独构造自己的JdbcTemplate,再通过@Qualifier注入。
第二个思路是彻底放弃自动配置,手动为每一个数据源建立独立的JdbcTemplate,并在使用时明确指定用哪个。
@Bean(name = "primaryJdbcTemplate") public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource dataSource) { return new JdbcTemplate(dataSource); } @Bean(name = "secondaryJdbcTemplate") public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource dataSource) { return new JdbcTemplate(dataSource); }相对应Service里注入时:
@Autowired @Qualifier("primaryJdbcTemplate") private JdbcTemplate primaryJdbcTemplate; @Autowired @Qualifier("secondaryJdbcTemplate") private JdbcTemplate secondaryJdbcTemplate;这样写的好处是彻底消除了歧义,坏处是代码稍微啰嗦。但现实中很多团队宁可要这种明确的啰嗦,也不要踩“注入错库”这种事故。
3.5 快速验证容器里的Bean状况
每次修复完,我都会先在测试里跑一遍ApplicationContext检查,确认Bean真的注册进去了。不要只在启动时等异常,那太被动了。
一个很简单的方法,在测试类里注入ApplicationContext,然后打印所有JdbcTemplate相关的Bean:
@SpringBootTest public class DatabaseConfigTest { @Autowired private ApplicationContext applicationContext; @Test public void testPrintJdbcTemplateBeans() { Map<String, JdbcTemplate> beansOfType = applicationContext.getBeansOfType(JdbcTemplate.class); System.out.println("JdbcTemplate count: " + beansOfType.size()); beansOfType.keySet().forEach(System.out::println); } }如果打印出来的数量是0,说明定义JdbcTemplate的配置类根本没有被加载,或者依赖缺失。如果打印出来的数量大于1,说明存在多个JdbcTemplate,后续注入就要考虑@Qualifier,否则也可能报错。
这个验证手段在我排查多数据源问题时救过我好几次,它不需要重启整个项目猜测结果,直接观察容器内容,效率高很多。
4. 常见问题与排查技巧实录
4.1 为什么配置看起来没问题,但还是找不到Bean
有一类场景特别气人:pom.xml里有spring-boot-starter-jdbc,application.yml里也配置了数据源,但启动还是报错。
这种时候,我通常会先怀疑配置有没有被生效。因为Spring Boot的自动配置可以被人为排除掉,比如在@SpringBootApplication上写了:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})一旦你排除了DataSourceAutoConfiguration,整个数据源自动配置链路都停了,后续的JdbcTemplateAutoConfiguration也因为找不到DataSource而放弃生效。于是容器里既没有数据源,也没有JdbcTemplate。
排查时可以先搜一下项目里有没有exclude相关的代码,尤其是在一些被拷贝过来的老项目里,这种坑藏得非常深。
4.2 为什么会有两个同类型的Bean,反而报错
有些人知道怎么创建JdbcTemplate Bean了,结果给两个数据源分别创建了两个JdbcTemplate,然后在某个Service里只写了@Autowired private JdbcTemplate jdbcTemplate;,启动报错从“No qualifying bean”变成了“expected single matching bean but found 2”。
一个是找不到,一个是找到好几个不知道该用谁,根源都在“容器与注入点没匹配好”。此时要么给其中一个Bean加@Primary,要么在注入点加@Qualifier,二选一即可。
我个人的习惯是:在一个数据源为主的系统里,用@Primary更简洁;在需要长期维护的多数据源系统里,用@Qualifier配合明确命名更安全。前者少写一点代码,后者排障时更容易看出来每个Bean分别对应哪个数据源。
4.3 Spring版本不同,报错细节有差异
Spring Framework 5.3到Spring Boot 2.x,再到Spring Boot 3.x,这段报错的文本表述基本稳定,但底层行为稍有差别。Spring Boot 3.x基于Spring Framework 6和Jakarta EE规范,依赖包名变化比较大,很多老项目在升级过程中会出现“类找不到”和“Bean找不到”混在一起的情况。
比如从javax.sql.DataSource改成jakarta.sql(实际上DataSource还在javax.sql包下,但数据库驱动和Spring内部对包的处理会有差异),如果你只改了部分代码,可能就会在数据源创建时出现奇怪的错误,进而导致JdbcTemplate没被创建。
我觉得这里值得提醒一句:遇到这类报错时,不要只盯着Spring JDBC的代码,先确认JDK版本、Spring Boot版本、各依赖版本之间是否兼容。我踩过一次很离谱的坑,就是MySQL驱动版本太旧,根本不认识新版Spring的配置属性,连接池创建失败,整个Bean链全崩了。
4.4 IDE缓存带来的假象
还有一个经常让人抓狂的问题:代码明明已经加了@Bean,但启动之后还是找不到JdbcTemplate。项目在IDE里重启,控制台输出的还是老错误。
这种情况十有八九是IDE没有重新编译,或者项目构建缓存中有旧产物。正常的处理方式是:
- 先执行
mvn clean compile或gradle clean build,强制刷新。 - 再重启应用。
- 如果还在报错,可能是因为IDEA的缓存问题,可以尝试
File -> Invalidate Caches,但这种情况比较少见。
我在团队里带新人时看到过好几次这种所谓“玄学问题”,其实都只是没清理target目录或者没重新编译而已。所以遇到“明明改了配置却没生效”的现象,先怀疑构建,再怀疑Spring,顺序不要反。
4.5 单元测试里也有一份独立的小上下文
如果你只在测试代码里遇到这个异常,而在正式启动应用时一切正常,那就要检查测试上下文和主应用上下文的差异。
典型的场景是测试类上标注了@WebMvcTest或@DataJpaTest,这些切片测试只会加载特定的一部分Bean,不会加载完整的配置类。如果一个测试类想测JdbcTemplate相关的Service,又用了@WebMvcTest,那自然找不到JdbcTemplate。
解决办法是取消切片注解,改用@SpringBootTest,或者在测试配置中单独定义需要的Bean。如果测试很轻量,只想测某一段SQL生成逻辑,甚至可以先不用Spring加载,直接手动new JdbcTemplate(dataSource)来调用,省去一堆复杂的上下文加载时间。
5. 实操心得与设计建议
5.1 写Bean声明时养成的好习惯
我在经历了无数次这种“找不到Bean”的排查后,逐渐总结出几个习惯。
第一,凡是手动注册JdbcTemplate,都会写明方法名,并刻意带上数据源来源。比如orderJdbcTemplate和productJdbcTemplate,这样在报错信息里看到Bean名时,能一眼定位它是哪条链路。
第二,配置类放在启动类同一根包路径下,我会尽量控制在两个包以内,避免组件扫描路径配置过于复杂。
第三,优先使用构造器注入而不是字段注入。字段注入写起来很爽,但依赖关系隐藏在字段声明里,出了问题不好排查,也不利于写单元测试。构造器注入的好处是依赖列表一目了然:
@Service public class ReportService { private final JdbcTemplate jdbcTemplate; public ReportService(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } }这段代码在Spring 4.3以后甚至不需要加@Autowired,类只有一个构造器时,Spring会自动进行构造器注入。如果容器里还是没有JdbcTemplate,报错信息依然会清晰指向这个构造器参数。我觉得对于新人来说,构造器注入还有一个隐性好处:字段注入在测试里必须靠反射或Spring上下文才能塞进去,而构造器注入直接new就能测试。
5.2 与事务相关的一些细节
JdbcTemplate本身不管理事务,它只是执行SQL。如果你在项目里用到@Transactional,那Spring是通过事务管理器向数据源连接挂上事务的。JdbcTemplate执行时会把连接视为事务性连接。
这里有一个容易踩的点:如果项目定义了多个DataSource,但只配置了一个DataSourceTransactionManager,那么事务只会绑定主数据源对应的JdbcTemplate。另一个JdbcTemplate的操作不会自动纳入事务管辖范围。
所以多数据源下,不仅要考虑JdbcTemplate怎么注入,还要考虑事务管理器怎么建立。常见做法是给每个数据源都建一个专用的DataSourceTransactionManager,并在需要事务的Service方法里通过@Transactional("orderTransactionManager")指定事务管理器。
单独用JdbcTemplate做数据访问的时候,事务还可以通过TransactionTemplate手动控制:
@Bean public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { return new TransactionTemplate(transactionManager); }这种写法在执行比较灵活的批处理任务时非常有用,你可以在代码里精确控制事务边界,比在Service方法上砸一堆@Transactional更灵活。
5.3 用测试隔离数据库,降低排障成本
处理“No qualifying bean”问题最快的方式,往往是先隔离掉外部数据库连接。一个很常见的场景是本地开发环境连接不到测试库,导致数据源创建失败,间接引发JdbcTemplate不可用。这时候如果不想连数据库,也不想引入复杂的嵌入式数据库依赖,可以选择先用H2数据库进行本地调试。
H2依赖引入很简单,它的最大特点是可以用内存模式启动:
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>test</scope> </dependency>测试环境里配置一下:
spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 driver-class-name: org.h2.Driver username: sa password:这样即便开发数据库不可用,本地测试依然可以验证代码逻辑是否正确。等到需要联调时再切回真实数据库,非常方便。我自己在本地跑很多老项目时都用这个方式,避免每次启动都被数据库连接问题挡住。
5.4 从异常信息里反推理,排错才是最高效的
最后我想说一个更底层的思路。无论是“No qualifying bean”还是其他的Spring异常,它们的提示信息本质上是Spring在容器装配过程中的“日志快照”。遇到任何装配异常,都可以反向问三个问题:
- 报错在找什么类型?也就是那个Bean的类型到底是什么。
- 这个类型应该由哪部分代码负责注册?
- 注册代码有没有被Spring执行到?
拿JdbcTemplate来说,报错在找JdbcTemplate,它应该由自动配置或手动@Bean负责注册,那我们就去检查这两条路径有没有生效。如果没有生效,再看DataSource是否有效,组件扫描路径是否正确,有没有排除自动配置,有没有版本不兼容问题。
这套反问机制不只是适用于JdbcTemplate,在遇到No qualifying bean of type 'XXX'的任何变体时都适用,比如DataSource、PlatformTransactionManager、SqlSessionFactory,背后的逻辑完全一致。搞懂了这个装配逻辑,你基本就告别了靠“瞎试配置”来修Bug的阶段。
回看这个错误的整个排查过程,我个人在实际操作里最大的体会是:不要被异常信息的外壳吓住,也不要急着往代码里堆@Bean。先把容器的装配链路理一遍,把数据源、自动配置、组件扫描、多个同类型候选者这四个方向挨个排查,大部分问题都能在五到十分钟内定位。再配合测试里打印getBeansOfType的方法,你甚至会慢慢发现Spring的依赖注入其实并没有那么神秘,它只是一个严格遵守规则的容器,你告诉它什么,它就给你什么,没告诉它的,它就老老实实说“No qualifying bean”。