SpringBoot及SpringCloud配置文件加载分析(源码解读)

SpringBoot版本 2.3.5.RELEASE
SpringCloud版本 Hoxton.SR9

本文只讨论配置文件加载,以bootstrap.yml和application.yml为例,后缀名的加载顺序可以通过源码看到.

bootstrap.yml是SpringCloud使用的配置文件,SpringBoot中其实并没有加载bootStrap.yml的默认实现

1. 概述

SpringBoot加载配置文件的方式是使用了观察者模式,在启动时发出一个事件(ApplicationEnvironmentPreparedEvent),然后基于这个事件,来做配置文件的加载或者其他的一些操作,这种模式扩展性较强.
而bootstrap.yml的加载就借助了这种模式,SpringCloud扩展了一个BootstrapApplicationListener监听器,来处理该事件,在这个监听器里做加载.

2. application.yml的加载

先看Springboot配置文件的加载

从springBoot启动起

2.1 启动类xxxApplication中 SpringApplication.run()

public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class, args);
    }

2.2 进入run方法 内部实现再次进入run方法,再次进入run方法

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
		return run(new Class<?>[] { primarySource }, args);
	}

	/**
	 * Static helper that can be used to run a {@link SpringApplication} from the
	 * specified sources using default settings and user supplied arguments.
	 * @param primarySources the primary sources to load
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return the running {@link ApplicationContext}
	 */
	public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
        // 此处new SpringApplication 进行一些默认的初始化
		return new SpringApplication(primarySources).run(args);
	}

2.3 prepareEnvironment

/**
	 * Run the Spring application, creating and refreshing a new
	 * {@link ApplicationContext}.
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return a running {@link ApplicationContext}
	 */
	public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            // 在此处进行环境的处理 也就是配置文件的的读取和加载
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); 
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

2.4 进入prepareEnvironment方法,此时会进入执行监听器的environmentPrepared方法

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// Create and configure the environment
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		ConfigurationPropertySources.attach(environment);
    	
    	// 执行监听器的方法
		listeners.environmentPrepared(environment);
    
		bindToSpringApplication(environment);
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
					deduceEnvironmentClass());
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}

2.5 进入environmentPrepared方法,此处循环所有的监听器,并执行方法

void environmentPrepared(ConfigurableEnvironment environment) {
		for (SpringApplicationRunListener listener : this.listeners) {
			listener.environmentPrepared(environment);
		}
	}

可以通过debug看到只有一个监听器SpringBoot及SpringCloud配置文件加载分析(源码解读)_第1张图片

2.6 进入listener.environmentPrepared,创建一个事件并执行事件

@Override
	public void environmentPrepared(ConfigurableEnvironment environment) {
		this.initialMulticaster
				.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
	}

2.7 进入该方法 multicastEvent,并再次进入重载方法 multicastEvent

@Override
	public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
		ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
		Executor executor = getTaskExecutor();
        // getApplicationListeners 获取监听器列表
		for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
			if (executor != null) {
				executor.execute(() -> invokeListener(listener, event));
			}
			else {
                // 执行处理方法
				invokeListener(listener, event);
			}
		}
	}

可以通过debug进入getApplicationListeners看一下,这里根据事件的类型查询可以处理该事件的监听器
SpringBoot及SpringCloud配置文件加载分析(源码解读)_第2张图片
其中ConfigFileApplicationListener就是重点对象,就是这个监听器加载了配置文件

2.8 invokeListener 调用监听器

继续进入重载方法

/**
	 * Invoke the given listener with the given event.
	 * @param listener the ApplicationListener to invoke
	 * @param event the current event to propagate
	 * @since 4.1
	 */
	protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
		ErrorHandler errorHandler = getErrorHandler();
		if (errorHandler != null) {
			try {
				doInvokeListener(listener, event);
			}
			catch (Throwable err) {
				errorHandler.handleError(err);
			}
		}
		else {
			doInvokeListener(listener, event);
		}
	}
// 重载方法
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
		try {
            // 可以看到是调用了对应监听器的onApplicationEvent方法
			listener.onApplicationEvent(event);
		}
		catch (ClassCastException ex) {
			String msg = ex.getMessage();
			if (msg == null || matchesClassCastMessage(msg, event.getClass())) {
				// Possibly a lambda-defined listener which we could not resolve the generic event type for
				// -> let's suppress the exception and just log a debug message.
				Log logger = LogFactory.getLog(getClass());
				if (logger.isTraceEnabled()) {
					logger.trace("Non-matching event type for listener: " + listener, ex);
				}
			}
			else {
				throw ex;
			}
		}
	}

2.9 ConfigFileApplicationListener$onApplicationEvent方法

ConfigFileApplicationListener就是最终进行操作的类,在这个类里定义了配置文件默认目录和默认名字
SpringBoot及SpringCloud配置文件加载分析(源码解读)_第3张图片
进入onApplicationEvent方法
看到基于不同的事件,进行不同的处理

@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationEnvironmentPreparedEvent) {
            // 进入此方法,加载配置文件
			onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
		}
		if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent(event);
		}
	}

2.10 配置文件的加载

接下来就是配置文件的加载,接下面的源码就是分析SpringBoot如何加载application-xxx.yml

2.10.1 onApplicationEnvironmentPreparedEvent方法

紧跟上文进入onApplicationEnvironmentPreparedEvent方法

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
		// 根据spring.factories文件加载处理器. 有兴趣可以debug进去看下
    	List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
		// 把ConfigFileApplicationListener也加进去
    	postProcessors.add(this);
    	// 根据Order排序处理器
		AnnotationAwareOrderComparator.sort(postProcessors);
    	
    	// 循环执行处理器的处理方法
		for (EnvironmentPostProcessor postProcessor : postProcessors) {
			postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
		}
	}

此处重点在于把ConfigFileApplicationListener加载到了postProcessors中,
可以debug看一下postProcessors
SpringBoot及SpringCloud配置文件加载分析(源码解读)_第4张图片
然后接下来就会执行ConfigFileApplicationListener.postProcessEnvironment方法

2.10.2 postProcessEnvironment

进入ConfigFileApplicationListener.postProcessEnvironment方法
在进入addPropertySources方法,
可以看到new Loader(environment, resourceLoader).load();
Loader是ConfigFileApplicationListener的一个内部类,在load方法内进行配置文件的加载

@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
    addPropertySources(environment, application.getResourceLoader());
}

/**
	 * Add config file property sources to the specified environment.
	 * @param environment the environment to add source to
	 * @param resourceLoader the resource loader
	 * @see #addPostProcessors(ConfigurableApplicationContext)
	 */
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
    RandomValuePropertySource.addToEnvironment(environment);
    new Loader(environment, resourceLoader).load();
}

2.10.3 load方法

这串代码就是配置文件的加载

void load() {
    FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
                                 (defaultProperties) -> {
                                     
                                     // 初始化配置
                                     this.profiles = new LinkedList<>();
                                     this.processedProfiles = new LinkedList<>();
                                     
                                     // 默认启用false 
                                     this.activatedProfiles = false;
                                     this.loaded = new LinkedHashMap<>();
                                     
                                     // 初始化配置, 也就是default
                                     initializeProfiles();
                                     
                                     // 初始化后this.profiles会有一个default的配置
                                     while (!this.profiles.isEmpty()) {
                                         Profile profile = this.profiles.poll();
                                         if (isDefaultProfile(profile)) {
                                             addProfileToEnvironment(profile.getName());
                                         }
                                         // 进入此方法就可以看到配置文件的加载了 addToLoaded是一个回调,主要是配置属性的合并
                                         load(profile, this::getPositiveProfileFilter,
                                              addToLoaded(MutablePropertySources::addLast, false));
                                         this.processedProfiles.add(profile);
                                     }
                                     load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
                                     addLoadedPropertySources();
                                     applyActiveProfiles(defaultProperties);
                                 });
}

initializeProfiles后会默认初始化一个default的配置
SpringBoot及SpringCloud配置文件加载分析(源码解读)_第5张图片
循环配置调用重载的load方法

2.10.4 根据profile获取配置文件目录和配置文件名

进入上文中的load方法

private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
    // getSearchLocations这个会获取默认的配置文件路径 
    getSearchLocations().forEach((location) -> {
        // 是否是目录,默认配置文件路径都是以/结尾的
        boolean isDirectory = location.endsWith("/");
        // 获取默认的配置文件名称也就是spring.config.name属性 默认application
        Set<String> names = isDirectory ? getSearchNames() : NO_SEARCH_NAMES;
        // 再次循环 调用load重载方法
        names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
    });
}

可以看到getSearchLocations方法会获取配置文件路径,如下,正是SpringBoot默认的配置文件加载顺序,但是这个顺序是反过来的
SpringBoot及SpringCloud配置文件加载分析(源码解读)_第6张图片

2.10.5 循环配置文件后缀名

进入load重载方法

private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
				DocumentConsumer consumer) {
    // 如果配置文件名称为空进行处理,默认都是有值的 application
    if (!StringUtils.hasText(name)) {
        for (PropertySourceLoader loader : this.propertySourceLoaders) {
            if (canLoadFileExtension(loader, location)) {
                load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
                return;
            }
        }
        throw new IllegalStateException("File extension of config file location '" + location
                                        + "' is not known to any PropertySourceLoader. If the location is meant to reference "
                                        + "a directory, it must end in '/'");
    }
    Set<String> processed = new HashSet<>();
    // this.propertySourceLoaders 配置文件加载器默认有两种实现 yaml和properties
    for (PropertySourceLoader loader : this.propertySourceLoaders) {
        // 根据配置文件加载器循环
        for (String fileExtension : loader.getFileExtensions()) {
            if (processed.add(fileExtension)) {
                // 根据路径+名字+文件后缀名加载配置文件 
                loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
                                     consumer);
            }
        }
    }
}

this.propertySourceLoaders看一下
SpringBoot及SpringCloud配置文件加载分析(源码解读)_第7张图片

2.10.6 loadForFileExtension方法

继续重载

private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
				Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
    DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
    DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
    // 当profile不为空时处理即 active.profiles=xxx时 第一次进行不走这,还是默认的
    if (profile != null) {
        // Try profile-specific file & profile section in profile file (gh-340)
        String profileSpecificFile = prefix + "-" + profile + fileExtension;
        load(loader, profileSpecificFile, profile, defaultFilter, consumer);
        load(loader, profileSpecificFile, profile, profileFilter, consumer);
        // Try profile specific sections in files we've already processed
        for (Profile processedProfile : this.processedProfiles) {
            if (processedProfile != null) {
                String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
                load(loader, previouslyLoaded, profile, profileFilter, consumer);
            }
        }
    }
    // 默认的配置文件处理 default
    // Also try the profile-specific section (if any) of the normal file
    load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}

2.10.7 配置文件的读取和属性合并

这次重载就是到头了,在这个方法里就会进行属性的读取

private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
				DocumentConsumer consumer) {
    // 根据2.10.6 中拼接的路径加载
    Resource[] resources = getResources(location);
    for (Resource resource : resources) {
        try {
            // 当该拼接的文件不存在时,会直接进行下一次循环
            if (resource == null || !resource.exists()) {
                if (this.logger.isTraceEnabled()) {
                    StringBuilder description = getDescription("Skipped missing config ", location, resource,
                                                               profile);
                    this.logger.trace(description);
                }
                continue;
            }
            // 配置文件为空时也不加载
            if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
                if (this.logger.isTraceEnabled()) {
                    StringBuilder description = getDescription("Skipped empty config extension ", location,
                                                               resource, profile);
                    this.logger.trace(description);
                }
                continue;
            }
            // 隐藏路径时返回 安全?
            if (resource.isFile() && hasHiddenPathElement(resource)) {
                if (this.logger.isTraceEnabled()) {
                    StringBuilder description = getDescription("Skipped location with hidden path element ",
                                                               location, resource, profile);
                    this.logger.trace(description);
                }
                continue;
            }
            
            // 开始正式加载
            String name = "applicationConfig: [" + getLocationName(location, resource) + "]";
            // 加载配置文件的元素
            List<Document> documents = loadDocuments(loader, name, resource);
            
            // 如果配置文件里没有解析出元素
            if (CollectionUtils.isEmpty(documents)) {
                if (this.logger.isTraceEnabled()) {
                    StringBuilder description = getDescription("Skipped unloaded config ", location, resource,
                                                               profile);
                    this.logger.trace(description);
                }
                continue;
            }
            List<Document> loaded = new ArrayList<>();
            
            // 默认active profile 为default
            for (Document document : documents) {
                if (filter.match(document)) {
                    addActiveProfiles(document.getActiveProfiles());
                    addIncludedProfiles(document.getIncludeProfiles());
                    loaded.add(document);
                }
            }
            
            // 反转属性顺序 目前没看到啥作用.
            Collections.reverse(loaded);
            if (!loaded.isEmpty()) {
                // consumer.accept(profile, document)在回调里合并属性 即高优先级的覆盖低优先级的属性
                loaded.forEach((document) -> consumer.accept(profile, document));
                if (this.logger.isDebugEnabled()) {
                    StringBuilder description = getDescription("Loaded config file ", location, resource,
                                                               profile);
                    this.logger.debug(description);
                }
            }
        }
        catch (Exception ex) {
            StringBuilder description = getDescription("Failed to load property source from ", location,
                                                       resource, profile);
            throw new IllegalStateException(description.toString(), ex);
        }
    }
}
  1. loadDocuments方法可以进去看下,PropertiesPropertySourceLoader 和 YamlPropertySourceLoader加载配置文件的详细代码,其实就是读取文件
  2. Collections.reverse(loaded);这个方法印证了SpringBoot配置文件的加载顺序,在上文中我们看到了SpringBoot读取配置文件目录的顺序是
    1. SpringBoot及SpringCloud配置文件加载分析(源码解读)_第8张图片
    2. 按这个顺序加载文件.这也印证了SpringBoot配置文件的加载优先级
    3. **consumer.accept(profile, document),**回调中合并

2.10.8 回调合并属性 addToLoaded

private DocumentConsumer addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
				boolean checkForExisting) {
    return (profile, document) -> {
        if (checkForExisting) { // 检查属性是否存在,如果是第一次加载默认的配置文件这个参数为false,2.10.3中可以看到
            for (MutablePropertySources merged : this.loaded.values()) {
                // 如果参数已存在 就不在加载了
                if (merged.contains(document.getPropertySource().getName())) {
                    return;
                }
            }
        }
        // 第一次加载或者属性不存在的化直接放入this.loaded
        MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
                                                                    (k) -> new MutablePropertySources());
        // 调用回调 MutablePropertySources::addLast
        addMethod.accept(merged, document.getPropertySource());
    };
}

addToLoaded 主要就是为了高优先级的属性覆盖低优先级的属性

2.11 总结

SpringBoot基于观察者模式,在ApplicationEnvironmentPreparedEvent事件中,对配置文件加载,配置文件的加载主要在ConfigFileApplicationListener这个类中,基于PropertiesPropertySourceLoader 和 YamlPropertySourceLoader加载配置文件的属性,并根据加载文件的顺序来做高优先级的覆盖低优先级的属性.

3.bootstrap.yml的加载

bootstrap.yml的加载其实还是使用SpringBoot加载配置文件,只不过在SpringBoot的加载之前,先创建了一个名为bootstrap(默认)的context

3.1 BootstrapApplicationListener

引入SpringCloud的依赖后
从上文中2.7开始,会发现listeners多了一个,并且优先级相当高
SpringBoot及SpringCloud配置文件加载分析(源码解读)_第9张图片
进入该监听器

3.2 BootstrapApplicationListener的onApplicationEvent

进入BootstrapApplicationListener的onApplicationEvent方法,在此方法中进行bootsrap.yml的读取

String configLocation = environment
				.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
String configAdditionalLocation = environment
				.resolvePlaceholders("${spring.cloud.bootstrap.additional-location:}");
Map<String, Object> bootstrapMap = new HashMap<>();

// 给配置文件一个名字 加载bootstrap,yml就是靠这个
bootstrapMap.put("spring.config.name", configName);
@Override
	public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
        // 获取上下文信息
		ConfigurableEnvironment environment = event.getEnvironment();
        
        // 判断是否启用spring.cloud.bootstrap.enabled属性,默认启用,如果不启用就不加载bootstrap.yml文件,直接结束
		if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class,
				true)) {
			return;
		}
        // 如果已经执行过bootstrap的监听事件,就不再重复执行了,这也是为什么bootstrap.yml属性不变的原因.
		// don't listen to events in a bootstrap context
		if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
			return;
		}
        
        // 开始初始化上下文
		ConfigurableApplicationContext context = null;
        // 获取配置文件名称 默认bootstrap (bootstrap.yml) 就是这里给配置文件复制了
		String configName = environment
				.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
        
        // 寻找是否有父容器上下文的初始化器 ParentContextApplicationContextInitializer 正常情况下是没有的 直接往下走
		for (ApplicationContextInitializer<?> initializer : event.getSpringApplication()
				.getInitializers()) {
			if (initializer instanceof ParentContextApplicationContextInitializer) {
				context = findBootstrapContext(
						(ParentContextApplicationContextInitializer) initializer,
						configName);
			}
		}
        // 没有ParentContextApplicationContextInitializer 父容器初始化的化,就创建一个
		if (context == null) {
			context = bootstrapServiceContext(environment, event.getSpringApplication(),
					configName);
			event.getSpringApplication()
					.addListeners(new CloseContextOnFailureApplicationListener(context));
		}

		apply(context, event.getSpringApplication(), environment);
	}

进入bootstrapServiceContext方法,方法太长不再全部粘贴,这个方法里最重要的就是根据bootstrap.yml创建出一个SpringApplication对象

// 创建SpringApplication对象 
SpringApplicationBuilder builder = new SpringApplicationBuilder()
				.profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
				.environment(bootstrapEnvironment)
				// Don't use the default properties in this builder
				.registerShutdownHook(false).logStartupInfo(false)
				.web(WebApplicationType.NONE);
final SpringApplication builderApplication = builder.application();

// 创建context对象 注意: 此处是又执行了一次 SpringApplication.run()方法.
builder.sources(BootstrapImportSelectorConfiguration.class);
final ConfigurableApplicationContext context = builder.run();

// 设置父容器对象
addAncestorInitializer(application, context);
// It only has properties in it now that we don't want in the parent so remove
// it (and it will be added back later)
bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);

// 合并属性
mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);

3.3 加载bootStarp.yml配置文件原理

加载配置文件还是上面SpringBoot加载application那一套,但是不同的是SpringBoot给的默认的application在这里并没有使用,而是使用了BootStrapApplicationListener中设置的spring.config.name 如下图.
SpringBoot及SpringCloud配置文件加载分析(源码解读)_第10张图片
在上文中2.10.4中使用getSearchNames获取要加载的文件名,看下这个方法

private Set<String> getSearchNames() {
    if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) { //SpringBoot默认不走这个 bootstrap.yml的加载就是依赖这里
        String property = this.environment.getProperty(CONFIG_NAME_PROPERTY); 
        Set<String> names = asResolvedSet(property, null);
        names.forEach(this::assertValidConfigName);
        return names;
    }
    return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES); // 返回默认的Application
}

这样一来,除了加载文件的名字改变了,其他的都没变,还是SpringBoot这一套.

4. 总结

通过对SpringBoot2.3.5的源码的阅读,学习到如下:

  1. SpringBoot启动时基于事件处理,增加了扩展性,SpringCloud配置文件的加载就是这样
  2. SpringBoot和SpringCloud配置文件的加载,从源码角度上分析配置文件的加载,还有顺序,到底是如何覆盖的
  3. SpringBoot的代码写的真好呀!!

你可能感兴趣的:(java,spring,boot,java,spring,cloud)