SpringBoot+Nacos:@RefreshScope自动刷新原理
Posted OkidoGreen
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot+Nacos:@RefreshScope自动刷新原理相关的知识,希望对你有一定的参考价值。
@RefreshScope的作用
经过@RefreshScope注解修饰的bean,将被RefreshScope进行代理,用来实现配置、实例热加载,即当配置变更时可以在不重启应用的前提下刷新bean中相关的属性值。
@RefreshScope注解
@RefreshScope的实现如下,非常简单,最主要是@Scope("refresh")和ScopedProxyMode.TARGET_CLASS,表示@RefreshScope 是scopeName="refresh"的 @Scope,且代理模式为TARGET_CLASS。所以要搞懂什么是@RefreshScope,必须先了解@Scope。
@Target(ElementType.TYPE, ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
@Scope注解
从上面可以看出,@RefreshScope是是一个符合注解,基于@Scope实现的,@Scope是spring ioc容器的作用域。
在 Spring IoC 容器中具有以下几种作用域:
- singleton:单例模式(默认),全局有且仅有一个实例
- prototype:原型模式,每次获取Bean的时候会有一个新的实例
- request:request表示针对每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP request内有效
- session:session作用域表示该针对每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP session内有效
- global session:global session作用域类似于标准的HTTP Session作用域,不过它仅仅在基于portlet的web应用中才有意义
以下是@Scope注解的源码:
@Target(ElementType.TYPE, ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope
// scopeName等价于value
@AliasFor("scopeName")
String value() default "";
// singleton(单例默认)、prototype、request、session、Global session、refresh
@AliasFor("value")
String scopeName() default "";
// DEFAULT :默认,不使用代理
// NO:不使用代理,等价于DEFAULT
// INTERFACES:使用基于JDK dynamic proxy实现代理
// TARGET_CLASS:使用基于cglib实现代理
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
在spring bean的生命周期管理中,所有经过@Scope注解装饰过的Bean实例都交由Scope自己创建,例如SessionScope是从Session中获取实例的,ThreadScope是从ThreadLocal中获取的,而RefreshScope是在内建缓存中获取的。
AbstractBeanFactory#doGetBean
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException
...
if (mbd.isSingleton())
...
//如果为单例(默认),则创建单例Bean
else if (mbd.isPrototype())
...
//如果为prototype,则创建原型Bean
else
//如果是Scope Bean,则交由对应Scope自己创建,如refresh则由RefreshScope创建
String scopeName = mbd.getScope();
Scope scope = (Scope)this.scopes.get(scopeName);
...
try
Object scopedInstance = scope.get(beanName, () ->
this.beforePrototypeCreation(beanName);
try
return this.createBean(beanName, mbd, args);
finally
this.afterPrototypeCreation(beanName);
);
beanInstance = this.getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
...
...
注意:
- 单例和原型scope的Bean是硬编码单独处理的
- 除了单例和原型Bean,其他Scope是由Scope对象处理的
- 具体创建Bean的过程都是由IOC做的,只不过Bean的获取是通过Scope对象
根据不同的scope类型,spring 提供了下列多种scope实现:
这次我们重点投了RefreshScope的实现。
RefreshScope的实现
RefreshScope extends GenericScope,其父类GenericScope的get方法实现获取Bean,注意创建Bean还是由IOC#createBean实现。
GenericScope#get
@Override
public Object get(String name, ObjectFactory<?> objectFactory)
//通过cache把bean缓存下来,如果不存在则创建
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;
BeanLifecycleWrapperCache#put
//这里将Bean包装并缓存下来,这里的cache为scopecache
public BeanLifecycleWrapper put(String name, BeanLifecycleWrapper value)
return (BeanLifecycleWrapper) this.cache.put(name, value);
StandardScopeCache
public class StandardScopeCache implements ScopeCache
private final ConcurrentMap<String, Object> cache = new ConcurrentHashMap<String, Object>();
public Object remove(String name)
return this.cache.remove(name);
public Collection<Object> clear()
Collection<Object> values = new ArrayList<Object>(this.cache.values());
this.cache.clear();
return values;
public Object get(String name)
return this.cache.get(name);
public Object put(String name, Object value)
// result若不等于null,表示缓存存在了,不会进行put操作
Object result = this.cache.putIfAbsent(name, value);
if (result != null)
// 直接返回旧对象
return result;
// put成功,返回新对象
return value;
获取BeanLifecycleWrapper后,通过getBean方法获取实际Bean,如果Bean不存在,则调用scope#get传输进来的ObjectFactory创建Bean。
BeanLifecycleWrapperCache#getBean
public Object getBean()
if (this.bean == null)
synchronized (this.name)
if (this.bean == null)
this.bean = this.objectFactory.getObject();
return this.bean;
RefreshScope刷新
当配置中心刷新配置之后,有两种方式可以动态刷新Bean的配置变量值:
向上下文发布一个RefreshEvent事件
Http访问/refresh这个EndPoint
无论是哪个方式,最终都会调用
RefreshScope#refreshAll
public void refreshAll()
super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
该方法会调用
GenericScope#destory
方法清空包装类缓存,然后发布一个RefreshScopeRefreshedEvent
@Override
public void destroy()
List<Throwable> errors = new ArrayList<Throwable>();
// 清空包装对象缓存
Collection<BeanLifecycleWrapper> 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();
查看
RefreshScope#refreshAll 的引用,这个方法被ContextRefresher#refresh方法调用:
private RefreshScope scope;
public synchronized Set<String> refresh()
// 刷新上下文环境变量
Set<String> keys = refreshEnvironment();
//调用scope对象的refreshAll方法
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();
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
查看ContextRefresher#refresh方法的引用,分别是:
RefreshEndpoint:Http访问/refresh这个EndPoint
RefreshEventListener:监听RefreshEvent事件
所以我们可以通过发起http://localhost:8080/actuator/refresh主动刷新,或者发送一个RefreshEvent事件。
这里以Nacos为例:
Nacos里定义了NacosContextRefresher,该类中注册了Nacos监听器,当监听到配置变更,则发送RefreshEvent事件。
private void registerNacosListener(final String groupKey, final String dataKey)
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = (Listener)this.listenerMap.computeIfAbsent(key, (lst) ->
return new AbstractSharedListener()
public void innerReceive(String dataId, String group, String configInfo)
NacosContextRefresher.refreshCountIncrement();
NacosContextRefresher.this.nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
//发送RefreshEvent事件
NacosContextRefresher.this.applicationContext.publishEvent(new RefreshEvent(this, (Object)null, "Refresh Nacos config"));
if (NacosContextRefresher.log.isDebugEnabled())
NacosContextRefresher.log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s", group, dataId, configInfo));
;
);
try
this.configService.addListener(dataKey, groupKey, listener);
catch (NacosException var6)
log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey, groupKey), var6);
经过上述的刷新动作后,cache缓存将被清空,那又是怎么获取最新值呢?
我们看回GenericScope#get就清楚了:
@Override
public Object get(String name, ObjectFactory<?> objectFactory)
//通过cache把bean缓存下来,如果不存在则创建
//由于cache已被清空,所以获取的对象肯定是新聪IOC容器创建出来的,由于前面已经刷新了环境变量,所以新对象将包含最新配置值
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;
参考资料
以上是关于SpringBoot+Nacos:@RefreshScope自动刷新原理的主要内容,如果未能解决你的问题,请参考以下文章
Springboot actuator refresh 和 bus-refresh 总是返回 401 Unauthorized