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

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

The annotation adds @Scope("refresh") to define a custom scope named refresh.

2 Register RefreshScope

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);
    }
}

RefreshScope extends GenericScope, which itself implements Scope and several post‑processor interfaces.

3 RefreshScope core class

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());
    }
}

4 Refresh endpoint activation

@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;
    }
}

Calling /actuator/refresh triggers ContextRefresher.refresh().

5 ContextRefresher implementation

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;
    }
}

6 ConfigurationProperties rebinding

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();
        }
    }
}

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

@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());
    }
}

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

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());
    }
}

The destroy() method in GenericScope clears cached bean instances, causing the next injection to create fresh objects.

9 Practical example

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

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

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ConfigurationspringSpringBootcloud@RefreshScope
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

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.