Spring Boot |如何让你的 bean 在其他 bean 之前完成加载

简介: 本文围绕 Spring Boot 中如何让你的 bean 在其他 bean 之前完成加载展开讨论。

问题

今天有个小伙伴给我出了一个难题:在 SpringBoot 中如何让自己的某个指定的 Bean 在其他 Bean 前完成被 Spring 加载?我听到这个问题的第一反应是,为什么会有这样奇怪的需求?


Talk is cheap,show me the code,这里列出了那个想做最先加载的“天选 Bean” 的代码,我们来分析一下:

/*** 系统属性服务**/@ServicepublicclassSystemConfigService {
// 访问 db 的 mapperprivatefinalSystemConfigMappersystemConfigMapper;
// 存放一些系统配置的缓存 mapprivatestaticMap<String, String>>SYS_CONF_CACHE=newHashMap<>()
// 使用构造方法完成依赖注入publicSystemConfigServiceImpl(SystemConfigMappersystemConfigMapper) {
this.systemConfigMapper=systemConfigMapper;
    }
// Bean 的初始化方法,捞取数据库中的数据,放入缓存的 map 中@PostConstructpublicvoidinit() {
// systemConfigMapper 访问 DB,捞取数据放入缓存的 map 中// SYS_CONF_CACHE.put(key, value);// ...    }
// 对外提供获得系统配置的 static 工具方法publicstaticStringgetSystemConfig(Stringkey) {
returnSYS_CONF_CACHE.get(key);
    }
// 省略了从 DB 更新缓存的代码// ...}


看过了上面的代码后,很容易就理解了为什么会标题中的需求了。

SystemConfigService 是一个提供了查询系统属性的服务,系统属性存放在 DB 中并且读多写少,在 Bean 创建的时候,通过 @PostConstruct 注解的 init() 方法完成了数据加载到缓存中,最关键的是,由于是系统属性,所以需要在很多地方都想使用,尤其需要在很多 bean 启动的时候使用为了方便就提供了 static 方法来方便调用,这样其他的 bean 不需要依赖注入就可以直接调用,但问题是系统属性是存在 db 里面的,这就导致了不能把 SystemConfigService做成一个纯「工具类」,它必须要被 Spring 托管起来,完成 mapper 的注入才能正常工作。因此这样一来就比较麻烦,其他的类或者 Bean 如果想安全的使用 SystemConfigService#getSystemConfig 中的获取配置的静态方法,就必须等 SystemConfigService 先被 Spring 创建加载起来,完成 init() 方法后才可以。


所以才有了最开头提到的问题,如何让这个 Bean 在其他的 Bean 之前加载。


SpringBoot 官方文档推荐做法

这里引用了一段 Spring Framework 官方文档的原文:

Constructor-based or setter-based DI?

Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies. Note that use of the @Autowired annotation on a setter method can be used to make the property be a required dependency; however, constructor injection with programmatic validation of arguments is preferable.


可以看到 Spring 对于依赖注入更推荐(is preferable)使用构造函数来注入必须的依赖,用 setter 方法来注入可选的依赖。至于我们平时工作中更多采用的 @Autowired 注解 + 属性的注入方式是不推荐的,这也是为什么你用 Idea 集成开发环境的时候会给你一个警告。

按照 Spring 的文档,我们应该直接去掉 getSystemConfig 的 static 修饰,让 getSystemConfig 变成一个实例方法,让每个需要依赖的 SystemConfigService 的 Bean 通过构造函数完成依赖注入,这样 Spring 会保证每个 Bean 在创建之前会先把它所有的依赖创建并初始化完成。


这也是我建议的做法,接下来我们以抱着学习和技术交流为目的看看还能做些什么来完成提名中的需求。


尝试解决问题的一些方法

@Order 注解或者实现 org.springframework.core.Ordered

最先想到的就是 Spring 提供的 Order 相关的注解和接口,实际上测试下来不可行。Order 相关的方法一般用来控制 Spring 自身组件相关 Bean 的顺序,比如 ApplicationListener,RegistrationBean 等,对于我们自己使用 @Service @Compont 注解注册的业务相关的 bean 没有排序的效果。


@AutoConfigureOrder/@AutoConfigureAfter/@AutoConfigureBefore 注解

测试下来这些注解也是不可行,它们和 Ordered 一样都是针对 Spring 自身组件 Bean 的顺序。


@DependsOn 注解

接下来是尝试加上 @DependsOn 注解:

@Service@DependsOn({"systemConfigService"})
publicclassBizService {
publicBizService() {
StringxxValue=SystemConfigService.getSystemConfig("xxKey");
// 可行    }
}

这样测试下来是可以是可以的,就是操作起来也太麻烦了,需要让每个每个依赖 SystemConfigService  的 Bean 都改代码加上注解,那有没有一种默认就让 SystemConfigService 提前的方法?


上面提到的方法都不好用,那我们只能利用 spring 给我们提供的扩展点来做文章了。


Spring 中 Bean 创建的相关知识

首先要明白一点,Bean 创建的顺序是怎么来的,如果你对 Spring 的源码比较熟悉,你会知道在 AbstractApplicationContext 里面有个 refresh 方法, Bean 创建的大部分逻辑都在 refresh 方法里面,在 refresh 末尾的 finishBeanFactoryInitialization(beanFactory) 方法调用中,会调用 beanFactory.preInstantiateSingletons(),在这里对所有的 beanDefinitionNames  一一遍历,进行 bean 实例化和组装:

image.png

这个 beanDefinitionNames 列表的顺序就决定了 Bean 的创建顺序,那么这个 beanDefinitionNames 列表又是怎么来的?答案是 ConfigurationClassPostProcessor 通过扫描你的代码和注解生成的,将 Bean 扫描解析成 Bean 定义(BeanDefinition),同时将 Bean 定义(BeanDefinition)注册到 BeanDefinitionRegistry 中,才有了 beanDefinitionNames 列表。


ConfigurationClassPostProcessor 的介绍

这里提到了 ConfigurationClassPostProcessor,实现了 BeanDefinitionRegistryPostProcessor 接口。它是一个非常非常重要的类,甚至可以说它是 Spring boot 提供的扫描你的注解并解析成 BeanDefinition 最重要的组件。我们在使用 SpringBoot 过程中用到的 @Configuration@ComponentScan@Import@Bean 这些注解的功能都是通过 ConfigurationClassPostProcessor 注解实现的。这里找了一篇文件介绍,就不多说了。https://juejin.cn/post/6844903944146124808


BeanDefinitionRegistryPostProcessor 相关接口的介绍

接下来还要介绍 Spring 中提供的一些扩展,它们在 Bean 的创建过程中起到非常重要的作用。


BeanFactoryPostProcessor 它的作用:

  • 在 BeanFactory 初始化之后调用,来定制和修改 BeanFactory 的内容
  • 所有的 Bean 定义(BeanDefinition)已经保存加载到 beanFactory,但是 Bean 的实例还未创建
  • 方法的入参是 ConfigurrableListableBeanFactory,意思是你可以调整 ConfigurrableListableBeanFactory 的配置

BeanDefinitionRegistryPostProcessor 它的作用:

  • 是 BeanFactoryPostProcessor 的子接口
  • 在所有 Bean 定义(BeanDefinition)信息将要被加载,Bean 实例还未创建的时候加载
  • 优先于 BeanFactoryPostProcessor 执行,利用 BeanDefinitionRegistryPostProcessor 可以给 Spring 容器中自定义添加 Bean
  • 方法入参是 BeanDefinitionRegistry,意思是你可以调整 BeanDefinitionRegistry 的配置

还有一个类似的 BeanPostProcessor 它的作用:

  • 在 Bean 实例化之后执行的
  • 执行顺序在 BeanFactoryPostProcessor 之后
  • 方法入参是 Object bean,意思是你可以调整 bean 的配置

搞明白了以上的内容,下面我们可以直接动手写代码了。


最终答案

第一步:通过 spring.factories 扩展来注册一个 ApplicationContextInitializer:

#注册ApplicationContextInitializerorg.springframework.context.ApplicationContextInitializer=com.antbank.demo.bootstrap.MyApplicationContextInitializer


注册 ApplicationContextInitializer 的目的其实是为了接下来注册 BeanDefinitionRegistryPostProcessor 到 Spring 中,我没有找到直接使用 spring.factories 来注册 BeanDefinitionRegistryPostProcessor 的方式,猜测是不支持的:

publicclassMyApplicationContextInitializerimplementsApplicationContextInitializer<ConfigurableApplicationContext> {
@Overridepublicvoidinitialize(ConfigurableApplicationContextapplicationContext) {
// 注意,如果你同时还使用了 spring cloud,这里需要做个判断,要不要在 spring cloud applicationContext 中做这个事// 通常 spring cloud 中的 bean 都和业务没关系,是需要跳过的applicationContext.addBeanFactoryPostProcessor(newMyBeanDefinitionRegistryPostProcessor());
    }
}


除了使用 spring 提供的 SPI 来注册 ApplicationContextInitializer,你也可以用 SpringApplication.addInitializers 的方式直接在 main 方法中直接注册一个 ApplicationContextInitializer 结果都是可以的:

@SpringBootApplicationpublicclassSpringBootDemoApplication {
publicstaticvoidmain(String[] args) {
SpringApplicationapplication=newSpringApplication(SpringBootDemoApplication.class);
// 通过 SpringApplication 注册 ApplicationContextInitializerapplication.addInitializers(newMyApplicationContextInitializer());
application.run(args);
    }
}


当然了,通过 Spring 的事件机制也可以做到注册 BeanDefinitionRegistryPostProcessor,选择实现合适的 ApplicationListener 事件,可以通过 ApplicationContextEvent 获得 ApplicationContext,即可注册 BeanDefinitionRegistryPostProcessor,这里就不多展开了。


这里需要注意一点,为什么需要用 ApplicationContextInitializer 来注册 BeanDefinitionRegistryPostProcessor,能不能用 @Component 或者其他的注解的方式注册?

答案是不能的。@Component 注解的方式注册能注册上的前提是能被 ConfigurationClassPostProcessor 扫描到,也就是说用 @Component 注解的方式来注册,注册出来的 Bean 一定不可能排在 ConfigurationClassPostProcessor 前面,而我们的目的就是在所有的 Bean 扫描前注册你需要的 Bean,这样才能排在其他所有 Bean 前面,所以这里的场景下是不能用注解注册的,这点需要额外注意。


第二步:实现 BeanDefinitionRegistryPostProcessor,注册目标 bean:

MyBeanDefinitionRegistryPostProcessorConfigurationClassPostProcessor 扫描前注册你需要的目标 bean 的 BeanDefinition 即可。


publicclassMyBeanDefinitionRegistryPostProcessorimplementsBeanDefinitionRegistryPostProcessor {
@OverridepublicvoidpostProcessBeanDefinitionRegistry(BeanDefinitionRegistryregistry) throwsBeansException {
// 手动注册一个 BeanDefinitionregistry.registerBeanDefinition("systemConfigService", newRootBeanDefinition(SystemConfigService.class));
    }
@OverridepublicvoidpostProcessBeanFactory(ConfigurableListableBeanFactorybeanFactory) throwsBeansException {}
}


当然你也可以使用一个类同时实现 ApplicationContextInitializerBeanDefinitionRegistryPostProcessor


通过  applicationContext#addBeanFactoryPostProcessor 注册的 BeanDefinitionRegistryPostProcessor,比 Spring 自带的优先级要高,所以这里就不需要再实现 Ordered 接口提升优先级就可以排在 ConfigurationClassPostProcessor 前面:

image.png

经过测试发现,上面的方式可行的,SystemConfigService 被排在第五个 Bean 进行实例化,排在前面的四个都是 Spring 自己内部的 Bean 了,也没有必要再提前了。


本文提供的方式并不是唯一的,如果你有更好的方法,欢迎在评论区留言交流。

相关文章
|
23天前
|
缓存 Java Spring
实战指南:四种调整 Spring Bean 初始化顺序的方案
本文探讨了如何调整 Spring Boot 中 Bean 的初始化顺序,以满足业务需求。文章通过四种方案进行了详细分析: 1. **方案一 (@Order)**:通过 `@Order` 注解设置 Bean 的初始化顺序,但发现 `@PostConstruct` 会影响顺序。 2. **方案二 (SmartInitializingSingleton)**:在所有单例 Bean 初始化后执行额外的初始化工作,但无法精确控制特定 Bean 的顺序。 3. **方案三 (@DependsOn)**:通过 `@DependsOn` 注解指定 Bean 之间的依赖关系,成功实现顺序控制,但耦合性较高。
实战指南:四种调整 Spring Bean 初始化顺序的方案
|
23天前
|
Java
SpringBoot构建Bean(RedisConfig + RestTemplateConfig)
SpringBoot构建Bean(RedisConfig + RestTemplateConfig)
38 2
|
2月前
|
XML Java 数据格式
Spring从入门到入土(bean的一些子标签及注解的使用)
本文详细介绍了Spring框架中Bean的创建和使用,包括使用XML配置文件中的标签和注解来创建和管理Bean,以及如何通过构造器、Setter方法和属性注入来配置Bean。
75 9
Spring从入门到入土(bean的一些子标签及注解的使用)
|
2月前
|
Java 测试技术 Windows
咦!Spring容器里为什么没有我需要的Bean?
【10月更文挑战第11天】项目经理给小菜分配了一个紧急需求,小菜迅速搭建了一个SpringBoot项目并完成了开发。然而,启动测试时发现接口404,原因是控制器包不在默认扫描路径下。通过配置`@ComponentScan`的`basePackages`字段,解决了问题。总结:`@SpringBootApplication`默认只扫描当前包下的组件,需要扫描其他包时需配置`@ComponentScan`。
|
3月前
|
XML Java 数据格式
spring复习02,xml配置管理bean
详细讲解了Spring框架中基于XML配置文件管理bean的各种方式,包括获取bean、依赖注入、特殊值处理、属性赋值、集合类型处理、p命名空间、bean作用域及生命周期和自动装配。
spring复习02,xml配置管理bean
|
2月前
|
架构师 Java 开发者
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
在40岁老架构师尼恩的读者交流群中,近期多位读者成功获得了知名互联网企业的面试机会,如得物、阿里、滴滴等。然而,面对“Spring Boot自动装配机制”等核心面试题,部分读者因准备不足而未能顺利通过。为此,尼恩团队将系统化梳理和总结这一主题,帮助大家全面提升技术水平,让面试官“爱到不能自已”。
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
|
2月前
|
Java 开发者 Spring
Spring bean的生命周期详解!
本文详细解析Spring Bean的生命周期及其核心概念,并深入源码分析。Spring Bean是Spring框架的核心,由容器管理其生命周期。从实例化到销毁,共经历十个阶段,包括属性赋值、接口回调、初始化及销毁等。通过剖析`BeanFactory`、`ApplicationContext`等关键接口与类,帮助你深入了解Spring Bean的管理机制。希望本文能助你更好地掌握Spring Bean生命周期。
98 1
|
2月前
|
Java Spring
获取spring工厂中bean对象的两种方式
获取spring工厂中bean对象的两种方式
45 1
|
2月前
|
Java 开发者 Spring
Spring bean的生命周期详解!
本文详细介绍了Spring框架中的核心概念——Spring Bean的生命周期,包括实例化、属性赋值、接口回调、初始化、使用及销毁等10个阶段,并深入剖析了相关源码,如`BeanFactory`、`DefaultListableBeanFactory`和`BeanPostProcessor`等关键类与接口。通过理解这些核心组件,读者可以更好地掌握Spring Bean的管理和控制机制。
93 1
|
2月前
|
Java Spring 容器
Springboot3.2.1搞定了类Service和bean注解同名同类型问题修复
这篇文章讨论了在Spring Boot 3.2.1版本中,同名同类型的bean和@Service注解类之间冲突的问题得到了解决,之前版本中同名bean会相互覆盖,但不会在启动时报错,而在配置文件中设置`spring.main.allow-bean-definition-overriding=true`可以解决这个问题。
92 0
Springboot3.2.1搞定了类Service和bean注解同名同类型问题修复