偶遇一坑,本意為實現apollo屬性組動態映射到app內存對象,網上提供方式有2種,見https://www.cnblogs.com/linyb-geek/p/13059720.html
方案如下2種:
- 基於RefreshScope實現刷新
- 基於EnvironmentChangeEvent實現刷新
一開始,用RefreshScope,功能實現了,但是發現eureka的探針線程數量明顯增多(提升系統網絡開銷),原因是RefreshScope再刷新過程中對容器中配置bean實例,銷毀並重建導致eureka短暫抖動(eureka也是從apollo拉配置)。之后采用applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));方才避坑。
搜了一下RefreshScope的原理和機制:見https://www.cnblogs.com/javastack/p/12049139.html,發現RefreshScope實現相對比較重,使用時需要明確refresh的范圍(不建議使用refreshScope.refreshAll();)
以下轉:
@RefreshScope那些事
要說清楚RefreshScope,先要了解Scope
-
Scope(
org.springframework.beans.factory.config.Scope
)是Spring 2.0開始就有的核心的概念 -
RefreshScope(
org.springframework.cloud.context.scope.refresh
)是spring cloud提供的一種特殊的scope實現,用來實現配置、實例熱加載。 -
Scope -> GenericScope -> RefreshScope
scope_hierarchy.jpeg -
Scope與ApplicationContext生命周期
- AbstractBeanFactory#doGetBean創建Bean實例
protected <T> T doGetBean(...){ final RootBeanDefinition mbd = ... if (mbd.isSingleton()) { ... } else if (mbd.isPrototype()) ... } else { String scopeName = mbd.getScope(); final Scope scope = this.scopes.get(scopeName); Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() {...}); ... } ... }
- Singleton和Prototype是硬編碼的,並不是Scope子類。 Scope實際上是自定義擴展的接口
- Scope Bean實例交由Scope自己創建,例如SessionScope是從Session中獲取實例的,ThreadScope是從ThreadLocal中獲取的,而RefreshScope是在內建緩存中獲取的。
-
@Scope 對象的實例化
- @RefreshScope 是scopeName="refresh"的 @Scope
... @Scope("refresh") public @interface RefreshScope { ... }
- @Scope 的注冊 AnnotatedBeanDefinitionReader#registerBean
public void registerBean(...){ ... ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); abd.setScope(scopeMetadata.getScopeName()); ... definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); }
- 讀取@Scope元數據, AnnotationScopeMetadataResolver#resolveScopeMetadata
public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) { AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor( annDef.getMetadata(), Scope.class); if (attributes != null) { metadata.setScopeName(attributes.getString("value")); ScopedProxyMode proxyMode = attributes.getEnum("proxyMode"); if (proxyMode == null || proxyMode == ScopedProxyMode.DEFAULT) { proxyMode = this.defaultProxyMode; } metadata.setScopedProxyMode(proxyMode); } }
- Scope實例對象通過ScopedProxyFactoryBean創建,其中通過AOP使其實現ScopedObject接口,這里不再展開
現在來說說RefreshScope是如何實現配置和實例刷新的
-
RefreshScope注冊
- RefreshAutoConfiguration#RefreshScopeConfiguration
@Component @ConditionalOnMissingBean(RefreshScope.class) protected static class RefreshScopeConfiguration implements BeanDefinitionRegistryPostProcessor{ ... registry.registerBeanDefinition("refreshScope", BeanDefinitionBuilder.genericBeanDefinition(RefreshScope.class) .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) .getBeanDefinition()); ... }
- RefreshScope extends GenericScope, 大部分邏輯在 GenericScope 中
- GenericScope#postProcessBeanFactory 中向AbstractBeanFactory注冊自己
public class GenericScope implements Scope, BeanFactoryPostProcessor...{ @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { beanFactory.registerScope(this.name/*refresh*/, this/*RefreshScope*/); ... } }
-
RefreshScope 刷新過程
- 入口在ContextRefresher#refresh
refresh() { Map<String, Object> before = ①extract( this.context.getEnvironment().getPropertySources()); ②addConfigFilesToEnvironment(); Set<String> keys = ④changes(before, ③extract(this.context.getEnvironment().getPropertySources())).keySet(); this.context.⑤publishEvent(new EnvironmentChangeEvent(keys)); this.scope.⑥refreshAll(); }
- ①提取標准參數(SYSTEM,JNDI,SERVLET)之外所有參數變量
- ②把原來的Environment里的參數放到一個新建的Spring Context容器下重新加載,完事之后關閉新容器
- ③提起更新過的參數(排除標准參數)
- ④比較出變更項
- ⑤發布環境變更事件,接收:EnvironmentChangeListener/LoggingRebinder
- ⑥RefreshScope用新的環境參數重新生成Bean
- 重新生成的過程很簡單,清除refreshscope緩存幷銷毀Bean,下次就會重新從BeanFactory獲取一個新的實例(該實例使用新的配置)
- RefreshScope#refreshAll
public void refreshAll() { <b>super.destroy();</b> this.context.publishEvent(new RefreshScopeRefreshedEvent()); }
- GenericScope#destroy
public void destroy() { ... Collection<BeanLifecycleWrapper> wrappers = <b>this.cache.clear()</b>; for (BeanLifecycleWrapper wrapper : wrappers) { <b>wrapper.destroy();</b> } }
-
Spring Cloud Bus 如何觸發 Refresh
- BusAutoConfiguration#BusRefreshConfiguration 發布一個RefreshBusEndpoint
@Configuration @ConditionalOnClass({ Endpoint.class, RefreshScope.class }) protected static class BusRefreshConfiguration { @Configuration @ConditionalOnBean(ContextRefresher.class) @ConditionalOnProperty(value = "endpoints.spring.cloud.bus.refresh.enabled", matchIfMissing = true) protected static class BusRefreshEndpointConfiguration { @Bean public RefreshBusEndpoint refreshBusEndpoint(ApplicationContext context, BusProperties bus) { return new RefreshBusEndpoint(context, bus.getId()); } } }
- RefreshBusEndpoint 會從http端口觸發廣播RefreshRemoteApplicationEvent事件
@Endpoint(id = "bus-refresh") public class RefreshBusEndpoint extends AbstractBusEndpoint { public void busRefresh() { publish(new RefreshRemoteApplicationEvent(this, getInstanceId(), null)); } }
- BusAutoConfiguration#refreshListener 負責接收事件(所有配置bus的節點)
@Bean @ConditionalOnProperty(value = "spring.cloud.bus.refresh.enabled", matchIfMissing = true) @ConditionalOnBean(ContextRefresher.class) public RefreshListener refreshListener(ContextRefresher contextRefresher) { return new RefreshListener(contextRefresher); }
- RefreshListener#onApplicationEvent 觸發 ContextRefresher
public void onApplicationEvent(RefreshRemoteApplicationEvent event) { Set<String> keys = contextRefresher.refresh(); }
-
大部分需要更新的服務需要打上@RefreshScope, EurekaClient是如何配置更新的
- EurekaClientAutoConfiguration#RefreshableEurekaClientConfiguration
@Configuration @ConditionalOnRefreshScope protected static class RefreshableEurekaClientConfiguration{ @Bean @RefreshScope public EurekaClient eurekaClient(...) { return new CloudEurekaClient(manager, config, this.optionalArgs, this.context); } @Bean @RefreshScope public ApplicationInfoManager eurekaApplicationInfoManager(...) { ... return new ApplicationInfoManager(config, instanceInfo); } }