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.

@Component
@RefreshScope
public class ApiProperties {}

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.

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

The ScopedProxyFactoryBean creates a proxy whose TargetSource retrieves the original bean from the container each time it is needed.

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

4. Refresh Mechanism

Calling the /actuator/refresh endpoint triggers ContextRefresher, which refreshes the environment and then invokes RefreshScope.refreshAll() to destroy cached scoped beans.

private ContextRefresher contextRefresher;
@WriteOperation
public Collection<String> refresh() {
  Set<String> keys = this.contextRefresher.refresh();
  return keys;
}

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.

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

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.

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.

Spring BootSpring Cloud@RefreshScopeBean 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

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.