Nacos+@RefreshScope 为什么配置能动态刷新?

简介: Nacos+@RefreshScope 为什么配置能动态刷新?

来源:JAVA旭阳


概述

RefeshScope这个注解想必大家都用过,在微服务配置中心的场景下经常出现,他可以用来刷新Bean中的属性配置,那大家对他的实现原理了解吗?它为什么可以做到动态刷新呢?

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

注解的作用

@RefreshScope注解是Spring Cloud中的一个注解,用来实现Bean中属性的动态刷新。

/**
 * Convenience annotation to put a <code>@Bean</code> definition in
 * {@link org.springframework.cloud.context.scope.refresh.RefreshScope refresh scope}.
 * Beans annotated this way can be refreshed at runtime and any components that are using
 * them will get a new instance on the next method call, fully initialized and injected
 * with all dependencies.
 *
 * @author Dave Syer
 *
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
 /**
  * @see Scope#proxyMode()
  * @return proxy mode
  */
 ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
  • 上面是RefreshScope的源码,该注解被@Scope注解使用,@Scope用来比较Spring Bean的作用域,具体使用参考相关文章。
  • 注解的属性proxyMode默认使用TARGET_CLASS作为代理。

实例

  1. controller中添加@RefreshScope

  1. nacos配置中心中配置

  1. 验证, 修改配置中心后,可以不重启动,刷新配置

  1. 去掉@RefreshScope 就不会自动刷新。

代码地址:https://github.com/alvinlkk/awesome-springcloud-demo/tree/master/springcloud-nacos/springcloud-nacos-config

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

原理解析

为了实现动态刷新配置,主要就是想办法达成以下两个核心目标:

  1. 让Spring容器重新加载Environment环境配置变量
  2. Spring Bean重新创建生成

@RefreshScope主要就是基于@Scope注解的作用域代理的基础上进行扩展实现的,加了@RefreshScope注解的类,在被Bean工厂创建后会加入自己的refresh scope 这个Bean缓存中,后续会优先从Bean缓存中获取,当配置中心发生了变更,会把变更的配置更新到spring容器的Environment中,并且同事bean缓存就会被清空,从而就会从bean工厂中创建bean实例了,而这次创建bean实例的时候就会继续经历这个bean的生命周期,使得@Value属性值能够从Environment中获取到最新的属性值,这样整个过程就达到了动态刷新配置的效果。

获取RefreshScope注解的Bean

通过打上断点查看堆栈可知:

  1. 因为Class被加上了@RefreshScope注解,那么这个BeanDefinition信息中的scope为refresh,在getBean的的时候会单独处理逻辑。
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 {
    // 如果scope是单例的情况, 这里不进行分析
    if (mbd.isSingleton()) {
     .....
    }
                // 如果scope是prototype的情况, 这里不进行分析
    else if (mbd.isPrototype()) {
     ......
    }
                // 如果scope是其他的情况,本例中是reresh
    else {
     String scopeName = mbd.getScope();
     if (!StringUtils.hasLength(scopeName)) {
      throw new IllegalStateException("No scope name defined for bean '" + beanName + "'");
     }
                    // 获取refresh scope的实现类RefreshScope,这个类在哪里注入,我们后面讲
     Scope scope = this.scopes.get(scopeName);
     if (scope == null) {
      throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
     }
     try {
                        // 这边是获取bean,调用的是RefreshScope中的的方法
      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);
     }
    }
   }
   catch (BeansException ex) {
    beanCreation.tag("exception", ex.getClass().toString());
    beanCreation.tag("message", String.valueOf(ex.getMessage()));
    cleanupAfterBeanCreationFailure(beanName);
    throw ex;
   }
   finally {
    beanCreation.end();
   }
  }
  return adaptBeanInstance(name, beanInstance, requiredType);
 }
}

2.RefreshScope继承成了GenericScope类,最终调用的的是GenericScopeget方法

public class GenericScope
  implements Scope, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, DisposableBean {
                 @Override
  public Object get(String name, ObjectFactory<?> objectFactory) {
  // 将bean添加到缓存cache中
        BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
            // 调用下面的getBean方法
   return value.getBean();
  }
  catch (RuntimeException e) {
   this.errors.put(name, e);
   throw e;
  }
 }       
private static class BeanLifecycleWrapper {
  public Object getBean() {
            // 如果bean为空,则创建bean
   if (this.bean == null) {
    synchronized (this.name) {
     if (this.bean == null) {
      this.bean = this.objectFactory.getObject();
     }
    }
   }
            // 否则返回之前创建好的bean
   return this.bean;
  }
            }
        }

小结:

从这边的代码中可以印证了上面的说法,创建后的Bean会缓存到scope的cache中,优先从缓存中获取,如果缓存中是null, 则重新走一遍create bean的流程。

RefeshScope Bean的创建

上面的在getBean的时候依赖到RefreshScope这个Bean,那么这个Bean是在什么时候加入到Spring Bean中的呢?答案就是RefreshAutoConfiguration

配置中心刷新后刷新Bean缓存

  1. 配置中心发生变化后,会收到一个RefreshEvent事件,RefreshEventListner监听器会监听到这个事件。
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());
            // 会调用refresh方法,进行刷新
   Set<String> keys = this.refresh.refresh();
   log.info("Refresh keys changed: " + keys);
  }
 }
}
// 这个是ContextRefresher类中的刷新方法
public synchronized Set<String> refresh() {
        // 刷新spring的envirionment 变量配置
  Set<String> keys = refreshEnvironment();
        // 刷新其他scope
  this.scope.refreshAll();
  return keys;
 }
  1. refresh方法最终调用destroy方法,清空之前缓存的bean
public 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() {
  // 调用父类的destroy
        super.destroy();
  this.context.publishEvent(new 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 {
                    // 这里主要就是把之前的bean设置为null, 就会重新走createBean的流程了
     wrapper.destroy();
    }
    finally {
     lock.unlock();
    }
   }
   catch (RuntimeException e) {
    errors.add(e);
   }
  }
  if (!errors.isEmpty()) {
   throw wrapIfNecessary(errors.get(0));
  }
  this.errors.clear();
 }

总结

上面是这个RefreshScope实现动态刷新大致的原理,其中里面还有很多细节,可能需要留给大家自己debug去深入理解。


相关文章
|
13天前
|
负载均衡 应用服务中间件 Nacos
Nacos配置中心
Nacos配置中心
44 1
Nacos配置中心
|
9天前
|
监控 Java 测试技术
Nacos 配置中心变更利器:自定义标签灰度
本文是对 MSE Nacos 应用自定义标签灰度的功能介绍,欢迎大家升级版本进行试用。
|
12天前
|
网络安全 Nacos 开发者
Nacos作为流行的微服务注册与配置中心,“节点提示暂时不可用”是常见的问题之一
Nacos作为流行的微服务注册与配置中心,其稳定性和易用性备受青睐。然而,“节点提示暂时不可用”是常见的问题之一。本文将探讨该问题的原因及解决方案,帮助开发者快速定位并解决问题,确保服务的正常运行。通过检查服务实例状态、网络连接、Nacos配置、调整健康检查策略等步骤,可以有效解决这一问题。
25 4
|
12天前
|
Java 网络安全 Nacos
Nacos作为流行的微服务注册与配置中心,其稳定性和易用性备受青睐。
Nacos作为流行的微服务注册与配置中心,其稳定性和易用性备受青睐。然而,实际使用中常遇到“客户端不发送心跳检测”的问题。本文深入探讨该问题的原因及解决方案,帮助开发者快速定位并解决问题,确保服务正常运行。通过检查客户端配置、网络连接、日志、版本兼容性、心跳策略、注册状态、重启应用和环境变量等步骤,系统地排查和解决这一问题。
31 3
|
12天前
|
安全 Nacos 数据库
Nacos是一款流行的微服务注册与配置中心,但直接暴露在公网中可能导致非法访问和数据库篡改
Nacos是一款流行的微服务注册与配置中心,但直接暴露在公网中可能导致非法访问和数据库篡改。本文详细探讨了这一问题的原因及解决方案,包括限制公网访问、使用HTTPS、强化数据库安全、启用访问控制、监控和审计等步骤,帮助开发者确保服务的安全运行。
26 3
|
21天前
|
SQL 关系型数据库 数据库连接
"Nacos 2.1.0版本数据库配置写入难题破解攻略:一步步教你排查连接、权限和配置问题,重启服务轻松解决!"
【10月更文挑战第23天】在使用Nacos 2.1.0版本时,可能会遇到无法将配置信息写入数据库的问题。本文将引导你逐步解决这一问题,包括检查数据库连接、用户权限、Nacos配置文件,并提供示例代码和详细步骤。通过这些方法,你可以有效解决配置写入失败的问题。
45 0
|
2月前
|
负载均衡 Java Nacos
SpringCloud基础2——Nacos配置、Feign、Gateway
nacos配置管理、Feign远程调用、Gateway服务网关
SpringCloud基础2——Nacos配置、Feign、Gateway
|
4月前
|
Java Nacos 数据库
使用 nacos 搭建注册中心及配置中心
使用 nacos 搭建注册中心及配置中心
101 5
|
4月前
|
NoSQL Java Nacos
SpringCloud集成Seata并使用Nacos做注册中心与配置中心
SpringCloud集成Seata并使用Nacos做注册中心与配置中心
140 3
|
3月前
|
安全 Nacos 数据安全/隐私保护
升级指南:从Nacos 1.3.0 到 2.3.0,并兼容 Seata 的鉴权配置
本文详细介绍了如何在微服务环境下从 Nacos 1.3.0 升级到 2.3.0,并确保 Seata 各版本的兼容性。作者小米分享了升级过程中的关键步骤,包括备份配置、更新鉴权信息及验证测试等,并解答了常见问题。通过这些步骤,可以帮助读者顺利完成升级并提高系统的安全性与一致性。
122 8
升级指南:从Nacos 1.3.0 到 2.3.0,并兼容 Seata 的鉴权配置