Spring boot 启动流程及外部化配置方法
目录
- Main 入口
- SpringApplication
- 判断运行环境
- 初始化器和监听器
- 总结
- 执行 run 方法
- 环境变量及配置
- prepareEnvironment
- getOrCreateEnvironment
- environmentPrepared
- environmentPrepared
- multicastEvent
- invokeListener
- doInvokeListener
- 文件监听代码分析
- 确定搜索路径
- 路径搜索
- 添加进PropertySources
- 自定义监听器
- refreshContext
- 总结
平时我们开发Spring boot 项目的时候,一个SpringBootApplication
注解加一个main
方法就可以启动服务器运行起来,那它到底是怎么运行起来的呢?
Main 入口
我们首先从main方法来看源码,逐步深入:
@SpringBootApplication public class AutoDemoApplication { public static void main(String[] args) { // run方法为入口 SpringApplication.run(AutoDemoApplication.class, args); } }
// 继续调用 run 方法 public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) { return run(new Class[]{primarySource}, args); }
// 这个run 方法也很简单 做了两件事 // 1. 实例化 SpringApplication 对象 // 2. 执行对象的 run 方法 public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) { return (new SpringApplication(primarySources)).run(args); }
SpringApplication
那我们来看下SpringApplication
这个对象里面做了什么:
public SpringApplication(Class<?>... primarySources) { this((ResourceLoader)null, primarySources); }
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.sources = new LinkedHashSet(); this.bannerMode = Mode.CONSOLE; this.logStartupInfo = true; this.addCommandLineProperties = true; this.addConversionService = true; this.headless = true; this.registerShutdownHook = true; this.additionalProfiles = Collections.emptySet(); this.isCustomEnvironment = false; this.lazyInitialization = false; this.applicationContextFactory = ApplicationContextFactory.DEFAULT; this.applicationStartup = ApplicationStartup.DEFAULT; this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); // 先把主类保存起来 this.primarySources = new LinkedHashSet(Arrays.asList(primarySources)); // 判断运行项目的类型 this.webApplicationType = WebApplicationType.deduceFromClasspath(); this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class)); // 初始化器:扫描当前路径下 META-INF/spring.factories 文件,加载ApplicationContextInitializer接口实例 this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class)); // 设置监听器: 加载 ApplicationListener 接口实例 this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class)); this.mainApplicationClass = this.deduceMainApplicationClass(); }
判断运行环境
构造方法内部会调用WebApplicationType.deduceFromClasspath()
方法获得应用类型并设置当前应用是普通WEB应用、响应式web应用(REACTIVE)还是非web应用。
this.webApplicationType = WebApplicationType.deduceFromClasspath();
deduceFromClasspath
方法由枚举类WebApplicationType
提供,具体实现如下:
static WebApplicationType deduceFromClasspath() { // 当classpath 下只存在 org.springframework.web.reactive.DispatcherHandler // 不存在 org.springframework.web.servlet.DispatcherServlet 和 org.glassfish.jersey.servlet.ServletContainer // 则运行模式为reactive 非阻塞模式 if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", (ClassLoader)null) && !ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", (ClassLoader)null) && !ClassUtils.isPresent("org.glassfish.jersey.servlet.ServletContainer", (ClassLoader)null)) { return REACTIVE; } else { String[] var0 = SERVLET_INDICATOR_CLASSES; int var1 = var0.length; for(int var2 = 0; var2 < var1; ++var2) { String className = var0[var2]; if (!ClassUtils.isPresent(className, (ClassLoader)null)) { // 普通应用程序 return NONE; } } // servlet 程序,需要加载并启动内嵌的web服务 return SERVLET; } }
推断的过程主要使用了ClassUtils.isPresent
方法,用来判断指定类名是否存在,是否可以进行加载。源代码如下:
public static boolean isPresent(String className, @Nullable ClassLoader classLoader) { try { forName(className, classLoader); return true; } catch (IllegalAccessError var3) { throw new IllegalStateException("Readability mismatch in inheritance hierarchy of class [" + className + "]: " + var3.getMessage(), var3); } catch (Throwable var4) { return false; } }
isPresent
方法调用了forName
方法,如果在调用过程中发生错误则返回false,代表目标类不存在。否则返回true。
我们看下 forName
的部分代码:
public static Class<?> forName(String name, @Nullable ClassLoader classLoader) throws ClassNotFoundException, LinkageError { // 省略部分代码 ClassLoader clToUse = classLoader; if (classLoader == null) { // 如果为空则使用默认类加载器 clToUse = getDefaultClassLoader(); } try { // 返回加载的 class return Class.forName(name, false, clToUse); } catch (ClassNotFoundException var9) { // 如果加载异常 则尝试加载内部类 int lastDotIndex = name.lastIndexOf(46); if (lastDotIndex != -1) { // 拼接内部类 String nestedClassName = name.substring(0, lastDotIndex) + '$' + name.substring(lastDotIndex + 1); try { return Class.forName(nestedClassName, false, clToUse); } catch (ClassNotFoundException var8) { } } throw var9; } }
通过以上python核心代码,可得知 forName
方法主要做的事情就是获得类加载器,尝试直接加载类,如果失败则尝试加载该类的内部类,如果依旧失败则抛出异常。
尝试加载的类其实就是去扫描项目中引入的依赖,看看是否能加载到对应的类。
因此,整个应用类型的判断分为以下几个步骤:
- spring boot 调用 SpringApplication构造方法
- SpringApplication构造方法调用枚举类的deduceFromClasspath方法
- deduceFromClasspath方法通过ClassUtils.isPresent来判断是否能成功加载指定的类
- ClassUtils.isPresent方法通过调用forName方法来判断是否能成功加载
初始化器和监听器
在构造器中我们可以看到尝试去加载两个接口实例,都是利用SPI机制扫描META-INF/spring.factories文件实现的。
ApplicationContextInitializer
这个类当spring boot 上下文Context初始化完成后会调用
ApplicationListener
当 spring boot 启动时 事件 change 都会触发;
该类是重中之重,比如 dubbo 、nacos集成 spring boot ,都是通过它进行拓展的。
我们可以通过打断点的方式来查看加载流程:
根据 接口名 作为 key 值去加载实现类,加载的时候可能分布在多个 jar 包里面,如果我们自己自定义的话,也可以被加载进去。
这两个类都是在spring boot 启动前执行的,能帮我们进行一些拓展操作。我们可以自定义初始化器,按照SPI机制即可:实现相关接口,同时在指定路径下创建文件。
- 我们首先创建一个初始化器以及监听器实现类:
public class StarterApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { System.out.println("applicationContext 初始化完成 ... "); } }
public class StarterApplicationListener implements ApplicationListener { @Override public void onApplicationEvent(ApplicationEvent event) { System.out.println(event.toString()); System.out.println("ApplicationListener .... " + System.currentTimeMillis()); } }
- 创建指定文件 META-INF/spring.factories
key 值为接口全限定名 :value 值为实现类全限定名
org.springframework.context.ApplicationContextInitializer=\ com.example.autodemo.listener.StarterApplicationContextInitializer org.springframework.context.ApplicationListener=\ com.example.autodemo.listener.StarterApplicationListener
接口名实在找不到,点进类里面自己拼装一下即可。
- 重新运行项目进行测试
可以看到我们自定义的初始化器和监听器加载成功
总结
如上就是 SpringApplication
初始化的代码,基本上没做啥事,主要就是判断了一下运行环境以及利用SPI机制加载META-INF/spring.factories下定义的事件监听器接口实现类。
执行 run 方法
我们直接看 run 方法的源码,逐步分析:
public ConfigurableApplicationContext run(String... args) { long startTime = System.nanoTime(); DefaultBootstrapContext bootstrapContext = this.createBootstrapContext(); ConfigurableApplicationContext context = null; // 不是重点,设置环境变量 this.configureHeadlessProperty(); // 获取事件监听器 SpringApplicationRunListeners 类型 SpringApplicationRunListeners listeners = this.getRunListeners(args); // 执行 start 方法 listeners.starting(bootstrapContext, this.mainApplicationClass); try { // 封装参数 ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // 重点来了嗷~~~ // 准备容器环境,会加载配置文件 // 在这个方法里面会调用所有监听器Listeners的 onApplicationEvent(event),其中就有一个与配置文件相关的监听器被加载:ConfigFileApplicationListener ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments); // 判断环境的值,并设置一些环境的值 this.configureIgnoreBeanInfo(environment); // 打印 banner 可以自定义 Banner printedBanner = this.printBanner(environment); // 根据项目类型创建上下文 context = this.createApplicationContext(); context.setApplicationStartup(this.applicationStartup); // 准备上下文 this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); // 重点流程!!! // spring 的启动代码 这里去扫描并且初始化单实例 bean ,同时启动了内置web容器 this.refreshContext(context); // 空的 this.afterRefresh(context, applicationArguments); Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime); if (this.logStartupInfo) { (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), timeTakenToStartup); } // 执行 AppilcationRunListeners 中的started方法 listeners.started(context, timeTakenToStartup); // 执行 runner this.callRunners(context, applicationArguments); } catch (Throwable var12) { this.handleRunFailure(context, var12, listeners); throw new IllegalStateException(var12); } try { Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime); listeners.ready(context, timeTakenToReady); return context; } catch (Throwable var11) { this.handleRunFailure(context, var11, (SpringApplicationRunListeners)null); throw new IllegalStateException(var11); } }
环境变量及配置
我们直接来看环境变量相关的实现流程:
prepareEnvironment
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) { // 创建和配置环境变量 ConfigurableEnvironment environment = this.getOrCreateEnvironment(); this.configureEnvironment((ConfigurableEnvironment)environment, applicationArguments.getSourceArgs()); ConfigurationPropertySources.attach((Environment)environment); listeners.environmentPrepared(bootstrapContext, (ConfigurableEnvironment)environment); DefaultPropertiesPropertySource.moveToEnd((ConfigurableEnvironment)environment); Assert.state(!((ConfigurableEnvironment)environment).containsProperty("spring.main.environment-prefix"), "Environment prefix cannot be set via properties."); this.bindToSpringApplication((ConfigurableEnvironment)environment); if (!this.isCustomEnvironment) { environment = this.convertEnvironment((ConfigurableEnvironment)environment); } ConfigurationPropertySources.attach((Environment)environment); return (ConfigurableEnvironment)environment; }
刚进来我们就可以看到一个核心方法,看名字我们就可以知道这里是去获取或者创建环境,我们看看其源码。
getOrCreateEnvironment
该方法根据 webApplicationType
判断当前项目是什么类型,这部分我们在SpringAlication
中看到过:WebApplicationType.deduceFromClasspath()
。
总共定义了三个类型:
- None :应用程序不作为WEB程序启动,不启动内嵌的服务。
- SERVLET:应用程序基于servlet的web应用启动,需启动内嵌servlet服务。
- REAVTIVE:应用程序以响应式web应用启动,需启动内嵌响应式web服务。
private ConfigurableEnvironment getOrCreateEnvironment() { if (this.environment != null) { return this.environment; } else { switch(this.webApplicationType) { case SERVLET: return new ApplicationServletEnvironment(); case REACTIVE: return new ApplicationReactiveWebEnvironment(); default: return new ApplicationEnvironment(); } } }
由于我们是 servlet 项目,所以应该调用 ApplicationServletEnvironment
,如果版本不一致,名字应该是:StandardServletEnvironment()
方法。
ApplicationServletEnvironment
继承了 StandardServletEnvironment
类,StandardServletEnvironment
又继承了StandardEnvironment
类,StandardEnvironment
又继承了AbstractEnvironment
类…(确实很多,但是到这里真没了…)
最终在构造方法中调用了customizePropertySources
方法:
public AbstractEnvironmen开发者_C教程t() { this(new MutablePropertySources()); } protected AbstractEnvironment(MutablePropertySources propertySources) { this.logger = LogFactory.getLog(this.getClass()); this.activeProfiles = new LinkedHashSet(); this.defaultProfiles = new LinkedHashSet(this.getReservedDefaultProfiles()); this.propertySources = propertySources; this.propertyResolver = this.createPropertyResolver(propertySources); this.customizePropertySources(propertySources); }
这里我们进去看,会发现是个空方法:
protected void customizePropertySources(MutablePropertySources propertySources) { }
既然这里是空方法,那肯定是调用了StandardServletEnvironment
中的方法:
protected void customizePropertySources(MutablePropertySources propertySources) { propertySources.addLast(new StubPropertySource("servletConfigInitParams")); propertySources.addLast(new StubPropertySource("servletContextInitParams")); if (jndiPresent && JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) { propertySources.addLast(new JndiPropertySource("jndiProperties")); } // 调用父类 super.customizePropertySources(propertySources); }
到这里位置,我们的propertySources
中就加载了servletConfigInitParams
,servletContextInitParams
,systemProperties
,systemEnvironment
,其中后两个是从父类中添加的。
看到这里加载环境全部结束,有没有发现一个问题?application
配置文件好像没有加进来?不要急,其他相关的配置文件都是通过监听器拓展加入进来的。
看了上面的代码,那我们应该如何将application
配置文件添加到propertySources
呢?是不是应该有如下几步:
- 找文件:去磁盘中查找对应的配置文件(这几个目录下:classpath、classpath/config、/、/config/、/*/config)
- 读文件内容:IO流
- 解析并封装成对象
propertySources.addLast
添加到最后
需要注意的是springboot 只会加载application命名的文件,大家以为的bootstrap
在这里也不会被加载,不要问为什么,那是 cloud 中的文件,也是通过自定义监听器加进去的,梳理完流程后我们可以自己手写一个试试。
我们重新回到prepareEnvironment
中,看看监听器相关,最后会走到ConfigFileApplicationListener
配置文件的监听器。入口在listeners.environmentPrepared(bootstrapContext, (ConfigurableEnvironment)environment);
。
environmentPrepared
environmentPrepared
void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) { this.doWithListeners("spring.boot.application.environment-prepared", (listener) -> { listener.environmentPrepared(bootstrapContext, environment); }); }
我们进入监听器的environmentPrepared
方法,最终会进入到 SpringApplicationRunListener
接口,这个接口在run
方法中的getRunListeners
里面获取,最终在spring.factories
里面加载实现类:EventPublishingRunListener
,执行它里面的environmentPrepared
方法:
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) { this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(bootstrapContext, this.application, this.args, environment)); }
multicastEvent
public void multicastEvent(ApplicationEvent event) { this.multicastEvent(event, this.resolveDefaultEventType(event)); } public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) { ResolvableType type = eventType != null ? eventType : this.resolveDefaultEventType(event); Executor executor = this.getTaskExecutor(); Iterator var5 = this.getApplicationListeners(event, type).iterator(); while(var5.hasNext()) { ApplicationListener<?> listener = (ApplicationListener)var5.next(); if (executor != null) { executor.execute(() -> { this.invokeListener(listener, event); }); } else { this.invokeListener(listener, event); } } }
invokeListener
protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) { ErrorHandler errorHandler = this.getErrorHandler(); if (errorHandler != null) { try { this.doInvokeListener(listener, event); } catch (Throwable var5) { errorHandler.handleError(var5); } } else { this.doInvokeListener(listener, event); } }
doInvokeListener
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) { try { listener.onApplicationEvent(event); } catch (ClassCastException var6) { String msg = var6.getMessage(); if (msg != null && !this.matchesClassCastMessage(msg, event.getClass()) && (!(event instanceof PayloadApplicationEvent) || !this.matchesClassCastMessage(msg, ((PayloadApplicationEvent)event).getPayload().getClass()))) { throw var6; } Log loggerToUse = this.lazyLogger; if (loggerToUse == null) { loggerToUse = LogFactory.getLog(this.getClass()); this.lazyLogger = loggerToUse; } if (loggerToUse.isTraceEnabled()) { loggerToUse.trace("Non-matching event type for listener: " + listener, var6); } } }
这个时候就可以进入到我们的ConfigFileApplicationListener
中了,看看是如何加载配置文件的。
public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ApplicationEnvironmentPreparedEvent) { this.onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent)event); } if (event instanceof ApplicationPreparedEvent) { this.onApplicationPreparedEvent(event); } }
在 onApplicationEnvironmentPreparedEvent
方法里面读取配置文件:
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) { List<EnvironmentPostProcessor> postProcessors = this.loadPostProcessors(); postProcessors.add(this); AnnotationAwareOrderComparator.sort(postProcessors); Iterator var3 = postProcessors.iterator(); while(var3.hasNext()) { EnvironmentPostProcessor postProcessor = (EnvironmentPostProcessor)var3.next(); postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication()); } }
进入postProcessEnvironment
方法:
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { this.addPropertySources(environment, application.getResourceLoader()); }
看到addPropertySources
方法是不是就感到很熟悉了,这不就是我们的最终目的吗,将文件添加到PropertySources
中。
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { RandomValuePropertySource.addToEnvironment(environment); (new ConfigFileApplicationListener.Loader(environment, resourceLoader)).load(); }
load
方法是读取配置文件的核心方法,需要走两层,最后从loadwithFilteredProperties
进入,有的版本代码是没有分开的,这里按照自己的版本来。
void load() { FilteredPropertySource.apply(this.environment, "defaultProperties", ConfigFileApplicationListener.LOAD_FILTERED_PROPERTY, this::loadWithFilteredProperties); } private void loadWithFilteredProperties(PropertySource<?> defaultProperties) { this.profiles = new LinkedList(); this.processedProfiles = new LinkedList(); this.activatedProfiles = false; this.loaded = new LinkedHashMap(); this.initializeProfiles(); while(!this.profiles.isEmpty()) { ConfigFileApplicationListener.Profile profile = (ConfigFileApplicationListener.Profile)this.profiles.poll(); if (this.isDefaultjsProfile(profile)) { this.addProfileToEnvironment(profile.getName()); } this.load(profile, this::getPositiveProfileFilter, this.addToLoaded(MutablePropertySources::addLast, false)); this.processedProfiles.add(profile); } this.load((ConfigFileApplicationListener.Profile)null, this::getNegativeProfileFilter, this.addToLoaded(MutablePropertySources::addFirst, true)); this.addLoadedPropertySources(); this.applyActiveProfiles(defaultProperties); }
文件监听代码分析
代码看的很乱,我们打断点来逐步分析,从this.load(profile, this::getPositiveProfileFilter, this.addToLoaded(MutablePropertySources::addLast, false));
处开始。
确定搜索路径
点进去可以发现这里再搜索路径,到底搜索了那些路径呢?
private void load(ConfigFileApplicationListener.Profile profile, ConfigFileApplicationListener.DocumentFilterFactory filterFactory, ConfigFileApplicationListener.DocumentConsumer consumer) { this.getSearchLocations().forEach((location) -> { String nonOptionalLocation = ConfigDataLocation.of(location).getValue(); boolean isDirectory = location.endsWith("/"); Set<String> names = isDirectory ? this.getSearchNames() : ConfigFileApplicationListener.NO_SEARCH_NAMES; names.forEach((name) -> { this.load(nonOptionalLocation, name, profile, filterFactory, consumer); }); }); } private Set<String> getSearchLocations() { Set<String> locations = this.getSearchLocations("spring.config.additional-location"); if (this.environment.containsProperty("spring.config.location")) { locations.addAll(this.getSearchLocations("spring.config.location")); } else { locations.addAll(this.asResolvedSet(ConfigFileApplicationListener.this.searchLocations, "classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/")); } return locations; }
是我们上面说的五个路径吧?以后别人问你从那几个路径可以读取springboot 的配置文件,大家应该都知道了吧,还能告诉他是怎么知道这五个路径的,因为内部已经将这五个路径写死了。
路径搜索
既然已经确定了从那几个路径搜索文件,那么接下来就是怎么搜索了。
private void load(ConfigFileApplicationListener.Profile profile, ConfigFileApplicationListener.DocumentFilterFactory filterFactory, ConfigFileApplicationListener.DocumentConsumer consumer) { this.getSearchLocations().forEach((location) -> { boolean isDirectory = location.endsWith("/"); Set<String> names = isDirectory ? this.getSearchNames() : ConfigFileApplicationListener.NO_SEARCH_NAMESGxCHGVFqsc; names.forEach((name) -> { this.load(location, name, profile, filterFactory, consumer); }); }); }
首先是判断是否为目录,然后去搜索文件,上面我们说过了,springboot 只加载名字为 application 的文件,是因为在代码里面也写死了。
private Set<String> getSearchNames() { if (this.environment.containsProperty("spring.config.name")) { String property = this.environment.getProperty("spring.config.name"); Set<String> names = this.asResolvedSet(property, (String)null); names.forEach(this::assertValidConfigName); return names; } else { return this.asResolvedSet(ConfigFileApplicationListener.this.names, "application"); } }
如果是目录,也知道我们要查询什么文件了,但是还要判断它的后缀名,我们只接受 yml
和propertise
后缀的文件。
private void load(String location, String name, ConfigFileApplicationListener.Profile profile, ConfigFileApplicationListener.DocumentFilterFactory filterFactory, ConfigFileApplicationListener.DocumentConsumer consumer) { if (!StringUtils.hasText(name)) { Iterator var13 = this.propertySourceLoaders.iterator(); PropertySourceLoader loader; do { if (!var13.hasNext()) { 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 '/'"); } loader = (PropertySourceLoader)var13.next(); } while(!this.canLoadFileExtension(loader, location)); this.load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer); } else { Set<String> processed = new HashSet(); Iterator var7 = this.propertySourceLoaders.iterator(); while(var7.hasNext()) { PropertySourceLoader loaderx = (PropertySourceLoader)var7.next(); String[] var9 = loaderx.getFileExtensions(); int var10 = var9.length; for(int var11 = 0; var11 < var10; ++var11) { String fileExtension = var9[var11]; if (processed.add(fileExtension)) { this.loadForFileExtension(loaderx, location + name, "." + fileExtension, profile, filterFactory, consumer); } } } } }
这里我们关注getFileExtensions
即可,因为只有这两个实例,所以我们只处理这两种后缀的。
需要注意的是:当某个配置既有yml
和propertise
,那我们以propertise
为准。
添加进PropertySources
上面文件搜索路径找到文件后我们就是要将文件添加到PropertySources
中了,这个时候回到核心方法处:
查看addLoadedPropertySources
方法。
这代码是不是也很简单,获取到环境变量后,然后将我们刚刚加载的文件先封装,然后在添加到最后。
这便是配置文件加载的全部操作。
private void addLoadedPropertySources() { MutablePropertySources destination = this.environment.getPropertySources(); List<MutablePropertySources> loaded = new ArrayList(this.loaded.values()); Collections.reverse(loaded); String lastAdded = null; Set<String> added = new HashSet(); Iterator var5 = loaded.iterator(); while(var5.hasNext()) { MutablePropertySources sources = (MutablePropertySources)var5.next(); Iterator var7 = sources.iterator(); while(var7.hasNext()) { PropertySource<?> source = (PropertySource)var7.next(); if (added.add(source.getName())) { this.addLoadedPropertySource(destination, lastAdded, source); lastAdded = source.getName(); } } } }
private void addLoadedPropertySource(MutablePropertySources destination, String lastAdded, PropertySource<?> source) { if (lastAdded == null) { if (destination.contains("defaultProperties")) { destination.addBefore("defaultProperties", source); } else { destination.addLast(source); } } else { destination.addAfter(lastAdded, source); } }
自定义监听器
上面我们说了 bootstrap 文件就是通过监听器的形式加载进来的,那我们也手写一个监听器来记载文件。
我们还按照如下步骤来做不就可以了嘛。
- 找文件:去磁盘中查找对应的配置文件(这几个目录下:classpath、classpath/config、/、/config/、/*/config)
- 读文件内容:IO流
- 解析并封装成对象
propertySources.addLast
添加到最后
首先创建一个监听器的实现类:
需要注意的是泛型是ApplicationEnvironmentPreparedEvent
类,不要问什么,你忘了我们看代码的时候,在doInvokeListener
中,文件监听器的触发事件类型是ApplicationEnvironmentPreparedEvent
吗?所以想要查询到我们的自定义文件,类型要设置正确。
代码完全是按照我们上面的步骤来的,所以没什么好说的。
public class MyPropertySourceListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> { @Override public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { MutablePropertySources destination = event.getEnvironment().getPropertySources(); Properties properties = new Properties(); try { InputStream in = MyPropertySourceListener.class.getClassLoader().getResourceAsStream("test.properties"); properties.load(in); ConcurrentHashMap<Object,Object> ch = new ConcurrentHashMap<>(); for (Map.Entry entry : properties.entrySet()) { ch.put(entry.getKey(),entry.getValue()); } OriginTrackedMapPropertySource source = new OriginTrackedMapPropertySource("test.properties",ch); destination.addLast(source); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
监听器写完之后,我们需要将实现类配置到SPI里面:(我这里面有两个自定义的初始化器和监听器,所以总共是三个了。)
org.springframework.context.ApplicationContextInitializer=\ com.example.autodemo.listener.StarterApplicationContextInitializer org.springframework.context.ApplicationListener=\ com.example.autodemo.listener.StarterApplicationListener,\ com.example.autodemo.listener.MyPropertySourceListener
这个时候我们重新运行项目,就可以看到我们的自定义监听器生效了,将文件加载进来了。
通过这种方式加载进来的配置信息我们就可以直接通过@Value
注解读取了,不需要在使用@ConfigurationProperties
等注解了。
refreshContext
该类启动spring的代码加载了bean,同时启动了内置的web容器,我们简单看下源码流程:
一直往下看 refresh 即可,最后跳入AbstractApplicationContext
类的refresh
方法,可以发现这里就是spring 容器的启动代码,还是那熟悉的12个流程:
该方法加载或者刷新一个持久化的配置,,可能是XML文件、属性文件或关系数据库模式。但是由于这是一种启动方式,如果失败,那么应该销毁所有以及创建好的实例。换句话说:在调用该方法之后,要么全部实例化,要么完全不实例化。
public void refresh() throws BeansException, IllegalStateException { // 加载或刷新配置前的同步处理 synchronized(this.startupShutdownMonitor) { StartupStep contextRefresh = this.applicationandroidStartup.start("spring.context.refresh"); // 为刷新而准备此上下文 this.prepareRefresh(); // 告诉子类去刷新内部 bean 工厂 ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory(); // 准备好bean 工厂,以便再此上下文中使用 this.prepareBeanFactory(beanFactory); try { // 允许在上下文子类中对bean工厂进行后置处理 this.postProcessBeanFactory(beanFactory); StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process"); // 调用在上下文中注册为bean的工厂处理器 this.invokeBeanFactoryPostProcessors(beanFactory); // 注册拦截bean创建的bean处理器 this.registerBeanPostProcessors(beanFactory); beanPostProcess.end(); // 初始化消息资源 this.initMessageSource(); // 初始化事件多路广播器 this.initApplicationEventMulticaster(); // 初始话上下文子类中的其他特殊bean this.onRefresh(); // 注册监听器(检查监听器的bean 并注册他们) this.registerListeners(); // 实例化所有剩余的单例 不包括懒加载的 this.finishBeanFactoryInitialization(beanFactory); // 发布相应的事件 php this.finishRefresh(); } catch (BeansException var10) { if (this.logger.isWarnEnabled()) { this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var10); } // 发生异常了销毁所有已经创建好的bean this.destroyBeans(); // 重置 active 标识 this.cancelRefresh(var10); throw var10; } finally { // 重置内核中的公用缓存 this.resetCommonCaches(); contextRefresh.end(); } } }
总结
以上便是spring boot 启动流程以及外部配置的所有内容。
从启动流程来说:
- 首先判断项目类型
- 初始化监听器和初始化器(可以理解为外部拓展)
- 创建环境变量和上下文,还会创建一个配置文件的统一容器
- 打印 banner
- 执行 spring 核心流程
- 启动 tomcat
从配置文件来说:
随着springboot 启动,会创建一个环境变量 environment ,在这个环境变量里面创建一个配置文件统一管理的容器,叫做PropertySources,创建好容器后,springboot 会基于监听器的形式从5个固定路径下查找以application命名且拓展名为properties和yml的文件,并通过io流读入内存,最后通过不同的解析器进行解析,解析完成后添加到最后。
到此这篇关于Spring boot 启动流程及外部化配置的文章就介绍到这了,更多相关Spring boot 启动流程内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!
精彩评论