Backend Development 9 min read

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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How Spring Cloud Dynamically Refreshes @ConfigurationProperties Beans

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.

JavaConfigurationPropertiesSpring Clouddynamic refreshRefreshScope
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.