环境:SpringBoot2.7.18
1. 简介
在应用开发中,外部配置信息的实时刷新已经成为一项重要的需求。Spring Boot提供了许多功能和工具来帮助开发者实现这一目标。本文将介绍两种在Spring Boot中实现外部配置实时刷新的最佳实践。通过对这些技术的了解和应用,开发者能够提高应用程序的灵活性和可维护性,更好地应对不同环境下的配置需求。
Spring Cloud新增了一个自定义的作用域refresh(可以理解为“动态刷新”),使得在应用不需要重启的情况下热加载新的配置值。这个scope是如何做到热加载的呢?RefreshScope的核心原理(简单说):
被@RefreshScope标注的类会生成代理类
调用时每次都从容器中getBean获取
当调用/refresh 接口时会销毁所有refresh作用域的bean
当销毁后就会调用对应的ObjectFactory重新创建
本篇文章不会介绍Spring Cloud提供的这种方式;而是我们自己动手实现配置信息的实时刷新。这里会介绍如下两种方式实现属性文件的动态刷新:
动态更新Environment配置信息
刷新@ConfigurationProperties注解的Bean
2. 动态更新Environment配置信息
通过自定义ApplicationContextInitializer实现,ApplicationContextInitializer是SpringBoot一个核心扩展点,在容器执行refresh之前会调用当前环境中所有的ApplicationContextInitializer#initialize回调方法。在该方法中我们可以启动一个定时任务调度程序,每隔一定时间对配置文件进行读取,然后更新当前环境Environment中的值。如下示例:
public class ExternalPropertiesApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {private static final Logger logger = LoggerFactory.getLogger(ExternalPropertiesApplicationContextInitializer.class) ;private static final long DEFAULT_REFRESH_DELAY = 10000 ;/**文件路径*/private String path = "" ;/**刷新间隔时间ms*/private long refreshDelay = DEFAULT_REFRESH_DELAY ;private static final String EXTERNAL_PROPERTY_NAME = "external" ;private static final String CONFIG_FILE_NAME = "pack.properties" ;private final ThreadPoolTaskScheduler taskScheduler ;private ConfigurableApplicationContext context ;public ExternalPropertiesApplicationContextInitializer() {this.taskScheduler = getTaskScheduler() ;}private static ThreadPoolTaskScheduler getTaskScheduler() {ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();taskScheduler.setBeanName("Pack-Watch-Task-Scheduler");taskScheduler.initialize();return taskScheduler;}@Overridepublic void initialize(ConfigurableApplicationContext context) {this.context = context ;ConfigurableEnvironment environment = context.getEnvironment();// 这里的两个配置在application.[yml|properties]配置即可this.path = environment.getProperty("external.path") ;this.refreshDelay = environment.getProperty("external.refreshDelay", Long.class, DEFAULT_REFRESH_DELAY) ;// 启动任务调度this.taskScheduler.scheduleWithFixedDelay(this::readFile, Duration.ofMillis(this.refreshDelay)) ;}public void readFile() {ConfigurableEnvironment environment = this.context.getEnvironment() ;// 如果已存在则删除;这里可以优化只有更改了才进行相应的处理if (environment.getPropertySources().contains(EXTERNAL_PROPERTY_NAME)) {environment.getPropertySources().remove(EXTERNAL_PROPERTY_NAME) ;}Resource resource = this.context.getResource("file:/" + path + "/" + CONFIG_FILE_NAME) ;try {Properties properties = new Properties() ;properties.load(resource.getInputStream()) ;PropertiesPropertySource externalPropertySource = new PropertiesPropertySource(EXTERNAL_PROPERTY_NAME, properties) ;this.context.getEnvironment().getPropertySources().addLast(externalPropertySource) ;logger.info("读取配置文件内容:{}", properties) ;} catch (IOException e) {e.printStackTrace() ;}}}
创建spring.factories
项目中建spring.factories文件,内容如下:
=\com.pack.external.properties.ExternalPropertiesApplicationContextInitializer
容器启动后默认就会每隔10s进行文件的读取,然后添加到当前的Environment对象中。
测试
@Resourceprivate Environment env ;@GetMapping("/read")public String read() {return env.getProperty("pack.version") ;}
只需要修改配置文件,这里每次都能读取到最新的配置信息。
3. 刷新@ConfigurationProperties注解的Bean
该种方式是参考了Spring Cloud中/refresh接口的实现方式。通过对bean执行销毁和初始化动作。这样就能保证属性的更新能实时的被读取到。示例如下:
定义PackProperties配置类
@Component@ConfigurationProperties(prefix = "pack")public class PackProperties {private String title ;private String description ;private String version ;private String author ;private Integer times ;private LocalDate releaseDate ;}
自定义一个事件监听器,监听ContextRefreshedEvent事件,也就是在容器准备好后(refresh后),开始执行定时任务
public class PackApplicationListener implements ApplicationListener<ApplicationEvent>, ApplicationContextAware {private ConfigurableApplicationContext context ;private static final long DEFAULT_REFRESH_DELAY = 10000 ;/**文件路径*/private String path = "" ;/**刷新间隔时间ms*/private long refreshDelay = DEFAULT_REFRESH_DELAY ;private static final String EXTERNAL_PROPERTY_NAME = "external" ;private static final String CONFIG_FILE_NAME = "pack.properties" ;private final ThreadPoolTaskScheduler taskScheduler ;public PackApplicationListener() {this.taskScheduler = getTaskScheduler() ;}private static ThreadPoolTaskScheduler getTaskScheduler() {ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();taskScheduler.setBeanName("Pack-Watch-Task-Scheduler");taskScheduler.initialize();return taskScheduler;}public void onApplicationEvent(ApplicationEvent event) {// 该事件会在refresh最后的完成阶段发布事件(所有的单例bean都初始化完成)if (event instanceof ContextRefreshedEvent e) {Environment environment = context.getEnvironment();this.path = environment.getProperty("external.path") ;this.refreshDelay = environment.getProperty("external.refreshDelay", Long.class, DEFAULT_REFRESH_DELAY) ;// 启动任务调度;当然我们也可以提供一个接口进行手动执行this.taskScheduler.scheduleWithFixedDelay(this::readFile, Duration.ofMillis(this.refreshDelay)) ;}}private void readFile() {ConfigurableEnvironment environment = context.getEnvironment() ;if (environment.getPropertySources().contains(EXTERNAL_PROPERTY_NAME)) {environment.getPropertySources().remove(EXTERNAL_PROPERTY_NAME) ;}Resource resource = this.context.getResource("file:/" + path + "/" + CONFIG_FILE_NAME) ;try {Properties properties = new Properties() ;properties.load(resource.getInputStream()) ;PropertiesPropertySource externalPropertySource = new PropertiesPropertySource(EXTERNAL_PROPERTY_NAME, properties) ;this.context.getEnvironment().getPropertySources().addLast(externalPropertySource) ;// 下面代码是关键,销毁bean,然后重新初始化bean即可。PackProperties packProperties = this.context.getBean(PackProperties.class) ;String beanName = this.context.getBeanNamesForType(PackProperties.class)[0] ;// 关键方法processBean(packProperties, beanName)} catch (IOException e) {e.printStackTrace() ;}}// 销毁&初始化beanpublic void processBean(PackProperties packProperties, String beanName) {this.context}public void setApplicationContext(ApplicationContext context) throws BeansException {if (context instanceof ConfigurableApplicationContext cac) {this.context = cac ;}}}
这样做Bean实例还是同一个,只是执行了Bean的生命周期钩子,重新对属性进行注入绑定。
测试
@Resourceprivate PackProperties props ;// 根据你配置的刷新策略,默认最多10s后就能得到最新的配置信息@GetMapping("/props")public PackProperties props() {return props ;}
以上两种方法都有效地解决了动态配置的问题,让我们的应用程序能够根据外部环境的改变,实时调整其行为和配置。
第一种方法是通过实现ApplicationContextInitializer接口,在initialize中启动一个定时任务,定时读取外部的配置文件。这种方式适合在应用程序启动时就需要加载的配置(在实例化单例bean之前)。
第二种方法是监听ContextRefreshedEvent事件,当容器初始化完成后,启动定时任务来读取外部的配置文件。这种方式适合那些在应用程序运行过程中,才需要刷新的配置。
以上是本篇文章的全部内容,如对你有帮助就请作者吃个棒棒糖🍭。
推荐文章
实战案例SpringBoot整合Seata AT模式实现分布式事务【超详细】
实战Spring Cloud Gateway自定义谓词及网关过滤器
精通Spring框架:从入门到精通Bean创建全流程(最新版6.1.2)
你以为只有Controller一种接口定义方式?详解Web函数式接口



