How @RefreshScope Dynamically Refreshes Beans in Spring Boot
This article explains the purpose and inner workings of Spring Boot's @RefreshScope annotation, showing how it creates proxy and target beans, the proxy generation process, and how the /actuator/refresh endpoint triggers a full refresh of scoped beans at runtime.
1. Introduction
When configuration changes, beans marked with @RefreshScope receive special handling. This solves problems with stateful beans that are only injected with configuration at initialization, such as data sources that need to pick up a new database URL without restarting the application.
2. Annotation Principle
Annotating a class with @RefreshScope causes the container to create two beans: a proxy bean (e.g., apiProperties ) that other beans can inject, and a target bean (e.g., scopedTarget.apiProperties ) that is not a candidate for injection.
<code>@Component
@RefreshScope
public class ApiProperties {}
</code>The proxy bean ( beanName=apiProperties ) can be injected by other beans, for example using @Resource .
The target bean ( beanName=scopedTarget.apiProperties ) is a regular bean that is not eligible for injection; attempting to inject it directly will cause an error.
3. Proxy Creation
The container creates the proxy using ScopedProxyCreator . The process involves generating a ScopedProxyFactoryBean that holds a SimpleBeanTargetSource pointing to the original bean name.
<code>public abstract class ScopedProxyUtils {
public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
BeanDefinitionRegistry registry, boolean proxyTargetClass) {
String originalBeanName = definition.getBeanName();
BeanDefinition targetDefinition = definition.getBeanDefinition();
String targetBeanName = getTargetBeanName(originalBeanName);
RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
proxyDefinition.getPropertyValues().add("targetBeanName", targetBeanName);
proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
proxyDefinition.setPrimary(targetDefinition.isPrimary());
targetDefinition.setAutowireCandidate(false);
targetDefinition.setPrimary(false);
registry.registerBeanDefinition(targetBeanName, targetDefinition);
return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
}
}
</code>The ScopedProxyFactoryBean creates a proxy whose TargetSource retrieves the original bean from the container each time it is needed.
<code>public class ScopedProxyFactoryBean {
private final SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource();
public void setBeanFactory(BeanFactory beanFactory) {
ConfigurableBeanFactory cbf = (ConfigurableBeanFactory) beanFactory;
this.scopedTargetSource.setBeanFactory(beanFactory);
ProxyFactory pf = new ProxyFactory();
pf.copyFrom(this);
pf.setTargetSource(this.scopedTargetSource);
this.proxy = pf.getProxy(cbf.getBeanClassLoader());
}
public Object getObject() { return this.proxy; }
public Class<?> getObjectType() { return this.scopedTargetSource.getTargetClass(); }
}
public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
public Object getTarget() throws Exception {
return getBeanFactory().getBean(getTargetBeanName());
}
}
</code>4. Refresh Mechanism
Calling the /actuator/refresh endpoint triggers ContextRefresher , which refreshes the environment and then invokes RefreshScope.refreshAll() to destroy cached scoped beans.
<code>private ContextRefresher contextRefresher;
@WriteOperation
public Collection<String> refresh() {
Set<String> keys = this.contextRefresher.refresh();
return keys;
}
</code>The RefreshScope extends GenericScope ; its refreshAll() method simply calls super.destroy() , clearing the cache of scoped beans so that subsequent getBean calls obtain fresh instances.
<code>public class RefreshScope extends GenericScope {
public void refreshAll() { super.destroy(); }
}
public class GenericScope {
public Object get(String name, ObjectFactory<?> objectFactory) {
BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
// ...
return value.getBean();
}
public void destroy() {
Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
// clear all scoped objects
}
}
</code>Because each call to BeanFactory#getBean for a refresh‑scoped bean goes through RefreshScope#get , any @Value annotations are re‑bound with the new configuration values after a refresh.
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.