Appearance
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: 11111.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方法:bashGET /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; }对应着:
bashPOST /actuator/refresh
@DeleteOperation:表示删除操作,对应着Web端点中的DELETE方法,例如:java@DeleteOperation public void delete() { }对应着:
bashDELETE /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:作用是重新绑定@ConfigurationPropertiesBean,核心逻辑是收到事件后,对受影响的配置 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绑定属性:
yamldemo: name: hello timeout: 60如果配置之后变为如下:
yamldemo: name: world即删掉了
timeout,如果属性没有重置为默认值,那体现不出来配置被删除了;之后,
autowireBean()重新对这个已经存在的对象执行依赖注入相关处理;最后,
initializeBean()重新执行 Bean 初始化生命周期,包括以下流程:txtAware 回调 ↓ 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#getObjectForBeanInstance3.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如下:
txtbeanName = scopedTarget.myService class = MyService scope = refresh beanName = myService class = ScopedProxyFactoryBean scope = singleton代理对象创建阶段
之后,从容器中获取Bean:
javacontext.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(...)参考资料
[2] Spring Boot Actuator:https://docs.spring.io/spring-boot/reference/actuator/endpoints.html