Backend Development 9 min read

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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How @RefreshScope Dynamically Refreshes Beans in Spring Boot

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.

JavaSpring BootSpring CloudRefreshScopeBean Refresh
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.