SpringBoot之浅析配置项解析(一)

在我们的开发工作总是离不了配置项相关的配置工作,SpringBoot也为我们提供了@ConfigurationProperties注解来进行配置项信息的配置工作,同时也提供了几个配置文件的默认加载位置,如:classpath:application.properties、classpath:application.yml、classpath:application.yaml、classpath:/config/application.properties、classpath:/config/application.yml、classpath:/config/application.yaml等。另外我们还可以在命令行中、系统属性中、虚拟机参数中、Servlet上下文中进行配置项的配置,既然有这么多的配置位置,程序在加载配置项的时候总得有一个先后顺序吧,要不然系统不就乱套了。在SpringBoot中大概有这样的一个先后加载顺序(优先级高的会覆盖优先级低的配置):

  1. 命令行参数。
  2. Servlet初始参数
  3. ServletContext初始化参数
  4. JVM系统属性
  5. 操作系统环境变量
  6. 随机生成的带random.*前缀的属性
  7. 应用程序以外的application.yml或者appliaction.properties文件
  8. classpath:/config/application.yml或者classpath:/config/application.properties
  9. 通过@PropertySource标注的属性源
  10. 默认属性

那么SpringBoot是怎么创建这样的一个优先顺序的呢?默认的application.properties是怎么被加载的呢?在本章中我将把其中奥秘慢慢道出:
我在之前的SpringBoot启动流程简析的文章中说过,SpringBoot在启动的过程中会从spring.factories中加载一些ApplicationListener,在这些ApplicationListener中其中就有一个我们今天要说的ConfigFileApplicationListener;我们之前也说过在启动过程中会创建ConfigurableEnvironment,也会进行命令行参数的解析工作。在org.springframework.boot.SpringApplication#prepareEnvironment这个方法中有这样的一段代码:
prepareEnvironment
先创建应用可配置的环境变量,为命令行进行环境变量配置工作:

    protected void configureEnvironment(ConfigurableEnvironment environment,
            String[] args) {
        //将命令行参数转换为org.springframework.core.env.PropertySource
        configurePropertySources(environment, args);
        //Profile的配置,这里先不说明
        configureProfiles(environment, args);
    }
    protected void configurePropertySources(ConfigurableEnvironment environment,
            String[] args) {
        //从上面创建的ConfigurableEnvironment实例中获取MutablePropertySources实例
        MutablePropertySources sources = environment.getPropertySources();
        //如果有defaultProperties属性的话,则把默认属性添加为最后一个元素
        if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
            sources.addLast(
                    new MapPropertySource("defaultProperties", this.defaultProperties));
        }
        //这里addCommandLineProperties默认为true 如果有命令行参数的数
        if (this.addCommandLineProperties && args.length > 0) {
            //name为:commandLineArgs
            String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
            //如果之前的MutablePropertySources中有name为commandLineArgs的PropertySource的话,则把当前命令行参数转换为CompositePropertySource类型,和原来的PropertySource进行合并,替换原来的PropertySource
            if (sources.contains(name)) {
                PropertySource<?> source = sources.get(name);
                CompositePropertySource composite = new CompositePropertySource(name);
                composite.addPropertySource(new SimpleCommandLinePropertySource(
                        name + "-" + args.hashCode(), args));
                composite.addPropertySource(source);
                sources.replace(name, composite);
            }
            else {
                //如果之前没有name为commandLineArgs的PropertySource的话,则将其添加为MutablePropertySources中的第一个元素,注意了这里讲命令行参数添加为ConfigurableEnvironment中MutablePropertySources实例的第一个元素,且永远是第一个元素
                sources.addFirst(new SimpleCommandLinePropertySource(args));
            }
        }
    }

从上面的代码中我们可以看到,SpringBoot把命令行参数转换为PropertySource,并添加为环境变量中的第一个元素!这里简单的提一下MutablePropertySources 这个类,它的UML如下所示:
MutablePropertySources
从上面的UML中我们可以看到,MutablePropertySources实现了Iterable接口,是一个可迭代的类,在这个类中有这样的一个属性:

private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<PropertySource<?>>();

一个类型为PropertySource的CopyOnWriteArrayList,这里用的是CopyOnWriteArrayList,而不是ArrayList、LinkedList,大家可以想一下这里为什么用了CopyOnWriteArrayList。
propertySourceList
MutablePropertySources中的这些方法都是通过CopyOnWriteArrayList中的方法来实现的。我们在之前的文章中说明,SpringBoot创建的ConfigurableEnvironment实例是StandardServletEnvironment,其UML类图如下:
StandardServletEnvironment,其在实例化的过程中,会调用父类的构造函数先实例化父类,其父类StandardEnvironment为默认无参构造函数,AbstractEnvironment中的无参构造函数如下:

    public AbstractEnvironment() {
    //调用customizePropertySources方法进行定制PropertySource
    customizePropertySources(this.propertySources);
    }

在StandardServletEnvironment和StandardEnvironment分别重写了这个方法,其调用为StandardServletEnvironment中的customizePropertySources方法,其源码如下:

    protected void customizePropertySources(MutablePropertySources propertySources) {
    //SERVLET_CONFIG_PROPERTY_SOURCE_NAME 为 servletConfigInitParams 添加servletConfigInitParams 的PropertySource
        propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
    //SERVLET_CONTEXT_PROPERTY_SOURCE_NAME为servletContextInitParams 添加servletContextInitParams 的PropertySource
        propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
    //如果有JNDI
        if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
    //JNDI_PROPERTY_SOURCE_NAME 为 jndiProperties 添加jndiProperties  的PropertySource
            propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
        }
        //调用父类的StandardEnvironment中的customizePropertySources
        super.customizePropertySources(propertySources);
    }
    protected void customizePropertySources(MutablePropertySources propertySources) {
    //SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME 为 systemProperties 添加systemProperties 的PropertySource
        propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
    //SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME 为 systemEnvironment 添加systemEnvironment的PropertySource
        propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
    }

从上面的分析中我们可以看到在创建StandardServletEnvironment的实例的时候,会向org.springframework.core.env.AbstractEnvironment#propertySources中按顺序添加:name分别为:servletConfigInitParams、servletContextInitParams、jndiProperties 、systemProperties、systemEnvironment 的PropertySource,再按照我们前面的分析将name为commandLineArgs的PropertySource放到第一位,则org.springframework.core.env.AbstractEnvironment#propertySources的顺序到现在为:commandLineArgs、servletConfigInitParams 、servletContextInitParams 、jndiProperties 、systemProperties 、systemEnvironment,是不是和我们前面说的对照起来了?我们一直在说PropertySource,也一直在说PropertySource中的name,对于PropertySource我们可以理解为带name的、存放 name/value 的property pairs;那么其中的name我们应该如何理解呢?在org.springframework.core.env.MutablePropertySources中有这样一个方法:addAfter,其作用是将某个PropertySource的实例添加到某个name的PropertySource的后面,其源码如下所示:

    public void addAfter(String relativePropertySourceName, PropertySource<?> propertySource) {
        //确定所传入的relativePropertySourceName和所传入的propertySource的name不相同
        assertLegalRelativeAddition(relativePropertySourceName, propertySource);
        //如果之前添加过此PropertySource 则移除
        removeIfPresent(propertySource);
        //获取所传入的relativePropertySourceName的位置
        int index = assertPresentAndGetIndex(relativePropertySourceName);
        //将传入的propertySource添加到相应的位置
        addAtIndex(index + 1, propertySource);
    }

在上面的代码中有assertPresentAndGetIndex这样的一段代码比较重要:

    int index = assertPresentAndGetIndex(relativePropertySourceName);

    private int assertPresentAndGetIndex(String name) {
        //获取name为某个值的PropertySource的位置,
        int index = this.propertySourceList.indexOf(PropertySource.named(name));
        if (index == -1) {
            throw new IllegalArgumentException("PropertySource named '" + name + "' does not exist");
        }
        return index;
    }

在上面的代码中获取PropertySource的元素的位置的时候,是调用List中的indexOf方法来进行查找的,但是其参数为PropertySource.named(name)产生的对象,我们之前往propertySourceList中放入的明明是PropertySource类型的对象,这里在查找的时候为什么要用PropertySource.named(name)产生的对象来进行索引位置的查找呢?PropertySource.named(name)产生的对象又是什么呢?

    public static PropertySource<?> named(String name) {
        return new ComparisonPropertySource(name);
    }

PropertySource.named(name)产生的对象是ComparisonPropertySource的实例,它也是PropertySource的一个子类,那么为什么用它也能查找到之前放入到propertySourceList中的元素的位置呢?通过翻开indexOf这个方法的源码我们知道,它是通过调用元素的equals方法来判断是否是同一个元素的,而凑巧的是在PropertySource中重写了equals这个方法:

    public boolean equals(Object obj) {
        return (this == obj || (obj instanceof PropertySource &&
                ObjectUtils.nullSafeEquals(this.name, ((PropertySource<?>) obj).name)));
    }

到这里就很明显了,PropertySource中的name属性是用来判断是否是同一个元素的,即是否是同一个PropertySource的实例!我们在创建PropertySource类型的子类的时候都会传入一个name,直接用我们创建的PropertySource来进行位置的查找不就可以了吗?为什么还要创建出来一个ComparisonPropertySource类呢?通过翻看ComparisonPropertySource这个类的源码我们可以发现,在这个类中调用getSource、containsProperty、getProperty方法都会抛出异常,并且除了这三个方法之外没有多余的方法,如果直接用我们创建的PropertySource的话,保不齐你会重写它的equals方法,是不是?用ComparisonPropertySource的话,即使你在别的PropertySource实现类重写了PropertySource方法,在查找其顺序是也要按照Spring定义的规则来,并且ComparisonPropertySource只能做干查找元素位置这一件事,其他的事它什么也干不了,这又是不是设计模式中的某一个原则的体现呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章