说说Spring中PropertySource属性源配置文件的加载流程

PropertySource

注意:此处指的是org.springframework.core.env.PropertySource,而不是注解org.springframework.context.annotation.PropertySource

PropertySource是抽象类,表示一个键值对,代表着属性源。Spring内部是通过它来加载来自不同地方的属性源的。

Spring认为每个属性源都应该是有名称的,也就是作为属性源的key~

// @since 3.1 
public abstract class PropertySource<T> {

    protected final String name; // 该属性源的名字
    protected final T source;

    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;
    }
    // 若没有指定source 默认就是object  而不是null
    public PropertySource(String name) {
    	this(name, (T) new Object());
    }

    // getProperty是个抽象方法  子类去实现~~~
    // 小细节:若对应的key存在但是值为null,此处也是返回false的  表示不包含~
    public boolean containsProperty(String name) {
    	return (getProperty(name) != null);
    }
    @Nullable
    public abstract Object getProperty(String name);


    // 此处特别特别注意重写的这两个方法,我们发现它只和name有关,只要name相等  就代表着是同一个对象~~~~ 这点特别重要~
    @Override
    public boolean equals(Object other) {
    	return (this == other || (other instanceof PropertySource &&
    			ObjectUtils.nullSafeEquals(this.name, ((PropertySource<?>) other).name)));
    }
    @Override
    public int hashCode() {
    	return ObjectUtils.nullSafeHashCode(this.name);
    }


    // 静态方法:根据name就创建一个属性源~  ComparisonPropertySource是StubPropertySource的子类~
    public static PropertySource<?> named(String name) {
    	return new ComparisonPropertySource(name);
    }
}

该类重写了equals()和hashCode()方法,所以对于List的remove、indexOf方法都是有影响的~~~

PropertySource提供了一个named(String name)方法用于构造基于name的PropertySource的空实现,从而便于PropertySource 集合中查找指定名称的PropertySource

这个抽象类告诉我们,PropertySource的name非常的重要。接下来重点就是它的实现们,它的继承树如下:

在这里插入图片描述

JndiPropertySource

显然它和Jndi有关。JNDI:Java Naming and Directory Interface Java命名和目录接口。

// @since 3.1  它的source源是JndiLocatorDelegate
public class JndiPropertySource extends PropertySource<JndiLocatorDelegate> {
    public Object getProperty(String name) {
    	...
    	Object value = this.source.lookup(name);
    	...
    }
}

它的lookup方法就是依赖查找的精髓。由于现在是Spring的天下,Jndi确实使用太少了,我们不用过多了解,知道有这么回事就行。

web环境默认情况下的StandardServletEnvironment初始化的时候是会把JndiPropertySource放进环境里去的,name为:jndiProperties

JndiTemplate是Spring提供的对JNDI的访问模版。

EnumerablePropertySource

这是PropertySource的一个最重要分支,绝大部分配置源都继承于它。Enumerable:可枚举的

public abstract class EnumerablePropertySource<T> extends PropertySource<T> {
    public EnumerablePropertySource(String name, T source) {
    	super(name, source);
    }
    protected EnumerablePropertySource(String name) {
    	super(name);
    }
    @Override
    public boolean containsProperty(String name) {
    	return ObjectUtils.containsElement(getPropertyNames(), name);
    }
    
    // 返回所有Property的names(keys)
    public abstract String[] getPropertyNames();
}

该抽象类主要提供抽象方法getPropertyNames()表示每个key都应该是可以枚举的。

ServletContextPropertySource

它的属性源是ServletContext,此源头用于暴露和访问Servlet上下文的一些InitParameters们

public class ServletContextPropertySource extends EnumerablePropertySource<ServletContext> {
    public ServletContextPropertySource(String name, ServletContext servletContext) {
    	super(name, servletContext);
    }
    @Override
    public String[] getPropertyNames() {
    	return StringUtils.toStringArray(this.source.getInitParameterNames());
    }
    @Override
    @Nullable
    public String getProperty(String name) {
    	return this.source.getInitParameter(name);
    }
}

ServletConfigPropertySource

source源为ServletConfig。

ConfigurationPropertySource

需要注意:这个不是Spring提供的,你导入了commons-configuration2这个jar时才会有这个类。source源为:org.apache.commons.configuration2.Configuration

MapPropertySource

这是一个较为常用的属性源,一般我们自己new往里添加时,会使用它。

它的source源为:Map<String, Object>,还是非常的通用的~

public class MapPropertySource extends EnumerablePropertySource<Map<String, Object>> {

    public MapPropertySource(String name, Map<String, Object> source) {
    	super(name, source);
    }
    @Override
    @Nullable
    public Object getProperty(String name) {
    	return this.source.get(name);
    }
    @Override
    public boolean containsProperty(String name) {
    	return this.source.containsKey(name);
    }
    // map里所有的key就行~
    @Override
    public String[] getPropertyNames() {
    	return StringUtils.toStringArray(this.source.keySet());
    }

}

PropertiesPropertySource

继承自MapPropertySource

ResourcePropertySource

我们注解导入使用的是它

ResourcePropertySource继承自PropertiesPropertySource。它处理用org.springframework.core.io.Resource装载的Properties文件

// @since 3.1  若你的Properties资源使用的Resource装机进来的  直接使用它即可
public class ResourcePropertySource extends PropertiesPropertySource {

    @Nullable
    private final String resourceName;

    public ResourcePropertySource(String name, EncodedResource resource) throws IOException {
    	// 注意此处加载的最好是EncodedResource,因为Properties文件是需要处理乱码的~
    	super(name, PropertiesLoaderUtils.loadProperties(resource));
    	this.resourceName = getNameForResource(resource.getResource());
    }
    public ResourcePropertySource(EncodedResource resource) throws IOException {
    	super(getNameForResource(resource.getResource()), PropertiesLoaderUtils.loadProperties(resource));
    	this.resourceName = null;
    }
    public ResourcePropertySource(String name, Resource resource) throws IOException {
    	super(name, PropertiesLoaderUtils.loadProperties(new EncodedResource(resource)));
    	this.resourceName = getNameForResource(resource);
    }
    public ResourcePropertySource(String name, String location, ClassLoader classLoader) throws IOException {
    	this(name, new DefaultResourceLoader(classLoader).getResource(location));
    }
    public ResourcePropertySource(String location, ClassLoader classLoader) throws IOException {
    	this(new DefaultResourceLoader(classLoader).getResource(location));
    }
    public ResourcePropertySource(String name, String location) throws IOException {
    	this(name, new DefaultResourceLoader().getResource(location));
    }
    ...
 }

它有非常多的重载构造函数,这是Spring设计中最为常用的模式之一~~~目的是为了让使用者越简单、越方便越好。

CommandLinePropertySource

顾名思义,它表示命令行属性源。它这个泛型T可以是最简单的String[],也可以是OptionSet(依赖joptsimple这个jar)。

在传统的Spring应用中,命令行参数一般存在于main方法的入参里就够了,但是在某些特殊的情况下,它需要被注入到Spring Bean中。

如下案例:我们手动把命令行参数放进Spring容器内:

public static void main(String[] args) throws Exception {
    CommandLinePropertySource clps = new SimpleCommandLinePropertySource(args);

    // 启动容器
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ConfigurableEnvironment environment = ctx.getEnvironment();
    environment.getPropertySources().addFirst(clps);
    ctx.register(RootConfig.class);
    ctx.refresh();

    System.out.println(ArrayUtils.toString(args)); //{--server.port=8080}
    System.out.println(environment.getProperty("server.port")); //8080
}

此处:Spring命令行参数为--server.port=8080,有时候你会看到-D参数,这里用一个示例注意区分一下这两者的区别。

在这里插入图片描述

运行结果如下:

@Slf4j
public class Main {

    public static void main(String[] args) throws Exception {
        // vm参数里(其实就是java -Xmx512m -Dmyname=fsx)  的-D参数最终都会到System系统属性里面去
        System.out.println(System.getProperty("myname")); //fsx
        
        // --开头的命令行参数  是可以被spring应用识别的特定格式
        System.out.println(ArrayUtils.toString(args)); // {--server.port=8080,fsx}
    }
}

Enviroment环境内容值截图如下:

在这里插入图片描述

使用Environment获取属性值的原理:属性源最终都被加入进Environment持有的属性:MutablePropertySources保存着。所以,我们使用@Value也可以从它里面取值的~

// @since 3.1
public abstract class CommandLinePropertySource<T> extends EnumerablePropertySource<T> {

    // 命令行选项参数
    public static final String COMMAND_LINE_PROPERTY_SOURCE_NAME = "commandLineArgs";
    // 非选项参数 的名称
    public static final String DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME = "nonOptionArgs";
    private String nonOptionArgsPropertyName = DEFAULT_NON_OPTION_ARGS_PROPERTY_NAME;


    // 可以看到若调用者没有指定  会使用这个默认值的~
    public CommandLinePropertySource(T source) {
    	super(COMMAND_LINE_PROPERTY_SOURCE_NAME, source);
    }
    ...

    // containsOption和getNonOptionArgs都是抽象方法  
    @Override
    public final boolean containsProperty(String name) {
    	// 若你是的name是`nonOptionArgs`  那就是非选项参数中不为空 就是true
    	if (this.nonOptionArgsPropertyName.equals(name)) {
    		return !this.getNonOptionArgs().isEmpty();
    	}
    	return this.containsOption(name);
    }

    @Override
    @Nullable
    public final String getProperty(String name) {
    	if (this.nonOptionArgsPropertyName.equals(name)) {
    		Collection<String> nonOptionArguments = this.getNonOptionArgs();
    		if (nonOptionArguments.isEmpty()) {
    			return null;
    		} else {
    			// 显然非选项参数是多个 最终逗号分隔后再返回
    			return StringUtils.collectionToCommaDelimitedString(nonOptionArguments);
    		}
    	}
    	
    	// 选项参数使用getOptionValues 若它是一个集合,那就用逗号分隔后再返回
    	Collection<String> optionValues = this.getOptionValues(name);
    	if (optionValues == null) {
    		return null;
    	}
    	else {
    		return StringUtils.collectionToCommaDelimitedString(optionValues);
    	}
    }

    protected abstract boolean containsOption(String name);

    @Nullable
    protected abstract List<String> getOptionValues(String name);
    protected abstract List<String> getNonOptionArgs();
}

选项参数:能够通过如上--server.port=9090这种方式传入的,可以传入多个值或者列表等

非选项参数:我们在命令行传递除了vm参数的所有其它参数。比如我上面写成--server.port=9090 fsx最终的结果如下(非选项参数是个List装载的)

在这里插入图片描述

SimpleCommandLinePropertySource

它是我们最为常用的一个属性源之一,source类型为CommandLineArgs:Spring内部使用的一个类。CommandLineArgs内部维护着Map<String, List<String>> optionArgs和List<String> nonOptionArgs来表示整个命令行消息

我们构造使用它只需要把命令行的String[]数组扔进来即可,非常的方便。

public class SimpleCommandLinePropertySource extends CommandLinePropertySource<CommandLineArgs> {

    // SimpleCommandLineArgsParser解析这个数组。
    // 注意:它识别的是--而不是-D
    public SimpleCommandLinePropertySource(String... args) {
    	super(new SimpleCommandLineArgsParser().parse(args));
    }
    public SimpleCommandLinePropertySource(String name, String[] args) {
    	super(name, new SimpleCommandLineArgsParser().parse(args));
    }
    // 可见最终的source类型是CommandLineArgs类型~~~
    // 下面实现最终都委托给CommandLineArgs去处理~
    @Override
    public String[] getPropertyNames() {
    	return StringUtils.toStringArray(this.source.getOptionNames());
    }
    @Override
    protected boolean containsOption(String name) {
    	return this.source.containsOption(name);
    }
    @Override
    @Nullable
    protected List<String> getOptionValues(String name) {
    	return this.source.getOptionValues(name);
    }
    @Override
    protected List<String> getNonOptionArgs() {
    	return this.source.getNonOptionArgs();
    }
}

JOptCommandLinePropertySource

基于JOpt Simple的属性源实现,JOpt Simple是一个解析命令行选项参数的第三方库。它能够自定义格式、从文件中解析等高级操作。

PropertySources

从命名中就可以看出,它是PropertySource的一个复数形式,但是它是接口而不是抽象类。

它如同一个容器可以包含一个或者多个PropertySource(可以粗暴把它理解成一个Collection)。

public interface PropertySources extends Iterable<PropertySource<?>> {

    // @since 5.1   注意这个default方法是5.1后才有的  方便遍历和流式操作
    default Stream<PropertySource<?>> stream() {
    	return StreamSupport.stream(spliterator(), false);
    }

    // 注意这个name指的是PropertySource的name属性~
    boolean contains(String name);
    // 根据name找到一个PropertySource~~~没找到返回null
    @Nullable
    PropertySource<?> get(String name);

}

Spring仅为我们提供一个实现类:MutablePropertySources

MutablePropertySources

Mutable:可变的。

它包含有多个数据源,并且提供对他们操作的方法~

public class MutablePropertySources implements PropertySources {

    // 持有多个PropertySource,并且它是个CopyOnWriteArrayList  放置了并发问题
    private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();

    public MutablePropertySources() {
    }
    // 注意:此处是循环调用的addLast方法~~~~~~~~~~~
    public MutablePropertySources(PropertySources propertySources) {
    	this();
    	for (PropertySource<?> propertySource : propertySources) {
    		addLast(propertySource);
    	}
    }

    @Override
    public Iterator<PropertySource<?>> iterator() {
    	return this.propertySourceList.iterator();
    }
    @Override
    public Spliterator<PropertySource<?>> spliterator() {
    	return Spliterators.spliterator(this.propertySourceList, 0);
    }
    // 复写了父类的Default方法~~~直接使用List的流~
    @Override
    public Stream<PropertySource<?>> stream() {
    	return this.propertySourceList.stream();
    }


    // 此处注意:使用的是index,并且使用的是named静态方法~~~  因为这里是根据name来查找
    // 而上面我们说了,关于PropertySource的相等只和name有关而已~
    @Override
    @Nullable
    public PropertySource<?> get(String name) {
    	int index = this.propertySourceList.indexOf(PropertySource.named(name));
    	return (index != -1 ? this.propertySourceList.get(index) : null);
    }

    // 放在List的顶部=======注意:都先remove了,避免重复出现多个============
    public void addFirst(PropertySource<?> propertySource) {
    	removeIfPresent(propertySource);
    	this.propertySourceList.add(0, propertySource);
    }
    public void addLast(PropertySource<?> propertySource) {
    	removeIfPresent(propertySource);
    	this.propertySourceList.add(propertySource);
    }
    // 把propertySource放在指定名字的relativePropertySourceName的前面
    public void addBefore(String relativePropertySourceName, PropertySource<?> propertySource) {
    	// 若relativePropertySourceName和propertySource同名,抛出异常~
    	assertLegalRelativeAddition(relativePropertySourceName, propertySource);
    	removeIfPresent(propertySource);
    	// 若relativePropertySourceName里不存在  这里也会抛出异常~
    	int index = assertPresentAndGetIndex(relativePropertySourceName);
    	
    	// 放在指定index的位置~
    	addAtIndex(index, propertySource);
    }
    
    public void addAfter(String relativePropertySourceName, PropertySource<?> propertySource) { ... }    
    // 获取指定propertySource的优先权,实际就是index角标。
    // 显然角标正数情况下越小越优先。0表示最优先,但是-1表示不存在~~~
    public int precedenceOf(PropertySource<?> propertySource) {
    	return this.propertySourceList.indexOf(propertySource);
    }
    
    // 根据名称来移除
    @Nullable
    public PropertySource<?> remove(String name) {
    	int index = this.propertySourceList.indexOf(PropertySource.named(name));
    	return (index != -1 ? this.propertySourceList.remove(index) : null);
    }

    public void replace(String name, PropertySource<?> propertySource) {
    	int index = assertPresentAndGetIndex(name);
    	this.propertySourceList.set(index, propertySource);
    }
    public int size() {
    	return this.propertySourceList.size();
    }
    @Override
    public String toString() {
    	return this.propertySourceList.toString();
    }
    ...
}

MutablePropertySources它更像是一个管理器,管理着所有的PropertySource们。然后调用者最终调用getProperty()的时候,就会按照优先级从所有的PropertySource取值。

下面以@PropertySource注解导入自定义属性源文件为例做个介绍。

@PropertySource属性源的加载流程

为了节约篇幅,这里直接从ConfigurationClassParser开始:

class ConfigurationClassParser {
    ...
    @Nullable
    protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
    		throws IOException {
    		//1、解析嵌套内部类
    		//2、解析@PropertySource  === 这是下面的内容 ====
    	// 相当于拿到所有的PropertySource注解,注意PropertySources属于重复注解的范畴~~~
    	for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
    			sourceClass.getMetadata(), PropertySources.class,
    			org.springframework.context.annotation.PropertySource.class)) {
    		
    		// 这个判断目前来说是个恒等式~~~  所以的内置实现都是子接口ConfigurableEnvironment的实现类~~~~
    		// processPropertySource:这个方法只真正解析这个注解的地方~~~
    		if (this.environment instanceof ConfigurableEnvironment) {
    			processPropertySource(propertySource);
    		} else {
    			logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
    					"]. Reason: Environment must implement ConfigurableEnvironment");
    		}
    	}
    		//3、解析@ComponentScan
    		//4、解析@Import
    		//5、解析@ImportResource
    		//6、解析@Bean
    		//7、解析接口default方法~~~ 也可以用@Bean标注
    		//8、解析super class父类
    }    
    // 处理每一个属性源,最终加入到环境上下文里面去~
    private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
    	// 属性源的name,大多数情况下我们并不指定~
    	String name = propertySource.getString("name");
    	if (!StringUtils.hasLength(name)) {
    		name = null;
    	}
    	String encoding = propertySource.getString("encoding");
    	if (!StringUtils.hasLength(encoding)) {
    		encoding = null;
    	}
    	String[] locations = propertySource.getStringArray("value");
    	Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
    	boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");    
    	// 此处注意:若我们都没有指定Factory的话,就会使用Spring默认的工厂,最终都是生成一个ResourcePropertySource(是个PropertiesPropertySource~~)
    	// 所以它默认是只能处理Properties文件的(当然指定的格式的xml也是可以的),yaml是不能被支持的~~~~~~~~~~~
    	Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
    	PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
    			DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));    
    	for (String location : locations) {
    		try {
    			String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
    			// 处理好占位符后,拿到这个资源~~~~
    			Resource resource = this.resourceLoader.getResource(resolvedLocation);    
    			// 重点就在这个方法里~~~把这个属性源添加进来~~~
    			addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
    		}
    	}
    }    
    private void addPropertySource(PropertySource<?> propertySource) {
    	String name = propertySource.getName();
    	// 从环境里把MutablePropertySources拿出来,准备向里面添加~~~~
    	MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();    
    	// 这里有个暖心的处理:若出现同名的配置文件,它会两个都保存着,联合形成一个CompositePropertySource  这样它哥俩就都会生效了
    	// 否则MutablePropertySources 的Map里面的name是不能同名的,我觉得这个做法还是很暖心的~~~
    	// 我觉得这个操作虽然小,但是足见Spring的小暖心~
    	if (this.propertySourceNames.contains(name)) {
    		// We've already added a version, we need to extend it
    		PropertySource<?> existing = propertySources.get(name);
    		if (existing != null) {
    			PropertySource<?> newSource = (propertySource instanceof ResourcePropertySource ?
    					((ResourcePropertySource) propertySource).withResourceName() : propertySource);
    			if (existing instanceof CompositePropertySource) {
    				((CompositePropertySource) existing).addFirstPropertySource(newSource);
    			}
    			else {
    				if (existing instanceof ResourcePropertySource) {
    					existing = ((ResourcePropertySource) existing).withResourceName();
    				}
    				CompositePropertySource composite = new CompositePropertySource(name);
    				// 小细节:后添加的反而在最上面的~~~ 已经存在会被挤下来一个位置~
    				composite.addPropertySource(newSource);
    				composite.addPropertySource(existing);
    
    				// 把已经存在的这个name替换成composite组合的~~~
    				propertySources.replace(name, composite);
    			}
    			return;
    		}
    	}    
    	// 重要:手动导入进来的propertySource是放在最后面的(优先级最低)
    	
    	// 这段代码处理的意思是:若你是自己导入进来的第一个,那就放在最末尾
    	// 若你不是第一个,那就把你放在已经导入过的最后一个的前一个里面~~~
    	if (this.propertySourceNames.isEmpty()) {
    		propertySources.addLast(propertySource);
    	} else {
    		String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1);
    		propertySources.addBefore(firstProcessed, propertySource);
    	}
    	this.propertySourceNames.add(name);
    }
}

从中可以看出一些小细节:

  • @PropertySource被解析的时机还是非常早的(次于内部类)
  • 它允许同名的PropertySource存在,并且两个最终都会添加进来不会覆盖
  • 通过注解@PropertySource导入进来的属性源的优先级是最低的~~~
  • location是支持占位符的,但是properties件里面其实也是支持占位符的(文件内的${xxx}这种占位符依旧可以用来引用本文件的内容、环境变量内容等等。它的解析实际是在给java属性赋值时~)

 

参考:

 

posted @ 2021-12-12 12:08  残城碎梦  阅读(1033)  评论(0编辑  收藏  举报