Backend Development 11 min read

Understanding Spring Cloud RefreshScope: How Dynamic Configuration Refresh Works

This article explains the inner workings of Spring Cloud's RefreshScope, detailing its source code, registration process, refresh endpoint activation, event-driven refresh mechanism, and how beans annotated with @RefreshScope or @ConfigurationProperties are dynamically reloaded without restarting the application.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Understanding Spring Cloud RefreshScope: How Dynamic Configuration Refresh Works

1 RefreshScope source code

<code>@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}</code>

The annotation adds

@Scope("refresh")

to define a custom scope named

refresh

.

2 Register RefreshScope

<code>public class RefreshAutoConfiguration {
    /** Name of the refresh scope */
    public static final String REFRESH_SCOPE_NAME = "refresh";

    // Register RefreshScope
    @Bean
    @ConditionalOnMissingBean(RefreshScope.class)
    public static RefreshScope refreshScope() {
        return new RefreshScope();
    }

    // Create ContextRefresher – the core component that reloads configuration
    @Bean
    @ConditionalOnMissingBean
    public ContextRefresher contextRefresher(ConfigurableApplicationContext context, RefreshScope scope) {
        return new ContextRefresher(context, scope);
    }

    // Listen to context refresh events
    @Bean
    public RefreshEventListener refreshEventListener(ContextRefresher contextRefresher) {
        return new RefreshEventListener(contextRefresher);
    }
}</code>

RefreshScope extends

GenericScope

, which itself implements

Scope

and several post‑processor interfaces.

3 RefreshScope core class

<code>public class RefreshScope extends GenericScope implements ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, Ordered {
    public boolean refresh(String name) {
        if (!name.startsWith(SCOPED_TARGET_PREFIX)) {
            name = SCOPED_TARGET_PREFIX + name;
        }
        if (super.destroy(name)) {
            this.context.publishEvent(new RefreshScopeRefreshedEvent(name));
            return true;
        }
        return false;
    }

    @ManagedOperation(description = "Dispose of the current instance of all beans in this scope and force a refresh on next method execution.")
    public void refreshAll() {
        super.destroy();
        this.context.publishEvent(new RefreshScopeRefreshedEvent());
    }
}</code>

4 Refresh endpoint activation

<code>@Endpoint(id = "refresh")
public class RefreshEndpoint {
    private final ContextRefresher contextRefresher;
    public RefreshEndpoint(ContextRefresher contextRefresher) {
        this.contextRefresher = contextRefresher;
    }
    @WriteOperation
    public Collection<String> refresh() {
        Set<String> keys = this.contextRefresher.refresh();
        return keys;
    }
}</code>

Calling

/actuator/refresh

triggers

ContextRefresher.refresh()

.

5 ContextRefresher implementation

<code>public class ContextRefresher {
    public synchronized Set<String> refresh() {
        Set<String> keys = refreshEnvironment();
        this.scope.refreshAll();
        return keys;
    }

    public synchronized Set<String> refreshEnvironment() {
        // Capture current property values
        Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
        // Reload configuration files
        addConfigFilesToEnvironment();
        // Determine changed keys
        Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
        // Publish change event
        this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
        return keys;
    }
}</code>

6 ConfigurationProperties rebinding

<code>public class ConfigurationPropertiesRebinder implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
    private final ConfigurationPropertiesBeans beans;
    private ApplicationContext applicationContext;
    private final Map<String, Exception> errors = new ConcurrentHashMap<>();

    @ManagedOperation
    public void rebind() {
        errors.clear();
        for (String name : beans.getBeanNames()) {
            rebind(name);
        }
    }

    @ManagedOperation
    public boolean rebind(String name) {
        if (!beans.getBeanNames().contains(name) || applicationContext == null) {
            return false;
        }
        Object bean = applicationContext.getBean(name);
        if (AopUtils.isAopProxy(bean)) {
            bean = ProxyUtils.getTargetObject(bean);
        }
        if (bean != null) {
            // Destroy and re‑initialize the bean
            applicationContext.getAutowireCapableBeanFactory().destroyBean(bean);
            applicationContext.getAutowireCapableBeanFactory().initializeBean(bean, name);
            return true;
        }
        return false;
    }

    @Override
    public void onApplicationEvent(EnvironmentChangeEvent event) {
        if (applicationContext.equals(event.getSource()) || event.getKeys().equals(event.getSource())) {
            rebind();
        }
    }
}</code>

The rebinder receives a

ConfigurationPropertiesBeans

holder created by an auto‑configuration class; it tracks all beans annotated with

@ConfigurationProperties

so they can be refreshed when an

EnvironmentChangeEvent

is published.

7 Bean post‑processor for @RefreshScope detection

<code>@Component
public class ConfigurationPropertiesBeans implements BeanPostProcessor, ApplicationContextAware {
    private final Map<String, ConfigurationPropertiesBean> beans = new HashMap<>();
    private ApplicationContext applicationContext;
    private ConfigurableListableBeanFactory beanFactory;
    private String refreshScope;
    private boolean refreshScopeInitialized;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (isRefreshScoped(beanName)) {
            return bean;
        }
        ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean.get(applicationContext, bean, beanName);
        if (propertiesBean != null) {
            beans.put(beanName, propertiesBean);
        }
        return bean;
    }

    private boolean isRefreshScoped(String beanName) {
        if (refreshScope == null && !refreshScopeInitialized) {
            refreshScopeInitialized = true;
            for (String scope : beanFactory.getRegisteredScopeNames()) {
                if (beanFactory.getRegisteredScope(scope) instanceof org.springframework.cloud.context.scope.refresh.RefreshScope) {
                    refreshScope = scope;
                    break;
                }
            }
        }
        if (beanName == null || refreshScope == null) {
            return false;
        }
        return beanFactory.containsBeanDefinition(beanName) &&
               refreshScope.equals(beanFactory.getBeanDefinition(beanName).getScope());
    }

    public Set<String> getBeanNames() {
        return new HashSet<>(beans.keySet());
    }
}</code>

This post‑processor records beans that are not in the

refresh

scope but have

@ConfigurationProperties

, enabling them to be re‑bound when a refresh occurs.

8 RefreshScope refresh handling

<code>public class ContextRefresher {
    public synchronized Set<String> refresh() {
        Set<String> keys = refreshEnvironment();
        this.scope.refreshAll();
        return keys;
    }
}

public class RefreshScope {
    public void refreshAll() {
        super.destroy();
        this.context.publishEvent(new RefreshScopeRefreshedEvent());
    }
}</code>

The

destroy()

method in

GenericScope

clears cached bean instances, causing the next injection to create fresh objects.

9 Practical example

<code>@RestController
@RequestMapping("/refreshBeanProp")
@RefreshScope
public class RefreshScopeBeanPropController {
    @Value("${custom}")
    private String custom;

    @GetMapping("/get")
    public String get() {
        return custom;
    }
}</code>

When

/actuator/refresh

is invoked, the

custom

property is re‑loaded, the cached bean is cleared, and a new instance reflecting the updated value is created.

configurationSpringSpringBootcloudRefreshScope
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.