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

这篇具有很好参考价值的文章主要介绍了Spring Boot |如何让你的 bean 在其他 bean 之前完成加载。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

问题

今天有个小伙伴给我出了一个难题:在 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 实例化和组装:
Spring Boot |如何让你的 bean 在其他 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文章来源地址https://www.toymoban.com/news/detail-451543.html

通过 applicationContext#addBeanFactoryPostProcessor 注册的 BeanDefinitionRegistryPostProcessor,比 Spring 自带的优先级要高,所以这里就不需要再实现 Ordered 接口提升优先级就可以排在 ConfigurationClassPostProcessor 前面:
Spring Boot |如何让你的 bean 在其他 bean 之前完成加载
经过测试发现,上面的方式可行的,SystemConfigService 被排在第五个 Bean 进行实例化,排在前面的四个都是 Spring 自己内部的 Bean 了,也没有必要再提前了。
本文提供的方式并不是唯一的,如果你有更好的方法,欢迎在评论区留言交流。
作者|刘顺(萧易)

到了这里,关于Spring Boot |如何让你的 bean 在其他 bean 之前完成加载的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 让你的Demo更安全--Spring Boot实现短信验证码

    随着移动互联网的普及,短信验证码已经成为了很多应用的常用登录和注册方式之一。在传统的应用中,我们一般使用第三方集成商提供的短信验证码服务来实现短信验证码的发送和验证,但是这些服务有可能存在时间限制、价格过高等限制。 为了解决这些问题,我们可以使

    2024年02月07日
    浏览(41)
  • 使用Spring AI让你的Spring Boot应用快速拥有生成式AI能力

    之前分享了关于Spring新项目 Spring AI 的介绍视频。视频里演示了关于使用Spring AI将Open AI的能力整合到Spring应用中的操作,但有不少读者提到是否有博客形式的学习内容。所以,本文就将具体介绍如何使用 Spring AI 快速让您的Spring应用拥有生成式AI的强大能力。 第一步:使用你

    2024年02月03日
    浏览(39)
  • Spring Boot进阶(58):轻松搞定数据存储!Spring Boot与PostgreSQL完美集成,让你的应用更稳定更高效!

            PostgreSQL是一种广泛使用的开源关系型数据库,具有可靠性高、性能优异、拥有丰富的数据类型和扩展等优点,越来越多的企业和开发者开始使用它来存储和管理数据。而Spring Boot是一种快速开发的框架,可以简化开发过程并提高开发效率。本文将介绍如何使用Sp

    2024年02月10日
    浏览(55)
  • 提升 Spring Boot 吞吐量的 7 个神技,让你的项目飞起来!

    实现方式二种: 使用异步注解 @aysnc、启动类:添加 @EnableAsync 注解 JDK 8 本身有一个非常好用的 Future 类——CompletableFuture 在该示例中,启动一个线程,此时 AskThread 对象还没有拿到它需要的数据,执行到 myRe = re.get() * re.get() 会阻塞。 我们用休眠 1 秒来模拟一个长时间的计算

    2024年02月08日
    浏览(44)
  • Spring Boot进阶(44):如何为你的项目开启HTTPS协议加密传输,让你的网站更加安全?

            随着互联网的发展,网络安全问题越来越引人关注。为了确保网站数据传输的安全性,大多数网站都选择使用HTTPS协议进行加密传输。而本篇文章就将带领大家,一步一步地了解如何为你的项目开启HTTPS协议,让你的网站更加安全可靠。不仅如此,我们还会介绍HTTPS协

    2024年02月07日
    浏览(56)
  • 超越竞争:Spring Boot如何在加载bean时优先选择我?

    🏅 欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正! Spring Boot 是当前业界最受欢迎和广泛使用的 Java Web 应用开发框架之一。在 Spring Boot 应用中,bean 是通过自动配置进行装载的,因为其按照约定顺序位置,Spring Boot 希望尽可能提供正确的自动配置,在应用运行时重写或自

    2023年04月08日
    浏览(37)
  • spring boot mybatis plus mapper如何自动注册到spring bean容器

    ##@Import(AutoConfiguredMapperScannerRegistrar.class) ##注册MapperScannerConfigurer ##MapperScannerConfigurer.postProcessBeanDefinitionRegistry方法扫描注册mapper ##找到mapper候选者 ##过滤mapper 类 候选者 ##BeanDefinitionHolder注册到spring 容器

    2024年01月17日
    浏览(48)
  • Spring Boot 在启动之前还做了哪些准备工作?

    目录 一:初始化资源加载器 二:校验主要源 三:设置主要源 四:推断 Web 应用类型

    2024年02月05日
    浏览(47)
  • 【Spring Security】让你的项目更加安全的框架

    🎉🎉欢迎来到我的CSDN主页!🎉🎉 🏅我是Java方文山,一个在CSDN分享笔记的博主。📚📚 🌟推荐给大家我的专栏《Spring Security》。🎯🎯 👉点击这里,就可以查看我的主页啦!👇👇 Java方文山的个人主页 🎁如果感觉还不错的话请给我点赞吧!🎁🎁 💖期待你的加入,一

    2024年02月04日
    浏览(58)
  • SpringBoot如何让业务Bean优先于其他Bean加载

    本博客原文地址:https://ntopic.cn/p/2023090901/ 源代码先行: Gitee本文介绍的完整仓库:https://gitee.com/obullxl/ntopic-boot GitHub本文介绍的完整仓库:https://github.com/obullxl/ntopic-boot 今天走读一个应用程序代码,发现一个有趣的现象:有多个不同的业务Bean中均依赖了一个参数工具类Para

    2024年02月09日
    浏览(61)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包