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有两个子类实现:ConfigDataContextRefresher和LegacyContextRefresher。

两个实现根据是否使用经典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 会话状态

singleton 和 prototype 虽然也是 scope 概念,但 Spring BeanFactory 对它们有专门分支;真正通过 Scope 接口扩展的通常是 request、session这类自定义 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 容器初始化中,会根据@Scope的ScopedProxyMode属性,判断是否调用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