大数跨境
0
0

两种方式实现SpringBoot外部配置实时刷新最佳实践

两种方式实现SpringBoot外部配置实时刷新最佳实践 Spring全家桶实战案例
2024-05-10
0
导读:【干货】外部配置实时刷新的最佳实践,这知识点你未必知道

环境:SpringBoot2.7.18



1. 简介

在应用开发中,外部配置信息的实时刷新已经成为一项重要的需求。Spring Boot提供了许多功能和工具来帮助开发者实现这一目标。本文将介绍两种在Spring Boot中实现外部配置实时刷新的最佳实践。通过对这些技术的了解和应用,开发者能够提高应用程序的灵活性和可维护性,更好地应对不同环境下的配置需求。

Spring Cloud新增了一个自定义的作用域refresh(可以理解为“动态刷新”),使得在应用不需要重启的情况下热加载新的配置值。这个scope是如何做到热加载的呢?RefreshScope的核心原理(简单说):

  1. 被@RefreshScope标注的类会生成代理类

  2. 调用时每次都从容器中getBean获取

  3. 当调用/refresh 接口时会销毁所有refresh作用域的bean

  4. 当销毁后就会调用对应的ObjectFactory重新创建

本篇文章不会介绍Spring Cloud提供的这种方式;而是我们自己动手实现配置信息的实时刷新。这里会介绍如下两种方式实现属性文件的动态刷新:

  1. 动态更新Environment配置信息

  2. 刷新@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; }
@Override public 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文件,内容如下:

org.springframework.context.ApplicationContextInitializer=\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后),开始执行定时任务

@Componentpublic 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; }
@Override 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() ; } } // 销毁&初始化bean  public void processBean(PackProperties packProperties, String beanName) {this.context}
@Override 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事件,当容器初始化完成后,启动定时任务来读取外部的配置文件。这种方式适合那些在应用程序运行过程中,才需要刷新的配置。

以上是本篇文章的全部内容,如对你有帮助就请作者吃个棒棒糖🍭。

推荐文章

SpringBoot3使用虚拟线程一定要小心了

SpringBoot一个非常强大的数据绑定类

不知道这些不要说玩转了Controller接口

实战案例SpringBoot整合Seata AT模式实现分布式事务【超详细】

彻底搞懂跨域问题SpringBoot助你畅通无阻

Spring创建AOP代理并非只有@Aspect一种方式

掌握Spring这些注入技巧让你事半功倍

实战Spring Cloud Gateway自定义谓词及网关过滤器

Spring中一个非常强大的类

Spring强大的URI操作工具类太方便了

精通Spring框架:从入门到精通Bean创建全流程(最新版6.1.2)

你以为只有Controller一种接口定义方式?详解Web函数式接口

SpringBoot这些条件注解助你高效开发

【声明】内容源于网络
0
0
Spring全家桶实战案例
Java全栈开发,前端Vue2/3全家桶;Spring, SpringBoot 2/3, Spring Cloud各种实战案例及源码解读
内容 832
粉丝 0
Spring全家桶实战案例 Java全栈开发,前端Vue2/3全家桶;Spring, SpringBoot 2/3, Spring Cloud各种实战案例及源码解读
总阅读38
粉丝0
内容832