欢迎来到飞鸟慕鱼博客,开始您的技术之旅!
当前位置: 首页知识笔记正文

springboot源码执行流程,springboot主要执行流程

墨初 知识笔记 30阅读
Spring Boot外化配置源码解析 外化配置简介
Spring Boot设计了非常特殊的加载指定属性文件PropertySouce的顺序允许属性值合理的覆盖属性值会以下面的优先级进行配置。
home目录下的Devtool全局设置属性~/.spring-boot-devtools.properties条件是当devtools激活时TestPropertySource注解的测试用例。SpringBootTest#properties注解的测试用例。命令行参数。来自SPRING_APPLICATION_JSON的属性内嵌在环境变量或系统属性中的内联JSONServletConfig初始化参数ServletContext初始化参数java:comp/env的JNDI属性Java系统属性System.getProperties()操作系统环境变量RandomValuePropertySource,只包含random.*中的属性jar包外的Profile_specific应用属性application-{profile}.propertis和YAML变量jar包内的Profile_specific应用属性application-{profile}.propertis和YAML变量jar包外的应用配置application.properties和YAML变量jar包内的应用配置application.properties和YAML变量Configuration类上的PropertySource注解默认属性通过SpringApplication.setDefaultProperties指定

在以上配置方式中我们经常使用的包括命令参数属性文件YAML文件等内容以下将围绕他们的运行及相关代码进行讲解。

ApplicationArguments参数处理
ApplicationArguments提供了针对参数的解析和查询功能。在Spring Boot运行阶段的章节中我们提到过通过SpringApplication.run(args)传递的参数会被封装在ApplicationArguments接口中。本节我们来详细了解下ApplicationArguments接口。
接口定义及初始化

首先看一下ApplicationArguments接口的具体方法定义及功能介绍。

package org.springframework.boot;import java.util.List;import java.util.Set; public interface ApplicationArguments {//返回原始未处理的参数通过application传入的String[] getSourceArgs();//返回所有参数的集合如参数为--foobar --debug,则返回【foo,debug】Set<String> getOptionNames();//选项参数中是否包含指定名称的参数boolean containsOption(String name);//根据选项参数的名称获取选项参数的值列表List<String> getOptionValues(String name);//返回非选项参数列表List<String> getNonOptionArgs();}

通过接口定义可以看出ApplicationArguments主要提供了针对参数名称和值的查询以及判断是否存在指定参数的功能。

在Spring Boot的初始化运行过程中ApplicationArguments接口的实例化操作默认是通过实现类DefaultApplicationArguments来完成的。DefaultApplicationArguments的底层又是基于Spring框架中的命令行配置源SimpleCommandLinePropertySource实现的SpringCommandLinePropertySource是PropertySource抽象类的派生类。

以下代码中内部类Source便是SimppleCommandLinePropertySource的子类。

public class DefaultApplicationArguments implements ApplicationArguments {private final Source source;private final String[] args;public DefaultApplicationArguments(String... args) {Assert.notNull(args, Args must not be null);this.source  new Source(args);this.args  args;}......private static class Source extends SimpleCommandLinePropertySource {......}}

我们再来看SimpleCommandLinePropertySource的构造方法通过代码会发现默认使用spring的SimpleCommandLineArgsParser对args参加进行解析。

public class SimpleCommandLinePropertySource extends CommandLinePropertySource<CommandLineArgs> {    public SimpleCommandLinePropertySource(String... args) {        super((new SimpleCommandLineArgsParser()).parse(args));    }//重载的构造方法    public SimpleCommandLinePropertySource(String name, String[] args) {        super(name, (new SimpleCommandLineArgsParser()).parse(args));    }    ......}

除了构造方法之外SimpleCommandLinePropertySource还提供了不同类型参数信息的获取和检查是否存在的功能代码如下

public class SimpleCommandLinePropertySource extends CommandLinePropertySource<CommandLineArgs> {    ......    //获取选项参数数组        public String[] getPropertyNames() {        return StringUtils.toStringArray(((CommandLineArgs)this.source).getOptionNames());    }//获取是否包含指定name的参数    protected boolean containsOption(String name) {        return ((CommandLineArgs)this.source).containsOption(name);    }//获取指定name的选项参数列表    Nullable    protected List<String> getOptionValues(String name) {        return ((CommandLineArgs)this.source).getOptionValues(name);    }//获取非选项参数列表    protected List<String> getNonOptionArgs() {        return ((CommandLineArgs)this.source).getNonOptionArgs();    }}

ApplicatinArguments或者更进一步说是SimpleCommandLinePropertySource对参数类型是有所区分的即选项参数和非选项参数。

选项参数必须以“–”为前缀参数值可为空该参数可以通过Spring Boot属性处理后使用比如在执行jar -jar命令时添加选项参数“–app.namespring boot start在代码中可以通过注解Value属性及其他方式获取到该参数的值。该参数可以通过逗号分隔多个参数值或者多次使用同一个参数来包含多个参数的值。

非选项参数并不要求以“–”前缀开始可自行定义。非选项参数可以直接在jar -jar命令中定义参数为“non-option的参数值。

以上所说的选项参数和非选项参数的解析是在SimpleCommandLinePropertySource构造方法中调用SimpleCommandLineArgsParser中完成的代码如下

class SimpleCommandLineArgsParser {    SimpleCommandLineArgsParser() {    }//解析args参数返回一个完整的CommandLineArgs对象    public CommandLineArgs parse(String... args) {        CommandLineArgs commandLineArgs  new CommandLineArgs();        String[] var3  args;        int var4  args.length;//遍历参数        for(int var5  0; var5 < var4; var5) {            String arg  var3[var5];            //解析选项参数以--开头            if (arg.startsWith(--)) {                String optionText  arg.substring(2, arg.length());                String optionValue  null;                String optionName;                //判断是--foobar参数格式还是-foo参数格式并分别处理获取值                if (optionText.contains()) {                    optionName  optionText.substring(0, optionText.indexOf(61));                    optionValue  optionText.substring(optionText.indexOf(61)  1, optionText.length());                } else {                    optionName  optionText;                }                if (optionName.isEmpty() || optionValue ! null && optionValue.isEmpty()) {                    throw new IllegalArgumentException(Invalid argument syntax:   arg);                }                commandLineArgs.addOptionArg(optionName, optionValue);            } else {                //处理非选项参数                commandLineArgs.addNonOptionArg(arg);            }        }        return commandLineArgs;    }}

通过SimpleCommandLineArgsParser的代码可以看出Spring对参数的解析是按照指定的参数格式分别解析字符串中的值来实现的。最终解析的结果均封装在CommandLineArgs中。而CommandLineArgs类只是命令行参数的简单表示形式内部分为“选项参数”和非选项参数

class CommandLineArgs {    private final Map<String, List<String>> optionArgs  new HashMap();    private final List<String> nonOptionArgs  new ArrayList();    CommandLineArgs() {    }......}

CommandLineArgs的核心存储结构包括存储选项参数的Map<String,List>optionArgs和存储非选项参数的ListnonOptionsArgs。同时针对这两个核心存储接口SpringBoot也提供了相关的读写操作的方法。

SimpleCommandLineArgsParser解析获得的CommandLineArgs对象最终会被SimpleCommandLinePropertySource的构造方法通过parser调用一层层地传递到PropertySource类的构造方法中最终封装到相应的属性当中。

public abstract class PropertySource<T> {    protected final Log logger;    //参数类别名称    protected final String name;    //参数封装类    protected final T source;    ......}

以在SimpleCommandLinePropertySource中的使用为例最终封装在PropertySource中的结构为name为“commandLineArgs”,source为解析出的CommandLineArgs对象。

而DefaultApplicationArguments的内部类Source作为SimpleCommandLinePropertySource的子类存储了以上解析的数据内容。同时args参数的原始值储存在DefaultApplicationArguments的String[]args属性中。

命令行参数的获取

命令行参数就是在启动Spring Boot项目时通过命令行传递的参数。比如通过一下命令来启动一个Spring Boot项目。

jar -jar app.jar --nameSpringBoot

那么–nameSpringBoot是如何一步步传递到Spring内部的呢

默认情况下SpringApplication会将以上类似name的命令行参数以“–”开头解析封装成一个PropertySource对象并将其添加到Spring-Environment当中而命令行参数的优先级要高于其他配置源。

下面我们来通过代码来追踪启动过程中整个参数的获取解析和分装过程。首先参数是通过SpringApplication的run方法的args传递参数。

在SpringApplication的run方法中通过以下操作先将args封装于ApplicationArguments中然后又将封装之后的对象传递入prepareEnvironment方法。

public ConfigurableApplicationContext run(String... args) {......try {ApplicationArguments applicationArguments  new DefaultApplicationArguments(args);ConfigurableEnvironment environment  prepareEnvironment(listeners, applicationArguments);......}catch (Throwable ex) {handleRunFailure(context, ex, exceptionReporters, listeners);throw new IllegalStateException(ex);}......}

在prepareEnvironment方法中通过applicationArguments.getSourceArgs()获得传递的参数数组并作为参数调用configureEnvironment方法此处获得的args依旧是未解析的参数值代码如下

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,ApplicationArguments applicationArguments) {......configureEnvironment(environment, applicationArguments.getSourceArgs());......}

在configureEnvironment方法中又将参数传递给configurePropertySource方法。

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {......configurePropertySources(environment, args);configureProfiles(environment, args);}

而在configurePropertySources方法中才对参数进行了真正的解析和封装

protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {        //获取环境变量中的属性资源信息MutablePropertySources sources  environment.getPropertySources();        //如果默认属性配置存在则将其放置在属性资源的最后位置if (this.defaultProperties ! null && !this.defaultProperties.isEmpty()) {sources.addLast(new MapPropertySource(defaultProperties, this.defaultProperties));}        //如果命令行属性未被禁用且存在if (this.addCommandLineProperties && args.length > 0) {String name  CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;            //如果默认属性资源中不包含该命令则将命令行属性放置在第一位            //如果包含则通过CompositePropertySource进行处理if (sources.contains(name)) {PropertySource<?> source  sources.get(name);CompositePropertySource composite  new CompositePropertySource(name);composite.addPropertySource(new SimpleCommandLinePropertySource(springApplicationCommandLineArgs, args));composite.addPropertySource(source);sources.replace(name, composite);}else {                //不存在则添加并放置在第一位sources.addFirst(new SimpleCommandLinePropertySource(args));}}}

因为configurePropertySources方法在之前章节中介绍过下面针对命令行参数再次进行讲解和深入分析重点介绍两个内容参数的优先级和命令行参数的解析。

参数的优先级从上面的代码注解中可以看到configurePropertySources方法

第一步获得环境变量中存储配置信息的sources第二步判断默认参数是否为空如果不为空则将默认参数放置在sources的最后位置这里已经明显反应了参数的优先级是通过顺序来体现的第三步如果命令行参数未被禁用且不为空则要么将原有默认参数替换掉要么直接放在第一位这一步中的替换操作也是另外一种优先级形式的体现。

在上面代码中可以通过SpringApplication的setAddCommandLineProperties方法将其设置为false来禁用。命令行参数的解析用到了SimpleCommandLinePropertySource类而该类的相关使用在上面以详细介绍过。下面将分析配置文件中的参数获取。

配置文件的加载

Spring Boot启动时默认会去加载classpah下的application.yml或application.properties文件。配置文件的加载过程中主要是利用了Spring Boot的事件机制来完成的也就是我们之前说的SpringApplicationRunListeners中的environmentPrepared方法来启动加载配置文件的事件。通过该方法发布的事件会被注册到ConfigFileApplicationListener监听到而实现资源的加载。

下面来通过源码的追踪分析这一过程。该事件同样是SpringApplication的run方法来完成的。前半部分的调用过程与上面命令行获取参数的方法调用一样不同的是当执行到prepareEnvironment中当执行完configureEnvironment方法之后便通过事件发布来通知监听器加载资源。

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,ApplicationArguments applicationArguments) {// Create and configure the environmentConfigurableEnvironment environment  getOrCreateEnvironment();    // 配置环境主要包括PropertySources和activeProfiles的配置configureEnvironment(environment, applicationArguments.getSourceArgs());ConfigurationPropertySources.attach(environment);    //listeners环境准备listeners.environmentPrepared(environment);......}

该事件监听器通过EventPublishingRunListener的environmentPrepared方法发布一个ApplicationEnvironmentPreparedEvent事件

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {......Overridepublic void environmentPrepared(ConfigurableEnvironment environment) {this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));}    ......}

在META-INF/spring.factories中注册的ConfigFileApplicationListener会监听到对应事件并进行相应的处理。spring.factories中ConfigFileApplicationListener的注册配置如下

# Application Listenersorg.springframework.context.ApplicationListener\org.springframework.boot.ClearCachesApplicationListener,\org.springframework.boot.builder.ParentContextCloserApplicationListener,\org.springframework.boot.context.FileEncodingApplicationListener,\org.springframework.boot.context.config.AnsiOutputApplicationListener,\org.springframework.boot.context.config.ConfigFileApplicationListener,\org.springframework.boot.context.config.DelegatingApplicationListener,\org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\org.springframework.boot.context.logging.LoggingApplicationListener,\org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

在ConfiFileApplicationListener类中我们会看到很多与配置文件加载相关的常量。

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {private static final String DEFAULT_PROPERTIES  defaultProperties;// 默认的加载配置文件路径private static final String DEFAULT_SEARCH_LOCATIONS  classpath:/,classpath:/config/,file:./,file:./config/;//默认的配置文件名称private static final String DEFAULT_NAMES  application;......//激活配置文件的属性名public static final String ACTIVE_PROFILES_PROPERTY  spring.profiles.active;......

通过这些基本的常量可以看出默认加载配置文件的路径和默认的名称。再回到刚才的事件监听入口方法为ConfigFileApplicationListener的onApplicationEvent方法。

Overridepublic void onApplicationEvent(ApplicationEvent event) {        //对应当前发布的事件执行次业务逻辑if (event instanceof ApplicationEnvironmentPreparedEvent) {onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);}if (event instanceof ApplicationPreparedEvent) {onApplicationPreparedEvent(event);}}

上面调用onApplicationEnvironmentPreparedEvent方法如下该方法会获得注册的处理器遍历并依次调用postPropcessEnvironment方法。

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {List<EnvironmentPostProcessor> postProcessors  loadPostProcessors();postProcessors.add(this);AnnotationAwareOrderComparator.sort(postProcessors);    //遍历并依次调用postProcessEnvironment方法for (EnvironmentPostProcessor postProcessor : postProcessors) {postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());}}

其中EnvironmentPostProcessor接口的实现类也是在META-INF/spring.factories文件中注册的。

# Environment Post Processorsorg.springframework.boot.env.EnvironmentPostProcessor\org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor

ConfigFileApplicationListener本身也是EvironmentPostProcessor接口的实现类可以跟着ConfigFileApplicationListener中postProcessEnvironment的调用链路代码一直往下看会发现最后在其内部类Loader的load方法进行配置文件的加载操作。其中关于文件路径及其名称的组合代码如下

// 1接口类查找实现类ConfiFileApplicationListenerpublic interface EnvironmentPostProcessor {void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application);}// 2 ConfiFileApplicationListener实现类Overridepublic void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {    addPropertySources(environment, application.getResourceLoader());}// 3 addPropertySourcesprotected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {    RandomValuePropertySource.addToEnvironment(environment);    new Loader(environment, resourceLoader).load();}// 4 load()void load() {    FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,            (defaultProperties) -> {                this.profiles  new LinkedList<>();                this.processedProfiles  new LinkedList<>();                this.activatedProfiles  false;                this.loaded  new LinkedHashMap<>();                initializeProfiles();                while (!this.profiles.isEmpty()) {                    Profile profile  this.profiles.poll();                    if (isDefaultProfile(profile)) {                        addProfileToEnvironment(profile.getName());                    }                    load(profile, this::getPositiveProfileFilter,                            addToLoaded(MutablePropertySources::addLast, false));                    this.processedProfiles.add(profile);                }                load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));                addLoadedPropertySources();                applyActiveProfiles(defaultProperties);            });}// 5 load()private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {    getSearchLocations().forEach((location) -> {        boolean isFolder  location.endsWith(/);        Set<String> names  isFolder ? getSearchNames() : NO_SEARCH_NAMES;        names.forEach((name) -> load(location, name, profile, filterFactory, consumer));    });}private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,DocumentConsumer consumer) {......Set<String> processed  new HashSet<>();for (PropertySourceLoader loader : this.propertySourceLoaders) {for (String fileExtension : loader.getFileExtensions()) {if (processed.add(fileExtension)) {loadForFileExtension(loader, location  name, .  fileExtension, profile, filterFactory,consumer);}}}}

在该方法中可以看到loadForFileExtension的第二个参数文件路径名称和第三个参数扩展名称的拼接组成方式。location默认值就是常量DEFAULT_SEARCH_LOCATIONS的值。

在for循环中遍历的PropertySourceLoader也是在META-INF/spring.factories中注册的并且在Loader的构造方法中通过SpringFactoriesLoader的loadFactories方法来获得。

# PropertySource Loadersorg.springframework.boot.env.PropertySourceLoader\org.springframework.boot.env.PropertiesPropertySourceLoader,\org.springframework.boot.env.YamlPropertySourceLoader

当查看PropertiesPropertySourceLoader和YamlPropertySourceLoader两个加载器代码时就会发现他们分别定义了所支持文件类型及其加载方法。PropertiesPropertySourceLoader支持配置文件类型的定义代码如下

public class PropertiesPropertySourceLoader implements PropertySourceLoader {private static final String XML_FILE_EXTENSION  .xml;Overridepublic String[] getFileExtensions() {return new String[] { properties, xml };}Overridepublic List<PropertySource<?>> load(String name, Resource resource) throws IOException {Map<String, ?> properties  loadProperties(resource);if (properties.isEmpty()) {return Collections.emptyList();}return Collections.singletonList(new OriginTrackedMapPropertySource(name, Collections.unmodifiableMap(properties), true));}SuppressWarnings({ unchecked, rawtypes })private Map<String, ?> loadProperties(Resource resource) throws IOException {String filename  resource.getFilename();if (filename ! null && filename.endsWith(XML_FILE_EXTENSION)) {return (Map) PropertiesLoaderUtils.loadProperties(resource);}return new OriginTrackedPropertiesLoader(resource).load();}}

YamlPropertySourceLoader支持配置文件类型的定义代码如下

public class YamlPropertySourceLoader implements PropertySourceLoader {Overridepublic String[] getFileExtensions() {return new String[] { yml, yaml };}Overridepublic List<PropertySource<?>> load(String name, Resource resource) throws IOException {if (!ClassUtils.isPresent(org.yaml.snakeyaml.Yaml, null)) {throw new IllegalStateException(Attempted to load   name   but snakeyaml was not found on the classpath);}List<Map<String, Object>> loaded  new OriginTrackedYamlLoader(resource).load();if (loaded.isEmpty()) {return Collections.emptyList();}List<PropertySource<?>> propertySources  new ArrayList<>(loaded.size());for (int i  0; i < loaded.size(); i) {String documentNumber  (loaded.size() ! 1) ?  (document #  i  ) : ;propertySources.add(new OriginTrackedMapPropertySource(name  documentNumber,Collections.unmodifiableMap(loaded.get(i)), true));}return propertySources;}}

其中PropertiesPropertySourceLoader对文件的加载通过PropertiesLoaderUtils类加载xml文件和OriginTrackedYamlLoader类来完成而YamlPropertySourceLoader对文件的加载主要通过OriginrackedYamlLoader来完成。

下面以PropertiesPropertySourceLoader使用的OriginTrackedPropertiesLoader为例进行源码分析。

PropertiesPropertySourceLoader中加载相关的代码如下

public class PropertiesPropertySourceLoader implements PropertySourceLoader {private static final String XML_FILE_EXTENSION  .xml;Overridepublic String[] getFileExtensions() {return new String[] { properties, xml };}//加载指定的配置文件Overridepublic List<PropertySource<?>> load(String name, Resource resource) throws IOException {        //调用load方法进行加载并返回Map形式的数据Map<String, ?> properties  loadProperties(resource);if (properties.isEmpty()) {return Collections.emptyList();}        //对返回结果进行处理和转换return Collections.singletonList(new OriginTrackedMapPropertySource(name, Collections.unmodifiableMap(properties), true));}//具体加载过程SuppressWarnings({ unchecked, rawtypes })private Map<String, ?> loadProperties(Resource resource) throws IOException {String filename  resource.getFilename();        //加载xml格式if (filename ! null && filename.endsWith(XML_FILE_EXTENSION)) {return (Map) PropertiesLoaderUtils.loadProperties(resource);}        //加载properties格式return new OriginTrackedPropertiesLoader(resource).load();}}

OriginTrackedPropertiesLoader的构造方法非常简单只是把resource设置给其成员变量Resource。

class OriginTrackedPropertiesLoader {private final Resource resource;OriginTrackedPropertiesLoader(Resource resource) {Assert.notNull(resource, Resource must not be null);this.resource  resource;}Map<String, OriginTrackedValue> load() throws IOException {return load(true);}//加载properties文件的数据并返回map类型    //其中expandLists用于指定参数为name[]a,b,c的列表是否进行扩展默认为trueMap<String, OriginTrackedValue> load(boolean expandLists) throws IOException {        //创建配置文件的readertry (CharacterReader reader  new CharacterReader(this.resource)) {Map<String, OriginTrackedValue> result  new LinkedHashMap<>();StringBuilder buffer  new StringBuilder();            //读取文件中数据while (reader.read()) {                //读取文件中的keyString key  loadKey(buffer, reader).trim();                //可扩展列表的处理if (expandLists && key.endsWith([])) {key  key.substring(0, key.length() - 2);int index  0;do {OriginTrackedValue value  loadValue(buffer, reader, true);put(result, key  [  (index)  ], value);if (!reader.isEndOfLine()) {reader.read();}}while (!reader.isEndOfLine());}else {                    //读取文件中value并封装为OriginTrackedValueOriginTrackedValue value  loadValue(buffer, reader, false);put(result, key, value);}}return result;}}}

以上代码展示了OriginTrackedPropertiesLoader的load方法的核心功能创建reader读取配置文件获得配置文件中配置的key获取配置文件中的value封装key-value到map中并返回。

标签:
声明:无特别说明,转载请标明本文来源!