目录

  • 主要相关类介绍
    • PropertySource
    • PropertySources
    • PropertySourceLoader
    • PropertySourcesLoader
    • PropertyResolver
    • Environment
    • StandardServletEnvironment
    • ConfigFileApplicationListener
    • ConfigFileApplicationListener.Loader

主要相关类介绍

PropertySource

是Spring对name/value键值对的封装接口。该定义了getSource()方法,这个方法会返回得到属性源的源头。比如MapPropertySource的源头就是一个Map,PropertiesPropertySource的源头就是一个Properties。

public abstract class PropertySource<T> {

	protected final Log logger = LogFactory.getLog(getClass());

	protected final String name;

	protected final T source;
	/**
	 * Create a new {@code PropertySource} with the given name and source object.
	 */
	public PropertySource(String name, T source) {
		Assert.hasText(name, "Property source name must contain at least one character");
		Assert.notNull(source, "Property source must not be null");
		this.name = name;
		this.source = source;
	}
	/**
	 * Return the name of this {@code PropertySource}
	 */
	public String getName() {
		return this.name;
	}
	/**
	 * Return the underlying source object for this {@code PropertySource}.
	 */
	public T getSource() {
		return this.source;
	}
	/**
	 * Return whether this {@code PropertySource} contains the given name.
	 */
	public boolean containsProperty(String name) {
		return (getProperty(name) != null);
	}
	/**
	 * Return the value associated with the given name,
	 */
	public abstract Object getProperty(String name);
}

PropertySource目前的实现类有不少,比如上面提到的MapPropertySource和PropertiesPropertySource,还有RandomValuePropertySource(source是Random)、SimpleCommandLinePropertySource(source是CommandLineArgs,命令行参数)、ServletConfigPropertySource(source是ServletConfig)等等。

PropertySources


/**
 * Holder containing one or more {@link PropertySource} objects.
 */
public interface PropertySources extends Iterable<PropertySource<?>> {

	/**
	 * Return whether a property source with the given name is contained.
	 * @param name the {@linkplain PropertySource#getName() name of the property source} to find
	 */
	boolean contains(String name);

	/**
	 * Return the property source with the given name, {@code null} if not found.
	 * @param name the {@linkplain PropertySource#getName() name of the property source} to find
	 */
	PropertySource<?> get(String name);

}

PropertySource的管理器,具体的实现有MutablePropertySources

PropertySourceLoader

  • 用于将一个文件资源(Resource)生成配置属性资源(PropertySource)
  • SpringBoot把配置文件的加载封装成了PropertySourceLoader接口,该接口的定义如下
    /**
     * Strategy interface located via {@link SpringFactoriesLoader} and used to load a
     * {@link PropertySource}.
     */
    public interface PropertySourceLoader {
        /**
         * Returns the file extensions that the loader supports (excluding the '.').
         */
        String[] getFileExtensions();
    
        /**
         * Load the resource into a property source.
         */
        PropertySource<?> load(String name, Resource resource, String profile)
                throws IOException;
    
    }
    
    • getFileExtensions: 该加载器支持的文件类型
    • load(String name, Resource resource, String profile): 加载该resource为PropertySource的某个实现类
  • SpringBoot 的配置文件内置支持 properties、xml、yml、yaml 几种格式,其中 properties和xml 对应的Loader类为 PropertiesPropertySourceLoader ,yml和yaml 对应的Loader类为 YamlPropertySourceLoader。观察这2个类可以发现,都实现自接口 PropertySourceLoader

下面实现了一个 json 格式的配置文件 Loader类:

package com.shanhy.sboot.property;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.boot.json.JsonParser;
import org.springframework.boot.json.JsonParserFactory;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;

/**
 * JSON格式配置文件加载器
 * 
 * @create 2017年4月20日
 */
public class JsonPropertySourceLoader implements PropertySourceLoader {

    public String[] getFileExtensions() {
        // 配置文件格式(扩展名)
        return new String[] { "json" };
    }

    public PropertySource<?> load(String name, Resource resource, String profile) throws IOException {
        // 处理机制参考PropertiesPropertySourceLoader
        // 无论profile有没有值,底层都会尝试先执行 load(String name, Resource resource, null),所以这个地方之间判断等于null即可。
        // 当前版本springboot-1.5.2(后续版本未知)详见 ConfigFileApplicationListener 的 445 行
        if (profile == null) {
            Map<String, Object> result = mapPropertySource(resource);
            return new MapPropertySource(name, result);
        }
        return null;
    }

    /**
     * 解析Resource为Map
     *
     * @param resource
     * @return
     * @throws IOException
     * 
     * @create 2017年4月20日
     */
    private Map<String, Object> mapPropertySource(Resource resource) throws IOException {
        if (resource == null) {
            return null;
        }
        Map<String, Object> result = new HashMap<String, Object>();
        JsonParser parser = JsonParserFactory.getJsonParser();
        Map<String, Object> map = parser.parseMap(readFile(resource));
        nestMap("", result, map);
        return result;
    }

    /**
     * 读取Resource文件内容为字符串
     *
     * @param resource
     * @return
     * @throws IOException
     * 
     * @create 2017年4月20日
     */
    private String readFile(Resource resource) throws IOException {
        InputStream inputStream = resource.getInputStream();
        List<Byte> byteList = new LinkedList<Byte>();
        byte[] readByte = new byte[1024];
        int length;
        while ((length = inputStream.read(readByte)) > 0) {
            for (int i = 0; i < length; i++) {
                byteList.add(readByte[i]);
            }
        }
        byte[] allBytes = new byte[byteList.size()];
        int index = 0;
        for (Byte soloByte : byteList) {
            allBytes[index] = soloByte;
            index += 1;
        }
        return new String(allBytes, "UTF-8");
    }

    /**
     * 处理map(map中可能还嵌套map,递归处理),最终输出一个非嵌套的map
     *
     * @param prefix
     *            前缀
     * @param result
     *            处理后的map
     * @param map
     *            处理前的map
     * 
     * @create 2017年4月20日
     */
    @SuppressWarnings("unchecked")
    private void nestMap(String prefix, Map<String, Object> result, Map<String, Object> map) {
        if (prefix.length() > 0) {
            prefix += ".";
        }
        for (Map.Entry<String, Object> entrySet : map.entrySet()) {
            if (entrySet.getValue() instanceof Map) {
                nestMap(prefix + entrySet.getKey(), result, (Map<String, Object>) entrySet.getValue());
            } else {
                result.put(prefix + entrySet.getKey().toString(), entrySet.getValue());
            }
        }
    }
}

然后在 src/main/resources 中创建 META-INF/spring.factories 文件,内容为:

org.springframework.boot.env.PropertySourceLoader=\
com.shanhy.sboot.property.JsonPropertySourceLoader

创建测试的配置文件 application.json

{
    "custom": {
        "property": {
            "message": "测试数据"
        }
    }
}

创建验证结果的 HelloController.java

@RestController
public class HelloController {

    @Value("${custom.property.message:}")
    private String customProperty;

    @RequestMapping("/test")
    public String test() {
        return customProperty;
    }
}

PropertySourcesLoader

  • PropertySourcesLoader内部有2个属性,分别是PropertySourceLoader集合和MutablePropertySources(内部有PropertySource的集合)。最终加载完毕之后MutablePropertySources属性中的PropertySource会被添加到环境Environment中的属性源列表中。PropertySourcesLoader被构造的时候会使用工厂加载机制获得PropertySourceLoader集合(默认就2个:PropertiesPropertySourceLoader和YamlPropertySourceLoader;可以自己扩展),然后设置到属性中
  • 该类存在四个多态load()方法,最终会循环所有的PropertySourceLoader,判断是否支持该文件类型,如果支持则通过该loader进行加载
  • 在加载后会通过addPropertySource()方法,将生成 的PropertySource存入MutablePropertySources
  • PropertySourcesLoader.getAllFileExtensions():获取所有支持的配置文件类型,实际为循环所有的PropertySourcesLoader的子加载器,获取每个支持的配置文件类型(loader.getFileExtensions())

PropertyResolver

/**
 * Interface for resolving properties against any underlying source.
 * 针对所有的source提供properties的接口
 */
public interface PropertyResolver {
	/**
	 * Return whether the given property key is available for resolution,
	 * i.e. if the value for the given key is not {@code null}.
	 */
	boolean containsProperty(String key);

	/**
	 * Return the property value associated with the given key,
	 * or {@code null} if the key cannot be resolved.
	 */
	String getProperty(String key);

	/**
	 * Return the property value associated with the given key, or
	 * {@code defaultValue} if the key cannot be resolved.
	 */
	String getProperty(String key, String defaultValue);

	/**
	 * Return the property value associated with the given key,
	 * or {@code null} if the key cannot be resolved.
	 */
	<T> T getProperty(String key, Class<T> targetType);

	/**
	 * Return the property value associated with the given key,
	 * or {@code defaultValue} if the key cannot be resolved.
	 */
	<T> T getProperty(String key, Class<T> targetType, T defaultValue);

	/**
	 * Convert the property value associated with the given key to a {@code Class}
	 * of type {@code T} or {@code null} if the key cannot be resolved.
	 * @throws org.springframework.core.convert.ConversionException if class specified
	 * by property value cannot be found or loaded or if targetType is not assignable
	 * from class specified by property value
	 */
	@Deprecated
	<T> Class<T> getPropertyAsClass(String key, Class<T> targetType);

	/**
	 * Return the property value associated with the given key (never {@code null}).
	 * @throws IllegalStateException if the key cannot be resolved
	 */
	String getRequiredProperty(String key) throws IllegalStateException;

	/**
	 * Return the property value associated with the given key, converted to the given
	 * targetType (never {@code null}).
	 * @throws IllegalStateException if the given key cannot be resolved
	 */
	<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;

	/**
	 * Resolve ${...} placeholders in the given text, replacing them with corresponding
	 * property values as resolved by {@link #getProperty}. Unresolvable placeholders with
	 * no default value are ignored and passed through unchanged.
	 */
	String resolvePlaceholders(String text);

	/**
	 * Resolve ${...} placeholders in the given text, replacing them with corresponding
	 * property values as resolved by {@link #getProperty}. Unresolvable placeholders with
	 * no default value will cause an IllegalArgumentException to be thrown.
	 * @return the resolved String (never {@code null})
	 * @throws IllegalArgumentException if given text is {@code null}
	 * or if any placeholders are unresolvable
	 */
	String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;

}

PropertyResolver属性解决器,主要具有两个功能:

  • 通过propertyName属性名获取与之对应的propertValue属性值(getProperty)。
  • 把${propertyName:defaultValue}格式的属性占位符,替换为实际的值(resolvePlaceholders)。

注意:getProperty获取的属性值,全都是调用resolvePlaceholders进行占位符替换后的值。

组件体系图如下:

Environment


/**
 * Interface representing the environment in which the current application is running.
 * Models two key aspects of the application environment: <em>profiles</em> and
 * <em>properties</em>. Methods related to property access are exposed via the
 * {@link PropertyResolver} superinterface.
 */
public interface Environment extends PropertyResolver {

	/**
	 * Return the set of profiles explicitly made active for this environment. Profiles
	 * are used for creating logical groupings of bean definitions to be registered
	 * conditionally, for example based on deployment environment.  Profiles can be
	 * activated by setting {@linkplain AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME
	 * "spring.profiles.active"} as a system property or by calling
	 * {@link ConfigurableEnvironment#setActiveProfiles(String...)}.
	 * <p>If no profiles have explicitly been specified as active, then any
	 * {@linkplain #getDefaultProfiles() default profiles} will automatically be activated.
	 */
	String[] getActiveProfiles();

	/**
	 * Return the set of profiles to be active by default when no active profiles have
	 * been set explicitly.
	 */
	String[] getDefaultProfiles();

	/**
	 * Return whether one or more of the given profiles is active or, in the case of no
	 * explicit active profiles, whether one or more of the given profiles is included in
	 * the set of default profiles. If a profile begins with '!' the logic is inverted,
	 * i.e. the method will return true if the given profile is <em>not</em> active.
	 * For example, <pre class="code">env.acceptsProfiles("p1", "!p2")</pre> will
	 * return {@code true} if profile 'p1' is active or 'p2' is not active.
	 * @throws IllegalArgumentException if called with zero arguments
	 * or if any profile is {@code null}, empty or whitespace-only
	 */
	boolean acceptsProfiles(String... profiles);

}
  • Environment接口是Spring对当前程序运行期间的环境的封装。主要提供了两大功能:profile和property(父接口PropertyResolver提供)。
  • 目前主要有StandardEnvironment、StandardServletEnvironment和MockEnvironment3种实现,分别代表普通程序、Web程序以及测试程序的环境。

StandardServletEnvironment

关系图
WX20200621-101745@2x

/**
 * {@link Environment} implementation to be used by {@code Servlet}-based web
 * applications. All web-related (servlet-based) {@code ApplicationContext} classes
 * initialize an instance by default.
 */
public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {

	/** Servlet context init parameters property source name: {@value} */
	public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";

	/** Servlet config init parameters property source name: {@value} */
	public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";

	/** JNDI property source name: {@value} */
	public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";


	/**
	 * Customize the set of property sources with those contributed by superclasses as
	 * well as those appropriate for standard servlet-based environments:
	 * 通过父类提供的source及适用于标准servlet的环境自定义property sources 集合,
	 */
	@Override
	protected void customizePropertySources(MutablePropertySources propertySources) {
		propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
		propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
		if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
			propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
		}
		super.customizePropertySources(propertySources);
	}

	@Override
	public void initPropertySources(ServletContext servletContext, ServletConfig servletConfig) {
		WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
	}

}

ConfigFileApplicationListener

SpringApplication启动的时候会使用工厂加载机制初始化一些初始化器和监听器。其中org.springframework.boot.context.config.ConfigFileApplicationListener这个监听器会被加载:

// spring-boot-version.release/META-INF/spring.factories
org.springframework.context.ApplicationListener=\
...
org.springframework.boot.context.config.ConfigFileApplicationListener,\
...

ConfigFileApplicationListener会监听SpringApplication启动的时候发生的事件,它的监听代码:

@Override
public void onApplicationEvent(ApplicationEvent event) {
	// 应用环境信息准备好的时候对应的事件。此时Spring容器尚未创建,但是环境已经创建
	if (event instanceof ApplicationEnvironmentPreparedEvent) {
		onApplicationEnvironmentPreparedEvent(
				(ApplicationEnvironmentPreparedEvent) event);
	}
	// Spring容器创建完成并在refresh方法调用之前对应的事件
	if (event instanceof ApplicationPreparedEvent) {
		onApplicationPreparedEvent(event);
	}
}

private void onApplicationEnvironmentPreparedEvent(
		ApplicationEnvironmentPreparedEvent event) {
	// 使用工厂加载机制读取key为org.springframework.boot.env.EnvironmentPostProcessor的实现类
	List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
	// 加上自己。ConfigFileApplicationListener也是一个EnvironmentPostProcessor接口的实现类
	postProcessors.add(this);
	// 排序
	AnnotationAwareOrderComparator.sort(postProcessors);
	// 遍历这些EnvironmentPostProcessor,并调用postProcessEnvironment方法
	for (EnvironmentPostProcessor postProcessor : postProcessors) {
		postProcessor.postProcessEnvironment(event.getEnvironment(),
				event.getSpringApplication());
	}
}

ConfigFileApplicationListener也是一个EnvironmentPostProcessor接口的实现类,在这里会被调用:

// ConfigFileApplicationListener的postProcessEnvironment方法
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
		SpringApplication application) {
	// 添加属性源到环境中
	addPropertySources(environment, application.getResourceLoader());
	// 配置需要ignore的beaninfo
	configureIgnoreBeanInfo(environment);
	// 从环境中绑定一些参数到SpringApplication中
	bindToSpringApplication(environment, application);
}

protected void addPropertySources(ConfigurableEnvironment environment,
		ResourceLoader resourceLoader) {
	// 添加一个RandomValuePropertySource到环境中
	// RandomValuePropertySource是一个用于处理随机数的PropertySource,内部存储一个Random类的实例
	RandomValuePropertySource.addToEnvironment(environment);
	try {
		// 构造一个内部类Loader,并调用它的load方法
		new Loader(environment, resourceLoader).load();
	}
	catch (IOException ex) {
		throw new IllegalStateException("Unable to load configuration files", ex);
	}
}

如上,在执行postProcessEnvironment时,Loader.load()方法将会调用

ConfigFileApplicationListener.Loader

加载候选属性源并配置活动配置文件

  • load()方法会在file:./config/,file:./,classpath:/config/classpath:/ 目录(location)下查找某个文件
  • load(String location, String name, Profile profile)会查找properties、xml、yml、yaml中某个类型的配置文件,如resources/bootstrap.yml
  • 最终会调用PropertySourcesLoader.load(Resource resource, String group, String name,String profile),在该方法中,会通过对应的PropertySourceLoader的实现类加载该配置文件
  • 加载完成后,会将加载到的配置添加至EnumerableCompositePropertySource,EnumerableCompositePropertySource中维护了一个集合LinkedHashSet<PropertySource<?>>()
  • 然后将EnumerableCompositePropertySource添加到MutablePropertySources中,该类中维护了一个集合List<PropertySource<?>>

值得注意的是EnumerableCompositePropertySource是一个PropertySource而MutablePropertySources是一个PropertySources

application.yml加载过程

在特定的目录下扫描配置文件,然后由于是.yml文件,故通过YamlPropertySourceLoader加载该配置文件

consul排序乱了,但是application.yml是有序的原因

是由于加载application.yml是通过YamlPropertySourceLoader加载的,该类中调用YamlProcessor.process(MatchCallback callback)时,result为LinkedHashMap<String, Object>(),是有序的

而consul是通过YamlPropertiesFactoryBean加载的,该类调用YamlProcessor.process(MatchCallback callback)时,result为Properties,是无序的,故consul是无序的

consul为什么使用YamlPropertiesFactoryBean而不使用YamlPropertySourceLoader

因为consul是从consul服务器拉取文件,文件是二进制,并不是File形式,无法直接使用YamlPropertySourceLoader,故需要通过YamlPropertiesFactoryBean以二进制的形式生成Properties,而Properties是继承HashTable实现的,故是无序的;
参考ConsulPropertySourceLocator.locator()方法

扩展

  • 开发者可通过开发PropertySourceLocator的实现类,加载配置文件
  • 如 从数据库,mongo库等加载配置文件
  • 如 从数据库读取zuul的路由,权限管理等