项目启动怎么读取的配置信息
自动装配
SpringBoot 自动装配机制 加载 WEB/INF spring.factories
会将如下几个Bean加载到ioc 容器中
@Bean
@ConditionalOnMissingBean
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);
}
/**
* Compatible with bootstrap way to start.
* @param beans configurationPropertiesBeans
* @return configurationPropertiesRebinder
*/
@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
@ConditionalOnNonDefaultBehavior
public ConfigurationPropertiesRebinder smartConfigurationPropertiesRebinder(
ConfigurationPropertiesBeans beans) {
// If using default behavior, not use SmartConfigurationPropertiesRebinder.
// Minimize te possibility of making mistakes.
return new SmartConfigurationPropertiesRebinder(beans);
}
在SpringBoot启动的时候会将触发 PropertySourceLocator的locate方法去读取配置 而NacosPropertySourceLocator实现了PropertySourceLocator接口所以会触发NacosPropertySourceLocator的locate方法
@Override
public PropertySource<?> locate(Environment env) {
//设置 Environment(上下文环境)
nacosConfigProperties.setEnvironment(env);
//获取远程调用服务 ConfigService
ConfigService configService = nacosConfigManager.getConfigService();
if (null == configService) {
log.warn("no instance of config service found, can't load config from nacos");
return null;
}
//超时时间 3000
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");
}
CompositePropertySource composite = new CompositePropertySource(
NACOS_PROPERTY_SOURCE_NAME);
//共享配置 shared-configs
loadSharedConfiguration(composite);
//扩展配置 extension-configs
loadExtConfiguration(composite);
//本地配置 dataId
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");
//加载配置信息
loadNacosConfiguration(compositePropertySource, sharedConfigs);
}
}
扩展配置加载
private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties
.getExtensionConfigs();
//如果配置了扩展配置
if (!CollectionUtils.isEmpty(extConfigs)) {
//扩展配置检查
checkConfiguration(extConfigs, "extension-configs");
//加载配置信息
loadNacosConfiguration(compositePropertySource, extConfigs);
}
}
private void loadNacosConfiguration(final CompositePropertySource composite,
List<NacosConfigProperties.Config> configs) {
//遍历配置信息
for (NacosConfigProperties.Config config : configs) {
//获取配置内容的数据格式 比如yaml 之类的
String fileExtension = config.getFileExtension();
//如果是空
if (StringUtils.isEmpty(fileExtension)) {
//从DataId里面获取
fileExtension = NacosDataParserHandler.getInstance()
.getFileExtension(config.getDataId());
}
//加载配置信息
loadNacosDataIfPresent(composite, config.getDataId(), config.getGroup(),
fileExtension, config.isRefresh());
}
}
本地配置加载
private void loadApplicationConfiguration(
CompositePropertySource compositePropertySource, String dataIdPrefix,
NacosConfigProperties properties, Environment environment) {
//文件扩展名 .ymal/properties (spring.cloud.nacos.config.file-extension)
String fileExtension = properties.getFileExtension();
//Group
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
//加载 配置文件名.yml/Property的文件
loadNacosDataIfPresent(compositePropertySource,
dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
// Loaded with profile, which have a higher priority than the suffix
//根据 .dev/.prd 进行逐项加载
for (String profile : environment.getActiveProfiles()) {
String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
fileExtension, true);
}
}
加载配置信息
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);
}
本地获取数据
当不支持动态刷新的时候直接从本地缓存拿数据
public static NacosPropertySource getNacosPropertySource(String dataId,
String group) {
return NACOS_PROPERTY_SOURCE_REPOSITORY.get(getMapKey(dataId, group));
}
从服务端获取数据
NacosPropertySource build(String dataId, String group, String fileExtension,
boolean isRefreshable) {
//加载服务端配置
Map<String, Object> p = loadNacosData(dataId, group, fileExtension);
//构建配置信息
NacosPropertySource nacosPropertySource = new NacosPropertySource(group, dataId,
p, new Date(), isRefreshable);
//保存本地缓存
NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
return nacosPropertySource;
}
private List<PropertySource<?>> loadNacosData(String dataId, String group,
String fileExtension) {
String data = null;
.....
//获取服务信息
data = configService.getConfig(dataId, group, timeout);
.....
return Collections.emptyList();
}
@Override
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 = null2defaultGroup(group);
ParamUtils.checkKeyParam(dataId, group);
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);
// 1.优先使用本地缓存配置
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 {
//2.委派给worker从远程服务器获取
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;
}
本地缓存文件读取
public static String getFailover(String serverName, String dataId, String group, String tenant) {
//从本地文件中获取
File localPath = getFailoverFile(serverName, dataId, group, tenant);
if (!localPath.exists() || !localPath.isFile()) {
return null;
}
try {
//读文件
return readFile(localPath);
} catch (IOException ioe) {
LOGGER.error("[" + serverName + "] get failover error, " + localPath, ioe);
return null;
}
}
从Nacos服务端获取数据
public ConfigResponse getServerConfig(String dataId, String group, String tenant, long readTimeout, boolean notify)
throws NacosException {
//分组等于空设置默认分组
if (StringUtils.isBlank(group)) {
group = Constants.DEFAULT_GROUP;
}
//配置信息读取
return this.agent.queryConfig(dataId, group, tenant, readTimeout, notify);
}
@Override
public ConfigResponse queryConfig(String dataId, String group, String tenant, long readTimeouts, boolean notify)
throws NacosException {
ConfigQueryRequest request = ConfigQueryRequest.build(dataId, group, tenant);
request.putHeader(NOTIFY_HEADER, String.valueOf(notify));
RpcClient rpcClient = getOneRunningClient();
if (notify) {
CacheData cacheData = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
if (cacheData != null) {
rpcClient = ensureRpcClient(String.valueOf(cacheData.getTaskId()));
}
}
// 发送一个grpc请求读取配置信息
ConfigQueryResponse response = (ConfigQueryResponse) requestProxy(rpcClient, request, readTimeouts);
ConfigResponse configResponse = new ConfigResponse();
......
}
服务端读取配置信息
ConfigQueryRequestHandler
@Override
@TpsControl(pointName = "ConfigQuery", parsers = {ConfigQueryGroupKeyParser.class, ConfigQueryGroupParser.class})
@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
public ConfigQueryResponse handle(ConfigQueryRequest request, RequestMeta meta) throws NacosException {
try {
return getContext(request, meta, request.isNotify());
} catch (Exception e) {
return ConfigQueryResponse
.buildFailResponse(ResponseCode.FAIL.getCode(), e.getMessage());
}
}
这里太长懒得贴 反正就是会先用groupKey=dataid+group+namespace 服务端对每一个配置文件都有服务端的内存缓存,这里使用读写锁获取锁,为了避免并发修改的情况
然后会判断当前模式是不是单机模式 并且不使用mysql 做数据存储的则从内置的数据库拿数据 如果配的是mysql 则从mysql中拿数据返回
缓存配置信息到本地
public static void collectNacosPropertySource(
NacosPropertySource nacosPropertySource) {
NACOS_PROPERTY_SOURCE_REPOSITORY
.putIfAbsent(getMapKey(nacosPropertySource.getDataId(),
nacosPropertySource.getGroup()), nacosPropertySource);
}
配置信息动态刷新
springboot自动注入的时候会去注入一个 NacosConfigAutoConfiguration 然后再这里面会注入一个 NacosContextRefresher进行数据的刷新 当spring启动的时候 会发送一个ApplicationReadyEvent事件而NacosContextRefresher会监听这个事件
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// many Spring context
if (this.ready.compareAndSet(false, true)) {
this.registerNacosListenersForApplications();
}
}
private void registerNacosListenersForApplications() {
//是否开启了刷新 配置文件中 spring.cloud.nacos.config.refresh-enabled = true/false
if (isRefreshEnabled()) {
//开启了遍历 外部化配置的属性元
for (NacosPropertySource propertySource : NacosPropertySourceRepository
.getAll()) {
//判断这个属性元是否开启了 Refreshable
if (!propertySource.isRefreshable()) {
continue;
}
String dataId = propertySource.getDataId();
//注册监听事件
registerNacosListener(propertySource.getGroup(), dataId);
}
}
}
注册监听事件
当配置发生变更的时候会回调到这个监听事件中去发送一个刷新事件
具体怎么判断数据变更看上一张的safeNotifyListener方法 这个方法中会有一个innerReceive方法的调用最终会到这里
private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = listenerMap.computeIfAbsent(key,
lst -> new AbstractSharedListener() {
//当NacosContextRefresher.getRefreshCount() != 0 判断配置发生变化发生回调
@Override
public void innerReceive(String dataId, String group,
String configInfo) {
refreshCountIncrement();
//添加刷新历史记录
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
// 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 {
configService.addListener(dataKey, groupKey, listener);
}
catch (NacosException e) {
log.warn(String.format(
"register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
groupKey), e);
}
}
监听刷新事件
RefreshEventListener会监听到刷新事件进行处理
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationReadyEvent) {
handle((ApplicationReadyEvent) event);
}
else if (event instanceof RefreshEvent) {
handle((RefreshEvent) event);
}
}
public void handle(RefreshEvent event) {
//是否就绪
if (this.ready.get()) { // don't handle events before app is ready
log.debug("Event received " + event.getEventDesc());
Set<String> keys = this.refresh.refresh();
log.info("Refresh keys changed: " + keys);
}
}
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
//重新刷新bean
this.scope.refreshAll();
return keys;
}
public synchronized Set<String> refreshEnvironment() {
Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
addConfigFilesToEnvironment();
//得到发生变更的key
Set<String> keys = changes(before,
extract(this.context.getEnvironment().getPropertySources())).keySet();
//发布EnvironmentChange事件
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}
Environment变更事件监听
ConfigurationPropertiesRebinder 监听到事件重新进行数据的绑定
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.applicationContext.equals(event.getSource())
// Backwards compatible
|| event.getKeys().equals(event.getSource())) {
//数据绑定
rebind();
}
}
@ManagedOperation
public void rebind() {
this.errors.clear();
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
}
@ManagedOperation
public boolean rebind(String name) {
if (!this.beans.getBeanNames().contains(name)) {
return false;
}
if (this.applicationContext != null) {
try {
Object bean = this.applicationContext.getBean(name);
if (AopUtils.isAopProxy(bean)) {
bean = ProxyUtils.getTargetObject(bean);
}
if (bean != null) {
// TODO: determine a more general approach to fix this.
// see https://github.com/spring-cloud/spring-cloud-commons/issues/571
if (getNeverRefreshable().contains(bean.getClass().getName())) {
return false; // ignore
}
//先卸载
this.applicationContext.getAutowireCapableBeanFactory()
.destroyBean(bean);
//重新初始化
this.applicationContext.getAutowireCapableBeanFactory()
.initializeBean(bean, name);
return true;
}
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
catch (Exception e) {
this.errors.put(name, e);
throw new IllegalStateException("Cannot rebind to " + name, e);
}
}
return false;
}
总结
springboot启动的时候怎么加载的nacos中的配置
1.springboot启动的时候会通过自动注入 将NacosPropertySourceLocator注入到bean容器中 然后会调用到locate方法中进行配置的读取
2.配置读取会先判断是否配置了自动刷新 如果没有配置则直接从缓存中读取
3.如果配置了自动刷新 会先从本地快照中读取 如果读取到了就返回并加入到本地缓存中
4.如果快照中也没有读取到 则通过grpc请求从服务端读取 从服务端读取的时候会先生成一个读写锁防止有问题 他会判断是集群还是单机启动 如果是单机启动并且没有使用mysql 则从内嵌的数据库里读取 如果是集群并且配置了mysql则从mysql 读取返回给客户端
5.如果从服务端也没读取到则从本地磁盘读取
怎么实现动态刷新配置
1.springboot启动的时候会通过自动注入 会注入一个NacosContextRefresher到bean容器中 这里面会注册一个监听器用来监听事件变更的一个回调
2.当服务端有配置变更之后会推送给客户端进行配置的修改并触发这个回调
3.然后回调中会触发一个刷新事件
4.当准备完毕之后会去发送一个spring中配置修改的一个事件
5.这个事件中会触发bean的重新绑定事件 实际上就是将老的bean给卸载将新的bean重新加载来实现配置的实时变更