How Spring Cloud Dynamically Refreshes @ConfigurationProperties Beans
This article explains how Spring Cloud discovers classes annotated with @ConfigurationProperties, wraps them into ConfigurationPropertiesBean objects, and dynamically rebinds them at runtime using RefreshScope, EnvironmentChangeEvent, and the RefreshEndpoint actuator, enhancing application flexibility and scalability.
Environment: SpringBoot 2.7.12 + SpringCloud 2021.0.7
1. Introduction
This article details how classes annotated with @ConfigurationProperties in Spring Cloud can be dynamically refreshed. Understanding the mechanism allows better use of Spring Cloud's dynamic configuration features to achieve flexible and scalable applications.
When configuration changes, beans marked with @RefreshScope receive special handling, solving stateful bean issues because such beans are only injected with configuration at initialization.
If a bean is immutable, you must apply @RefreshScope or specify the class name under the property spring.cloud.refresh.extra-refreshable . Conversely, to prevent a bean from being refreshed, use spring.cloud.refresh.never-refreshable .
2. Implementation Principle
2.1 Locate Classes with @ConfigurationProperties
The container automatically registers the following bean:
<code>@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
public static ConfigurationPropertiesBeans configurationPropertiesBeans() {
return new ConfigurationPropertiesBeans();
}
</code>This bean manages classes annotated with @ConfigurationProperties and acts as a BeanPostProcessor :
<code>public class ConfigurationPropertiesBeans implements BeanPostProcessor {
private Map<String, ConfigurationPropertiesBean> beans = new HashMap<>();
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (isRefreshScoped(beanName)) {
return bean;
}
ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName);
if (propertiesBean != null) {
this.beans.put(beanName, propertiesBean);
}
return bean;
}
}
</code>The ConfigurationPropertiesBean#get method creates a bean wrapper:
<code>public final class ConfigurationPropertiesBean {
public static ConfigurationPropertiesBean get(ApplicationContext applicationContext, Object bean, String beanName) {
Method factoryMethod = findFactoryMethod(applicationContext, beanName);
return create(beanName, bean, bean.getClass(), factoryMethod);
}
private static ConfigurationPropertiesBean create(String name, Object instance, Class<?> type, Method factory) {
ConfigurationProperties annotation = findAnnotation(instance, type, factory, ConfigurationProperties.class);
if (annotation == null) {
return null;
}
// bind configuration properties to the instance
// ... (binding logic omitted for brevity)
return new ConfigurationPropertiesBean(name, instance, annotation, bindTarget);
}
}
</code>All classes annotated with @ConfigurationProperties are thus wrapped into ConfigurationPropertiesBean objects and stored in a map inside ConfigurationPropertiesBeans . The next step is to re‑bind those whose configuration has changed.
2.2 Re‑bind @ConfigurationProperties Classes
The container also registers a rebinder bean:
<code>@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
public ConfigurationPropertiesRebinder configurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
return new ConfigurationPropertiesRebinder(beans);
}
</code>ConfigurationPropertiesRebinder is an event listener that reacts to EnvironmentChangeEvent :
<code>@Component
@ManagedResource
public class ConfigurationPropertiesRebinder implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
private ConfigurationPropertiesBeans beans;
private ApplicationContext applicationContext;
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.applicationContext.equals(event.getSource()) || event.getKeys().equals(event.getSource())) {
rebind();
}
}
public void rebind() {
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
}
public boolean rebind(String name) {
ApplicationContext ctx = this.applicationContext;
while (ctx != null) {
if (ctx.containsLocalBean(name)) {
return rebind(name, ctx);
} else {
ctx = ctx.getParent();
}
}
return false;
}
private boolean rebind(String name, ApplicationContext ctx) {
try {
Object bean = ctx.getBean(name);
if (AopUtils.isAopProxy(bean)) {
bean = ProxyUtils.getTargetObject(bean);
}
if (bean != null) {
if (getNeverRefreshable().contains(bean.getClass().getName())) {
return false; // ignore
}
ctx.getAutowireCapableBeanFactory().destroyBean(bean);
ctx.getAutowireCapableBeanFactory().initializeBean(bean, name);
return true;
}
} catch (Exception ignored) {}
return false;
}
}
</code>The rebinder destroys the existing bean instance and re‑initializes it, causing the ConfigurationPropertiesBindingPostProcessor to re‑bind the latest configuration values.
2.3 Triggering the Refresh
Spring Cloud provides a RefreshEndpoint actuator. Invoking /actuator/refresh triggers the refresh process:
<code>public abstract class ContextRefresher {
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
this.scope.refreshAll();
return keys;
}
public synchronized Set<String> refreshEnvironment() {
Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
updateEnvironment();
Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}
}
</code>After the environment is refreshed, an EnvironmentChangeEvent is published, which the ConfigurationPropertiesRebinder listens to and performs the re‑binding.
Note: If you use HikariDataSource as a data source bean, it is excluded from refresh by default via spring.cloud.refresh.never-refreshable . Choose a different data source implementation if you need it to be refreshable.
In summary, this article explains the full mechanism by which Spring Cloud discovers, wraps, and dynamically re‑binds classes annotated with @ConfigurationProperties , and how the refresh is triggered via the actuator endpoint, helping developers build more flexible and maintainable Spring Cloud applications.
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.