技术创想106 | 源码剖析:理解Spring Cloud中的@RefreshScope

文摘   科技   2024-06-14 17:15   北京  

背景

最近项目中完成了配置中心从Spring Cloud Config Server到Alibaba Nacos的迁移工作,那Spring Cloud应用集成Nacos后,配置的自动刷新机制是什么呢?配置变更后,又是怎样影响已经装配好的Bean?在Nacos官方文档中,找到以下一段关于配置自动刷新配置的描述:

通过Spring Cloud原生注解@RefreshScope实现配置自动更新:

@RestController@RequestMapping("/config")@RefreshScopepublic class ConfigController {
@Value("${useLocalCache:false}") private boolean useLocalCache;
@RequestMapping("/get") public boolean get() { return useLocalCache; }}

示例

我们创建一个简单的Spring Cloud项目来验证配置的自动更新是否生效。

  1. 引入相关依赖并使用上面的ConfigController定义来获取实时的配置值。

  2. 在Nacos中更新配置值后,调用/config/get 接口返回了配置的新值。

  3. 查看日志

2024-01-18 10:58:45.143  INFO 2289 --- [or-127.0.0.1-59] com.alibaba.nacos.common.remote.client   : [7ff5ec9b-478a-40e7-b505-77af547b7d6d_config-0] Receive server push request, request = ConfigChangeNotifyRequest, requestId = 1332024-01-18 10:58:45.144  INFO 2289 --- [or-127.0.0.1-59] c.a.n.client.config.impl.ClientWorker    : [7ff5ec9b-478a-40e7-b505-77af547b7d6d_config-0] [server-push] config changed. dataId=config-demo.yaml, group=DEFAULT_GROUP,tenant=null2024-01-18 10:58:45.144  INFO 2289 --- [or-127.0.0.1-59] com.alibaba.nacos.common.remote.client   : [7ff5ec9b-478a-40e7-b505-77af547b7d6d_config-0] Ack server push request, request = ConfigChangeNotifyRequest, requestId = 1332024-01-18 10:58:45.553  INFO 2289 --- [s.client.Worker] c.a.n.client.config.impl.ClientWorker    : [fixed-127.0.0.1_8848] [data-received] dataId=config-demo.yaml, group=DEFAULT_GROUP, tenant=, md5=00b77727c9b4a661b8ae12cd7de1395f, content=useLocalCache: true, type=yaml2024-01-18 10:58:45.557  INFO 2289 --- [ternal.notifier] c.a.nacos.client.config.impl.CacheData   : [fixed-127.0.0.1_8848] [notify-context] dataId=config-demo.yaml, group=DEFAULT_GROUP, md5=00b77727c9b4a661b8ae12cd7de1395f2024-01-18 10:58:45.557  INFO 2289 --- [s.client.Worker] c.a.nacos.client.config.impl.CacheData   : [fixed-127.0.0.1_8848] [notify-listener] time cost=0ms in ClientWorker, dataId=config-demo.yaml, group=DEFAULT_GROUP, md5=00b77727c9b4a661b8ae12cd7de1395f, listener=com.alibaba.cloud.nacos.refresh.NacosContextRefresher$1@e15133a 2024-01-18 10:58:45.770  WARN 2289 --- [ternal.notifier] c.a.c.n.c.NacosPropertySourceBuilder     : Ignore the empty nacos configuration and get it based on dataId[config-demo] & group[DEFAULT_GROUP]2024-01-18 10:58:46.093  INFO 2289 --- [ternal.notifier] b.c.PropertySourceBootstrapConfiguration : Located property source: [BootstrapPropertySource {name='bootstrapProperties-config-demo.yaml,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-config-demo,DEFAULT_GROUP'}]2024-01-18 10:58:46.255  INFO 2289 --- [ternal.notifier] o.s.boot.SpringApplication               : No active profile set, falling back to 1 default profile: "default"2024-01-18 10:58:46.264  INFO 2289 --- [ternal.notifier] o.s.boot.SpringApplication               : Started application in 0.685 seconds (JVM running for 119.187)2024-01-18 10:58:46.337  INFO 2289 --- [ternal.notifier] o.s.c.e.event.RefreshEventListener       : Refresh keys changed: [useLocalCache]2024-01-18 10:58:46.337  INFO 2289 --- [ternal.notifier] c.a.nacos.client.config.impl.CacheData   : [fixed-127.0.0.1_8848] [notify-ok] dataId=config-demo.yaml, group=DEFAULT_GROUP, md5=00b77727c9b4a661b8ae12cd7de1395f, listener=com.alibaba.cloud.nacos.refresh.NacosContextRefresher$1@e15133a ,cost=780 millis.

添加@RefreshScope注解后,更新Nacos配置中心的配置可以实现应用的自动更新。

注意:Nacos 2.x版本之后使用gRpc通信框架同步配置变更,请确保9848端口开放否则无法获取最新的配置。示例使用的相关版本: 
- Nacos Server: 2.2.3 
- Spring Boot: 2.7.16 
- Spring Cloud: 2021.0.8

源码剖析

2.1 Nacos是如何同步配置数据并引发配置在应用内更新的?

1. 程序启动时,spring-cloud-alibaba-nacos-config 集成包会自动装配一个NacosPropertySourceLocator

@Configuration(proxyBeanMethods = false)@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)public class NacosConfigBootstrapConfiguration {
...
@Bean public NacosPropertySourceLocator nacosPropertySourceLocator( NacosConfigManager nacosConfigManager) { return new NacosPropertySourceLocator(nacosConfigManager); }}

NacosPropertySourceLocator 继承了Spring Cloud ContextPropertySourceLocator接口并实现了locate 方法。PropertySourceLocatorSpring Cloud Context用来支持用户扩展定位外部配置源,比如Nacos或其他分布式配置中心(Spring Cloud Config Server或Git仓库),详细内容可阅读Reference 1. Spring Cloud Context

locate方法中,Nacos定位到应用对应dataId的配置内容并返回了CompositePropertySource。最终创建一个ApplicationContext作为子上下文添加到应用程序中,子上下文中的属性配置会覆盖父上下文中配置,并通过BeanFactory初始化Bean。

public class NacosPropertySourceLocator implements PropertySourceLocator {  @Override  public PropertySource<?> locate(Environment env) {    ...    CompositePropertySource composite = new CompositePropertySource(NACOS_PROPERTY_SOURCE_NAME);
loadSharedConfiguration(composite); loadExtConfiguration(composite); loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env); return composite; }}

2. 程序启动的同时,Nacos为每个dataId注册一个单独的listener缓存到CacheData中,并在接收到配置更新内容后,回调Listener中的方法并推送一个RefreshEvent事件。

private void registerNacosListener(final String groupKey, final String dataKey) {    String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);    Listener listener = listenerMap.computeIfAbsent(key,        lst -> new AbstractSharedListener() {          @Override          public void innerReceive(String datad, String group,String configInfo) {            refreshCountIncrement();            nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);            applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config"));            ...          }        });    try {      configService.addListener(dataKey, groupKey, listener);    }    catch (NacosException e) {      ...    }}

3. RefreshEventListener 监听RefreshEvent事件并更新应用程序上下文的属性和配置。

public class RefreshEventListener implements SmartApplicationListener {  ...
public void handle(RefreshEvent event) { if (this.ready.get()) { // don't handle events before app is ready log.debug("Event received " + event.getEventDesc()); Set<String> keys = this.refresh.refresh(); log.info("Refresh keys changed: " + keys); } }}

通过分析日志和阅读源代码,我们基本理解了Nacos的初始化过程以及配置同步的基本逻辑。然而,是否在类上添加@RefreshScope注解并不影响Nacos的同步最新配置以及发布RefreshEvent事件。那么,为什么只有在添加@RefreshScope后,配置才会自动刷新呢?

2.2 @RefreshScope是如何更新配置的?

1. 查看@RefreshScope源代码定义,@RefreshScope继承自@Scope 注解。ScopedProxyMode.TARGET_CLASS 的原理是使用CGLIB创建一个基于类的代理,这个代理会拦截所有对Bean的调用,并根据Bean的作用域(例如,原型作用域或会话作用域)来决定是否需要生成新的实例。应用启动时,Spring会为标记了@RefreshScope注解的Bean生成两个实例,一个代理类和一个实际使用的Bean。代理类的作用是拦截所有对Bean的请求,在请求到达之前,检查配置是否已经刷新。如果配置已经刷新,代理会先销毁旧的bean实例,然后创建一个新的bean实例。

@Target({ ElementType.TYPE, ElementType.METHOD })@Retention(RetentionPolicy.RUNTIME)@Scope("refresh")@Documentedpublic @interface RefreshScope {
/** * @see Scope#proxyMode() * @return proxy mode */ ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

Spring Bean的作用域决定了Spring IoC容器如何创建Bean的实例。Spring定义了以下几种作用域:

  • 单例(Singleton):在整个Spring IoC容器中,只创建Bean的一个实例。这是默认的作用域。

  • 原型(Prototype):每次请求都会创建一个新的Bean实例。

  • 请求(Request):在一个HTTP请求中,一个Bean对应一个实例。

  • 会话(Session):在一个HTTP会话中,一个Bean对应一个实例。这个作用域只在基于web的Spring ApplicationContext情境有效。

Request,Session只针对基于web的Spring ApplicationContext上下文有效。RefreshScope 是Spring Cloud在此基础上扩展的一种作用域。BeanFactory 对于不同作用域的Bean选择不同的Scope实现来管理Bean的生命周期。

public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException { ... if (mbd.isSingleton()) { ... }
else if (mbd.isPrototype()) { // It's a prototype -> create a new instance. ... }
else { String scopeName = mbd.getScope(); if (!StringUtils.hasLength(scopeName)) { throw new IllegalStateException("No scope name defined for bean '" + beanName + "'"); } Scope scope = this.scopes.gt(scopeName); ... } }

2. RefreshScope 继承了GenericScope的实现,应用启动后,会将生成好的Bean实例放到cache中,确保配置没有刷新的情况下,每次获取到的是同一个实例,减少应用的开销。如果缓存中不存在,则调用BeanFactory重新创建Bean。

public class GenericScope    implements Scope, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, DisposableBean {  ...  @Override  public Object get(String name, ObjectFactory<?> objectFactory) {    BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));   // 存入缓存    this.locks.putIfAbsent(name, new ReentrantReadWriteLock());    try {      return value.getBean();    }    catch (RuntimeException e) {      this.errors.put(name, e);      throw e;    }  }
private static class BeanLifecycleWrapper { public Object getBean() { if (this.bean == null) { synchronized (this.name) { if (this.bean == null) { this.bean = this.objectFactory.getObject(); // 创建Bean } } } return this.bean; } }}

3. Nacos配置更新后推送RefreshEvent事件,RefreshEventListener 监听RefreshEvent事件调用ContextRefresher#refresh方法。该方法会重新从远端配置源获取最新的配置信息,并调用RefreshScope#refershAll方法清空RefreshScope中的缓存,并在下次获取Bean时重新创建。

public abstract class ContextRefresher {  ...  public synchronized Set<String> refresh() {    Set<String> keys = refreshEnvironment(); // 将新配置加载到环境中    this.scope.refreshAll(); // 刷新配置    return keys;  }}
@ManagedResourcepublic class RefreshScope extends GenericScope    implements ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, Ordered {  ...
@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()); }}
public class GenericScope    implements Scope, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, DisposableBean {  ...  @Override  public void destroy() {    List<Throwable> errors = new ArrayList<Throwable>();    Collection<BeanLifecycleWrapper> wrappers = this.cache.clear(); // 清空RefreshScope的缓存,下次重新创建    ...}


总结

Scope 是Spring IoC用于管理Bean生命周期和作用域的重要概念,Spring IoC也是Spring框架的核心。加深对Spring IoC的理解对于学习Spring框架很有必要,详细信息可阅读Reference 2. Core Technologies。

Reference

1. Spring Cloud Context: Application Context Services :https://cloud.spring.io/spring-cloud-commons/multi/multi__spring_cloud_context_application_context_services.html#_application_context_hierarchies

2. Core Technologies: https://docs.spring.io/spring-framework/docs/5.3.31/reference/html/core.html#beans


关于领创集团

(Advance Intelligence Group)
领创集团成立于 2016年,致力于通过科技创新的本地化应用,改造和重塑金融和零售行业,以多元化的业务布局打造一个服务于消费者、企业和商户的生态圈。集团旗下包含企业业务和消费者业务两大板块,企业业务包含 ADVANCE.AI 和 Ginee,分别为银行、金融、金融科技、零售和电商行业客户提供基于 AI 技术的数字身份验证、风险管理产品和全渠道电商服务解决方案;消费者业务 Atome Financial 包括亚洲领先的数字金融服务平台 Atome 等。2021年 9月,领创集团宣布完成超4亿美元 D 轮融资,融资完成后领创集团估值已超 20亿美元,成为新加坡最大的独立科技创业公司之一。




领创集团Advance Group
领创集团是亚太地区AI技术驱动的科技集团。
 最新文章