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屬性賦值時~)
參考: |