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自动刷新原理的主要内容,如果未能解决你的问题,请参考以下文章

Nacos学习笔记 Nacos整合SpringBoot流程

Springboot actuator refresh 和 bus-refresh 总是返回 401 Unauthorized

SpringBoot使用Nacos服务发现

springboot nacos配置详解

springboot nacos配置详解

SpringBoot与Nacos整合