在前面章节我们讲解了Spring Boot的运作核心原理及启动过程中进行的一系列核心操作。从本章开始,我们将针对在实践过程中应用的不同知识点的源代码进行解读和分析,内容上可能会与之前章节有所重叠,但这些重叠的内容更有助于我们在实践和应用中形成前后呼应,加深记忆学习效果。 
 
本章将重点讲解Spring Boot外化配置文件相关内容,核心包括:外化配置文件、命令行参数、Profile实现机制及整个加载处理业务逻辑。 
 
 
 
 
.1 外化配置简介 
Spring Boot允许我们将配置进行外部化处理,以便我们使用相同的代码在不同的环境中运行。我们可以使用属性文件、YAML文件、环境变量和命令参数来进行外化配置。这些配置中的属性可以通过@Value注解直接注入到对应的Bean中,也可以通过Spring的Environment抽象访问,还可以通过@ConfigurationProperties绑定到结构化的对象上。 
Spring Boot设计了非常特殊的加载指定属性文件(PropertySource)的顺序,以允许对属性值进行合理的覆盖。属性值会以下面的优先级进行设置。 
·home目录下的Devtools全局设置属性(~/.spring-boot-devtools.properties,条件是当devtools激活时)。 
·@TestPropertySource注解的测试用例。 
·@SpringBootTest#properties注解的测试用例。 
·命令行参数。 
·来自SPRING_APPLICATION_JSON的属性(内嵌在环境变量或系统属性中的内联JSON)。 
·ServletConfig初始化参数。 
·ServletContext初始化参数。 
·java:comp/env的JNDI属性。 
·Java系统属性(System.getProperties())。 
·操作系统环境变量。 
·RandomValuePropertySource,只包含random.*中的属性。 
·jar包外的Profile-specific应用属性(application-{profile}.properties和YAML变量)。 
·jar包内的Profile-specific应用属性(application-{profile}.properties和YAML变量)。 
·jar包外的应用配置(application.properties和YAML变量)。 
·jar包内的应用配置(application.properties和YAML变量)。 
·@Configuration类上的@PropertySource注解。 
·默认属性(通过SpringApplication.setDefaultProperties指定)。 
在以上配置方式中,我们经常使用的包括:命令参数、属性文件、YAML文件等内容,以下将围绕它们的运作及相关代码进行讲解。 
 
 
5.2 ApplicationArguments参数处理 
ApplicationArguments提供了针对参数的解析和查询功能。在Spring Boot运行阶段的章节中我们提到过,通过SpringApplication.run(args)传递的参数会被封装在ApplicationArguments接口中。本节我们来详细了解一下ApplicationArguments接口。 
 
 
5.2.1 接口定义及初始化 
首先看一下ApplicationArguments接口的具体方法定义及功能介绍(注释部分)。 
public interface ApplicationArguments { 
    // 返回原始未处理的参数(通过application传入的) 
    String[] getSourceArgs(); 
    // 返回所有参数名称的集合,如参数为:--foo=bar --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实现的。SimpleCommandLinePropertySource是PropertySource抽象类的派生类。 
以下代码中内部类Source便是SimpleCommandLinePropertySource的子类。 
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; 
    } 
    // 在此省略ApplicationArguments的其他接口实现方法 
    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> { 
    ... 
    // 获取选项参数数组 
    @Override 
    public String[] getPropertyNames() { 
        return StringUtils.toStringArray(this.source.getOptionNames()); 
    } 
 
    // 获取是否包含指定name的参数 
 
 
@Override 
    protected boolean containsOption(String name) { 
        return this.source.containsOption(name); 
    } 
 
    // 获取指定name的选项参数列表 
    @Override 
    @Nullable 
    protected List<String> getOptionValues(String name) { 
        return this.source.getOptionValues(name); 
    } 
 
    // 获取非选项参数列表 
    @Override 
    protected List<String> getNonOptionArgs() { 
        return this.source.getNonOptionArgs(); 
    } 
} 
ApplicationArguments,或者更进一步说是SimpleCommandLinePropertySource对参数类型是有所区分的,即选项参数和非选项参数。 
选项参数必须以“--”为前缀,参数值可为空,该参数我们可以通过Spring Boot属性处理后使用,比如在执行jar -jar命令时,添加选项参数“--app.name=spring boot learn”,在代码中可通过@Value属性或其他方式获取到该参数的值。该参数可以通过逗号分隔多个参数值,或多次使用同一个参数来包含多个参数的值。 
非选项参数并不要求以“--”前缀开始,可自行定义。非选项参数可以是除了传递的VM参数之外的其他参数。比如我们可以直接在jar -jar命令中定义参数为“non-option”的参数值。 
以上所说的选项参数和非选项参数的解析是在SimpleCommandLinePropertySource构造方法中调用的SimpleCommandLineArgsParser中完成的,代码如下。 
class SimpleCommandLineArgsParser { 
    // 解析args参数,返回一个完整的CommandLineArgs对象 
    public CommandLineArgs parse(String... args) { 
        CommandLineArgs commandLineArgs = new CommandLineArgs(); 
        // 遍历参数 
        for (String arg : args) { 
            // 解析选项参数,以"--"开头 
            if (arg.startsWith("--")) { 
                String optionText = arg.substring(2, arg.length()); 
                String optionName; 
                String optionValue = null; 
                // 判断是--foo=bar参数格式,还是—foo参数格式,并分别处理获取值 
                if (optionText.contains("=")) { 
                    optionName = optionText.substring(0, optionText.indexOf('=')); 
                    optionValue = optionText.substring(optionText.indexOf('=')+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的核心存储结构包括:存储选项参数的Map<String,List<String>> optionArgs和存储非选项参数的List<String> nonOptionArgs。同时,针对这两个核心存储结构,Spring Boot也提供了读写操作的方法。 
SimpleCommandLineArgsParser解析获得的CommandLineArgs对象,最终会被SimpleCommand-LinePropertySource的构造方法通过super调用,一层层地传递到PropertySource类的构造方法,最终封装到对应的属性当中。 
public abstract class PropertySource<T> { 
    // 参数类别名称 
    protected final String name; 
    // 参数封装类 
    protected final T source; 
    ... 
} 
以在SimpleCommandLinePropertySource中的使用为例,最终封装在PropertySource中的结构为:name为“commandLineArgs”,source为解析出的CommandLineArgs对象。 
而DefaultApplicationArguments的内部类Source作为SimpleCommandLinePropertySource的子类存储了以上解析的数据内容。同时,args参数的原始值存储在DefaultApplicationArguments的String[ ] args属性中。 
 
 
 
 
5.2.2 使用实例 
在实践中我们可能会遇到这样的疑问:如何访问应用程序变量?或者,如何访问通过SpringApplication.run(args)传入的参数?下面我们以具体的例子来说明如何通过ApplicationArguments获得对应的参数。 
ApplicationArguments接口的使用非常简单,在我们使用参数值的Bean中直接注入ApplicationArguments即可,然后调用其方法即可获得对应的参数值。 
注入ApplicationArguments,并提供打印所需参数信息的方法,代码如下。 
@Component 
public class ArgsBean { 
    @Resource 
    private ApplicationArguments arguments; 
 
    public void printArgs() { 
        System.out.println("# 非选项参数数量: " + arguments.getNonOptionArgs().size()); 
        System.out.println("# 选项参数数量: " + arguments.getOptionNames().size()); 
        System.out.println("# 非选项参数具体参数:"); 
        arguments.getNonOptionArgs().forEach(System.out::println); 
 
        System.out.println("# 选项参数具体参数:"); 
        arguments.getOptionNames().forEach(optionName -> { 
            System.out.println("--" + optionName + "=" + arguments.getOptionValues 
(optionName)); 
        }); 
    } 
} 
在main方法中获得ArgsBean实例化对象,并调用其printArgs方法,代码如下。 
public static void main(String[] args) { 
    SpringApplication app = new SpringApplication(SpringLearnApplication.class); 
    ConfigurableApplicationContext context = app.run(args); 
    ArgsBean bean = context.getBean(ArgsBean.class); 
    bean.printArgs(); 
} 
启动项目,控制台打印结果,代码如下。 
# 非选项参数数量: 1 
# 选项参数数量: 2 
# 非选项参数具体参数: 
non-option 
# 选项参数具体参数: 
--jdk.support=[1.7,1.8,1.8+] 
--app.name=[springBootLearn] 
以上只是示例,在上面的介绍中也提到了,选项参数可通过@Value直接注入Bean中使用。关于ApplicationArguments其他方法的使用以此类推即可。 
 
 
 
 
5.3 命令参数的获取 
命令行参数就是在启动Spring Boot项目时通过命令行传递的参数。比如,用如下命令来启动一个Spring Boot的项目。 
java -jar app.jar --name=SpringBoot 
那么,参数--name=SpringBoot是如何一步步传递到Spring内部的呢?这就是本节要分析的代码内容。 
默认情况下,SpringApplication会将以上类似name的命令行参数(以“--”开通)解析封装成一个PropertySource对象(5.2节已经具体讲到),并将其添加到Spring-Environment当中,而命令行参数的优先级要高于其他配置源。 
下面,我们通过代码来追踪启动过程中整个参数的获取、解析和封装过程。首先,参数是通过SpringApplication的run方法的args参数来传递的。 
在SpringApplication的run方法中,通过以下操作先将args封装于对象ApplicationArguments中,然后又将封装之后的对象传递入prepareEnvironment方法。 
public ConfigurableApplicationContext run(String... args) { 
    ... 
    try { 
        ApplicationArguments applicationArguments = new DefaultApplicationArgu-  
ments(args); 
        ConfigurableEnvironment environment = prepareEnvironment(listeners, 
                applicationArguments); 
        } catch (Throwable ex) { 
        ... 
    } 
    ... 
} 
在prepareEnvironment方法中,通过applicationArguments.getSourceArgs()获得传递的参数数组,并作为参数调用configureEnvironment方法,此处获得的args依旧是未解析的参数值,代码如下。 
private ConfigurableEnvironment prepareEnvironment( 
        SpringApplicationRunListeners listeners, 
        ApplicationArguments applicationArguments) { 
    ... 
    configureEnvironment(environment, applicationArguments.getSourceArgs()); 
    ... 
} 
在configureEnvironment方法中又将参数传递给configurePropertySources方法。 
protected void configureEnvironment(ConfigurableEnvironment environment, 
        String[] args) { 
    ... 
    configurePropertySources(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.default-  
Properties)); 
    } 
    // 如果命令行属性未被禁用且存在 
    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的最后位置,这里已经明显反映了参数的优先级是通过顺序来体现的;第三步,如果命令参数未被禁用,且不为空,则要么将原有默认参数替换掉,要么直接放在第一位,这一步中的替换操作也是另外一种优先级形式的体现。 
顺便提一下,在上面的代码中,addCommandLineProperties参数是可以进行设置的,当不允许使用命令行参数时,可以通过SpringApplication的setAddCommandLineProperties方法将其设置为false来禁用。 
命令行参数的解析用到了SimpleCommandLinePropertySource类,而该类的相关使用在上一节中已经详细介绍了。 
通过上面一系列的代码追踪,我们了解了通过命令传递的参数是如何一步步被封装入Spring的Environment当中的。下一节,我们将分析配置文件中的参数获取。 
 
 |