How Spring Cloud RefreshScope Works: From Annotation to Runtime Refresh
This article explains the inner workings of Spring Cloud's RefreshScope, covering its annotation definition, registration in the auto‑configuration, the refresh endpoint workflow, event handling, and how beans annotated with @ConfigurationProperties or @RefreshScope are dynamically refreshed at runtime.
Environment: spring‑cloud‑context 2.2.8.RELEASE + spring‑boot 2.3.9.RELEASE.
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 mark the scope name as
refresh.
2. Registering RefreshScope
<code>public class RefreshAutoConfiguration {
public static final String REFRESH_SCOPE_NAME = "refresh";
@Bean
@ConditionalOnMissingBean(RefreshScope.class)
public static RefreshScope refreshScope() {
return new RefreshScope();
}
@Bean
@ConditionalOnMissingBean
public ContextRefresher contextRefresher(ConfigurableApplicationContext context, RefreshScope scope) {
return new ContextRefresher(context, scope);
}
@Bean
@ConditionalOnMissingBean
public RefreshEventListener refreshEventListener(ContextRefresher contextRefresher) {
return new RefreshEventListener(contextRefresher);
}
}</code>The
RefreshScopeclass extends
GenericScopeand implements
ApplicationContextAwareand
ApplicationListener<ContextRefreshedEvent>. Its core methods are defined in the parent class.
<code>public class GenericScope implements Scope, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, DisposableBean {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
beanFactory.registerScope(this.name, this);
setSerializationId(beanFactory);
}
}</code>During bean retrieval, the registered
RefreshScopeis looked up in
AbstractBeanFactory#doGetBean, and its
getmethod is invoked.
3. Refresh endpoint trigger
<code>@Endpoint(id = "refresh")
public class RefreshEndpoint {
private ContextRefresher contextRefresher;
public RefreshEndpoint(ContextRefresher contextRefresher) { this.contextRefresher = contextRefresher; }
@WriteOperation
public Collection<String> refresh() { return this.contextRefresher.refresh(); }
}</code>The endpoint calls
ContextRefresher#refresh, which first refreshes the environment and then invokes
RefreshScope#refreshAll.
<code>public 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());
addConfigFilesToEnvironment();
Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}
}</code>The
EnvironmentChangeEventis listened to by
ConfigurationPropertiesRebinder, which rebinds all beans annotated with
@ConfigurationProperties:
<code>public class ConfigurationPropertiesRebinder implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
@ManagedOperation
public void rebind() {
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
}
public boolean rebind(String name) {
Object bean = this.applicationContext.getBean(name);
if (bean != null) {
this.applicationContext.getAutowireCapableBeanFactory().destroyBean(bean);
this.applicationContext.getAutowireCapableBeanFactory().initializeBean(bean, name);
return true;
}
return false;
}
}</code>The supporting auto‑configuration creates
ConfigurationPropertiesBeans, a
BeanPostProcessorthat collects beans with
@ConfigurationPropertiesduring initialization.
4. RefreshScope refresh handling
When
RefreshScope#refreshAllis invoked, it calls the parent
GenericScope#destroyto clear cached bean instances and then publishes a
RefreshScopeRefreshedEvent.
<code>public void refreshAll() {
super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}</code>The
GenericScope#destroyiterates over cached
BeanLifecycleWrapperobjects, acquiring write locks and invoking their
destroymethods to remove old bean instances.
<code>public void destroy() {
Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
for (BeanLifecycleWrapper wrapper : wrappers) {
Lock lock = this.locks.get(wrapper.getName()).writeLock();
lock.lock();
try { wrapper.destroy(); } finally { lock.unlock(); }
}
this.errors.clear();
}</code>After the cache is cleared, the next injection triggers
ObjectFactory#getObjectto create a fresh bean instance.
Summary
Triggering
/actuator/refreshcauses all beans annotated with
@ConfigurationPropertiesto be automatically refreshed without needing
@RefreshScope. The
@RefreshScopeannotation is typically applied to non‑configuration beans that use
@Valuefor property injection, allowing their values to be updated dynamically when the refresh endpoint is called.
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.