How to Dynamically Refresh Spring Boot Configurations at Runtime
This article explains two practical approaches for achieving real‑time external configuration refresh in Spring Boot 2.7, covering a custom ApplicationContextInitializer that periodically updates the Environment and a bean‑refresh technique using ContextRefreshedEvent to rebind @ConfigurationProperties, complete with code examples and testing endpoints.
1. Introduction
In modern application development, real‑time refresh of external configuration is a common requirement. Spring Boot offers tools to achieve this, and this article presents two best‑practice methods for dynamically updating configuration without restarting the application.
2. Dynamically Updating Environment Configuration
By implementing a custom ApplicationContextInitializer , a scheduled task can periodically read an external properties file and update the Environment . The initializer registers the task before the container refreshes.
<code>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 = "";
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();
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("Read config file: {}", properties);
} catch (IOException e) {
e.printStackTrace();
}
}
}
</code>Register the initializer in spring.factories :
<code>org.springframework.context.ApplicationContextInitializer=\
com.pack.external.properties.ExternalPropertiesApplicationContextInitializer
</code>After the container starts, the task runs every 10 seconds, updating the environment with the latest properties.
3. Refreshing @ConfigurationProperties Beans
This approach mimics Spring Cloud’s /refresh endpoint: it listens for ContextRefreshedEvent , schedules a task, reads the external file, and then destroys and re‑initialises beans annotated with @ConfigurationProperties so that new values are bound.
<code>@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;
}
</code> <code>@Component
public class PackApplicationListener implements ApplicationListener<ApplicationEvent>, ApplicationContextAware {
private ConfigurableApplicationContext context;
private static final long DEFAULT_REFRESH_DELAY = 10000;
private String path = "";
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 = 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) {
if (event instanceof ContextRefreshedEvent) {
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 = context.getResource("file:/" + path + "/" + CONFIG_FILE_NAME);
try {
Properties properties = new Properties();
properties.load(resource.getInputStream());
PropertiesPropertySource externalPropertySource = new PropertiesPropertySource(EXTERNAL_PROPERTY_NAME, properties);
context.getEnvironment().getPropertySources().addLast(externalPropertySource);
PackProperties packProperties = context.getBean(PackProperties.class);
String beanName = context.getBeanNamesForType(PackProperties.class)[0];
processBean(packProperties, beanName);
} catch (IOException e) {
e.printStackTrace();
}
}
public void processBean(PackProperties packProperties, String beanName) {
// destroy and re‑initialize the bean so new values are bound
((ConfigurableApplicationContext) context).getBeanFactory().destroyBean(packProperties);
((ConfigurableApplicationContext) context).getBeanFactory().initializeBean(packProperties, beanName);
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
if (context instanceof ConfigurableApplicationContext) {
this.context = (ConfigurableApplicationContext) context;
}
}
}
</code>Testing endpoints:
<code>@Resource
private Environment env;
@GetMapping("/read")
public String read() {
return env.getProperty("pack.version");
}
</code> <code>@Resource
private PackProperties props;
@GetMapping("/props")
public PackProperties props() {
return props;
}
</code>Both methods enable the application to react to external configuration changes in real time, with the first suited for early‑stage loading and the second for runtime refreshes.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.