项目启动怎么读取的配置信息
自动装配
SpringBoot 自动装配机制 加载 WEB/INF spring.factories
会将如下几个Bean加载到ioc 容器中
@Bean@ConditionalOnMissingBeanpublic NacosConfigProperties nacosConfigProperties() {return new NacosConfigProperties();}@Bean@ConditionalOnMissingBeanpublic NacosConfigManager nacosConfigManager(NacosConfigProperties nacosConfigProperties) {return new NacosConfigManager(nacosConfigProperties);}@Beanpublic NacosPropertySourceLocator nacosPropertySourceLocator(NacosConfigManager nacosConfigManager) {return new NacosPropertySourceLocator(nacosConfigManager);}/*** Compatible with bootstrap way to start.* @param beans configurationPropertiesBeans* @return configurationPropertiesRebinder*/@Bean@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)@ConditionalOnNonDefaultBehaviorpublic 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方法
@Overridepublic PropertySource<?> locate(Environment env) {//设置 Environment(上下文环境)nacosConfigProperties.setEnvironment(env);//获取远程调用服务 ConfigServiceConfigService configService = nacosConfigManager.getConfigService();if (null == configService) {log.warn("no instance of config service found, can't load config from nacos");return null;}//超时时间 3000long 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-configsloadSharedConfiguration(composite);//扩展配置 extension-configsloadExtConfiguration(composite);//本地配置 dataIdloadApplicationConfiguration(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();//GroupString 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();}
@Overridepublic 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会监听这个事件
@Overridepublic void onApplicationEvent(ApplicationReadyEvent event) {// many Spring contextif (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()) { //判断这个属性元是否开启了 Refreshableif (!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 判断配置发生变化发生回调@Overridepublic 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会监听到刷新事件进行处理
@Overridepublic 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 readylog.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();//重新刷新beanthis.scope.refreshAll();return keys;}
public synchronized Set<String> refreshEnvironment() {Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());addConfigFilesToEnvironment();//得到发生变更的keySet<String> keys = changes(before,extract(this.context.getEnvironment().getPropertySources())).keySet();//发布EnvironmentChange事件this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));return keys;}
Environment变更事件监听
ConfigurationPropertiesRebinder 监听到事件重新进行数据的绑定
@Overridepublic void onApplicationEvent(EnvironmentChangeEvent event) {if (this.applicationContext.equals(event.getSource())// Backwards compatible|| event.getKeys().equals(event.getSource())) {//数据绑定rebind();}}
@ManagedOperationpublic void rebind() {this.errors.clear();for (String name : this.beans.getBeanNames()) {rebind(name);}}
@ManagedOperationpublic 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/571if (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重新加载来实现配置的实时变更