Java基础、中级、高级、架构面试资料

Spring Boot爆错NoSuchBeanDefinitionException,找不到Bean的11步生死劫

JAVA herman 17浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog2,发送下载链接帮助你免费下载!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领
腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云

最近一个网友急冲冲的通过微信找到我,说“哥,我的 SpringBoot 项目启动报错 NoSuchBeanDefinitionException,我排查了很久没找到原因,帮我看看呗!”

这个网友,我细看了一下图像和微信名,虽然我们加过好友,但几乎没啥“交情”。这“交情”不是说嘘寒问暖、不是请茶吃饭也不是我需要红包套好处,而是和我的公众号没交集。即,他没点过赞,也没留过言,转发和在看也没有。到这个份上,实战找不出帮他的理由。

我说让你团队里的小伙伴看看呗,找 leader 也行。你 leader 不会只分任务“不管兵”吧。

后来,他找各种理由,磨我了很多次,我于心不忍,就帮他看了看问题。所以,今天我们就一起来聊聊这个让无数 Java 开发者“怀疑人生”的经典问题:NoSuchBeanDefinitionException。这个异常就像 Spring 世界的“404”,明明看着代码没问题,它就告诉你,Bean 找不到!

简化的案例

下面我简化了网友的代码,就先从他这个“诡异”的案例说起吧。

在他的代码中,确实定义了 UserService,也加了 @Service 注解,但在项目启动时,@Autowired 依赖的 bean 就是被 Spring 说找不到 bean?

简化的代码长这样。

@Service
public class UserService {
    // ...
}

@RestController
public class UserController {
    // 启动报错 NoSuchBeanDefinitionException
    @Autowired
    private UserService userService; 
}

类似这种“明明有却找不到”的问题,我敢断言 90% 都和 Bean 的生命周期有关。接下来我们就一起抽丝剥茧,看看这背后到底藏着什么秘密。

NoSuchBeanDefinitionException 是什么?

简单来说,这是 Spring 容器在“依赖注入”阶段发出的“求救信号”。当 Spring 尝试:

  1. 按类型注入(@Autowired
  2. 按名称注入(@Resource
  3. 显式获取 Bean(getBean(Class<?>)

但找不到符合条件的 Bean 时,就会抛出这个异常。它的核心信息是,容器里没有,我注入不了

高频原因

下面是我总结的高频原因 Top 5,很可能 90% 的问题都在这里。

原因分类具体表现解决方案
1. 包扫描失效Bean 不在 @SpringBootApplication 扫描路径下检查包结构,或使用 @ComponentScan手动指定
2. 条件装配的坑@ConditionalOnProperty 等条件不满足检查 application.properties 配置是否匹配
3. 作用域冲突多例 Bean 依赖单例 Bean 的特殊场景使用 @Lazy 或 ObjectFactory 延迟获取
4. 循环依赖两个 Bean 互相依赖,A 依赖 B,B 又依赖 A重构代码,或使用构造函数注入 + @Lazy
5. 自定义生命周期处理器坏事情BeanPostProcessor 等实现不当划重点!下面展开讲

下面我们慢慢展开。

排查思路

这里我推荐三步定位法。

第一步,确认 Bean 是否被加载过。

启动时加 VM 参数 -Ddebug,如-Dspring-boot.run.arguments=--debug或者配置logging.level.org.springframework.boot.autoconfigure=DEBUG等。然后启动项目,观察控制台打印的自动配置报告,看你关注的 Bean 是否在positive matches里。

第二步,检查 BeanDefinition 是否存在。

在启动类中加一段调试代码,如下所示。

public static void main(String[] args) {
    ConfigurableApplicationContext ctx = SpringApplication.run(DemoApp.class, args);
    // 查看容器中所有UserService的BeanDefinition
    String[] beanNames = ctx.getBeanNamesForType(UserService.class);
    System.out.println("找到UserService Bean: " + Arrays.toString(beanNames));
}

如果前两步,还搞不定,则可以使用第三步,trace 日志全开。

在 application.properties 中配置以下内容。

logging.level.org.springframework.beans.factory.support=trace
logging.level.org.springframework.context.annotation=trace

观察日志,你会看到 Bean 的注册、实例化、注入全过程,就像看电影一样。

Bean生命周期中的定时炸弹

这个我们重点讲讲,算是今天的重头戏。

Spring Bean 的生命周期有11 个关键步骤,某些自定义实现如果没写好,就会在早期阶段导致其他 Bean 找不到。

Bean生命周期全景图

步骤描述如下,供参考。

1. 加载BeanDefinition → 
2. 实例化 → 
3. 填充属性 → 
4. Aware接口回调 → 
5. BeanPostProcessor.postProcessBeforeInitialization → 
6. @PostConstruct → 
7. InitializingBean → 
8. init-method → 
9. BeanPostProcessor.postProcessAfterInitialization → 
10. Bean就绪 → 
11. @PreDestroy → 销毁

手绘序列图参考https://mp.weixin.qq.com/s/WD8QBT_sc3rK4MiLc7CKsw中所示。

看懂了上面的 Bean 的生命周期后,其中有 3 个比较危险(或高危)的接口,下面我们展开说说。

最危险的 BeanPostProcessor

它的工作原理是,这个接口会在每个 Bean 初始化前后被调用。如果你在这里面尝试获取其他 Bean,容器可能还没准备好。

致命案例如下所示。

@Component
public class DangerousPostProcessor implements BeanPostProcessor {
    // 危险!BeanPostProcessor 会提前实例化
    @Autowired
    private UserService userService; 

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        // 如果在这里调用getBean()找其他Bean,而那个Bean依赖UserService...
        // 就会循环报错:NoSuchBeanDefinitionException
        return bean;
    }
}

为什么出问题?因为BeanPostProcessor本身会在容器早期阶段就被实例化,此时它依赖的 Bean 可能还没完成注册,导致连锁反应。

对应的解决方案如下。

@Component
public class SafePostProcessor implements BeanPostProcessor {
    // 不要直接注入,改用BeanFactory延迟获取
    @Autowired
    private ConfigurableListableBeanFactory beanFactory;

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        // 等容器稳定后再获取
        if (beanFactory.containsBean("userService")) {
            UserService userService = beanFactory.getBean(UserService.class);
            // ... 使用
        }
        return bean;
    }
}

ImportBeanDefinitionRegistrar

它的工作原理是,用于动态注册 BeanDefinition。如果注册时机不对,后续@Autowired时 Bean 还未完成配置。所以,它对注册时机非常敏感。

看下面这个致命案例。

public class BadRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        // 错误:直接注册一个依赖其他 Bean 的配置类
        registry.registerBeanDefinition("myConfig", new RootBeanDefinition(MyConfig.class));
    }
}

@Configuration
public class MyConfig {
    // 可能还没注册!启动时报错
    @Autowired
    private DataSource dataSource; 
}

对应的解决方案是,使用BeanFactoryPostProcessor确保在标准 Bean 注册完成后执行。

@Component
public class SafeRegistrar implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        // 此时其他BeanDefinition已基本加载完毕
        if (registry.containsBeanDefinition("dataSource")) {
            registry.registerBeanDefinition("myBean", ...);
        }
    }
}

FactoryBean

注意,这里的工厂模式陷阱。它的工作原理是,FactoryBean 的getObject()返回实际 Bean。如果这个方法依赖其他 Bean,但工厂本身初始化过早…

对应致命案例。

@Component
public class MyFactoryBean implements FactoryBean<TargetBean> {
    // 可能还没就绪
    @Autowired
    private DependencyBean dependency; 
    @Override
    public TargetBean getObject() {
        // 如果dependency为null,创建出来的TargetBean可能不完整
        return new TargetBean(dependency); 
    }
}

对应的解决方案是,让 FactoryBean 本身延迟加载,或不用@Autowired改用ObjectFactory

@Component
@Lazy // 关键!让工厂延迟初始化
public class MyFactoryBean implements FactoryBean<TargetBean> {
     // 延迟获取
    @Autowired
    private ObjectProvider<DependencyBean> dependencyProvider;

    @Override
    public TargetBean getObject() {
        return new TargetBean(dependencyProvider.getIfAvailable());
    }
}

生命周期中的蝴蝶效应

注意,看下面这个问题传播路径。

[你的自定义处理器] 
    → 提前触发实例化 
    → 依赖其他 Bean 
    → 那些 Bean 还没注册/初始化 
    → 容器放弃治疗:NoSuchBeanDefinitionException

自定义处理器要千万小心,时刻提防 bean 的生命周期。

搞懂这些后,我们回到开头的案例,经过我的排查,发现真相是他自定义了一个注解,里面用到了注册后置处理器 BeanDefinitionRegistryPostProcessor,在实现里面不小心把一些 bean 搞丢了。

最佳实践

预防大于治疗,扁鹊的大哥医术最高超。

  1. 谨慎实现生命周期接口:90% 的需求用@PostConstruct就能解决
  2. BeanPostProcessor 中不要注入 Bean:用BeanFactoryApplicationContext延迟获取
  3. 善用@Lazy:对不确定的依赖加@Lazy打破循环
  4. 日志是王道:启动时打开 trace 日志,看清 Bean 的加载顺序
  5. 单测验证:用ApplicationContextRunner测试配置类
@Test
void testMyConfiguration() {
    new ApplicationContextRunner()
        .withUserConfiguration(MyConfig.class)
        .run(context -> {
            assertThat(context).hasSingleBean(UserService.class);
        });
}

总结

以后再遇到NoSuchBeanDefinitionException问题,可以像 Spring 一样思考,记住这个排查口诀:

先确认存不存在,再看加载顺序对不对,最后检查自定义处理器

Spring 容器就像一个精密钟表,每个齿轮(Bean)都有固定位置和时间。你的自定义代码如果打乱了节奏,钟表就会罢工。

下次再遇到这个问题,别急着重构,先问自己三个问题。

  1. 我的 Bean 在扫描路径里吗?
  2. 有没有条件注解把它“过滤”了?
  3. 是不是我的 Processor 手伸太长了?

以上,希望大家都能学到经验,能帮大家在 Spring 的世界里少踩坑、多优雅。我们明天见!

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » Spring Boot爆错NoSuchBeanDefinitionException,找不到Bean的11步生死劫