spring cloud 版本2020.0.0
前言
距离上一篇《Spring Cloud Config Client源码分析之加载外部化配置》已经有一段时间没有更新了,这一篇应该是2022年春节前最后一篇了。本篇我们一起走进spring cloud配置动态刷新的原理。在上一篇的结尾,提到在实际的生产环境spring cloud config会结合spring cloud bus一起使用。但是本篇呢,我们只探究spring cloud抽象出的ContextRefresher接口规范,以手动的方式,通过/actuator/refresh的接口api来刷新配置。
基本使用
在开始之前呢,先简单介绍一下基本用法,以方便还没有用过的同学学习和理解。
引入依赖
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-config
配置bootstrap.yml
management:
endpoint:
refresh:
enabled: true 默认true
endpoints:
web:
exposure:
include: * 默认情况下只对外暴露了health和info端点
spring:
application:
name: ms-client-a
cloud:
config:
discovery:
enabled: true
service-id: ms-config-server
uri: http://localhost:8080/
profile: dev
label: master
编码
在需要动态刷新的类上添加@RefreshScope注解并通过@value注解引用配置文件中的配置
@RestController
@RequestMapping("/adviser/loss")
@RefreshScope
public class AdviserLossController {
@Value("${custom.name}")
private String name;
@Value("${custom.age}")
private Integer age;
@GetMapping("/test")
public String test(){
return name + ":" + age;
}
}
配置git远程文件
custom:
name: 张三
age: 20
启动项目
首次启动时,config client会请求config server加载远程配置
测试
首次请求/adviser/loss/test接口,会发现返回=》张三:18,然后修改git上的配置,将年龄修改为20,再次请求/adviser/loss/test接口,发现配置并未更新。那么此时就需要我们手动刷新配置了:发送http post /actuator/refresh接口,此时再次请求/adviser/loss/test接口,会发现返回=》张三:20,说明配置被成功刷新。
源码分析
@RefreshScope
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
/**
* @see ScopeproxyMode()
* @return proxy mode
*/
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
该注解上添加了@Scope("refresh")
注解指明了作用域名为refresh
。
RefreshScope
public class RefreshScope extends GenericScope implements ApplicationContextAware,
ApplicationListener, Ordered {
// 省略代码。。。。。。
}
该类继承自GenericScope
,其核心方法都在父类GenericScope
中,而GenericScope
继承BeanFactoryPostProcessor
GenericScope
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
this.beanFactory = beanFactory;
// 放入到缓存中
beanFactory.registerScope(this.name, this);
setSerializationId(beanFactory);
}
注册scope
,名字为refresh
,值为RefreshScope
实例。
添加了@RefreshScope
注解的Bean
对象会被@ComponentScan
注解扫描到,核心代码在ClassPathBeanDefinitionScanner
类中的doScan
方法中:
ClassPathBeanDefinitionScanner
protected Set doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
Set candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
// 关键代码
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
核心代码在this.scopeMetadataResolver.resolveScopeMetadata(candidate)
这一行,这里就不展开讲了,希望同学自己去扩展阅读相关源码。
注册的Scope
将会在AbstractBeanFactorydoGetBean
方法中调用,该方法中会先拿到当前BeanDefinition
中定义的Scope
,通过scopeName
从Map
集合中拿到Scope
类,最后调用Scope
的get
方法获取实例对象。
AbstractBeanFactory
protected T doGetBean(
String name, @Nullable Class requiredType, @Nullable Object[] args, boolean typeCheckOnly)
throws BeansException {
// 省略部分代码
String scopeName = mbd.getScope();
if (!StringUtils.hasLength(scopeName)) {
throw new IllegalStateException("No scope name defined for bean " + beanName + "");
}
// 1.从缓存中取,在beanFactory的registerScope方法调用时放入缓存
Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name " + scopeName + "");
}
try {
// 2.调用scope的get方法创建bean,通过匿名内部类创建ObjectFactory对象并实现getObject方法。在GenericScope的get方法会回调此处的实现,创建一个bean。
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new ScopeNotActiveException(beanName, scopeName, ex);
}
// 省略部分代码
}
在2处,最终会调用RefreshScopeget
方法:
GenericScope
@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;
}
}
先是将ObjectFactory
实例对象包装成BeanLifecycleWrapper
放入缓存中(缓存中没有则放入,有则返回原对象),然后调用BeanLifecycleWrapper
的getBean
方法:
GenericScope@BeanLifecycleWrapper
public Object getBean() {
if (this.bean == null) {
synchronized (this.name) {
if (this.bean == null) {
this.bean = this.objectFactory.getObject();
}
}
}
return this.bean;
}
如果bean对象等于null,则通过this.objectFactory.getObject()
,又回到AbstractBeanFactory
的doGetBean
方法中,创建一个bean并返回。
至此已经分析完了RefreshScope
类和@RefreshScope
注解的来龙去脉,下面分析下Refresh端点触发时机。
Refresh端点触发时机
当调用/actuator/refresh
端点时,执行如下refresh:
@Endpoint(id = "refresh")
public class RefreshEndpoint {
private ContextRefresher contextRefresher;
public RefreshEndpoint(ContextRefresher contextRefresher) {
this.contextRefresher = contextRefresher;
}
@WriteOperation
public Collection refresh() {
Set keys = this.contextRefresher.refresh();
return keys;
}
}
RefreshEndpoint
是在RefreshEndpointAutoConfiguration
类中创建并配置的。内部会通过调用ContextRefresherrefresh
方法:
public synchronized Set refresh() {
//1.刷新环境
Set keys = refreshEnvironment();
//2.清空缓存
this.scope.refreshAll();
return keys;
}
public synchronized Set refreshEnvironment() {
//1:提取配置信息修改之前的值,排除systemEnvironment、systemProperties、jndiProperties、servletConfigInitParams、servletContextInitParams、configurationProperties相关配置
Map before = extract(this.context.getEnvironment().getPropertySources());
//2:重新加载读取配置信息,调用内部的抽象方法,由具体的子类实现
updateEnvironment();
//3:获取所有改变的配置
Set keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
//4:发布EnvironmentChangeEvent事件
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}
在2处,重新加载配置,在当前的版本,有2中实现方式:一种是兼容旧版本的实现,一种是新版本的实现,这两种实现分别在RefreshAutoConfiguration
中配置:
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);
}
旧版本的实现其原理是:内部启动一个非web环境的SpringBoot应用,重新读取配置信息,用新配置替换旧配置,这个很容易理解。 新版本的实现方式,我也跟踪了源码,也在网上找了很多资料,但是目前网上的都是基于旧版本的分析。关于新版本的实现属实不太理解(功力尚浅),我个人也在学习和总结,后续如果有新的进展,我也会及时更新的,如果您有相关资料或见解,也烦请指教。
在4处发布一个EnvironmentChangeEvent
事件,查看该事件的监听器:
ConfigurationPropertiesRebinder
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.applicationContext.equals(event.getSource())
// Backwards compatible
|| event.getKeys().equals(event.getSource())) {
rebind();
}
}
@ManagedOperation
public void rebind() {
this.errors.clear();
//遍历所有的配置类(带有@ConfigurationProperties注解的类)
for (String name : this.beans.getBeanNames()) {
//对每一个bean进行重新绑定
rebind(name);
}
}
@ManagedOperation
public boolean rebind(String name) {
if (!this.beans.getBeanNames().contains(name)) {
return false;
}
if (this.applicationContext != null) {
try {
Object bean = this.applicationContext.getBean(name);
if (AopUtils.isAopProxy(bean)) {
bean = ProxyUtils.getTargetObject(bean);
}
if (bean != null) {
// TODO: determine a more general approach to fix this.
// see https://github.com/spring-cloud/spring-cloud-commons/issues/571
if (getNeverRefreshable().contains(bean.getClass().getName())) {
return false; // ignore
}
//销毁当前的bean this.applicationContext.getAutowireCapableBeanFactory().destroyBean(bean);
//初始化Bean this.applicationContext.getAutowireCapableBeanFactory().initializeBean(bean, name);
return true;
}
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
catch (Exception e) {
this.errors.put(name, e);
throw new IllegalStateException("Cannot rebind to " + name, e);
}
}
return false;
}
补充
在ConfigurationPropertiesRebinder
类中的beans
是通过构造函数传过来的,接下来先查看这个对象是如何被构造的:
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(ConfigurationPropertiesBindingPostProcessor.class)
public class ConfigurationPropertiesRebinderAutoConfiguration
implements ApplicationContextAware, SmartInitializingSingleton {
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.context = applicationContext;
}
@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
public static ConfigurationPropertiesBeans configurationPropertiesBeans() {
return new ConfigurationPropertiesBeans();
}
@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
public ConfigurationPropertiesRebinder configurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
ConfigurationPropertiesRebinder rebinder = new ConfigurationPropertiesRebinder(beans);
return rebinder;
}
@Override
public void afterSingletonsInstantiated() {
// After all beans are initialized explicitly rebind beans from the parent
// so that changes during the initialization of the current context are
// reflected. In particular this can be important when low level services like
// decryption are bootstrapped in the parent, but need to change their
// configuration before the child context is processed.
if (this.context.getParent() != null) {
// TODO: make this optional? (E.g. when creating child contexts that prefer to
// be isolated.)
ConfigurationPropertiesRebinder rebinder = this.context.getBean(ConfigurationPropertiesRebinder.class);
for (String name : this.context.getParent().getBeanDefinitionNames()) {
rebinder.rebind(name);
}
}
}
}
在这个自动配置类中创建了ConfigurationPropertiesRebinder
并且将ConfigurationPropertiesBeans
注入。ConfigurationPropertiesBeans
是个BeanPostProcessor
处理器:
@Component
public class ConfigurationPropertiesBeans implements BeanPostProcessor, ApplicationContextAware {
//省略部分代码...
private Map beans = new HashMap<>();
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (isRefreshScoped(beanName)) {
return bean;
}
ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean.get(this.applicationContext, bean,
beanName);
if (propertiesBean != null) {
//添加有@ConfigurationProperties注解的bean都将保存在该集合中
this.beans.put(beanName, propertiesBean);
}
return bean;
}
private boolean isRefreshScoped(String beanName) {
if (this.refreshScope == null && !this.refreshScopeInitialized) {
this.refreshScopeInitialized = true;
for (String scope : this.beanFactory.getRegisteredScopeNames()) {
if (this.beanFactory.getRegisteredScope(
scope) instanceof org.springframework.cloud.context.scope.refresh.RefreshScope) {
this.refreshScope = scope;
break;
}
}
}
if (beanName == null || this.refreshScope == null) {
return false;
}
return this.beanFactory.containsBeanDefinition(beanName)
&& this.refreshScope.equals(this.beanFactory.getBeanDefinition(beanName).getScope());
}
//省略部分代码...
}
该BeanPostProcessor
的postProcessBeforeInitialization
方法执行,在初始化bean
的时候执行,在这里就会判断当前的Bean
是否是RefreshScope
Bean。
在isRefreshScoped
方法中遍历注册的所有Scope
并且判断是否是有RefreshScope
,先从注册的所有Scope
中查找RefreshScope
,如果不是返回false
,如果是则返回true
。如果isRefreshScoped
方法返回的false
就判断当前Bean
是否有@ConfigurationProperties
注解如果有会被包装成ConfigurationPropertiesBean
存入当前的beans
集合中(当有refresh
发生时会重新绑定这些bean
)。接下来继续进入到上面的ConfigurationPropertiesRebinderrebind
方法中。
RefreshScope刷新处理
回到ContextRefresher
的refresh
方法:
public synchronized Set refresh() {
Set keys = refreshEnvironment();
//清空缓存
this.scope.refreshAll();
return keys;
}
RefreshScoperefreshAll
@ManagedOperation(description = "Dispose of the current instance of all beans "
+ "in this scope and force a refresh on next method execution.")
public void refreshAll() {
//调用父类GenericScope的destroy方法
super.destroy();
//发布RefreshScopeRefreshedEvent事件,我们可以写个监听程序监听该事件
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
GenericScope
@Override
public void destroy() {
List errors = new ArrayList();
//清空缓存
Collection wrappers = this.cache.clear();
for (BeanLifecycleWrapper wrapper : wrappers) {
try {
Lock lock = this.locks.get(wrapper.getName()).writeLock();
lock.lock();
try {
//清空上次创建的对象信息
wrapper.destroy();
}
finally {
lock.unlock();
}
}
catch (RuntimeException e) {
errors.add(e);
}
}
if (!errors.isEmpty()) {
throw wrapIfNecessary(errors.get(0));
}
this.errors.clear();
}
public void destroy() {
if (this.callback == null) {
return;
}
synchronized (this.name) {
Runnable callback = this.callback;
if (callback != null) {
callback.run();
}
this.callback = null;
this.bean = null;
}
}
当前清空了缓存对象后,下次再进入注入的时候会再次调用ObjectFacotrygetObject
方法创建新的对象。
总结
当触发了refresh
后,所有的带有@ConfigurationProperties
注解的Bean都会自动的刷新并不需要@RefreshScope
注解。而有@RefreshScope
注解的一般在应用在非配置类上,有成员属性使用@Value
注解的,如下:
@RestController
@RequestMapping("/refreshBeanProp")
@RefreshScope
public class RefreshScopeBeanPropController {
@Value("${custom}")
private String custom ;
@GetMapping("/get")
public String get() {
return custom ;
}
}
在此种情况下,调用/actuator/refresh
可使custom动态刷新,在ContextRefresherrefresh
中将缓存的Bean清空了后重新生成Bean。
欢迎关注我的公众号:程序员L札记
声明:本文部分素材转载自互联网,如有侵权立即删除 。
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
7. 如遇到加密压缩包,请使用WINRAR解压,如遇到无法解压的请联系管理员!
8. 精力有限,不少源码未能详细测试(解密),不能分辨部分源码是病毒还是误报,所以没有进行任何修改,大家使用前请进行甄别
丞旭猿论坛
暂无评论内容