說說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屬性賦值時~)

 

參考:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM