SpringCloud Nacos

  1. 本文主要分为SpringCloud Nacos的设计思路
  2. 简单分析一下触发刷新事件后发生的过程以及一些踩坑经验

org.springframework.cloud.bootstrap.config.PropertySourceLocator

  1. 这是一个SpringCloud提供的启动器加载配置类,实现locate,注入到上下文中即可发现配置
  1. /**
  2. * @param environment The current Environment.
  3. * @return A PropertySource, or null if there is none.
  4. * @throws IllegalStateException if there is a fail-fast condition.
  5. */
  6. PropertySource<?> locate(Environment environment);
  1. com.alibaba.cloud.nacos.client.NacosPropertySourceLocator
  • 该类为nacos实现的配置发现类
  1. org.springframework.core.env.PropertySource
  • 改类为springcloud抽象出来表达属性源的类
  • com.alibaba.cloud.nacos.client.NacosPropertySource / nacos实现了这个类,并赋予了其他属性
  1. /**
  2. * Nacos Group.
  3. */
  4. private final String group;
  5. /**
  6. * Nacos dataID.
  7. */
  8. private final String dataId;
  9. /**
  10. * timestamp the property get.
  11. */
  12. private final Date timestamp;
  13. /**
  14. * Whether to support dynamic refresh for this Property Source.
  15. */
  16. private final boolean isRefreshable;

大概讲解com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#locate

  1. 源码解析
  1. @Override
  2. public PropertySource<?> locate(Environment env) {
  3. nacosConfigProperties.setEnvironment(env);
  4. // 获取nacos配置的服务类,http协议,访问nacos的api接口获得配置
  5. ConfigService configService = nacosConfigManager.getConfigService();
  6. if (null == configService) {
  7. log.warn("no instance of config service found, can't load config from nacos");
  8. return null;
  9. }
  10. long timeout = nacosConfigProperties.getTimeout();
  11. // 构建一个builder
  12. nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
  13. timeout);
  14. String name = nacosConfigProperties.getName();
  15. String dataIdPrefix = nacosConfigProperties.getPrefix();
  16. if (StringUtils.isEmpty(dataIdPrefix)) {
  17. dataIdPrefix = name;
  18. }
  19. if (StringUtils.isEmpty(dataIdPrefix)) {
  20. dataIdPrefix = env.getProperty("spring.application.name");
  21. }
  22. // 构建一个复合数据源
  23. CompositePropertySource composite = new CompositePropertySource(
  24. NACOS_PROPERTY_SOURCE_NAME);
  25. // 加载共享的配置
  26. loadSharedConfiguration(composite);
  27. // 加载扩展配置
  28. loadExtConfiguration(composite);
  29. // 加载应用配置,应用配置的优先级是最高,所以这里放在最后面来做,是因为添加配置的地方都是addFirst,所以最先的反而优先级最后
  30. loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
  31. return composite;
  32. }
  1. 每次nacos检查到配置更新的时候就会触发上下文配置刷新,就会调取locate这个方法

org.springframework.cloud.endpoint.event.RefreshEvent

  1. 该事件为spring cloud内置的事件,用于刷新配置

com.alibaba.cloud.nacos.refresh.NacosRefreshHistory

  1. 该类用于nacos刷新历史的存放,用来保存每次拉取的配置的md5值,用于比较配置是否需要刷新

com.alibaba.cloud.nacos.refresh.NacosContextRefresher

  1. 该类是Nacos用来管理一些内部监听器的,主要是配置刷新的时候可以出发回调,并且发出spring cloud上下文的配置刷新事件

com.alibaba.cloud.nacos.NacosPropertySourceRepository

  1. 该类是nacos用来保存拉取到的数据的
  2. 流程:
  • 刷新器检查到配置更新,保存到NacosPropertySourceRepository
  • 发起刷新事件
  • locate执行,直接读取NacosPropertySourceRepository

com.alibaba.nacos.client.config.NacosConfigService

  1. 该类是nacos的主要刷新配置服务类
  2. com.alibaba.nacos.client.config.impl.ClientWorker
  • 该类是服务类里主要的客户端,协议是HTTP
  • clientWorker启动的时候会初始化2个线程池,1个用于定时检查配置,1个用于辅助检查
  1. executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
  2. @Override
  3. public Thread newThread(Runnable r) {
  4. Thread t = new Thread(r);
  5. t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
  6. t.setDaemon(true);
  7. return t;
  8. }
  9. });
  10. executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
  11. @Override
  12. public Thread newThread(Runnable r) {
  13. Thread t = new Thread(r);
  14. t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
  15. t.setDaemon(true);
  16. return t;
  17. }
  18. });
  19. executor.scheduleWithFixedDelay(new Runnable() {
  20. @Override
  21. public void run() {
  22. try {
  23. checkConfigInfo();
  24. } catch (Throwable e) {
  25. LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
  26. }
  27. }
  28. }, 1L, 10L, TimeUnit.MILLISECONDS);
  1. com.alibaba.nacos.client.config.impl.ClientWorker.LongPollingRunnable
  • 该类用于长轮询任务
  • com.alibaba.nacos.client.config.impl.CacheData#checkListenerMd5比对MD5之后开始刷新配置

com.alibaba.cloud.nacos.parser

  1. 该包提供了很多文件类型的转换器
  2. 加载数据的时候会根据文件扩展名去查找一个转换器实例
  1. // com.alibaba.cloud.nacos.client.NacosPropertySourceBuilder#loadNacosData
  2. private Map<String, Object> loadNacosData(String dataId, String group,
  3. String fileExtension) {
  4. String data = null;
  5. try {
  6. data = configService.getConfig(dataId, group, timeout);
  7. if (StringUtils.isEmpty(data)) {
  8. log.warn(
  9. "Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]",
  10. dataId, group);
  11. return EMPTY_MAP;
  12. }
  13. if (log.isDebugEnabled()) {
  14. log.debug(String.format(
  15. "Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId,
  16. group, data));
  17. }
  18. Map<String, Object> dataMap = NacosDataParserHandler.getInstance()
  19. .parseNacosData(data, fileExtension);
  20. return dataMap == null ? EMPTY_MAP : dataMap;
  21. }
  22. catch (NacosException e) {
  23. log.error("get data from Nacos error,dataId:{}, ", dataId, e);
  24. }
  25. catch (Exception e) {
  26. log.error("parse data from Nacos error,dataId:{},data:{},", dataId, data, e);
  27. }
  28. return EMPTY_MAP;
  29. }
  1. 数据会变成key value的形式,然后转换成PropertySource

如何配置一个启动配置类

  1. 由于配置上下文是属于SpringCloud管理的,所以本次的注入跟以往SpringBoot不一样
  1. org.springframework.cloud.bootstrap.BootstrapConfiguration=\
  2. com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
  1. 如何在SpringCloud和SpringBoot共享一个bean呢(举个例子)
  1. @Bean
  2. public NacosConfigProperties nacosConfigProperties(ApplicationContext context) {
  3. if (context.getParent() != null
  4. && BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
  5. context.getParent(), NacosConfigProperties.class).length > 0) {
  6. return BeanFactoryUtils.beanOfTypeIncludingAncestors(context.getParent(),
  7. NacosConfigProperties.class);
  8. }
  9. return new NacosConfigProperties();
  10. }

关于刷新机制的流程

org.springframework.cloud.endpoint.event.RefreshEventListener
  1. // 外层方法
  2. public synchronized Set<String> refresh() {
  3. Set<String> keys = refreshEnvironment();
  4. this.scope.refreshAll();
  5. return keys;
  6. }
  7. //
  8. public synchronized Set<String> refreshEnvironment() {
  9. Map<String, Object> before = extract(
  10. this.context.getEnvironment().getPropertySources());
  11. addConfigFilesToEnvironment();
  12. Set<String> keys = changes(before,
  13. extract(this.context.getEnvironment().getPropertySources())).keySet();
  14. this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
  15. return keys;
  16. }
  1. 该类是对RefreshEvent监听的处理
  2. 直接定位到org.springframework.cloud.context.refresh.ContextRefresher#refreshEnvironment,这个方法是主要的刷新配置的方法,具体做的事:
  • 归并得到刷新之前的配置key value
  • org.springframework.cloud.context.refresh.ContextRefresher#addConfigFilesToEnvironment 模拟一个新的SpringApplication,触发大部分的SpringBoot启动流程,因此也会触发读取配置,于是就会触发上文所讲的Locator,然后得到一个新的Spring应用,从中获取新的聚合配置源,与旧的Spring应用配置源进行比较,并且把本次变更的配置放置到旧的去,然后把新的Spring应用关闭
  • 比较新旧配置,把配置拿出来,触发一个事件org.springframework.cloud.context.environment.EnvironmentChangeEvent
  • 跳出该方法栈后,执行org.springframework.cloud.context.scope.refresh.RefreshScope#refreshAll
简单分析 EnvironmentChangeEvent
  1. org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#rebind()
  • 代码如下:
  1. @ManagedOperation
  2. public boolean rebind(String name) {
  3. if (!this.beans.getBeanNames().contains(name)) {
  4. return false;
  5. }
  6. if (this.applicationContext != null) {
  7. try {
  8. Object bean = this.applicationContext.getBean(name);
  9. // 获取source对象
  10. if (AopUtils.isAopProxy(bean)) {
  11. bean = ProxyUtils.getTargetObject(bean);
  12. }
  13. if (bean != null) {
  14. // 重新触发销毁和初始化的周期方法
  15. this.applicationContext.getAutowireCapableBeanFactory()
  16. .destroyBean(bean);
  17. // 因为触发初始化生命周期,就可以触发
  18. // org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization
  19. this.applicationContext.getAutowireCapableBeanFactory()
  20. .initializeBean(bean, name);
  21. return true;
  22. }
  23. }
  24. catch (RuntimeException e) {
  25. this.errors.put(name, e);
  26. throw e;
  27. }
  28. catch (Exception e) {
  29. this.errors.put(name, e);
  30. throw new IllegalStateException("Cannot rebind to " + name, e);
  31. }
  32. }
  33. return false;
  34. }
  • 该方法时接受到事件后,对一些bean进行属性重绑定,具体哪些Bean呢?
  • org.springframework.cloud.context.properties.ConfigurationPropertiesBeans#postProcessBeforeInitialization 该方法会在Spring refresh上下文时候执行的bean生命后期里的其中一个后置处理器,它会检查注解 @ConfigurationProperties,这些bean就是上面第一步讲的重绑定的bean
  1. @Override
  2. public Object postProcessBeforeInitialization(Object bean, String beanName)
  3. throws BeansException {
  4. if (isRefreshScoped(beanName)) {
  5. return bean;
  6. }
  7. ConfigurationProperties annotation = AnnotationUtils
  8. .findAnnotation(bean.getClass(), ConfigurationProperties.class);
  9. if (annotation != null) {
  10. this.beans.put(beanName, bean);
  11. }
  12. else if (this.metaData != null) {
  13. annotation = this.metaData.findFactoryAnnotation(beanName,
  14. ConfigurationProperties.class);
  15. if (annotation != null) {
  16. this.beans.put(beanName, bean);
  17. }
  18. }
  19. return bean;
  20. }
简单分析org.springframework.cloud.context.scope.refresh.RefreshScope#refreshAll
  1. @ManagedOperation(description = "Dispose of the current instance of all beans "
  2. + "in this scope and force a refresh on next method execution.")
  3. public void refreshAll() {
  4. super.destroy();
  5. this.context.publishEvent(new RefreshScopeRefreshedEvent());
  6. }
  1. org.springframework.cloud.context.scope.GenericScope#destroy()
  • 对BeanLifecycleWrapper实例集合进行销毁
  • BeanLifecycleWrapper是什么?
  1. private static class BeanLifecycleWrapper {
  2. // bean的名字
  3. private final String name;
  4. // 获取bean
  5. private final ObjectFactory<?> objectFactory;
  6. // 真正的实例
  7. private Object bean;
  8. // 销毁函数
  9. private Runnable callback;
  10. }
  • BeanLifecycleWrapper是怎么构造的?
  1. @Override
  2. public Object get(String name, ObjectFactory<?> objectFactory) {
  3. BeanLifecycleWrapper value = this.cache.put(name,
  4. new BeanLifecycleWrapper(name, objectFactory));
  5. this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  6. try {
  7. return value.getBean();
  8. }
  9. catch (RuntimeException e) {
  10. this.errors.put(name, e);
  11. throw e;
  12. }
  13. }
  • 以上代码可以追溯到Spring在创建bean的某一个分支代码,org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean 347行代码
  1. String scopeName = mbd.getScope();
  2. final Scope scope = this.scopes.get(scopeName);
  3. if (scope == null) {
  4. throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
  5. }
  6. try {
  7. Object scopedInstance = scope.get(beanName, () -> {
  8. beforePrototypeCreation(beanName);
  9. try {
  10. return createBean(beanName, mbd, args);
  11. }
  12. finally {
  13. afterPrototypeCreation(beanName);
  14. }
  15. });
  16. bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
  17. }
  • 销毁完之后呢?其实就是把BeanLifecycleWrapper绑定的bean变成了null,那配置怎么刷新呢?@RefreshScope标记的对象一开始就是被初始化为代理对象,然后在执行它的@Value的属性的get操作的时候,会进入代理方法,代理方法里会去获取Target,这里就会触发 org.springframework.cloud.context.scope.GenericScope#get
  1. public Object getBean() {
  2. if (this.bean == null) {
  3. synchronized (this.name) {
  4. if (this.bean == null) {
  5. // 因为bean为空,所以会触发一次bean的重新初始化,走了一遍生命周期流程所以配置又回来了
  6. this.bean = this.objectFactory.getObject();
  7. }
  8. }
  9. }
  10. return this.bean;
  11. }

踩坑

  1. 上面的分析简单分析到那里,那么在使用这种配置自动刷新机制有什么坑呢?
  • 使用@RefreshScople的对象,如果把配置中心的某一行属性删掉,那么对应的bean对应的属性会变为null,但是使用@ConfigaruationProperties的对象则不会,为什么呢?因为前者是整个bean重新走了一遍生命流程,但是后者只会执行init方法
  • 不管使用@RefreshScople和@ConfigaruationProperties都不应该在destory和init方法中执行过重的逻辑,前者会影响服务的可用性,在高并发下会阻塞太多数的请求。后者会影响配置刷新的时延性

最后

  1. 感谢阅读完这篇文章的大佬们,如果发现文章中有什么错误的话,请留言,不甚感激!

SpringCloud配置刷新机制的简单分析[nacos为例子]的更多相关文章

  1. 下拉刷新XListView的简单分析

    依照这篇博文里的思路分析和理解的 先要理解Scroller,看过的博文: http://ipjmc.iteye.com/blog/1615828 http://blog.csdn.net/wangji ...

  2. 深入理解SpringCloud之配置刷新

    我们知道在SpringCloud中,当配置变更时,我们通过访问http://xxxx/refresh,可以在不启动服务的情况下获取最新的配置,那么它是如何做到的呢,当我们更改数据库配置并刷新后,如何能 ...

  3. SpringCloud 详解配置刷新的原理 使用jasypt自动加解密后 无法使用 springcloud 中的自动刷新/refresh功能

    之所以会查找这篇文章,是因为要解决这样一个问题: 当我使用了jasypt进行配置文件加解密后,如果再使用refresh 去刷新配置,则自动加解密会失效. 原因分析:刷新不是我之前想象的直接调用conf ...

  4. springMVC源码分析--异常处理机制HandlerExceptionResolver简单示例(一)

    springMVC对Controller执行过程中出现的异常提供了统一的处理机制,其实这种处理机制也简单,只要抛出的异常在DispatcherServlet中都会进行捕获,这样就可以统一的对异常进行处 ...

  5. Redis数据持久化机制AOF原理分析一---转

    http://blog.csdn.net/acceptedxukai/article/details/18136903 http://blog.csdn.net/acceptedxukai/artic ...

  6. 又卡了~从王者荣耀看Android屏幕刷新机制

    前言 正在带妹子上分的我,团战又卡了,我该怎么向妹子解释?在线等. "卡"的意思 不管是端游还是手游,我们都会时不时遇到"卡"的时候,一般这个卡有两种含义: 掉 ...

  7. Android 屏幕刷新机制

    这次就来梳理一下 Android 的屏幕刷新机制,把我这段时间因为研究动画而梳理出来的一些关于屏幕刷新方面的知识点分享出来,能力有限,有错的地方还望指点一下.另外,内容有点多,毕竟要讲清楚不容易,所以 ...

  8. ffplay.c函数结构简单分析(画图)

    最近重温了一下FFplay的源代码.FFplay是FFmpeg项目提供的播放器示例.尽管FFplay只是一个简单的播放器示例,它的源代码的量也是不少的.之前看代码,主要是集中于某一个"点&q ...

  9. Springboot学习04-默认错误页面加载机制源码分析

    Springboot学习04-默认错误页面加载机制源码分析 前沿 希望通过本文的学习,对错误页面的加载机制有这更神的理解 正文 1-Springboot错误页面展示 2-Springboot默认错误处 ...

随机推荐

  1. Code-Review-Maven编译(第三方jar包引用)

    Code-Review-SpringBoot-Maven编译(第三方jar包引用) 在使用maven编译项目时,有时候咱们可能会使用一些第三方的jar包依赖库,比如第三方支付类的接入,大多出于安全考虑 ...

  2. JavaScript 获取数组对象中某一值封装为数组

    1.获取数组对象中某一值封装为数组(一) data = [["2000-06-05",116],["2000-06-06",129]]; var dateLis ...

  3. SQL Server中模式(schema)、数据库(database)、表(table)、用户(user)之间的关系

    数据库的初学者往往会对关系型数据库模式(schema).数据库(database).表(table).用户(user)之间感到迷惘,总感觉他们的关系千丝万缕,但又不知道他们的联系和区别在哪里,对一些问 ...

  4. Spring Security OAuth2.0认证授权一:框架搭建和认证测试

    一.OAuth2.0介绍 OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不 需要将用户名和密码提供给第三方应用或分享他们数据的所有内容. 1.s ...

  5. Fresco 源码分析 —— 整体架构

    Fresco 是我们项目中图片加载专用框架.虽然我不是负责 Fresco 框架,但是由本人负责组里的图片加载浏览等工作,因此了解 Fresco 的源码有助于我今后的工作,也可以学习 Fresco 的源 ...

  6. Linux服务器初始化调优及安全加固

    一,开启iptables 仅开放必要的SSH端口和监控端口 示例:SSH tcp 22snmpd udp 161nrpe tcp 5666本人公网IP全端口开放 二,除非特别熟悉selinux配置,否 ...

  7. uber_go_guide解析(一)

    前言 实力有限,guide啃着好费劲 原地址https://github.com/xxjwxc/uber_go_guide_cn 加我自己的体会和补充 基于Golang 1.14 正文 Interfa ...

  8. python virtualenv 基本使用

    下载 pip install virtualenv 校验是否成功 virtualenv --version 使用 创建env环境 要写一个新项目,使用env先创建环境 cd xx\xx\xx\ # 进 ...

  9. oop的三大特性和传统dom如何渲染

    OOP的三大特性是什么: 封装 :就是将一个类的使用和实现分开,只保留部分接口和方法与外部联系继承:子类自动继承其父级类中的属性和方法,并可以添加新的属性和方法或者对部分属性和方法进行重写.继承增加了 ...

  10. 【JDBC核心】操作 BLOB 类型字段

    操作 BLOB 类型字段 MySQL BLOB 类型 MySQL 中,BLOB 是一个二进制大型对象,是一个可以存储大量数据的容器,它能容纳不同大小的数据. 插入 BLOB 类型的数据必须使用 Pre ...