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

news2025/1/13 2:32:22

问题

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

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

/**
 * 系统属性服务
**/
@Service
public class SystemConfigService {

    // 访问 db 的 mapper
    private final SystemConfigMapper systemConfigMapper;

    // 存放一些系统配置的缓存 map
    private static Map<String, String>> SYS_CONF_CACHE = new HashMap<>()

    // 使用构造方法完成依赖注入
    public SystemConfigServiceImpl(SystemConfigMapper systemConfigMapper) {
        this.systemConfigMapper = systemConfigMapper;
    }

    // Bean 的初始化方法,捞取数据库中的数据,放入缓存的 map 中
    @PostConstruct
    public void init() {
        // systemConfigMapper 访问 DB,捞取数据放入缓存的 map 中
        // SYS_CONF_CACHE.put(key, value);
        // ...
    }

    // 对外提供获得系统配置的 static 工具方法
    public static String getSystemConfig(String key) {
        return SYS_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"})
public class BizService {

    public BizService() {
        String xxValue = SystemConfigService.getSystemConfig("xxKey");
        // 可行
    }
}

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

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

Spring 中 Bean 创建的相关知识

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

图片

这个 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:

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

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

public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        // 注意,如果你同时还使用了 spring cloud,这里需要做个判断,要不要在 spring cloud applicationContext 中做这个事
        // 通常 spring cloud 中的 bean 都和业务没关系,是需要跳过的
        applicationContext.addBeanFactoryPostProcessor(new MyBeanDefinitionRegistryPostProcessor());
    }
}

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

@SpringBootApplication
public class SpringBootDemoApplication {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(SpringBootDemoApplication.class);
        // 通过 SpringApplication 注册 ApplicationContextInitializer
        application.addInitializers(new MyApplicationContextInitializer());
        application.run(args);
    }
}

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

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

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

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

用 MyBeanDefinitionRegistryPostProcessor 在 ConfigurationClassPostProcessor 扫描前注册你需要的目标 bean 的 BeanDefinition 即可。

public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // 手动注册一个 BeanDefinition
        registry.registerBeanDefinition("systemConfigService", new RootBeanDefinition(SystemConfigService.class));
    }
    
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}
}

当然你也可以使用一个类同时实现 ApplicationContextInitializer 和BeanDefinitionRegistryPostProcessor

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

图片

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/501823.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

精力管理金字塔

精力管理金字塔 由协和医学院的张遇升博士在《掌控精力&#xff1a;不疲惫的身心管理术》一书中提出&#xff0c;分层次对精力管理提出了解析和有效的建议。 模型介绍 精力管理是一个可以学会的技能&#xff0c;学会了科学的精力管理方法&#xff0c;就能使自己的精力越来越好…

实在智能RPA首推集约式“智能门户超自动化办公"新模式,加速司法、政企数字化升级

随着数字化和智能化的快速发展&#xff0c;数字技术已经深入到各个行业和领域。实在智能基于数字员工在行业的深厚理解和丰富的实践经验&#xff0c;打造一站式的智能化统一平台——智能门户&#xff0c;打破了技术壁垒和系统数据之间的割裂感&#xff0c;实现政府、企业内部信…

2023最新版本Camtasia电脑录屏软件好不好用?

在当今数字化时代&#xff0c;屏幕录制成为了许多用户制作教学视频、演示文稿、游戏攻略等内容的首选。本文将为您介绍几款常用的电脑录屏软件&#xff0c;包括Camtasia、OBS Studio、Bandicam等&#xff0c;并对其进行功能和用户体验方面的比较&#xff0c;同时给出10款电脑录…

PHP语言技术开发的手术麻醉管理系统源码

手术麻醉管理系统用于各个手术室和麻醉科&#xff0c;接受医生工作站、护士工作站发送过来的手术申请单 手术麻醉管理系统(DORIS)是应用于医院手术室、麻醉科室的计算机软件系统。该系统针对整个围术期&#xff0c;对病人进行全程跟踪与信息管理&#xff0c;自动集成病人HIS、…

上海约瑟 HJZ-J913静态中间继电器 导轨安装 触点容量16A/250VAC

品牌&#xff1a;JOSEF约瑟&#xff0c;型号&#xff1a;HJZ-J913&#xff0c;名称&#xff1a;静态中间继电器&#xff0c;额定电压&#xff1a;48220VDC&#xff1b;48415VAC&#xff0c;触点容量&#xff1a;250V/5A&#xff0c;功率消耗&#xff1a;≤5W&#xff0c;动作时…

零入门kubernetes网络实战-31->基于bridge+veth pair+MASQUERADE技术实现内网可以访问外网

《零入门kubernetes网络实战》视频专栏地址 https://www.ixigua.com/7193641905282875942 本篇文章视频地址(稍后上传) 本篇文章我们使用nat技术来实现bridge管理的网络能够访问外网。 1、测试环境介绍 两台centos虚拟机 # 查看操作系统版本 cat /etc/centos-release # 内核…

(5)Qt—ui常用类

目录 1. QString 字符串类* 2. 容器类 2.1 顺序容器 QList 1. QString 字符串类* QString是Qt中的字符串类&#xff0c;与C和C不同的是&#xff0c;Qt的字符串使用Unicode编码。每一个字符使用一个16位的QChar&#xff0c;而不是之前8位的char&#xff0c;所以Qt处理中文没有问…

基于ssm医药药品管理系统

开发工具eclipse&#xff0c;jdk1.8 数据库mysql5.7,tomcat8 技术&#xff1a;springmvcspringmybatis(ssm) 主要功能如下&#xff1a; ①登录系统&#xff1a;管理员需要输入正确的用户名和密码来登录系统&#xff0c;从而完成各类信息的管理工作&#xff1b; ②信息查询…

前端007_类别模块_列表功能

1、需求分析 类别模块主要文章类别进行管理,首先实现类别列表功能,包含数据列表、分页、条件查询 。 2、Mock模拟接口数据 请求URL: /article/category/search请求方式: post描述: 文章类别分页条件查询列表mock.js 配置: 带分页功能,每页显示20条{"code": 2…

【网络取证篇】宝塔面板server和panel的目录功能说明

【网络取证篇】宝塔面板server和panel的目录功能说明 宝塔面板的目录结构在不同的情况下可能有所不同—【蘇小沐】 文章目录 【网络取证篇】宝塔面板server和panel的目录功能说明1、宝塔优点&#xff1a; &#xff08;一&#xff09;宝塔面板/www/server主要目录&#xff08;二…

每天一道算法练习题--Day25 第一章 --算法专题 --- ----------蓄水池抽样

蓄水池抽样 问题描述算法描述相关题目总结 力扣中关于蓄水池抽样问题官方标签是 2 道&#xff0c;根据我的做题情况来看&#xff0c;可能有三四道。比重算是比较低的&#xff0c;大家可以根据自己的实际情况选择性掌握。 蓄水池抽样的算法思维很巧妙&#xff0c;代码简单且容易…

TFT屏幕在arduino (esp 32)下的驱动,汉字显示和字库比较

一、TFT屏幕的驱动列表支持 绝大多数的tft屏幕在esp32 下都可以被 TFT_eSPI 驱动显示&#xff0c;以下清单中的驱动模块都可以用tft_espi来显示&#xff1a; // #define ILI9341_DRIVER // Generic driver for common displays //#define ILI9341_2_DRIVER // Alter…

借助国内ChatGPT平替+MindShow,飞速制作PPT

系列文章目录 后续补充 文章目录 系列文章目录前言一、科大讯飞“星火”认知大模型介绍二、使用步骤1.借助讯飞星火生成ppt的文案2.借助MindShow生成ppt 前言 随着人工智能技术的不断发展&#xff0c;越来越多的企业和个人开始利用自然语言处理技术来提高工作效率。其中&#…

【移动端网页布局】flex 弹性布局 ⑥ ( 设置主轴方向和是否自动换行 | flex-flow 样式说明 | 代码示例 )

文章目录 一、设置主轴方向和是否自动换行 : flex-flow 样式说明1、flex-flow 样式2、flex-flow 样式属性值 二、代码示例 一、设置主轴方向和是否自动换行 : flex-flow 样式说明 1、flex-flow 样式 flex-flow 样式 是 flex-direction 属性和 flex-wrap 属性的 复合写法 ; 设置…

Redis底层结构

https://mp.weixin.qq.com/s/MGcOl1kGuKdA7om0Ahz5IA 小林coding 介绍 Redis 数据结构并不是指 tring&#xff08;字符串&#xff09;、List&#xff08;列表&#xff09;、Hash&#xff08;哈希&#xff09;、Set&#xff08;集合&#xff09;和 Zset&#xff08;有序集合&am…

日撸 Java 三百行day43-44

文章目录 说明day43 插入排序思路代码 day44 希尔排序思路代码 对比 说明 闵老师的文章链接&#xff1a; 日撸 Java 三百行&#xff08;总述&#xff09;_minfanphd的博客-CSDN博客 自己也把手敲的代码放在了github上维护&#xff1a;https://github.com/fulisha-ok/sampledat…

磁盘空间不足怎么办?释放磁盘空间的4种方法

虽然现在硬盘的空间越来越大&#xff0c;但是在这个数据爆炸的时代中&#xff0c;总是会觉得存储空间不够用&#xff0c;一不注意磁盘就满了&#xff0c;那么除了清空回收站、卸载某些程序外&#xff0c;还能怎么释放磁盘空间呢&#xff1f; 方案一&#xff1a;禁用休眠 休眠是…

KD305Y带吸收比极化指数兆欧表

一、概述 KD305Y绝缘电阻测试仪对众多的电力设备如&#xff1a;电缆、电机、发电机、变压器、互感器、高压开关、避雷器等要求做一系列的绝缘性能试验&#xff0c;首先是要做绝缘电阻测试。近年来随着电力事业的飞速发展,大容量设备的使用不断增加&#xff0c;用普通的兆欧表无…

Springboot +Flowable,服务任务ServiceTask执行的三种方式(二)

一.简介 ServiceTask 从名字上看就是服务任务&#xff0c;它的图标是像下面这样&#xff0c;截图如下&#xff1a; ServiceTask 一般由系统自动完成&#xff0c;当流程走到这一步的时候&#xff0c;不会自动停下来&#xff0c;而是会去执行我们提前在 ServiceTask 中配置好的…

ubuntu下用VirtualBox安装Windows虚拟机

1.准备工作 从清华大学信息门户——公共服务及其他——软件资源——公共软件——操作系统——Windows11处下载对应的ios系统安装软件 注意下载后将文件名中的 “ .IOS ” 大写改为 “ .ios ” 小写&#xff0c;否则后续虚拟机无法直接识别 问题提示&#xff1a; 解决iso文件不…