Spring Cloud Config为分布式系统中的外部化配置提供服务器和客户端支持。在应用启动的时候,通过网络请求,从配置服务器上拉去项目的配置。这样可以集中管理项目的配置文件,并且可以保护项目中重要配置的安全。
大致的流程如下:
可以看到,配置中心的核心部分在与配置中心的实现。现在流行的配置中心有zookeeper,eureka,consul,apollo,nacos等等。其中zookeeper是没有管理界面。eureka配置界面功能太单一,且已经停止更新。也就是说实际项目中最好从剩下三个去选。这里我推荐使用阿里巴巴的nacos作为配置中心和注册中心。因为nacos对Spring Cloud Alibaba生态的微服务组件有着更好的兼容性。
本文将以nacos作为配置中心来讲解Spring Cloud Config实现的原理
动态改spring boot配置的核心在于,在spring boot应用初始化之前能将配置存入Environment实例中。
导入自定义配置:
- @PropertySource 是 Spring Framework 3.1 引入的标准导入属性配置资源注解,它可以为应用指定其他的配置文件。
- 自定义EnvironmentPostProcessor接口,并在META-INF/spring.factories文件中配置org.springframework.boot.env.EnvironmentPostProcessor=com.example.YourEnvironmentPostProcessor 。就可以实现自定义Environment的功能,也能够导入自定义的配置。
在Nacos的实现中。分为两个步骤。
① 在prepareContext时,使用ApplicationContextInitializer来从远程配置中心服务器加载配置,初始化Application
② 在应用启动后。发布ApplicationReadyEvent事件,注册监听器来监听配置中心对配置改变。
- 从配置中心加载配置
这里直接从源码开始。Spring Cloud Config之所以能实现动态从远程服务器加载配置。主要依赖PropertySourceBootstrapConfiguration这个类。我们来看一下源码:
可以发现PropertySourceBootstrapConfiguration是一个ApplicationContextInitializer实例。看过Spring Boot源码的小伙伴对ApplicationContextInitializer应该很熟悉。
不了解的可以看下我往期文章 Spring Boot 2.2.6.RELEASE原理剖析
在Spring Boot应用启动到prepareContext时,会执行所有的ApplicationContextInitializer实例的initialize方法。
下面还是直接撸源码:
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration implements
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
/**
* Bootstrap property source name.
*/
public static final String BOOTSTRAP_PROPERTY_SOURCE_NAME = BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME + "Properties";
private static Log logger = LogFactory.getLog(PropertySourceBootstrapConfiguration.class);
private int order = Ordered.HIGHEST_PRECEDENCE + 10;
/**
* 注入PropertySourceLocator 配置源定位器
**/
@Autowired(required = false)
private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
@Override
public int getOrder() {
return this.order;
}
public void setPropertySourceLocators(Collection<PropertySourceLocator> propertySourceLocators) {
this.propertySourceLocators = new ArrayList<>(propertySourceLocators);
}
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
List<PropertySource<?>> composite = new ArrayList<>();
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
//获取应用的Environment实例
ConfigurableEnvironment environment = applicationContext.getEnvironment();
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) {
//将从配置定位器中获取到配置封装为BootstrapPropertySource实例存起来
sourceList.add(new BootstrapPropertySource<>(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);
//移除系统中配置名以bootstrapProperties开头的配置
for (PropertySource<?> p : environment.getPropertySources()) {
//BOOTSTRAP_PROPERTY_SOURCE_NAME值为bootstrapProperties
if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
propertySources.remove(p.getName());
}
}
//将从外部加载的配置插入到当前系统配置中
insertPropertySources(propertySources, composite);
//重新初始化日志系统
reinitializeLoggingSystem(environment, logConfig, logFile);
//设置日志级别
setLogLevels(applicationContext, environment);
//处理使用@Profile的类
handleIncludedProfiles(environment);
}
}
private void reinitializeLoggingSystem(ConfigurableEnvironment environment,
String oldLogConfig, LogFile oldLogFile) {
Map<String, Object> props = Binder.get(environment)
.bind("logging", Bindable.mapOf(String.class, Object.class))
.orElseGet(Collections::emptyMap);
if (!props.isEmpty()) {
String logConfig = environment.resolvePlaceholders("${logging.config:}");
LogFile logFile = LogFile.get(environment);
LoggingSystem system = LoggingSystem
.get(LoggingSystem.class.getClassLoader());
try {
ResourceUtils.getURL(logConfig).openStream().close();
// Three step initialization that accounts for the clean up of the logging
// context before initialization. Spring Boot doesn't initialize a logging
// system that hasn't had this sequence applied (since 1.4.1).
system.cleanUp();
system.beforeInitialize();
system.initialize(new LoggingInitializationContext(environment),
logConfig, logFile);
}
catch (Exception ex) {
PropertySourceBootstrapConfiguration.logger
.warn("Error opening logging config file " + logConfig, ex);
}
}
}
private void setLogLevels(ConfigurableApplicationContext applicationContext,
ConfigurableEnvironment environment) {
LoggingRebinder rebinder = new LoggingRebinder();
rebinder.setEnvironment(environment);
// We can't fire the event in the ApplicationContext here (too early), but we can
// create our own listener and poke it (it doesn't need the key changes)
rebinder.onApplicationEvent(new EnvironmentChangeEvent(applicationContext,
Collections.<String>emptySet()));
}
private void insertPropertySources(MutablePropertySources propertySources,
List<PropertySource<?>> composite) {
MutablePropertySources incoming = new MutablePropertySources();
List<PropertySource<?>> reversedComposite = new ArrayList<>(composite);
// Reverse the list so that when we call addFirst below we are maintaining the
// same order of PropertySources
// Wherever we call addLast we can use the order in the List since the first item
// will end up before the rest
Collections.reverse(reversedComposite);
for (PropertySource<?> p : reversedComposite) {
incoming.addFirst(p);
}
PropertySourceBootstrapProperties remoteProperties = new PropertySourceBootstrapProperties();
Binder.get(environment(incoming)).bind("spring.cloud.config",
Bindable.ofInstance(remoteProperties));
if (!remoteProperties.isAllowOverride() || (!remoteProperties.isOverrideNone()
&& remoteProperties.isOverrideSystemProperties())) {
for (PropertySource<?> p : reversedComposite) {
propertySources.addFirst(p);
}
return;
}
if (remoteProperties.isOverrideNone()) {
for (PropertySource<?> p : composite) {
propertySources.addLast(p);
}
return;
}
if (propertySources.contains(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) {
if (!remoteProperties.isOverrideSystemProperties()) {
for (PropertySource<?> p : reversedComposite) {
propertySources.addAfter(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, p);
}
}
else {
for (PropertySource<?> p : composite) {
propertySources.addBefore(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, p);
}
}
}
else {
for (PropertySource<?> p : composite) {
propertySources.addLast(p);
}
}
}
private Environment environment(MutablePropertySources incoming) {
StandardEnvironment environment = new StandardEnvironment();
for (PropertySource<?> source : environment.getPropertySources()) {
environment.getPropertySources().remove(source.getName());
}
for (PropertySource<?> source : incoming) {
environment.getPropertySources().addLast(source);
}
return environment;
}
private void handleIncludedProfiles(ConfigurableEnvironment environment) {
Set<String> includeProfiles = new TreeSet<>();
for (PropertySource<?> propertySource : environment.getPropertySources()) {
addIncludedProfilesTo(includeProfiles, propertySource);
}
List<String> activeProfiles = new ArrayList<>();
Collections.addAll(activeProfiles, environment.getActiveProfiles());
// If it's already accepted we assume the order was set intentionally
includeProfiles.removeAll(activeProfiles);
if (includeProfiles.isEmpty()) {
return;
}
// Prepend each added profile (last wins in a property key clash)
for (String profile : includeProfiles) {
activeProfiles.add(0, profile);
}
environment.setActiveProfiles(
activeProfiles.toArray(new String[activeProfiles.size()]));
}
private Set<String> addIncludedProfilesTo(Set<String> profiles,
PropertySource<?> propertySource) {
if (propertySource instanceof CompositePropertySource) {
for (PropertySource<?> nestedPropertySource : ((CompositePropertySource) propertySource)
.getPropertySources()) {
addIncludedProfilesTo(profiles, nestedPropertySource);
}
}
else {
Collections.addAll(profiles, getProfilesForValue(propertySource.getProperty(
ConfigFileApplicationListener.INCLUDE_PROFILES_PROPERTY)));
}
return profiles;
}
private String[] getProfilesForValue(Object property) {
final String value = (property == null ? null : property.toString());
return property == null ? new String[0]
: StringUtils.tokenizeToStringArray(value, ",");
}
}
从源码可以看到它提供了一个动态加载配置的接口PropertySourceLocator。因此要想动态加载自己的配置。必须实现这个接口。
Spring Cloud官方给出的说明是:
因此要动态加载配置,只需要实现PropertySourceLocator,并将实现类放入到spring容器中就行了。
本例中使用的nacos .因此spring-cloud-starter-alibaba-nacos-config中肯定会提供nacos对应PropertySourceLocator的实现。
也确实如此,包中NacosPropertySourceLocator实现了这个接口。如下:
NacosPropertySourceLocator 有三个重要的属性。
① NacosConfigProperties
nacos配置属性的封装类,封装连接到Nacos Server的相关配置
② NacosPropertySourceBuilder
名字可以看到,使用了建造者设计模式,用于构建NacosPropertySource实例。这个里面还封装了从远程加载配置的逻辑。
③NacosConfigManager
Nacos配置管理器,主要实现ConfigService的构建逻辑。
ConfigService是Nacos提供的用于操作Nacos配置的接口。有兴趣去Nacos官网一探究竟。
分析加载配置源码之前,来了解一下Nacos的数据模型:
Nacos 数据模型 Key 由三元组唯一确定, 值为:Namespace(默认为"")+ Group + Service/DataId 。这样能唯一确定Nacos的一条配置数据。
实际项目中,一个key对应一个应用的配置(prod/dev/test)
再来看PropertySourceLocator#locate(Environment env)方法,它是PropertySourceLocator留给各个配置中心厂商实现的获取配置的核心方法。
@Order(0)
public class NacosPropertySourceLocator implements PropertySourceLocator {
private static final Logger log = LoggerFactory
.getLogger(NacosPropertySourceLocator.class);
private static final String NACOS_PROPERTY_SOURCE_NAME = "NACOS";
private static final String SEP1 = "-";
private static final String DOT = ".";
private NacosPropertySourceBuilder nacosPropertySourceBuilder;
private NacosConfigProperties nacosConfigProperties;
private NacosConfigManager nacosConfigManager;
/**
* recommend to use
* {@link NacosPropertySourceLocator#NacosPropertySourceLocator(com.alibaba.cloud.nacos.NacosConfigManager)}.
* @param nacosConfigProperties nacosConfigProperties
*/
@Deprecated
public NacosPropertySourceLocator(NacosConfigProperties nacosConfigProperties) {
this.nacosConfigProperties = nacosConfigProperties;
}
public NacosPropertySourceLocator(NacosConfigManager nacosConfigManager) {
//构造函数必须传入NacosConfigManager 。包装ConfigService和NacosConfigProperties
this.nacosConfigManager = nacosConfigManager;
this.nacosConfigProperties = nacosConfigManager.getNacosConfigProperties();
}
@Override
public PropertySource<?> locate(Environment env) {
nacosConfigProperties.setEnvironment(env);
ConfigService configService = nacosConfigManager.getConfigService();
//如果ConfigService没有初始化,则返回null
if (null == configService) {
log.warn("no instance of config service found, can't load config from nacos");
return null;
}
//从Nacos Server拉取配置超时时间
long timeout = nacosConfigProperties.getTimeout();
//新建一个NacosPropertySourceBuilder实例,用于拉取配置文件
nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,timeout);
// nacos config dataId name
String name = nacosConfigProperties.getName();
//nacos config dataId prefix
String dataIdPrefix = nacosConfigProperties.getPrefix();
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = name;
}
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = env.getProperty("spring.application.name");
}
//NACOS_PROPERTY_SOURCE_NAME=NACOS
//CompositePropertySource是一个PropertySource容器
CompositePropertySource composite = new CompositePropertySource(NACOS_PROPERTY_SOURCE_NAME);
//加载nacos三种不同命名方式配置
//获取spring.cloud.nacos.config.shared-configs[0]=xxx 这种方式配置的配置
loadSharedConfiguration(composite);
//获取spring.cloud.nacos.config.extension-configs[0]=xxx这种方式配置的配置
loadExtConfiguration(composite);
//使用fileExtension和dataId等属性拼三种组合的配置文件名,然后去nacos Server获取
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
//将从nacosServer获取的配置存放在CompositePropertySource中后返回
return composite;
}
/**
* load shared configuration.
*/
private void loadSharedConfiguration(
CompositePropertySource compositePropertySource) {
List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties
.getSharedConfigs();
if (!CollectionUtils.isEmpty(sharedConfigs)) {
checkConfiguration(sharedConfigs, "shared-configs");
loadNacosConfiguration(compositePropertySource, sharedConfigs);
}
}
/**
* load extensional configuration.
*/
private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties
.getExtensionConfigs();
if (!CollectionUtils.isEmpty(extConfigs)) {
checkConfiguration(extConfigs, "extension-configs");
loadNacosConfiguration(compositePropertySource, extConfigs);
}
}
/**
* load configuration of 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
for (String profile : environment.getActiveProfiles()) {
String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
fileExtension, true);
}
}
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 checkConfiguration(List<NacosConfigProperties.Config> configs,
String tips) {
String[] dataIds = new String[configs.size()];
for (int i = 0; i < configs.size(); i++) {
String dataId = configs.get(i).getDataId();
if (dataId == null || dataId.trim().length() == 0) {
throw new IllegalStateException(String.format(
"the [ spring.cloud.nacos.config.%s[%s] ] must give a dataId",
tips, i));
}
dataIds[i] = dataId;
}
// Just decide that the current dataId must have a suffix
NacosDataParserHandler.getInstance().checkDataId(dataIds);
}
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);
}
/**
* Add the nacos configuration to the first place and maybe ignore the empty
* configuration.
*/
private void addFirstPropertySource(final CompositePropertySource composite,
NacosPropertySource nacosPropertySource, boolean ignoreEmpty) {
if (null == nacosPropertySource || null == composite) {
return;
}
if (ignoreEmpty && nacosPropertySource.getSource().isEmpty()) {
return;
}
composite.addFirstPropertySource(nacosPropertySource);
}
public void setNacosConfigManager(NacosConfigManager nacosConfigManager) {
this.nacosConfigManager = nacosConfigManager;
}
}
以上分析了从配置中心拉取配置的流程。那么问题来了,当从配置中心改了应用的配置。应用怎样第一时间感知呢?
- 添加监听器,监视配置中心对配置的修改
这一点Nacos给的实现是,在Nacos Server配置文件上添加监听器。当Nacos Server上修改配置时可以及时发布到应用上。
具体实现类是NacosContextRefresher ,这是一个事件监听器,在spring boot应用启动后触发ApplicationReadyEvent,给NacosServer添加监听器,监听对应Nacos数据模型。
NacosContextRefresher结构图如下:
熟悉Spring Boot事件源码的小伙伴肯定对这个又很熟悉。不熟悉的看我往期文章:Spring Boot之事件原理剖析
继续直接撸源码:
public class NacosContextRefresher
implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
private final static Logger log = LoggerFactory
.getLogger(NacosContextRefresher.class);
private static final AtomicLong REFRESH_COUNT = new AtomicLong(0);
private NacosConfigProperties nacosConfigProperties;
private final boolean isRefreshEnabled;
private final NacosRefreshHistory nacosRefreshHistory;
private final ConfigService configService;
private ApplicationContext applicationContext;
private AtomicBoolean ready = new AtomicBoolean(false);
private Map<String, Listener> listenerMap = new ConcurrentHashMap<>(16);
public NacosContextRefresher(NacosConfigManager nacosConfigManager,
NacosRefreshHistory refreshHistory) {
this.nacosConfigProperties = nacosConfigManager.getNacosConfigProperties();
this.nacosRefreshHistory = refreshHistory;
this.configService = nacosConfigManager.getConfigService();
this.isRefreshEnabled = this.nacosConfigProperties.isRefreshEnabled();
}
/**
* recommend to use
* {@link NacosContextRefresher#NacosContextRefresher(NacosConfigManager, NacosRefreshHistory)}.
* @param refreshProperties refreshProperties
* @param refreshHistory refreshHistory
* @param configService configService
*/
@Deprecated
public NacosContextRefresher(NacosRefreshProperties refreshProperties,
NacosRefreshHistory refreshHistory, ConfigService configService) {
this.isRefreshEnabled = refreshProperties.isEnabled();
this.nacosRefreshHistory = refreshHistory;
this.configService = configService;
}
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// many Spring context
//这里给注册逻辑上了一个锁,让注册事件逻辑在spring boot应用启动时只执行一次
if (this.ready.compareAndSet(false, true)) {
//注册监听器到NacosServer
this.registerNacosListenersForApplications();
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/**
* register Nacos Listeners.
*/
private void registerNacosListenersForApplications() {
if (isRefreshEnabled()) {
//NacosPropertySourceRepository是一个内存容器,存放已经加载过的NacosPropertySource
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) {
//生产监听器的key
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
//新建一个Listener
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
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 Server添加监听
configService.addListener(dataKey, groupKey, listener);
}
catch (NacosException e) {
log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,groupKey), e);
}
}
public NacosConfigProperties getNacosConfigProperties() {
return nacosConfigProperties;
}
public NacosContextRefresher setNacosConfigProperties(
NacosConfigProperties nacosConfigProperties) {
this.nacosConfigProperties = nacosConfigProperties;
return this;
}
public boolean isRefreshEnabled() {
if (null == nacosConfigProperties) {
return isRefreshEnabled;
}
// Compatible with older configurations
if (nacosConfigProperties.isRefreshEnabled() && !isRefreshEnabled) {
return false;
}
return isRefreshEnabled;
}
public static long getRefreshCount() {
return REFRESH_COUNT.get();
}
public static void refreshCountIncrement() {
REFRESH_COUNT.incrementAndGet();
}
}
- 这里注册逻辑上了把锁,防止注册多次,因为在spring boot应用中,可能存在多个SpringApplication(父子关系)实例的情况。
每次初始化SpringApplication都会重新走一次事件发布流程。因此可能会发布两次ApplicationReadyEvent事件。- NacosPropertySourceRepository是一个NacosPropertySource实例容器。在NacosPropertySourceBuilder执行build()方法加载远端配置时,会将加载成功的配置(NacosPropertySource)存入到NacosPropertySourceRepository容器中。
到此Nacos作为配置中心,Spring Cloud项目动态从Nacos配置中心拉取配置,初始化应用的流程就全部理清楚了。现在来看一下实际实战。
- 启动Nacos Server 。如果想让配置持久化请使用DB的方式存储数据
- 新建配置
① 新建一个命名空间,实际项目中最好不要使用默认public命名空间
新增后,会生成一个命名空间的ID,现在项目配置命名空间时,就是使用这个id作为命名空间。如下:
② 新建一个配置
在刚新建的命名空间下,新增一个配置
DataId可以填两种,官方给出的格式是: s p r i n g . a p p l i c a t i o n . n a m e . {spring.application.name}. spring.application.name.{file-extension:properties} 或者 s p r i n g . a p p l i c a t i o n . n a m e − {spring.application.name}- spring.application.name−{profile}.${file-extension:properties}
或者参考上文中获取配置的逻辑里面定义的要获取的配置文件的名称
- 配置应用。使其能连接到NacosServer成功获取配置
如上配置,就可以成功的从配置中心,获取配置启动Spring Cloud应用啦!
以上都是作者辛苦浏览各个技术框架的官方文档以及结合框架源码总结而来。转载请注明出处!