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.
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
Scopeand 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/refreshtriggers
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
ConfigurationPropertiesBeansholder created by an auto‑configuration class; it tracks all beans annotated with
@ConfigurationPropertiesso they can be refreshed when an
EnvironmentChangeEventis 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
refreshscope 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
GenericScopeclears 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/refreshis invoked, the
customproperty is re‑loaded, the cached bean is cleared, and a new instance reflecting the updated value is created.
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.