Skip to content

Spring Cloud Context的环境刷新机制

在Spring Boot中,提供了Environment 基础设施,但是,如果配置变化了,Spring Boot并不会刷新配置,并且重新绑定到Bean。Spring Cloud Context则提供了完整的“配置变化 → Bean 刷新”机制。

1. 基础实验

1.1 pom.xml

首先新建项目,pom.xml如下:

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.1.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.lee</groupId>
    <artifactId>context-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>context-demo</name>
    <description>context-demo</description>

    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2025.1.3</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

1.2 application.yaml

配置文件application.yaml如下:

yaml
spring:
  application:
    name: context-demo
  config:
    import:
      # 引入外部文件,注意地址是文件系统地址
      - "file:/config/b.yml"

server:
  port: 8080

management:
  endpoint:
    env:
      show-values: always  # 总是允许暴露配置值
  endpoints:
    web:
      exposure:
        include: env,refresh,health   # 暴露三个端点,其中refresh可以重新配置

1.3 b.yaml

外部配置文件b.yaml内容如下:

yaml
test:
  b: 1111

1.4 主类

主类如下,主要是允许配置绑定@EnableConfigurationProperties

java
@SpringBootApplication
@EnableConfigurationProperties
public class ContextDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(ContextDemoApplication.class, args);
    }

}

1.5 配置类

在项目中新建配置类,绑定b.yaml文件中的配置:

java
@Component
@ConfigurationProperties(prefix = "test")
public class TestBean {

    private String b;

    public String getB() {
        return b;
    }

    public void setB(String b) {
        this.b = b;
    }
}

1.6 Controller

新建Controller,返回test.b的值:

java
@RestController
public class TestController {

    @Resource
    private TestBean testBean;

    @GetMapping("/b")
    public String b(){
        return testBean.getB();
    }

}

1.7 实验过程

首先启动项目,然后访问 http://localhost:8080/b,结果如下:

txt
1111

之后,手动修改b.yaml文件,将test.b的值改为2222,然后调用以下接口:

bash
curl -X POST http://localhost:8080/actuator/refresh

返回:

txt
["test.b"]%

说明test.b的值刷新了。

之后,再次访问 http://localhost:8080/b,结果变为了:

txt
2222

以上,配置变更并重新绑定到Bean,就是Spring Cloud Context提供的功能。

2. 原理剖析

2.1 RefreshEndpoint

RefreshEndpoint是Spring Cloud Context提供的Spring Boot Actuator Endpoint,对外暴露/actuator/refresh端点,用于刷新环境:

java
package org.springframework.cloud.endpoint;

@Endpoint(id = "refresh")
public class RefreshEndpoint {

	private static final Log LOG = LogFactory.getLog(RefreshEndpoint.class);

	private final ContextRefresher contextRefresher;

	public RefreshEndpoint(ContextRefresher contextRefresher) {
		this.contextRefresher = contextRefresher;
	}

	@WriteOperation
	public Collection<String> refresh() {
		Set<String> keys = this.contextRefresher.refresh();
		LOG.info("Refreshed keys : " + keys);
		return keys;
	}

}

Spring Boot Actuator 是 Spring Boot 提供的一套运行时管理和监控机制。它可以把应用内部状态暴露成管理端点,这些端点既可以通过 HTTP 暴露,也可以通过 JMX 暴露。

主要注解有四个:

  • @Endpoint:用来定义一个 Actuator 端点,例如:

    java
    @Endpoint(id = "refresh")
    public class RefreshEndpoint {
    }

    表示逻辑端点名叫 refresh。如果通过 Web 暴露,通常对应路径:

    /actuator/refresh
  • @ReadOperation:表示读取操作,例如:

    java
    @ReadOperation
    public String info() {
        return "hello";
    }

    如果通过Web暴露,通常对应GET方法:

    bash
    GET /actuator/xxx

    注意,这里的xxx@Endpoint中对应的id,而不是方法名。

  • @WriteOperation:表示修改操作,对应着Web端点中的POST方法,例如:

    java
    @WriteOperation
    public Collection<String> refresh() {
      Set<String> keys = this.contextRefresher.refresh();
      LOG.info("Refreshed keys : " + keys);
      return keys;
    }

    对应着:

    bash
    POST /actuator/refresh
  • @DeleteOperation:表示删除操作,对应着Web端点中的DELETE方法,例如:

    java
    @DeleteOperation
    public void delete() {
    }

    对应着:

    bash
    DELETE /actuator/xxx

TIP

注意,@Endpoint提供的端点类,需要注册为Bean,在Spring Cloud Context,是通过配置类注册的:

java
org.springframework.cloud.autoconfigure.RefreshEndpointAutoConfiguration.RefreshEndpointConfiguration#refreshEndpoint
  
@Configuration
@ConditionalOnBean(PropertySourceBootstrapConfiguration.class)
protected static class RefreshEndpointConfiguration {

  @Bean
  @ConditionalOnBean(ContextRefresher.class)
  @ConditionalOnEnabledEndpoint
  @ConditionalOnMissingBean
  public RefreshEndpoint refreshEndpoint(ContextRefresher contextRefresher) {
    return new RefreshEndpoint(contextRefresher);
  }

}

2.2 ContextRefresher

RefreshEndpoint只是提供一个入口,真正刷新配置的功能,是由ContextRefresher提供的,主要内容如下:

java
public abstract class ContextRefresher {
  
  // 容器
	private ConfigurableApplicationContext context;
  // RefreshScope 下一节介绍
	private RefreshScope scope;
  
  // 刷新容器中的环境
  public synchronized Set<String> refresh() {
    // 第一步:刷新环境
		Set<String> keys = refreshEnvironment();
    // 第二步:刷新RefreshScope
		this.scope.refreshAll();
    // 返回变化了的配置名
		return keys;
	}

	public synchronized Set<String> refreshEnvironment() {
    // 获取更新前的环境
		Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
    // 更新环境
		updateEnvironment();
    // 获取更新了的属性名称
		Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
    // 发布EnvironmentChangeEvent事件
		this.context.publishEvent(new EnvironmentChangeEvent事件(this.context, keys));
		return keys;
	}

  // 抽象方法,供子类实现
	protected abstract void updateEnvironment();

}

ContextRefresher有两个子类实现:ConfigDataContextRefresherLegacyContextRefresher

两个实现根据是否使用经典Spring Cloud Context二选其一,如果使用了Spring Cloud Context,则使用LegacyContextRefresher,否则使用ConfigDataContextRefresher

java
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RefreshScope.class)
@ConditionalOnProperty(name = RefreshAutoConfiguration.REFRESH_SCOPE_ENABLED, matchIfMissing = true)
@AutoConfigureBefore(name = "org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration")
@EnableConfigurationProperties(RefreshAutoConfiguration.RefreshProperties.class)
public class RefreshAutoConfiguration {
  
  @Bean
	@ConditionalOnMissingBean
	@ConditionalOnBootstrapEnabled
	public LegacyContextRefresher legacyContextRefresher(ConfigurableApplicationContext context, RefreshScope scope,
			RefreshProperties properties) {
		return new LegacyContextRefresher(context, scope, properties);
	}

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnBootstrapDisabled
	public ConfigDataContextRefresher configDataContextRefresher(ConfigurableApplicationContext context,
			RefreshScope scope, RefreshProperties properties) {
		return new ConfigDataContextRefresher(context, scope, properties);
	}
}

ConfigDataContextRefresher实现中,updateEnvironment()方法其实就是重新调用EnvironmentPostProcessor,然后更新环境。

2.3 EnvironmentChangeEvent事件监听器

ContextRefresher刷新环境后,会发布EnvironmentChangeEvent事件,并且Spring Cloud Context提供了以下监听器:

  • ConfigurationPropertiesRebinder:作用是重新绑定 @ConfigurationProperties Bean,核心逻辑是收到事件后,对受影响的配置 Bean 重新走绑定流程;
  • LoggingRebinder:作用是当日志相关配置变化时,重新应用 logging level 等设置;

ConfigurationPropertiesRebinder的主要方法如下:

java
public void rebind() {
  this.errors.clear();
  // 它不是只根据 EnvironmentChangeEvent 里的 changed keys 精确挑出“受影响的 Bean”再重绑,而是通常会把已登记的所有 @ConfigurationProperties Bean 都走一遍 rebind
  for (String name : this.beans.getBeanNames()) {
    rebind(name);
  }
}

重新绑定流程如下:

java
appContext.getAutowireCapableBeanFactory().destroyBean(target);
resetBeanToDefaults(target);
appContext.getAutowireCapableBeanFactory().autowireBean(target);
appContext.getAutowireCapableBeanFactory().initializeBean(target, name);
  • 首先,destroyBean()执行这个 Bean 的销毁生命周期,但不是把这个对象从 Spring 容器里删除,也不是重新创建 Bean;

  • 然后,将这个Bean的属性置为默认值,这是为了解决以下问题:

    假设最初Bean绑定属性:

    yaml
    demo:
      name: hello
      timeout: 60

    如果配置之后变为如下:

    yaml
    demo:
      name: world

    即删掉了timeout,如果属性没有重置为默认值,那体现不出来配置被删除了;

  • 之后,autowireBean()重新对这个已经存在的对象执行依赖注入相关处理;

  • 最后,initializeBean()重新执行 Bean 初始化生命周期,包括以下流程:

    txt
    Aware 回调
    
    BeanPostProcessor.postProcessBeforeInitialization()
    
    InitializingBean.afterPropertiesSet()
    
    init-method
    
    BeanPostProcessor.postProcessAfterInitialization()

    其中ConfigurationPropertiesBindingPostProcessor就会重新将这个Bean和环境重新绑定。

至此,Spring Cloud Context中环境变化,触发@ConfigurationPropertiesBean属性重新绑定流程已介绍完毕。

3. Refresh Scope

ContextRefresher中刷新容器环境后,还调用了this.scope.refreshAll()

java
public synchronized Set<String> refresh() {
  Set<String> keys = refreshEnvironment();
  this.scope.refreshAll();
  return keys;
}

这是Spring Cloud Context引入的Refresh Scope机制重新刷新。

3.1 实验

当我们使用@Value("${xxx}")给Bean的属性注入配置后,如果后续配置刷新了,那@Value("${xxx}")自动注入的属性值不会自动变更。

例如,在Controller中注入属性:

java
@RestController
public class TestController {

    @Resource
    private TestBean testBean;

    @Value("${test.b}")
    private String b;

    @GetMapping("/b")
    public String b(){
        return testBean.getB();
    }

    @GetMapping("/notChanged")
    public String notChanged(){
        return b;
    }
}

访问 http://localhost:8080/notChanged,返回 1111

然后,修改b.yaml的值为3333,并触发POST /actuator/refresh

再次访问 http://localhost:8080/notChanged,仍然返回 1111。可以证明,使用 @Value("${xxx}") 绑定的配置值没有自动更新。

而Spring Cloud Context提供的RefreshScope就可以解决以上问题。

3.2 原理解析

3.2.1 概述

一句话总结,@RefreshScope 的原理就是:如果一个Bean加上了@RefreshScope注解,那么Spring Cloud会为该 Bean 创建一个代理对象,并把真实 Bean 交给 RefreshScope 缓存管理;刷新时销毁真实 Bean,代理不变,下一次调用再基于最新 Environment 创建新的真实 Bean。

3.2.2 Scope

Scope 是 Spring Bean 生命周期管理里的一个扩展点,用来决定“某个 Bean 实例如何获取、保存多久、何时销毁”。

接口定义如下:

java
package org.springframework.beans.factory.config;

import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.ObjectFactory;

public interface Scope {

	// 从Scope中获取名为name的Bean,如果Scope 里已经有实例,就直接返回;没有就调用objectFactory.getObject()创建
	Object get(String name, ObjectFactory<?> objectFactory);

	// 把某个 Bean 从当前 Scope 移除。
	@Nullable Object remove(String name);

	// 注册 Bean 被 Scope 销毁时要执行的回调
	void registerDestructionCallback(String name, Runnable callback);

	// 根据当前 Scope 上下文获取附加对象,主要用于 request/session 这类上下文型 Scope。
	default @Nullable Object resolveContextualObject(String key) {
		return null;
	}

	// 返回当前 Scope 会话标识。比如 session scope 可以和某个 session 关联;很多自定义 Scope 可以直接返回 null。
	default @Nullable String getConversationId() {
		return null;
	}

}

在Spring Beans中,定义了以下6个Scope:

Scope实例生命周期典型场景
singleton默认值,每个 Spring IoC 容器中,一个 BeanDefinition 对应一个实例,每次获取都是同一个对象默认业务 Bean
prototype每次获取都创建一个新实例临时、有状态对象
request每个 HTTP Request 一个请求级上下文
session每个 HTTP Session 一个用户会话级状态
application每个 ServletContext 一个实例Web 应用全局状态
websocket每个 WebSocket Session 一个实例WebSocket 会话状态

singletonprototype 虽然也是 scope 概念,但 Spring BeanFactory 对它们有专门分支;真正通过 Scope 接口扩展的通常是 requestsession这类自定义 scope。

在Spring Cloud Context,新定义了一个名为refresh的Scope:

java
public class RefreshScope extends GenericScope
		implements ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, Ordered {

	/**
	 * Creates a scope instance and gives it the default name: "refresh".
	 */
	public RefreshScope() {
		super.setName("refresh");
	}
}

其并没有重写get方法,使用了GenericScope中的实现,大致流程就是先从缓存中获取,如果没有就新建:

java
// 非源码,逻辑流程
public Object get(String name, ObjectFactory<?> factory) {

    Object bean = cache.get(name);

    if (bean == null) {
        bean = factory.getObject();
        cache.put(name, bean);
    }

    return bean;
}

最重要的是,在RefreshScope中定义了refresh方法,主要作用就是清除缓存中的Bean对象,当下一次获取Bean对象时,需要重新创建Bean对象,这样就能根据当前环境重新注入属性:

java
@ManagedOperation(description = "Dispose of the current instance of bean name "
    + "provided and force a refresh on next method execution.")
public boolean refresh(String name) {
  if (!ScopedProxyUtils.isScopedTarget(name)) {
    // User wants to refresh the bean with this name but that isn't the one in the
    // cache...
    name = ScopedProxyUtils.getTargetBeanName(name);
  }
  // Ensure lifecycle is finished if bean was disposable
  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());
}

3.2.3 @RefreshScope

@RefreshScope其实就是定义了Bean的Scope为refresh

java
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {

	@AliasFor(annotation = Scope.class)
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}

3.2.4 BeanFactory

BeanFactory是Spring 容器的核心接口,定义了容器的相关功能,例如getBean(),在AbstractBeanFactory实现中,getBean()部分代码如下:

java
@Override
public Object getBean(String name) throws BeansException {
  return doGetBean(name, null, null, false);
}

// 核心在doGetBean(),部分代码如下:

protected <T> T doGetBean(
			String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
			throws BeansException {
  
  String beanName = transformedBeanName(name);
	Object beanInstance;
  
  // mbd 是BeanDefinition
  // 如果BeanDefinition的Scope不是singleton或prototype,就会进入以下逻辑
  // 获取scope名称,如果是@RefreshScope,这里就是refresh
  String scopeName = mbd.getScope();
  if (!StringUtils.hasLength(scopeName)) {
    throw new IllegalStateException("No scope name defined for bean '" + beanName + "'");
  }
  // 获取Scope对象,即RefreshScope
  Scope scope = this.scopes.get(scopeName);
  if (scope == null) {
    throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
  }
  try {
    // 从RefreshScope中获取对象
    Object scopedInstance = scope.get(beanName, () -> {
      beforePrototypeCreation(beanName);
      try {
        return createBean(beanName, mbd, args);
      }
      finally {
        afterPrototypeCreation(beanName);
      }
    });
    // 如果Bean是FactoryBean,那么会调用FactoryBean的getObject()方法,获取Bean
    beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
  }
  catch (IllegalStateException ex) {
    throw new ScopeNotActiveException(beanName, scopeName, ex);
  }
}

3.2.5 FactoryBean

FactoryBean是一个接口,定义了一类特殊的Bean:从容器中获取FactoryBean,只能获取到FactoryBean通过getObject()返回的Bean。

java
public interface FactoryBean<T> {
  String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";
  
  @Nullable
	T getObject() throws Exception;
  
  @Nullable
	Class<?> getObjectType();
  
  default boolean isSingleton() {
		return true;
	}

}

也就是说,假设有如下FactoryBean

java
public class MyFactoryBean implements FactoryBean<MyService> {

    @Override
    public MyService getObject() {
        return new MyService();
    }

    @Override
    public Class<?> getObjectType() {
        return MyService.class;
    }
}

注册到容器后,从容器中获取该Bean:

java
context.getBean("myFactoryBean")

获取到的是MyService对象。

如果要获取MyFactoryBean对象,需要在Bean名称前加前缀&

java
context.getBean("&myFactoryBean")

以上逻辑在BeanFactory中有源码体现:

txt
org.springframework.beans.factory.support.AbstractBeanFactory#getObjectForBeanInstance

3.2.6 创建代理Bean

org.springframework.aop.scope.ScopedProxyUtils#createScopedProxy中创建代理Bean,源码如下:

java
// definition : 原始的BeanDefinition持有者
// registry : 管理BeanDefinitions
// proxyTargetClass : 决定使用哪种代理方式
public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
    BeanDefinitionRegistry registry, boolean proxyTargetClass) {

  // 获取原始的bean名称,例如:
  // @Component
  // @RefreshScope
  // public class MyService {}
  //
  // originalBeanName = "myService"
  String originalBeanName = definition.getBeanName();
  // 原始的BeanDefinition
  BeanDefinition targetDefinition = definition.getBeanDefinition();
  // 生成原始 Bean 的内部名称,即加前缀 scopedTarget.
  // 例如,原始Bean的名称为myService,targetBeanName为 scopedTarget.myService
  String targetBeanName = getTargetBeanName(originalBeanName);

  // 创建“代理 Bean”的 BeanDefinition
  // 注意这里的 BeanClass 不是 MyService,而是 ScopedProxyFactoryBean。
  RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
  proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName));
  proxyDefinition.setOriginatingBeanDefinition(targetDefinition);
  proxyDefinition.setSource(definition.getSource());
  proxyDefinition.setRole(targetDefinition.getRole());

  // 给 ScopedProxyFactoryBean 设置 targetBeanName
  proxyDefinition.getPropertyValues().add("targetBeanName", targetBeanName);
  if (proxyTargetClass) {
    // 如果proxyTargetClass为true,表示希望基于目标类做代理,通常意味着使用 CGLIB / class-based proxy。
    // ScopedProxyFactoryBean 本身默认:proxyTargetClass = true
    // 所以这里不需要再给 proxyDefinition 设置属性。
    // 这里给真实目标 BeanDefinition 设置:PRESERVE_TARGET_CLASS_ATTRIBUTE = true
    // 告诉后续的自动代理机制:如果还需要进一步创建代理,应尽量保留目标类代理语义。
    targetDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
  }
  else {
    // 如果proxyTargetClass为false,使用接口代理
    proxyDefinition.getPropertyValues().add("proxyTargetClass", Boolean.FALSE);
  }

  // 把自动注入相关属性转移给“代理 Bean”,也就是说,以后用到MyService的地方,都应该使用代理对象,而不是原始MyService对象
  proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
  proxyDefinition.setPrimary(targetDefinition.isPrimary());
  if (targetDefinition instanceof AbstractBeanDefinition abd) {
    // 将 @Qualifier 等限定符信息复制到代理 BeanDefinition。
    proxyDefinition.copyQualifiersFrom(abd);
  }

  // 禁止原始 Bean 直接参与依赖注入
  targetDefinition.setAutowireCandidate(false);
  targetDefinition.setPrimary(false);

  // 将原始 BeanDefinition 以新的名字注册:scopedTarget.myService
  // 此时容器中实际上会存在:
  // scopedTarget.myService
  //     class = MyService
  //     scope = refresh
  // 这个才是真正由 RefreshScope 管理的 Bean。
  registry.registerBeanDefinition(targetBeanName, targetDefinition);

  // 返回代理Bean,此时代理Bean的名称为原始Bean名称,即myService
  return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
}

在Spring 容器初始化中,会根据@ScopeScopedProxyMode属性,判断是否调用ScopedProxyUtils#createScopedProxy(),之后,会把返回的代理Bean加入到BeanDefinitionRegistry中,并且是以原始Bean的名称注册的。

3.2.7 ScopedProxyFactoryBean

在上一节创建代理Bean中,针对@RefreshScope创建的代理Bean,其实是ScopedProxyFactoryBean,而这是一个FactoryBean,并且是实现了BeanFactoryAware接口:

java
public class ScopedProxyFactoryBean extends ProxyConfig
		implements FactoryBean<Object>, BeanFactoryAware, AopInfrastructureBean {
  
  // TargetSource,用于获取原始bean的
  private final SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource();

  // 原始的bean名称
	@Nullable
	private String targetBeanName;

  // 实际的代理对象
	@Nullable
	private Object proxy;

	public ScopedProxyFactoryBean() {
		setProxyTargetClass(true);
	}

	public void setTargetBeanName(String targetBeanName) {
		this.targetBeanName = targetBeanName;
    // 在TargetSource上也设置内部原始bean名称,即scopedTarget.xxx
		this.scopedTargetSource.setTargetBeanName(targetBeanName);
	}
}

BeanFactoryAware 是 Spring 的一个回调接口。某个 Bean 实现它之后,Spring 在创建这个 Bean 的过程中,会把当前 BeanFactory 回调给它。

ScopedProxyFactoryBean中,实现的setBeanFactory()其实就是创建代理对象:

java
@Override
public void setBeanFactory(BeanFactory beanFactory) {
  // 如果beanFactory不是ConfigurableBeanFactory,抛出异常,因为需要ConfigurableBeanFactory的一些功能
  if (!(beanFactory instanceof ConfigurableBeanFactory cbf)) {
    throw new IllegalStateException("Not running in a ConfigurableBeanFactory: " + beanFactory);
  }
  
  // 把 BeanFactory 交给 scopedTargetSource
  this.scopedTargetSource.setBeanFactory(beanFactory);

  // 构建ProxyFactory,用于创建代理对象。ProxyFactory 是 Spring AOP 用来构造代理对象的核心类之一。
  ProxyFactory pf = new ProxyFactory();
  // ScopedProxyFactoryBean 继承自 ProxyConfig。因此它本身保存了一些代理配置
  pf.copyFrom(this);
  // 重要!给代理设置 TargetSource,TargetSource用于获取原始Bean。
  // 每次调用代理对象方法时,通过以下流程获取原始Bean:
  // Proxy
  //   ↓
  // TargetSource
  //   ↓
  // SimpleBeanTargetSource
  //   ↓
  // BeanFactory.getBean("scopedTarget.xxx")
  pf.setTargetSource(this.scopedTargetSource);

  Assert.notNull(this.targetBeanName, "Property 'targetBeanName' is required");
  Class<?> beanType = beanFactory.getType(this.targetBeanName);
  if (beanType == null) {
    throw new IllegalStateException("Cannot create scoped proxy for bean '" + this.targetBeanName +
        "': Target type could not be determined at the time of proxy creation.");
  }
  // 这里有三种情况会配置接口代理:
  // ① proxyTargetClass = false
  // 即明确要求:不基于目标类代理。
  // ② beanType 本身就是 interface
  // 接口没法被继承生成 class-based proxy,因此直接走接口。
  // ③ beanType 是 private class
  // private 类无法被正常继承生成子类代理,因此只能尝试接口代理
  if (!isProxyTargetClass() || beanType.isInterface() || Modifier.isPrivate(beanType.getModifiers())) {
    pf.setInterfaces(ClassUtils.getAllInterfacesForClass(beanType, cbf.getBeanClassLoader()));
  }

  // 给代理额外引入 ScopedObject 接口
  ScopedObject scopedObject = new DefaultScopedObject(cbf, this.scopedTargetSource.getTargetBeanName());
  pf.addAdvice(new DelegatingIntroductionInterceptor(scopedObject));

  // 给代理对象增加 AopInfrastructureBean 标记接口
  pf.addInterface(AopInfrastructureBean.class);

  // 创建代理对象
  this.proxy = pf.getProxy(cbf.getBeanClassLoader());
}

FactoryBean的接口方法实现如下:

java
@Override
@Nullable
public Object getObject() {
  if (this.proxy == null) {
    throw new FactoryBeanNotInitializedException();
  }
  // 返回代理对象
  return this.proxy;
}

@Override
@Nullable
public Class<?> getObjectType() {
  if (this.proxy != null) {
    return this.proxy.getClass();
  }
  return this.scopedTargetSource.getTargetClass();
}

3.2.8 SimpleBeanTargetSource

代理对象实际是通过SimpleBeanTargetSource来获取原始Bean 的,获取原始Bean方法如下:

java
public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {

	@Override
	public Object getTarget() throws Exception {
    // getTargetBeanName()返回的是 scopedTarget.xxx
		return getBeanFactory().getBean(getTargetBeanName());
	}

}

以上最终调用的就是AbstractBeanFactory.doGetBean(),也就是会调用到RefreshScope中获取Bean的方法。

3.2.9 小结

RefreshScope 的核心可以概括成一句话:

它不是“修改原 Bean”,而是通过 Scoped Proxy + Scope 缓存,让代理对象长期不变、真实 Bean 可以被销毁并按需重新创建。

整体流程可以分成 4 个阶段:

  • BeanDefinition注册阶段

    如果某个Bean(例如,MyService)标注了@RefreshScope,那么最开始会在容器中注册名称为myService的Bean;

    之后,创建代理Bean,容器中的Bean如下:

    txt
    beanName = scopedTarget.myService
    	class = MyService
    	scope = refresh
    
    beanName = myService
    	class = ScopedProxyFactoryBean
    	scope = singleton
  • 代理对象创建阶段

    之后,从容器中获取Bean:

    java
    context.getBean("myService")

    实际会触发ScopedProxyFactoryBean的创建,并且回调setFactory(),创建真实的代理对象,注意,这里的代理对象有一个属性TargetSource

    之后,调用FactoryBean.getObject()方法,返回代理对象。

  • 调用代理对象

    当调用代理对象方法时,通过TargetSource从容器中获取到原始Bean对象。而TargetSource最终也是根据scope调用不同的scope实现,获取Bean对象,也就是从RefreshScope中获取,如果有缓存,则直接返回缓存对象,如果没有,则触发Bean创建过程。

  • 刷新阶段

    当调用refreshScope.refreshAll();后,会把RefreshScope中的缓存全部清空,因此,调用代理对象方法时,不得不重新创建原始Bean,达到配置重新绑定的效果。

以上大致介绍了RefreshScope的流程,可以发现,复用了很多Spring Bean和Spring AOP的机制,此处也没有深入展开研究。

4. nacos远程配置刷新原理

如果项目中使用了Nacos,那么配置是放在远程服务器中的,那服务是如何发现配置更新了呢,并且如何刷新Bean?整个流程可以分为三部分:

  • Nacos Client 怎么知道“配置变了”。
  • 如何把变化了的配置重新应用到容器环境中。
  • Bean如何刷新:这一部分就是上面3节介绍的Spring Cloud Context环境刷新机制,掠过;

4.1 Nacos如何监听配置变了

最简单的方法,就是服务启动后,Nacos后台启动一个线程,定时轮询服务端,确定与该服务相关的配置是否发生了改变。

对于 Nacos 2.x,客户端与服务端之间使用 RPC/gRPC 通信。配置变化时,可以看到类似ConfigChangeNotifyRequest的请求由服务端推送到客户端,也就是说,最新版本的Nacos,当服务端配置发生变化时,会主动推送消息给客户端。

当客户端接收到ConfigChangeNotifyRequest后,触发:

java
ConfigChangeNotifyResponse handleConfigChangeNotifyRequest(ConfigChangeNotifyRequest configChangeNotifyRequest, String clientName) {
  	// 日志记录
    ClientWorker.LOGGER.info("[{}] [server-push] config changed. dataId={}, group={},tenant={}", new Object[]{clientName, configChangeNotifyRequest.getDataId(), configChangeNotifyRequest.getGroup(), configChangeNotifyRequest.getTenant()});
  	// 从Request中获取groupKey
    String groupKey = GroupKey.getKeyTenant(configChangeNotifyRequest.getDataId(), configChangeNotifyRequest.getGroup(), configChangeNotifyRequest.getTenant());
  	// 从本地缓存的配置表中获取有没有对应groupKey的配置
    CacheData cacheData = (CacheData)((Map)ClientWorker.this.cacheMap.get()).get(groupKey);
    if (cacheData != null) {
      	// 如果客户端确实正在管理 / 监听这份配置
        synchronized(cacheData) {
          	// 标记:“已经收到服务端的变更通知”
            cacheData.getReceiveNotifyChanged().set(true);
          	// 标记:当前本地缓存已经不能认为和服务端一致
            cacheData.setConsistentWithServer(false);
          	// 立即触发监听配置逻辑
            this.notifyListenConfig();
        }
    }

  	// 返回响应给 Nacos Server
    return new ConfigChangeNotifyResponse();
}

notifyListenConfig()中,实际就是往队列中添加一个事件:

java
@Override
public void notifyListenConfig() {
    listenExecutebell.offer(bellItem);
}

com.alibaba.nacos.client.config.impl.ClientWorker.ConfigRpcTransportClient#startInternal中,启动了线程池,不停监听listenExecutebell队列:

java
@Override
public void startInternal() {
    ScheduledExecutorService executor = getExecutor();
    executor.schedule(() -> {
        while (!executor.isShutdown() && !executor.isTerminated()) {
            try {
                listenExecutebell.poll(5L, TimeUnit.SECONDS);
                if (executor.isShutdown() || executor.isTerminated()) {
                    continue;
                }
                executeConfigListen();
            } catch (Throwable e) {
                LOGGER.error("[rpc listen execute] [rpc listen] exception", e);
                try {
                    Thread.sleep(50L);
                } catch (InterruptedException interruptedException) {
                    //ignore
                }
                notifyListenConfig();
            }
        }
    }, 0L, TimeUnit.MILLISECONDS);

}

当从队列中获取到标志后,执行executeConfigListen(),在executeConfigListen中,最终会执行safeNotifyListener,也就是会调用Nacos中定义的Listener

java
public interface Listener {
    
    /**
     * Get executor for execute this receive.
     *
     * @return Executor
     */
    Executor getExecutor();
    
    /**
     * Receive config info.
     *
     * @param configInfo config info
     */
    void receiveConfigInfo(final String configInfo);
}

其中一个实现就是发布NacosConfigRefreshEvent事件:

java
new AbstractSharedListener() {
  @Override
  public void innerReceive(String dataId, String group,
      String configInfo) {

    log.info("[Nacos Config] Receive Nacos config change: dataId={}, group={}", dataKey,
        groupKey);
    refreshCountIncrement();
    nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
    NacosSnapshotConfigManager.putConfigSnapshot(dataId, group,
        configInfo);
    NacosConfigRefreshEvent event = new NacosConfigRefreshEvent(this, null, "Refresh Nacos config");
    event.setDataId(dataId);
    event.setGroup(group);
    applicationContext.publishEvent(event);
    if (log.isDebugEnabled()) {
      log.debug(String.format(
          "Publish Nacos config Refresh Event group=%s,dataId=%s,configInfo=%s",
          group, dataId, configInfo));
    }
  }
}

4.2 应用变化

当发布NacosConfigRefreshEvent事件后,根据是否为Spring Cloud 环境,有两条处理路径:

txt
                         NacosConfigRefreshEvent

                 ┌────────────────┴────────────────┐
                 │                                 │
                 ▼                                 ▼
      Spring Cloud 集成存在                  不走 Spring Cloud 刷新链
                 │                                 │
                 │                                 │
                 ▼                                 ▼
NacosConfigRefreshEventListener        NacosPropertySourceRefreshListener
                 │                                 │
                 │ publishEvent(...)               │
                 ▼                                 │
            RefreshEvent                           │
                 │                                 │
                 ▼                                 │
          RefreshEventListener                     │
                 │                                 │
                 │ contextRefresher.refresh()      │
                 ▼                                 │
           ContextRefresher                        │
                 │                                 │
          ┌──────┴──────┐                          │
          ▼             ▼                          │
 refreshEnvironment()  RefreshScope.refreshAll()  │
          │                                        │
          ▼                                        ▼
 ConfigDataContextRefresher              NacosPropertySourceBuilder
          │                                        │
          ▼                                        ▼
重新加载 Environment                      build(...)


                                    MutablePropertySources.replace(...)

参考资料

[1] https://docs.spring.io/spring-cloud-commons/reference/spring-cloud-commons/application-context-services.html#environment-changes

[2] Spring Boot Actuator:https://docs.spring.io/spring-boot/reference/actuator/endpoints.html