一、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上提交包含注释的代码。
- spring-cloud-alibaba (版本:2.2.2.RELEASE)
- nacos (版本:1.3.2)