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()
publicstaticvoidmain(String[] args) { SpringApplication.run(Demo1Application.class, args); }
2.2 进入run方法 内部实现再次进入run方法,再次进入run方法
publicstaticConfigurableApplicationContextrun(Class<?>primarySource, String... args) { returnrun(newClass<?>[] { 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}*/publicstaticConfigurableApplicationContextrun(Class<?>[] primarySources, String[] args) { // 此处new SpringApplication 进行一些默认的初始化returnnewSpringApplication(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}*/publicConfigurableApplicationContextrun(String... args) { StopWatchstopWatch=newStopWatch(); stopWatch.start(); ConfigurableApplicationContextcontext=null; Collection<SpringBootExceptionReporter>exceptionReporters=newArrayList<>(); configureHeadlessProperty(); SpringApplicationRunListenerslisteners=getRunListeners(args); listeners.starting(); try { ApplicationArgumentsapplicationArguments=newDefaultApplicationArguments(args); // 在此处进行环境的处理 也就是配置文件的的读取和加载ConfigurableEnvironmentenvironment=prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); BannerprintedBanner=printBanner(environment); context=createApplicationContext(); exceptionReporters=getSpringFactoriesInstances(SpringBootExceptionReporter.class, newClass[] { ConfigurableApplicationContext.class }, context); prepareContext(context, environment, listeners, applicationArguments, printedBanner); refreshContext(context); afterRefresh(context, applicationArguments); stopWatch.stop(); if (this.logStartupInfo) { newStartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } listeners.started(context); callRunners(context, applicationArguments); } catch (Throwableex) { handleRunFailure(context, ex, exceptionReporters, listeners); thrownewIllegalStateException(ex); } try { listeners.running(context); } catch (Throwableex) { handleRunFailure(context, ex, exceptionReporters, null); thrownewIllegalStateException(ex); } returncontext; }
2.4 进入prepareEnvironment方法,此时会进入执行监听器的environmentPrepared方法
privateConfigurableEnvironmentprepareEnvironment(SpringApplicationRunListenerslisteners, ApplicationArgumentsapplicationArguments) { // Create and configure the environmentConfigurableEnvironmentenvironment=getOrCreateEnvironment(); configureEnvironment(environment, applicationArguments.getSourceArgs()); ConfigurationPropertySources.attach(environment); // 执行监听器的方法listeners.environmentPrepared(environment); bindToSpringApplication(environment); if (!this.isCustomEnvironment) { environment=newEnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, deduceEnvironmentClass()); } ConfigurationPropertySources.attach(environment); returnenvironment; }
2.5 进入environmentPrepared方法,此处循环所有的监听器,并执行方法
voidenvironmentPrepared(ConfigurableEnvironmentenvironment) { for (SpringApplicationRunListenerlistener : this.listeners) { listener.environmentPrepared(environment); } }
可以通过debug看到只有一个监听器
2.6 进入listener.environmentPrepared,创建一个事件并执行事件
publicvoidenvironmentPrepared(ConfigurableEnvironmentenvironment) { this.initialMulticaster .multicastEvent(newApplicationEnvironmentPreparedEvent(this.application, this.args, environment)); }
2.7 进入该方法 multicastEvent,并再次进入重载方法 multicastEvent
publicvoidmulticastEvent(finalApplicationEventevent, ResolvableTypeeventType) { ResolvableTypetype= (eventType!=null?eventType : resolveDefaultEventType(event)); Executorexecutor=getTaskExecutor(); // getApplicationListeners 获取监听器列表for (ApplicationListener<?>listener : getApplicationListeners(event, type)) { if (executor!=null) { executor.execute(() ->invokeListener(listener, event)); } else { // 执行处理方法invokeListener(listener, event); } } }
可以通过debug进入getApplicationListeners看一下,这里根据事件的类型查询可以处理该事件的监听器
其中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*/protectedvoidinvokeListener(ApplicationListener<?>listener, ApplicationEventevent) { ErrorHandlererrorHandler=getErrorHandler(); if (errorHandler!=null) { try { doInvokeListener(listener, event); } catch (Throwableerr) { errorHandler.handleError(err); } } else { doInvokeListener(listener, event); } } // 重载方法privatevoiddoInvokeListener(ApplicationListenerlistener, ApplicationEventevent) { try { // 可以看到是调用了对应监听器的onApplicationEvent方法listener.onApplicationEvent(event); } catch (ClassCastExceptionex) { Stringmsg=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.Loglogger=LogFactory.getLog(getClass()); if (logger.isTraceEnabled()) { logger.trace("Non-matching event type for listener: "+listener, ex); } } else { throwex; } } }
2.9 ConfigFileApplicationListener$onApplicationEvent方法
ConfigFileApplicationListener就是最终进行操作的类,在这个类里定义了配置文件默认目录和默认名字
进入onApplicationEvent方法
看到基于不同的事件,进行不同的处理
publicvoidonApplicationEvent(ApplicationEventevent) { if (eventinstanceofApplicationEnvironmentPreparedEvent) { // 进入此方法,加载配置文件onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event); } if (eventinstanceofApplicationPreparedEvent) { onApplicationPreparedEvent(event); } }
2.10 配置文件的加载
接下来就是配置文件的加载,接下面的源码就是分析SpringBoot如何加载application-xxx.yml
2.10.1 onApplicationEnvironmentPreparedEvent方法
紧跟上文进入onApplicationEnvironmentPreparedEvent方法
privatevoidonApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEventevent) { // 根据spring.factories文件加载处理器. 有兴趣可以debug进去看下List<EnvironmentPostProcessor>postProcessors=loadPostProcessors(); // 把ConfigFileApplicationListener也加进去postProcessors.add(this); // 根据Order排序处理器AnnotationAwareOrderComparator.sort(postProcessors); // 循环执行处理器的处理方法for (EnvironmentPostProcessorpostProcessor : postProcessors) { postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication()); } }
此处重点在于把ConfigFileApplicationListener加载到了postProcessors中,
可以debug看一下postProcessors
然后接下来就会执行ConfigFileApplicationListener.postProcessEnvironment方法
2.10.2 postProcessEnvironment
进入ConfigFileApplicationListener.postProcessEnvironment方法
在进入addPropertySources方法,
可以看到new Loader(environment, resourceLoader).load();
Loader是ConfigFileApplicationListener的一个内部类,在load方法内进行配置文件的加载
publicvoidpostProcessEnvironment(ConfigurableEnvironmentenvironment, SpringApplicationapplication) { 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)*/protectedvoidaddPropertySources(ConfigurableEnvironmentenvironment, ResourceLoaderresourceLoader) { RandomValuePropertySource.addToEnvironment(environment); newLoader(environment, resourceLoader).load(); }
2.10.3 load方法
这串代码就是配置文件的加载
voidload() { FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY, (defaultProperties) -> { // 初始化配置this.profiles=newLinkedList<>(); this.processedProfiles=newLinkedList<>(); // 默认启用false this.activatedProfiles=false; this.loaded=newLinkedHashMap<>(); // 初始化配置, 也就是defaultinitializeProfiles(); // 初始化后this.profiles会有一个default的配置while (!this.profiles.isEmpty()) { Profileprofile=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的配置
循环配置调用重载的load方法
2.10.4 根据profile获取配置文件目录和配置文件名
进入上文中的load方法
privatevoidload(Profileprofile, DocumentFilterFactoryfilterFactory, DocumentConsumerconsumer) { // getSearchLocations这个会获取默认的配置文件路径 getSearchLocations().forEach((location) -> { // 是否是目录,默认配置文件路径都是以/结尾的booleanisDirectory=location.endsWith("/"); // 获取默认的配置文件名称也就是spring.config.name属性 默认applicationSet<String>names=isDirectory?getSearchNames() : NO_SEARCH_NAMES; // 再次循环 调用load重载方法names.forEach((name) ->load(location, name, profile, filterFactory, consumer)); }); }
可以看到getSearchLocations方法会获取配置文件路径,如下,正是SpringBoot默认的配置文件加载顺序,但是这个顺序是反过来的
2.10.5 循环配置文件后缀名
进入load重载方法
privatevoidload(Stringlocation, Stringname, Profileprofile, DocumentFilterFactoryfilterFactory, DocumentConsumerconsumer) { // 如果配置文件名称为空进行处理,默认都是有值的 applicationif (!StringUtils.hasText(name)) { for (PropertySourceLoaderloader : this.propertySourceLoaders) { if (canLoadFileExtension(loader, location)) { load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer); return; } } thrownewIllegalStateException("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=newHashSet<>(); // this.propertySourceLoaders 配置文件加载器默认有两种实现 yaml和propertiesfor (PropertySourceLoaderloader : this.propertySourceLoaders) { // 根据配置文件加载器循环for (StringfileExtension : loader.getFileExtensions()) { if (processed.add(fileExtension)) { // 根据路径+名字+文件后缀名加载配置文件 loadForFileExtension(loader, location+name, "."+fileExtension, profile, filterFactory, consumer); } } } }
this.propertySourceLoaders看一下
2.10.6 loadForFileExtension方法
继续重载
privatevoidloadForFileExtension(PropertySourceLoaderloader, Stringprefix, StringfileExtension, Profileprofile, DocumentFilterFactoryfilterFactory, DocumentConsumerconsumer) { DocumentFilterdefaultFilter=filterFactory.getDocumentFilter(null); DocumentFilterprofileFilter=filterFactory.getDocumentFilter(profile); // 当profile不为空时处理即 active.profiles=xxx时 第一次进行不走这,还是默认的if (profile!=null) { // Try profile-specific file & profile section in profile file (gh-340)StringprofileSpecificFile=prefix+"-"+profile+fileExtension; load(loader, profileSpecificFile, profile, defaultFilter, consumer); load(loader, profileSpecificFile, profile, profileFilter, consumer); // Try profile specific sections in files we've already processedfor (ProfileprocessedProfile : this.processedProfiles) { if (processedProfile!=null) { StringpreviouslyLoaded=prefix+"-"+processedProfile+fileExtension; load(loader, previouslyLoaded, profile, profileFilter, consumer); } } } // 默认的配置文件处理 default// Also try the profile-specific section (if any) of the normal fileload(loader, prefix+fileExtension, profile, profileFilter, consumer); }
2.10.7 配置文件的读取和属性合并
这次重载就是到头了,在这个方法里就会进行属性的读取
privatevoidload(PropertySourceLoaderloader, Stringlocation, Profileprofile, DocumentFilterfilter, DocumentConsumerconsumer) { // 根据2.10.6 中拼接的路径加载Resource[] resources=getResources(location); for (Resourceresource : resources) { try { // 当该拼接的文件不存在时,会直接进行下一次循环if (resource==null||!resource.exists()) { if (this.logger.isTraceEnabled()) { StringBuilderdescription=getDescription("Skipped missing config ", location, resource, profile); this.logger.trace(description); } continue; } // 配置文件为空时也不加载if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) { if (this.logger.isTraceEnabled()) { StringBuilderdescription=getDescription("Skipped empty config extension ", location, resource, profile); this.logger.trace(description); } continue; } // 隐藏路径时返回 安全?if (resource.isFile() &&hasHiddenPathElement(resource)) { if (this.logger.isTraceEnabled()) { StringBuilderdescription=getDescription("Skipped location with hidden path element ", location, resource, profile); this.logger.trace(description); } continue; } // 开始正式加载Stringname="applicationConfig: ["+getLocationName(location, resource) +"]"; // 加载配置文件的元素List<Document>documents=loadDocuments(loader, name, resource); // 如果配置文件里没有解析出元素if (CollectionUtils.isEmpty(documents)) { if (this.logger.isTraceEnabled()) { StringBuilderdescription=getDescription("Skipped unloaded config ", location, resource, profile); this.logger.trace(description); } continue; } List<Document>loaded=newArrayList<>(); // 默认active profile 为defaultfor (Documentdocument : 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()) { StringBuilderdescription=getDescription("Loaded config file ", location, resource, profile); this.logger.debug(description); } } } catch (Exceptionex) { StringBuilderdescription=getDescription("Failed to load property source from ", location, resource, profile); thrownewIllegalStateException(description.toString(), ex); } } }
- loadDocuments方法可以进去看下,PropertiesPropertySourceLoader 和 YamlPropertySourceLoader加载配置文件的详细代码,其实就是读取文件
- Collections.reverse(loaded);这个方法印证了SpringBoot配置文件的加载顺序,在上文中我们看到了SpringBoot读取配置文件目录的顺序是
- 按这个顺序加载文件.这也印证了SpringBoot配置文件的加载优先级
- consumer.accept(profile, document),回调中合并
2.10.8 回调合并属性 addToLoaded
privateDocumentConsumeraddToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>>addMethod, booleancheckForExisting) { return (profile, document) -> { if (checkForExisting) { // 检查属性是否存在,如果是第一次加载默认的配置文件这个参数为false,2.10.3中可以看到for (MutablePropertySourcesmerged : this.loaded.values()) { // 如果参数已存在 就不在加载了if (merged.contains(document.getPropertySource().getName())) { return; } } } // 第一次加载或者属性不存在的化直接放入this.loadedMutablePropertySourcesmerged=this.loaded.computeIfAbsent(profile, (k) ->newMutablePropertySources()); // 调用回调 MutablePropertySources::addLastaddMethod.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多了一个,并且优先级相当高
进入该监听器
3.2 BootstrapApplicationListener的onApplicationEvent
进入BootstrapApplicationListener的onApplicationEvent方法,在此方法中进行bootsrap.yml的读取
StringconfigLocation=environment .resolvePlaceholders("${spring.cloud.bootstrap.location:}"); StringconfigAdditionalLocation=environment .resolvePlaceholders("${spring.cloud.bootstrap.additional-location:}"); Map<String, Object>bootstrapMap=newHashMap<>(); // 给配置文件一个名字 加载bootstrap,yml就是靠这个bootstrapMap.put("spring.config.name", configName); publicvoidonApplicationEvent(ApplicationEnvironmentPreparedEventevent) { // 获取上下文信息ConfigurableEnvironmentenvironment=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 contextif (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) { return; } // 开始初始化上下文ConfigurableApplicationContextcontext=null; // 获取配置文件名称 默认bootstrap (bootstrap.yml) 就是这里给配置文件复制了StringconfigName=environment .resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}"); // 寻找是否有父容器上下文的初始化器 ParentContextApplicationContextInitializer 正常情况下是没有的 直接往下走for (ApplicationContextInitializer<?>initializer : event.getSpringApplication() .getInitializers()) { if (initializerinstanceofParentContextApplicationContextInitializer) { context=findBootstrapContext( (ParentContextApplicationContextInitializer) initializer, configName); } } // 没有ParentContextApplicationContextInitializer 父容器初始化的化,就创建一个if (context==null) { context=bootstrapServiceContext(environment, event.getSpringApplication(), configName); event.getSpringApplication() .addListeners(newCloseContextOnFailureApplicationListener(context)); } apply(context, event.getSpringApplication(), environment); }
进入bootstrapServiceContext方法,方法太长不再全部粘贴,这个方法里最重要的就是根据bootstrap.yml创建出一个SpringApplication对象
// 创建SpringApplication对象 SpringApplicationBuilderbuilder=newSpringApplicationBuilder() .profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF) .environment(bootstrapEnvironment) // Don't use the default properties in this builder .registerShutdownHook(false).logStartupInfo(false) .web(WebApplicationType.NONE); finalSpringApplicationbuilderApplication=builder.application(); // 创建context对象 注意: 此处是又执行了一次 SpringApplication.run()方法.builder.sources(BootstrapImportSelectorConfiguration.class); finalConfigurableApplicationContextcontext=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 如下图.
在上文中2.10.4中使用getSearchNames获取要加载的文件名,看下这个方法
privateSet<String>getSearchNames() { if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) { //SpringBoot默认不走这个 bootstrap.yml的加载就是依赖这里Stringproperty=this.environment.getProperty(CONFIG_NAME_PROPERTY); Set<String>names=asResolvedSet(property, null); names.forEach(this::assertValidConfigName); returnnames; } returnasResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES); // 返回默认的Application}
这样一来,除了加载文件的名字改变了,其他的都没变,还是SpringBoot这一套.
4. 总结
通过对SpringBoot2.3.5的源码的阅读,学习到如下:
- SpringBoot启动时基于事件处理,增加了扩展性,SpringCloud配置文件的加载就是这样
- SpringBoot和SpringCloud配置文件的加载,从源码角度上分析配置文件的加载,还有顺序,到底是如何覆盖的
- SpringBoot的代码写的真好呀!!