版本说明
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba</artifactId>
<version>2.2.0.RELEASE</version>
1、nacos配置中心客户端使用
1.1 导入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
1.2 配置中心的地址信息
spring:
application:
name: nacos-config-example
profiles:
active: DEV
---
spring:
profiles: DEV
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
namespace: 789b5be0-0286-4cda-ac0c-e63f5bae3652
group: DEFAULT_GROUP
extension-configs:
- data_id: arch.properties
group: arch
refresh: true
- data_id: jdbc.properties
group: data
refresh: false
shared-configs:
- data_id: share.properties
group: DEFAULT_GROUP
refresh: true
2、 spring.factories
springboot应用在启动时会加载类路径下META-INF/spring.factories文件,将key为 org.springframework.cloud.bootstrap.BootstrapConfiguration(对应spring的bootstrap容器)和org.springframework.boot.autoconfigure.EnableAutoConfiguration(对应spring的Application容器) 对应的value加载到spring容器中。
在spring-cloud-alibaba-nacos-config模块类路径下META-INF/spring.factories文件中,导入了NacosConfigBootstrapConfiguration、NacosConfigAutoConfiguration等配置类。
2.1 NacosConfigBootstrapConfiguration spring bootstrap容器配置
/**
* 向Bootstrap容器中注入了三个Bean
*
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigBootstrapConfiguration {
// spring.cloud.nacos.config配置
@Bean
@ConditionalOnMissingBean
public NacosConfigProperties nacosConfigProperties() {
return new NacosConfigProperties();
}
// 管理ConfigService
//ConfigService是nacos配置中心顶层接口
@Bean
@ConditionalOnMissingBean
public NacosConfigManager nacosConfigManager(
NacosConfigProperties nacosConfigProperties) {
return new NacosConfigManager(nacosConfigProperties);
}
// PropertySourceBootstrapConfiguration会加载NacosPropertySourceLocator提供的配置
@Bean
public NacosPropertySourceLocator nacosPropertySourceLocator(
NacosConfigManager nacosConfigManager) {
return new NacosPropertySourceLocator(nacosConfigManager);
}
}
2.1.1 NacosConfigProperties naocs配置中心的配置信息
@ConfigurationProperties(NacosConfigProperties.PREFIX)
public class NacosConfigProperties {
/**
* Prefix of {@link NacosConfigProperties}.
*/
public static final String PREFIX = "spring.cloud.nacos.config";
//配置中心地址
private String serverAddr;
//配置中心内容编码
private String encode;
//分组
private String group = "DEFAULT_GROUP";
/**
* 这块建议结合NacosPropertySourceLocator#locate()代码
* 1、application配置:对应Nacos的dataId
* {prefix}-{spring.profiles.active}.{file-extension}。
* 对于prefix前缀,优先级 prefix > name > spring.application.name。
* 应用配置内部也有优先级,从低到高:
* {prefix}-{spring.profiles.active}.{file-extension}
* {prefix}.{file-extension}
* {prefix}
*
* 2、sharedConfigs:共享配置。
* 3、extensionConfigs:扩展配置。
*
* 三种配置优先级:share < extension < application
*/
private String prefix;
// 配置文件扩展名
private String fileExtension = "properties";
/**
*
* 命名空间 namespace, separation configuration of different environments.
*/
private String namespace;
/**
* context path for nacos config server.
*/
private String contextPath;
/**
* nacos config cluster name.
*/
private String clusterName;
/**
* nacos config dataId name.
*/
private String name;
/**
*
* 共享配置 a set of shared configurations .e.g:
* spring.cloud.nacos.config.shared-configs[0]=xxx .
*/
private List<Config> sharedConfigs;
/**
* 扩展配制 a set of extensional configurations .e.g:
* spring.cloud.nacos.config.extension-configs[0]=xxx .
*/
private List<Config> extensionConfigs;
/**
* 总控配置是否可以刷新,shareConfigs和extensionConfigs内部可以定义自己的刷新机制。
*/
private boolean refreshEnabled = true;
//省略一些属性
}
2.1.2 NacosConfigManager 管理ConfigService
public class NacosConfigManager {
private static final Logger log = LoggerFactory.getLogger(NacosConfigManager.class);
// 单例ConfigService
private static ConfigService service = null;
private NacosConfigProperties nacosConfigProperties;
public NacosConfigManager(NacosConfigProperties nacosConfigProperties) {
this.nacosConfigProperties = nacosConfigProperties;
//构造方法中创建ConfigService实例
createConfigService(nacosConfigProperties);
}
//单例模式创建ConfigService
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;
}
}
2.1.3 NacosPropertySourceLocator加载外部配置
NacosPropertySourceLocator实现了spring提供的PropertySourceLocator接口 PropertySourceLocator的作用是提供PropertySource,开发人员需要实现该接口的locate方法, 并返回需要添加到容器中的PropertySource。
public class NacosPropertySourceLocator implements PropertySourceLocator {
}
2.2 NacosConfigAutoConfiguration spring Application容器
public class NacosConfigAutoConfiguration {
//nacos配置文件
@Bean
public NacosConfigProperties nacosConfigProperties(ApplicationContext context) {
// 使用bootstrap父容器中的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();
}
// 管理ConfigService,内部使用的ConfigService是Bootstrap容器中的
@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);
}
}
2.2.1 NacosRefreshHistory 基于内存记录配置刷新历史
//在内存中记录最近20次配置刷新历史
public class NacosRefreshHistory {
private static final int MAX_SIZE = 20;
public void addRefreshRecord(String dataId, String group, String data) {
records.addFirst(new Record(DATE_FORMAT.get().format(new Date()), dataId, group,
md5(data), null));
// 保留最近20条
if (records.size() > MAX_SIZE) {
records.removeLast();
}
}
}
2.2.2 NacosContextRefresher 上下文刷新
//实现了ApplicationListener接口 监听ApplicationReadyEvent事件
public class NacosContextRefresher
implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
public void onApplicationEvent(ApplicationReadyEvent event) {
// many Spring context
if (this.ready.compareAndSet(false, true)) {
// 注册监听
this.registerNacosListenersForApplications();
}
}
private void registerNacosListenersForApplications() {
// spring.cloud.nacos.config.isRefreshEnabled总控开关默认开启
if (isRefreshEnabled()) {
// 从缓存中获取所有Nacos配置
for (NacosPropertySource propertySource : NacosPropertySourceRepository
.getAll()) {
if (!propertySource.isRefreshable()) {
continue;
}
String dataId = propertySource.getDataId();
//注册监听
registerNacosListener(propertySource.getGroup(), dataId);
}
}
}
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);
/**
* 发布RefreshEvent事件 RefreshEventListener 监听该事件
* spring-cloud-starter-alibaba-nacos-config必须配合RefreshScope一同使用才能实现配置动态更新,
* 并且一个配置文件的更新会导致所有RefreshScope里的Bean重新实例化,暂时还不支持单个配置文件的刷新。
*/
// todo feature: support single refresh for listening
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 {
/**
* 向nacos注册监听,注意 :此处并不是向spring注册监听 nacos有自己的一套事件监听组件
*/
configService.addListener(dataKey, groupKey, listener);
}
catch (NacosException e) {
log.warn(String.format(
"register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
groupKey), e);
}
}
}
3、配置注入
3.1 spring容器的配置注入
PropertySourceBootstrapConfiguration是ApplicationContextInitializer类型的bean。容器启动过程中会回调ApplicationContextInitializer.initialize方法。具体PropertySourceBootstrapConfiguration.initialize方法是何时调用的本文不做赘述(跟了一下代码,非常复杂...)
public class PropertySourceBootstrapConfiguration implements
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
@Autowired(required = false)
private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
public void initialize(ConfigurableApplicationContext applicationContext) {
List<PropertySource<?>> composite = new ArrayList<>();
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
ConfigurableEnvironment environment = applicationContext.getEnvironment();
//遍历所有的PropertySourceLocator 执行locate方法(locateCollection方法中会执行loacte方法),将返回的PropertySource加到集合中
for (PropertySourceLocator locator : this.propertySourceLocators) {
Collection<PropertySource<?>> source = locator.locateCollection(environment);
if (source == null || source.size() == 0) {
continue;
}
List<PropertySource<?>> sourceList = new ArrayList<>();
for (PropertySource<?> p : source) {
if (p instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) p;
sourceList.add(new BootstrapPropertySource<>(enumerable));
}
else {
sourceList.add(new SimpleBootstrapPropertySource(p));
}
}
logger.info("Located property source: " + sourceList);
composite.addAll(sourceList);
empty = false;
}
if (!empty) {
MutablePropertySources propertySources = environment.getPropertySources();
String logConfig = environment.resolvePlaceholders("${logging.config:}");
LogFile logFile = LogFile.get(environment);
for (PropertySource<?> p : environment.getPropertySources()) {
if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
propertySources.remove(p.getName());
}
}
//将收集到的composite,添加到environment中的中
insertPropertySources(propertySources, composite);
reinitializeLoggingSystem(environment, logConfig, logFile);
setLogLevels(applicationContext, environment);
handleIncludedProfiles(environment);
}
}
}
3.2 NacosPropertySourceLocator
public class NacosPropertySourceLocator implements PropertySourceLocator {
public PropertySource<?> locate(Environment env) {
nacosConfigProperties.setEnvironment(env);
// 1. 获取Nacos配置管理核心API ConfigService
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();
// 2. 构造Nacos配置Builder 真正读取配置
nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
timeout);
String name = nacosConfigProperties.getName();
// 3. dataId前缀按照优先级获取
// prefix > name > spring.application.name
String dataIdPrefix = nacosConfigProperties.getPrefix();
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = name;
}
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = env.getProperty("spring.application.name");
}
CompositePropertySource composite = new CompositePropertySource(
NACOS_PROPERTY_SOURCE_NAME);
// 加载共享配置
loadSharedConfiguration(composite);
// 加载扩展配制
loadExtConfiguration(composite);
// 加载应用配置
// 先加载的优先级低,会被覆盖
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
return composite;
}
//加载共享配置
private void loadSharedConfiguration(
CompositePropertySource compositePropertySource) {
List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties
.getSharedConfigs();
if (!CollectionUtils.isEmpty(sharedConfigs)) {
checkConfiguration(sharedConfigs, "shared-configs");
//从nacos server加载共享配置
loadNacosConfiguration(compositePropertySource, sharedConfigs);
}
}
//加载配置
private void loadNacosConfiguration(final CompositePropertySource composite,
List<NacosConfigProperties.Config> configs) {
for (NacosConfigProperties.Config config : configs) {
String dataId = config.getDataId();
String fileExtension = dataId.substring(dataId.lastIndexOf(DOT) + 1); //重点
loadNacosDataIfPresent(composite, dataId, config.getGroup(), fileExtension,
config.isRefresh());
}
}
//加载扩展配置
private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties
.getExtensionConfigs();
if (!CollectionUtils.isEmpty(extConfigs)) {
checkConfiguration(extConfigs, "extension-configs");
loadNacosConfiguration(compositePropertySource, extConfigs);
}
}
//加载application配制
private void loadApplicationConfiguration(
CompositePropertySource compositePropertySource, String dataIdPrefix,
NacosConfigProperties properties, Environment environment) {
String fileExtension = properties.getFileExtension();
String nacosGroup = properties.getGroup();
// load directly once by default
// 第一次加载 : 默认情况下直接加载一次
loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
fileExtension, true);
// load with suffix, which have a higher priority than the default
// 第二次加载 : 加载带有后缀,具有比默认值更高的优先级
loadNacosDataIfPresent(compositePropertySource,
dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
// Loaded with profile, which have a higher priority than the suffix
// 第三次加载 :用配置文件 Profile 加载,它比后缀具有更高的优先级
// 此处循环所有的 Profiles , 分别进行处理
for (String profile : environment.getActiveProfiles()) {
String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
fileExtension, true);
}
}
}
无论加载共享和扩展配置,还是加载application配置,最终都调用到了loadNacosDataIfPresent方法
public class NacosPropertySourceLocator implements PropertySourceLocator {
private void loadNacosDataIfPresent(final CompositePropertySource composite,
final String dataId, final String group, String fileExtension,
boolean isRefreshable) {
if (null == dataId || dataId.trim().length() < 1) {
return;
}
if (null == group || group.trim().length() < 1) {
return;
}
// 加载
NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
fileExtension, isRefreshable);
this.addFirstPropertySource(composite, propertySource, false);
}
private NacosPropertySource loadNacosPropertySource(final String dataId,
final String group, String fileExtension, boolean isRefreshable) {
if (NacosContextRefresher.getRefreshCount() != 0) {
if (!isRefreshable) {
return NacosPropertySourceRepository.getNacosPropertySource(dataId,
group);
}
}
// 加载
return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
isRefreshable);
}
com.alibaba.cloud.nacos.client.NacosPropertySourceBuilder#build
public class NacosPropertySourceBuilder {
//配置中心顶层接口
private ConfigService configService;
NacosPropertySource build(String dataId, String group, String fileExtension,
boolean isRefreshable) {
// 1. 调用ConfigService读取远程配置
Map<String, Object> p = loadNacosData(dataId, group, fileExtension);
// 2. 封装为NacosPropertySource
NacosPropertySource nacosPropertySource = new NacosPropertySource(group, dataId,
p, new Date(), isRefreshable);
// 3. 将配置缓存到NacosPropertySourceRepository
NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
// 4. 返回给外部,加入Environment
return nacosPropertySource;
}
private Map<String, Object> loadNacosData(String dataId, String group,
String fileExtension) {
String data = null;
try {
// 1、获取远程配置信息(向Config Serevr发http请求)
data = configService.getConfig(dataId, group, timeout);
if (StringUtils.isEmpty(data)) {
log.warn(
"Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]",
dataId, group);
return EMPTY_MAP;
}
if (log.isDebugEnabled()) {
log.debug(String.format(
"Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId,
group, data));
}
// 2、解析远程配置信息
Map<String, Object> dataMap = NacosDataParserHandler.getInstance()
.parseNacosData(data, fileExtension);
return dataMap == null ? EMPTY_MAP : dataMap;
}
catch (NacosException e) {
log.error("get data from Nacos error,dataId:{}, ", dataId, e);
}
catch (Exception e) {
log.error("parse data from Nacos error,dataId:{},data:{},", dataId, data, e);
}
// 此处省略了异常处理逻辑 , 出现异常不会抛出 ,而是返回空集合
// PS : 这里也导致部分错误不好从日志判断原因
return EMPTY_MAP;
}
}
com.alibaba.nacos.client.config.NacosConfigService#getConfig是nacos对外提供的服务发现接口。
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
return getConfigInner(namespace, dataId, group, timeoutMs);
}
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
//group默认设置为DEFAULT_GROUP
group = null2defaultGroup(group);
ParamUtils.checkKeyParam(dataId, group);
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);
/**
* failover文件在Nacos里是优先级最高的,如果failover文件存在则不会使用nacos服务端的配置,永远会使用failover文件,
* 即使服务端的配置发生了变化,类似于Apollo中-Denv=LOCAL时只会使用本地配置文件。
* 需要注意的是,Nacos的failover文件内容没有更新的入口,也就是说这个文件只能在文件系统中修改生效,生效时机在长轮询过程
* failover文件的路径:
* 默认namespace:/{user.home}/{agentName}_nacos/data/config-data/{group}/{dataId}
* 指定namespace:/{user.home}/{agentName}_nacos/data/config-data-tenant/{namespace}/{group}/{dataId}
*/
// LEVEL1 : 使用本地文件系统的failover配置
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;
}
//一般情况下,failover文件不会存在,那么都会走ClientWorker.getServerConfig方法
// LEVEL2 : 读取config-server实时配置,并将snapshot保存到本地文件系统
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;
}
// 非403错误进入LEVEL3
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));
// LEVEL3 : 如果读取config-server发生非403Forbidden错误,使用本地snapshot
/**
* snapshot文件的路径:
*
* 默认namespace:/{user.home}/{agentName}_nacos/snapshot/{group}/{dataId}
* 指定namespace:/{user.home}/{agentName}_nacos/snapshot-tenant/{namespace}/{group}/{dataId}
*/
content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
com.alibaba.cloud.nacos.parser.NacosDataParserHandler#parseNacosData解析配置,决定了nacos支持的配置文件格式
public final class NacosDataParserHandler {
public Map<String, Object> parseNacosData(String data, String extension)
throws IOException {
if (null == parser) {
// 创建解析器链
parser = this.createParser();
}
return parser.parseNacosData(data, extension);
}
//创建解析器 可以看出nacos支持properties格式和yaml格式
private AbstractNacosDataParser createParser() {
return new NacosDataPropertiesParser().addNextParser(new NacosDataYamlParser())
.addNextParser(new NacosDataXmlParser())
.addNextParser(new NacosDataJsonParser());
}
}
com.alibaba.cloud.nacos.parser.AbstractNacosDataParser#parseNacosData 真正的解析配置
public abstract class AbstractNacosDataParser {
public final Map<String, Object> parseNacosData(String data, String extension)
throws IOException {
if (extension == null || extension.length() < 1) {
throw new IllegalStateException("The file extension cannot be empty");
}
// 判断解析器是否支持解析当前格式的配置
if (this.isLegal(extension.toLowerCase())) {
return this.doParse(data);
}
if (this.nextParser == null) {
throw new IllegalStateException(getTips(extension));
}
// 调用下一个解析器
return this.nextParser.parseNacosData(data, extension);
}
}
在新版本中,nacos的数据解析器整合了spring的SPI机制(spring.factories),可以让用户实现PropertySourceLoader用于处理不同扩展名的配置文件,只需要用户在自己的spring.factories中添加如下配置:
org.springframework.boot.env.PropertySourceLoader=\
com.alibaba.cloud.nacos.parser.NacosJsonPropertySourceLoader,\
com.alibaba.cloud.nacos.parser.NacosXmlPropertySourceLoader
public final class NacosDataParserHandler {
private static List<PropertySourceLoader> propertySourceLoaders;
private NacosDataParserHandler() {
propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,getClass().getClassLoader());
}
public List<PropertySource<?>> parseNacosData(String configName, String configValue,String extension) throws IOException {
// ...
for (PropertySourceLoader propertySourceLoader : propertySourceLoaders) {
// 找到可以处理 扩展名配置文件的对应PropertySourceLoader
if (!canLoadFileExtension(propertySourceLoader, extension)) {
continue;
}
// ...
}
return Collections.emptyList();
}
}
4、配置监听
4.1 构造NacosConfidService对象
在NacosConfigBootstrapConfiguration 自动配置类中,向容器中注入了NacosConfigManager对象。
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigBootstrapConfiguration {
// spring.cloud.nacos.config配置
@Bean
@ConditionalOnMissingBean
public NacosConfigProperties nacosConfigProperties() {
return new NacosConfigProperties();
}
// 管理ConfigService
@Bean
@ConditionalOnMissingBean
public NacosConfigManager nacosConfigManager(
NacosConfigProperties nacosConfigProperties) {
return new NacosConfigManager(nacosConfigProperties);
}
}
在NacosConfigManager的构造方法中,初始化了NacosConfigService,ConfigService是Nacos暴露给客户端的配置服务接口。
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.
*/
static ConfigService createConfigService(
NacosConfigProperties nacosConfigProperties) {
if (Objects.isNull(service)) {
synchronized (NacosConfigManager.class) {
try {
if (Objects.isNull(service)) {
//构造NacosConfigService
service = NacosFactory.createConfigService(
nacosConfigProperties.assembleConfigServiceProperties());
}
}
catch (NacosException e) {
log.error(e.getMessage());
throw new NacosConnectionFailureException(
nacosConfigProperties.getServerAddr(), e.getMessage(), e);
}
}
}
return service;
}
NacosFactory#createConfigService()
public class NacosFactory {
/**
* Create config service
*
* @param properties init param
* @return config
* @throws NacosException Exception
*/
public static ConfigService createConfigService(Properties properties) throws NacosException {
return ConfigFactory.createConfigService(properties);
}
}
public class ConfigFactory {
/**
* Create Config.
*
* @param properties init param
* @return ConfigService
* @throws NacosException Exception
*/
public static ConfigService createConfigService(Properties properties) throws NacosException {
try {
//为什么要通过反射创建NacosConfigService实现类?主要是为了将api层单独拆分出来。
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);
}
}
}
4.2 ClientWorker
在NacosCofigService的构造方法中,创建了ClientWorker对象。
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,
final Properties properties) {
this.agent = agent;
this.configFilterChainManager = configFilterChainManager;
// Initialize the timeout parameter
// 初始化一些参数,如:timeout
init(properties);
// 单线程执行器
this.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;
}
});
// 执行LongPollingRunnable的执行器,固定线程数=核数
this.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;
}
});
this.executor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
/** 10ms执行一次
* 负责检测当前情况(cacheMap大小及当前已经提交的长轮询任务数),是否需要提交新的长轮询任务到executorService中,固定线程数=1。
*/
checkConfigInfo();
} catch (Throwable e) {
LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);
}
}
}, 1L, 10L, TimeUnit.MILLISECONDS);
}
ClientWorker主要的任务就是执行长轮询。
public void checkConfigInfo() {
// Dispatch taskes.
// cacheMap大小
int listenerSize = cacheMap.size();
// Round up the longingTaskCount.
// cacheMap大小 / 3000 向上取整
int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());
/**
* 一个长轮询任务处理3000个listener,listener监听的是某个group的dataid
* 每个listener在添加到cacheMap之前会计算所属的taskId
*/
// 计算longingTaskCount 大于 当前实际长轮询任务数量
if (longingTaskCount > currentLongingTaskCount) {
for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {
// The task list is no order.So it maybe has issues when changing.
// 开启新的长轮询任务
/**
* 所以开启长轮询任务的时机,一般是注册监听之后创建了CacheData,checkConfigInfo定时任务扫描到需要开启新的长轮询任务时,触发长轮询任务提交
*/
executorService.execute(new LongPollingRunnable(i));
}
currentLongingTaskCount = longingTaskCount;
}
}
checkConfigInfo每10ms执行一次,负责检测当前情况(cacheMap大小及当前已经提交的长轮询任务数),是否需要提交新的长轮询任务到executorService中,固定线程数=1。
4.3 LongPollingRunnable
class LongPollingRunnable implements Runnable {
private final int taskId;
public LongPollingRunnable(int taskId) {
this.taskId = taskId;
}
/**
* 1、处理failover配置:判断当前CacheData是否使用failover配置(ClientWorker.checkLocalConfig),
* 如果使用failover配置,则校验本地配置文件内容是否发生变化,发生变化则触发监听器(CacheData.checkListenerMd5)。
* 这一步其实和长轮询无关。
* 2、对于所有非failover配置,执行长轮询( /v1/cs/configs/listener 服务端会hold住请求),返回发生改变的groupKey(ClientWorker.checkUpdateDataIds)。
* 3、根据返回的groupKey,查询服务端实时配置并保存snapshot(ClientWorker.getServerConfig)
* 4、更新内存CacheData的配置content。
* 5、校验配置是否发生变更,通知监听器(CacheData.checkListenerMd5)。
* 6、如果正常执行本次长轮询,立即提交长轮询任务,执行下一次长轮询;发生异常,延迟2s提交长轮询任务。
*/
@Override
public void run() {
// 当前长轮询任务负责的CacheData集合
List<CacheData> cacheDatas = new ArrayList<CacheData>();
// 正在初始化的CacheData 即刚构建的CacheData,内部的content仍然是snapshot版本
List<String> inInitializingCacheList = new ArrayList<String>();
try {
// 1. 对于failover配置文件的处理
// check failover config
for (CacheData cacheData : cacheMap.values()) {
//当前长轮询任务负责的CacheData
if (cacheData.getTaskId() == taskId) {
cacheDatas.add(cacheData);
try {
// 判断cacheData是否需要使用failover配置,设置isUseLocalConfigInfo
// 如果需要则更新内存中的配置
checkLocalConfig(cacheData);
// 使用failover配置则检测content内容是否发生变化,如果变化则通知监听器
if (cacheData.isUseLocalConfigInfo()) {
cacheData.checkListenerMd5();
}
} catch (Exception e) {
LOGGER.error("get local config info error", e);
}
}
}
// 2. 对于所有非failover配置,执行长轮询( /v1/cs/configs/listener),返回发生改变的groupKey
// check server config
List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
if (!CollectionUtils.isEmpty(changedGroupKeys)) {
LOGGER.info("get changedGroupKeys:" + changedGroupKeys);
}
//每个元素代表一个发生配置变化的groupKey。
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 {
// 3. 对于发生改变的配置,查询实时配置并保存snapshot
String[] ct = getServerConfig(dataId, group, tenant, 3000L);
// 4. 更新内存中的配置
CacheData cache = cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
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);
}
}
// 5. 对于非failover配置,触发监听器
for (CacheData cacheData : cacheDatas) {
// 排除failover文件
if (!cacheData.isInitializing() || inInitializingCacheList
.contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {
/**
* 校验md5是否发生变化,如果发生变化通知listener
*/
cacheData.checkListenerMd5();
cacheData.setInitializing(false);
}
}
inInitializingCacheList.clear();
// 6-1. 都执行完成以后,再次提交长轮询任务
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);
// 6-2. 如果长轮询执行发生异常,延迟2s执行下一次长轮询
executorService.schedule(this, taskPenaltyTime, TimeUnit.MILLISECONDS);
}
}
}
当文件系统指定路径下的failover配置文件存在时,就会优先使用failover配置文件
private void checkLocalConfig(CacheData cacheData) {
final String dataId = cacheData.dataId;
final String group = cacheData.group;
final String tenant = cacheData.tenant;
// 当文件系统指定路径下的failover配置文件存在时,就会优先使用failover配置文件;当failover配置文件被删除时,又会切换为使用server端配置
File path = LocalConfigInfoProcessor.getFailoverFile(agent.getName(), dataId, group, tenant);
// 当isUseLocalConfigInfo=false(使用failover配置文件) 且 failover配置文件存在时,使用failover配置文件,并更新内存中的配置
if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
cacheData.setUseLocalConfigInfo(true);
cacheData.setLocalConfigInfoVersion(path.lastModified());
cacheData.setContent(content);
LOGGER.warn(
"[{}] [failover-change] failover file created. dataId={}, group={}, tenant={}, md5={}, content={}",
agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
return;
}
// 当isUseLocalConfigInfo=true 且 failover配置文件不存在时,不使用failover配置文件
// If use local config info, then it doesn't notify business listener and notify after getting from server.
if (cacheData.isUseLocalConfigInfo() && !path.exists()) {
cacheData.setUseLocalConfigInfo(false);
LOGGER.warn("[{}] [failover-change] failover file deleted. dataId={}, group={}, tenant={}", agent.getName(),
dataId, group, tenant);
return;
}
// When it changed.
// 当isUseLocalConfigInfo=true 且 failover配置文件存在时 并且 记录failover配置文件的上次更新时间戳不等于当前failover配置文件的时间,使用failover配置文件并更新内存中的配置
if (cacheData.isUseLocalConfigInfo() && path.exists() && cacheData.getLocalConfigInfoVersion() != path
.lastModified()) {
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
cacheData.setUseLocalConfigInfo(true);
cacheData.setLocalConfigInfoVersion(path.lastModified());
cacheData.setContent(content);
LOGGER.warn(
"[{}] [failover-change] failover file changed. dataId={}, group={}, tenant={}, md5={}, content={}",
agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content));
}
}