一起来读naocs源码-配置中心客户端

1,148 阅读10分钟

一、config使用方法

config的使用方法这里就不详细说明了,请大家查看官网配置

二、目录结构

三、源码阅读

1、spring.factories

首先我们来看spring.factories文件,来查看默认初始化配置。不懂的同学可以查看spring boot插件开发实战和原理

# 这里是程序默认加载的配置类
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.nacos.NacosConfigAutoConfiguration,\
com.alibaba.cloud.nacos.endpoint.NacosConfigEndpointAutoConfiguration
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.alibaba.cloud.nacos.diagnostics.analyzer.NacosConnectionFailureAnalyzer

2、NacosConfigBootstrapConfiguration

// 如果spring.cloud.nacos.config.enabled为true的话,则该配置类启用。如果属性没有配置过,默认启用配置类
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigBootstrapConfiguration {

	@Bean
	// 该类没有初始化到spring容器中,则实例化到容器中
	@ConditionalOnMissingBean
	// 从bootstrap(.yml或.properties获取配置)
	public NacosConfigProperties nacosConfigProperties() {
		return new NacosConfigProperties();
	}

	@Bean
	@ConditionalOnMissingBean
	public NacosConfigManager nacosConfigManager(
			NacosConfigProperties nacosConfigProperties) {
		return new NacosConfigManager(nacosConfigProperties);
	}

	@Bean
	// 自定义配置获取
	public NacosPropertySourceLocator nacosPropertySourceLocator(
			NacosConfigManager nacosConfigManager) {
		return new NacosPropertySourceLocator(nacosConfigManager);
	}

}

  • NacosConfigProperties:获取配置,主要获取配置中心相关信息
// 读取spring.cloud.nacos.config的配置
@ConfigurationProperties(NacosConfigProperties.PREFIX)
public class NacosConfigProperties {
	public static final String PREFIX = "spring.cloud.nacos.config";
	...
    
    	@Autowired
	// 序列化成json的时候忽略该属性
	@JsonIgnore
	// 获取环境配置
	private Environment environment;

	// 初始化执行
	@PostConstruct
	public void init() {
		this.overrideFromEnv();
	}

	private void overrideFromEnv() {
		if (StringUtils.isEmpty(this.getServerAddr())) {
			// 获取配置中${key}路径的值
			String serverAddr = environment
					.resolvePlaceholders("${spring.cloud.nacos.config.server-addr:}");
			if (StringUtils.isEmpty(serverAddr)) {
				serverAddr = environment.resolvePlaceholders(
						"${spring.cloud.nacos.server-addr:localhost:8848}");
			}
			this.setServerAddr(serverAddr);
		}
		if (StringUtils.isEmpty(this.getUsername())) {
			this.setUsername(
					environment.resolvePlaceholders("${spring.cloud.nacos.username:}"));
		}
		if (StringUtils.isEmpty(this.getPassword())) {
			this.setPassword(
					environment.resolvePlaceholders("${spring.cloud.nacos.password:}"));
		}
	}
    ...
}
  • NacosConfigManager:创建nacos的配置管理类
public class NacosConfigManager {
	...
	private static ConfigService service = null;
	...
	public NacosConfigManager(NacosConfigProperties nacosConfigProperties) {
		this.nacosConfigProperties = nacosConfigProperties;
		// Compatible with older code in NacosConfigProperties,It will be deleted in the
		// future.
		createConfigService(nacosConfigProperties);
	}

	/**
	 * Compatible with old design,It will be perfected in the future.
	 */
    // 初始化nacos的请求service,使用2次判断null和锁保证只初始化一次
	static ConfigService createConfigService(
			NacosConfigProperties nacosConfigProperties) {
		if (Objects.isNull(service)) {
			synchronized (NacosConfigManager.class) {
				try {
					if (Objects.isNull(service)) {
						service = NacosFactory.createConfigService(
								nacosConfigProperties.assembleConfigServiceProperties());
					}
				}
				catch (NacosException e) {
					log.error(e.getMessage());
					throw new NacosConnectionFailureException(
							nacosConfigProperties.getServerAddr(), e.getMessage(), e);
				}
			}
		}
		return service;
	}
	...
}

这里会根据NacosConfigProperties的配置去创建ConfigService,使用2次判断null和锁保证只初始化一次,继续看NacosFactory.createConfigService。

public class ConfigFactory {
    ...
    
    public static ConfigService createConfigService(Properties properties) throws NacosException {
        try {
            // 通过反射获取NacosConfigService对象
            Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
            Constructor constructor = driverImplClass.getConstructor(Properties.class);
            ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
            return vendorImpl;
        } catch (Throwable e) {
            throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
        }
    }

    ...
}

通过反射获取NacosConfigService对象,为什么使用反射创建这个类呢,因为ConfigFatory类是在nacos-api这个工程中的,而NacosConfigService是在nacos-client工程中,而nacos-client又引用nacos-api工程,所以认为是防止循环依赖和工程更好的责任划分,所以这里才采用反射并且放到nacos-api工程中的。
进入NacosConfigService类可以看到如下代码:

public class NacosConfigService implements ConfigService {
	...
	public NacosConfigService(Properties properties) throws NacosException {
        ValidatorUtils.checkInitParam(properties);
        String encodeTmp = properties.getProperty(PropertyKeyConst.ENCODE);
        if (StringUtils.isBlank(encodeTmp)) {
            encode = Constants.ENCODE;
        } else {
            encode = encodeTmp.trim();
        }
        initNamespace(properties);
        agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
        agent.start();
        worker = new ClientWorker(agent, configFilterChainManager, properties);
    }
    ...
}

MetricsHttpAgent是使用了装饰者定义了一些httpGet,httpPost,httpDelete,主要还是ServerHttpAgent类。他初始化了一些安全和nacos配置中心地址, 里面有一个比较重要的属性就是isFixed,如果没有配置nacos的地址,则会在start的时候循环3次获取其地址,如果3次都获取不到则抛出异常,如果获取到则开启一个线程每30秒更新一次地址。
接下来我们看下最核心的ClentWorker。

public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager, final Properties properties) {
        this.agent = agent;
        this.configFilterChainManager = configFilterChainManager;

        // Initialize the timeout parameter

        init(properties);

        executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
                t.setDaemon(true);
                return t;
            }
        });

        executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
                t.setDaemon(true);
                return t;
            }
        });

        executor.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                try {
                    checkConfigInfo();
                } catch (Throwable e) {
                    LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
                }
            }
        }, 1L, 10L, TimeUnit.MILLISECONDS);
    }

ClientWorker内会创建两个定时线程池,第一个为单个的线程池,每隔10ms调用一次。进行缓存任务分割,每3000个为一组,交给一个线程去处理。cacheMap中的数据为配置文件中配置的nacos服务器端获取的配置文件信息。后面我们会具体讲解是怎么放入到cacheMap中的。

public void checkConfigInfo() {
    // Dispatch taskes.
    // 获取监听任务数量,cacheMap中装的是我们需要远程获取的配置,每一个对应cacheMap中的一个。
    int listenerSize = cacheMap.get().size();
    // Round up the longingTaskCount.
    // 为了防止传输数据过大,按照每3000个为一组去执行检测。 
    int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
    if (longingTaskCount > currentLongingTaskCount) {
        for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
            // The task list is no order.So it maybe has issues when changing.
            executorService.execute(new LongPollingRunnable(i));
        }
        currentLongingTaskCount = longingTaskCount;
    }
}

这里分批次执行LongPollingRunnable类,通过名字我们就能看出他是一个长轮训类,接着我们看代码。

class LongPollingRunnable implements Runnable {

    private final int taskId;

    public LongPollingRunnable(int taskId) {
        this.taskId = taskId;
    }

    @Override
    public void run() {

        List<CacheData> cacheDatas = new ArrayList<CacheData>();
        List<String> inInitializingCacheList = new ArrayList<String>();
        try {
            // check failover config
            for (CacheData cacheData : cacheMap.get().values()) {
                // 判断缓存中的批次号和该现成的批次号是否一样
                if (cacheData.getTaskId() == taskId) {
                    //如果是该线程批次号的则加入cacheDatas中
                    cacheDatas.add(cacheData);
                    try {
                        // 如果客户端和服务端部署在同一个节点,则直接去本地获取配置信息,并设置数据是从本地获取的
                        checkLocalConfig(cacheData);
                        // 如果是数据是从本地获取的则进行检验md5是否更新,如果更新,则广播通知
                        if (cacheData.isUseLocalConfigInfo()) {
                            cacheData.checkListenerMd5();
                        }
                    } catch (Exception e) {
                        LOGGER.error("get local config info error", e);
                    }
                }
            }

            // check server config
            // 通过cacaheData中的dataId和group和tenant去服务器端查看是否有更新,返回更新列表(cacheData中如果是本地获取的配置,则过滤掉)
            List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
            if (!CollectionUtils.isEmpty(changedGroupKeys)) {
                LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
            }

            // 循环更新列表,去服务器端获取到更新后的配置,更新到本地和缓存中,更更新md5值
            for (String groupKey : changedGroupKeys) {
                String[] key = GroupKey.parseKey(groupKey);
                String dataId = key[0];
                String group = key[1];
                String tenant = null;
                if (key.length == 3) {
                    tenant = key[2];
                }
                try {
                    // 获取服务器端更新后的配置
                    String[] ct = getServerConfig(dataId, group, tenant, 3000L);
                    CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
                    // 设置更新后的配置,并更新md5值
                    cache.setContent(ct[0]);
                    if (null != ct[1]) {
                        cache.setType(ct[1]);
                    }
                    LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}",
                            agent.getName(), dataId, group, tenant, cache.getMd5(),
                            ContentUtils.truncateContent(ct[0]), ct[1]);
                } catch (NacosException ioe) {
                    String message = String
                            .format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",
                                    agent.getName(), dataId, group, tenant);
                    LOGGER.error(message, ioe);
                }
            }
            // 遍历cacheDatas,判断md5值和listener中的MD5值是否一样,如果不一样则广播通知更新,并设置不是第一次初始化
            for (CacheData cacheData : cacheDatas) {
                if (!cacheData.isInitializing() || inInitializingCacheList
                        .contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
                    cacheData.checkListenerMd5();
                    cacheData.setInitializing(false);
                }
            }
            inInitializingCacheList.clear();

            //从新执行该线程
            executorService.execute(this);

        } catch (Throwable e) {

            // If the rotation training task is abnormal, the next execution time of the task will be punished
            LOGGER.error("longPolling error : ", e);
            executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
        }
    }
}

1.获取cacheMap数据,判断是不是该线程批次的,如果是则放入cacheDatas中。
2.遍历cacheDatas,判断哪些配置是本地服务端配置获取的,则加载并更新isUseLocalConfig=true。
3.直接判断isUseLocalConfig=true的配置是否更新,如果更新则发布广播。
4.循环cacheDatas,过滤掉isUseLocalConfig=true的配置,并使用dataId和group和tenant去服务器端查看是否有更新,返回更新列表。(这里长轮训的时间是30s,如果有修改会立马返回,如果没有修改会在29.5s的时候返回)
5.遍历更新列表,去服务端获取配置,更新到cacheDatas并设置新的md5。
6.遍历cacheDatas,判断md5是否和listener中的md5值是否一样,如果不一样则广播。
接下来我们来看下checkListenerMd5方法

void checkListenerMd5() {
    for (ManagerListenerWrap wrap : listeners) {
        // 校验md5值和listener中最后的md5值是否一样
        if (!md5.equals(wrap.lastCallMd5)) {
            safeNotifyListener(dataId, group, content, type, md5, wrap);
        }
    }
}

我们可以看到这步校验md5值和listener中最后的md5值是否一样,如果不一样,则调用safeNotifyListener方法

ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setGroup(group);
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
String contentTmp = cr.getContent();
// 调用NacosContextRefresher中配置的innerReceive方法
listener.receiveConfigInfo(contentTmp);

// compare lastContent and content
if (listener instanceof AbstractConfigChangeListener) {
    Map data = ConfigChangeHandler.getInstance()
            .parseChangeData(listenerWrap.lastContent, content, type);
    ConfigChangeEvent event = new ConfigChangeEvent(data);
    ((AbstractConfigChangeListener) listener).receiveConfigChange(event);
    listenerWrap.lastContent = content;
}
// 更新最后一次md5值
listenerWrap.lastCallMd5 = md5;

这里我们可以看这里调用了listener中的方法,会找到它是定义在NacosContextRefresher类中registerNacosListener的方法里,主要功能就是告诉spring容器去刷新配置,具体我们在讲到NacosContextRefresher类的时候详细讲解。

  • NacosPropertySourceLocator 可以看到该类实现了PropertySourceLocator接口。会自动调用其中的locate方法,只要把咱们自定义的配置在这return出去,就可以在spring中获取到。 具体实现原理可以参考Spring Cloud Config 规范,这里我们就不细讲了。 下面我们看下locate里到底做了什么?
@Override
public PropertySource<?> locate(Environment env) {
    nacosConfigProperties.setEnvironment(env);
    ConfigService configService = nacosConfigManager.getConfigService();

    if (null == configService) {
        log.warn("no instance of config service found, can't load config from nacos");
        return null;
    }
    long timeout = nacosConfigProperties.getTimeout();
    nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
            timeout);
    String name = nacosConfigProperties.getName();

    String dataIdPrefix = nacosConfigProperties.getPrefix();
    if (StringUtils.isEmpty(dataIdPrefix)) {
        dataIdPrefix = name;
    }

    if (StringUtils.isEmpty(dataIdPrefix)) {
        dataIdPrefix = env.getProperty("spring.application.name");
    }
    // 包含所有配置的对象,通过下面3个load把配置加载到对象中,返回出去
    CompositePropertySource composite = new CompositePropertySource(
            NACOS_PROPERTY_SOURCE_NAME);

    // 加载shared-configs配置的配置
    loadSharedConfiguration(composite);
    // 加载extension-configs配置的配置
    loadExtConfiguration(composite);
    // 加载系统默认配置
    loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);

    return composite;
}

1.可以看到这3个load方法最后都会走到loadNacosPropertySource方法,然后执行nacosPropertySourceBuilder.build(dataId, group, fileExtension,isRefreshable)。 2.在build中会调用loadNacosData(dataId, group, fileExtension)方法,并把配置放入到NACOS_PROPERTY_SOURCE_REPOSITORY中。
3.loadNacosData中会调用data = configService.getConfig(dataId, group, timeout)获取data。
这里会把配置逆向的加载到composite,然后在获取配置的时候,按照顺序去获取,如果了需要的配置项就会立马返回,这也就是为什么系统>extension>shared了。 接着往里看会看到NacosConfigService中的getConfigInner方法。

    private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
        group = null2defaultGroup(group);
        ParamUtils.checkKeyParam(dataId, group);
        ConfigResponse cr = new ConfigResponse();

        cr.setDataId(dataId);
        cr.setTenant(tenant);
        cr.setGroup(group);

        // 优先使用本地配置
        String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
        if (content != null) {
            LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
                dataId, group, tenant, ContentUtils.truncateContent(content));
            cr.setContent(content);
            configFilterChainManager.doFilter(null, cr);
            content = cr.getContent();
            return content;
        }

        try {
            String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs);
            cr.setContent(ct[0]);

            configFilterChainManager.doFilter(null, cr);
            content = cr.getContent();

            return content;
        } catch (NacosException ioe) {
            if (NacosException.NO_RIGHT == ioe.getErrCode()) {
                throw ioe;
            }
            LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}",
                agent.getName(), dataId, group, tenant, ioe.toString());
        }

        LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
            dataId, group, tenant, ContentUtils.truncateContent(content));
        content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
        cr.setContent(content);
        configFilterChainManager.doFilter(null, cr);
        content = cr.getContent();
        return content;
    }

会优先到本地获取配置,如果本地没有,则会去服务器上拉去,并保持到本地。

3、NacosConfigAutoConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigAutoConfiguration {

	@Bean
	public NacosConfigProperties nacosConfigProperties(ApplicationContext context) {
    	// 如果context父类不为空,咱把父类的context中的NacosConfigProperties复制一份
		if (context.getParent() != null
				&& BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
						context.getParent(), NacosConfigProperties.class).length > 0) {
			return BeanFactoryUtils.beanOfTypeIncludingAncestors(context.getParent(),
					NacosConfigProperties.class);
		}
		return new NacosConfigProperties();
	}

	@Bean
	public NacosRefreshProperties nacosRefreshProperties() {
		return new NacosRefreshProperties();
	}

	@Bean
	public NacosRefreshHistory nacosRefreshHistory() {
		return new NacosRefreshHistory();
	}

	@Bean
	public NacosConfigManager nacosConfigManager(
			NacosConfigProperties nacosConfigProperties) {
		return new NacosConfigManager(nacosConfigProperties);
	}

	@Bean
	public NacosContextRefresher nacosContextRefresher(
			NacosConfigManager nacosConfigManager,
			NacosRefreshHistory nacosRefreshHistory) {
		// Consider that it is not necessary to be compatible with the previous
		// configuration
		// and use the new configuration if necessary.
		return new NacosContextRefresher(nacosConfigManager, nacosRefreshHistory);
	}

}

NacosConfigProperties bean的初始化,我们会发现在NacosConfigBootstrapConfiguration配置类的时候已经生命过一次bean,为什么还要在声明一次呢?
那是因为NacosConfigBootstrapConfiguration是运行在BootstrapContext生命周期里的,而现在使用的才是我们熟悉的ApplicationContext生命周期内。 我们可以看到他会先获取父级的context,也就是BootstrapContext生命周期,如果有这个生命周期并且这个周期内实例化过NacosConfigProperties,那就直接把BootstrapContext生命周期中的值拷贝一份,生成新的bean。
其他的bean我们就不做过多的讲解了,接下来说下最重要的一个bean,NacosContextRefresher类。

public class NacosContextRefresher
		implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
    ...
    @Override
    // application初始化完成后发送事件执行
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // many Spring context
        if (this.ready.compareAndSet(false, true)) {
            this.registerNacosListenersForApplications();
        }
    }
    ...
    private void registerNacosListenersForApplications() {
        // 判断是否开启自动刷新配置
        if (isRefreshEnabled()) {
            for (NacosPropertySource propertySource : NacosPropertySourceRepository
                    .getAll()) {
                // 配置该配置文件是否开启自动刷新配置
                if (!propertySource.isRefreshable()) {
                    continue;
                }
                String dataId = propertySource.getDataId();
                // 注册listener
                registerNacosListener(propertySource.getGroup(), dataId);
            }
        }
    }
    ...
}

1.接受到ApplicationReadyEvent事件后,执行registerNacosListenersForApplications并设置ready为true。
2.判断是否开启自动刷新配置,如果开启继续执行,如果没开启,则结束。
3.判断配置该配置文件是否开启自动刷新配置。如果开启继续执行registerNacosListener方法,如果为开启则退出。
4.registerNacosListener中定义了listener,也就是上面说到的广播更新配置所要执行的方法。

private void registerNacosListener(final String groupKey, final String dataKey) {
    String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
    Listener listener = listenerMap.computeIfAbsent(key,
            lst -> new AbstractSharedListener() {
                @Override
                public void innerReceive(String dataId, String group,
                        String configInfo) {
                    refreshCountIncrement();
                    nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
                    // todo feature: support single refresh for listening
                    // 重新初始化event相关的bean
                    applicationContext.publishEvent(
                            new RefreshEvent(this, null, "Refresh Nacos config"));
                    if (log.isDebugEnabled()) {
                        log.debug(String.format(
                                "Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
                                group, dataId, configInfo));
                    }
                }
            });
    try {
        // 添加listener到cacheMap中
        configService.addListener(dataKey, groupKey, listener);
    }
    catch (NacosException e) {
        log.warn(String.format(
                "register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
                groupKey), e);
    }
}

1.当listener被执行时,执行applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config"))刷新事件,重新初始化event相关的bean
2.把listener添加到cacheData中。

    public void addTenantListeners(String dataId, String group, List<? extends Listener> listeners)
            throws NacosException {
        group = null2defaultGroup(group);
        String tenant = agent.getTenant();
        
        CacheData cache = addCacheDataIfAbsent(dataId, group, tenant);
        for (Listener listener : listeners) {
            cache.addListener(listener);
        }
    }

四、总结

至此整个客户端的代码就都已经讲完了。如果有哪些地方讲的不是太详细,大家可以留言,或者查看我github上提交包含注释的代码。