Spring源碼閱讀-加載配置文件(一)

Spring啓動流程-加載配置文件以前的工做解析文件名

說到Spring啓動流程,仍是要在加載配置主文件開始.html

準備的Demo

我這裏有一個小Demo,項目的結構是這樣的java

image-20210811082808487

而它的配置文件也是很簡單的spring

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
	<bean id="person" class="com.sourcodestudy.pojo.Person">
		<property name="name" value="Sdayup"/>
		<property name="age" value="22"/>
	</bean>
</beans>
複製代碼

用於測試的類數組

import com.sourcodestudy.pojo.Person;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class StartDemo {

	public static void main(String[] args) {
		ApplicationContext ac =	new ClassPathXmlApplicationContext("classpath:application.xml");
		Person person = (Person) ac.getBean("person");
		System.out.println(person.toString());
	}

}
複製代碼

解析流程

這一切的開始都源自於ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:application.xml");,markdown

想必用過Spring的小夥伴對於這行代碼已經很熟悉了,這行代碼是加載Spring配置文件的代碼。它是讀取的classpath路徑下面的文件,所在上面代碼中的classpath前綴寫不寫均可app

在開始加載配置文件以前作了哪些工做?

當跟着斷點走到了ClassPathXmlApplicationContext類中,最後他們會走到這樣的一個構造方法裏面去ide

public ClassPathXmlApplicationContext( String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) throws BeansException {

    super(parent);
    setConfigLocations(configLocations);
    if (refresh) {
        refresh();
    }
}
複製代碼

注:當你看到有的方法裏面調用了super時,不要跳過,由於裏面可能會進行建立一些對象,後面會用到,最好是看一下測試

來看下這裏的super幹了什麼?它究竟是何方牛馬

public AbstractApplicationContext(@Nullable ApplicationContext parent) {
    this();
    setParent(parent);
}
複製代碼

當一路點着super最後來到一個名叫AbstractApplicationContext的類時,這裏會建立出一些對象,先了解下,後面可能會用到ui

  • id:這個是context裏面惟一的一個上下文
  • startupShutdownMonitor:你能夠理解這個是一個刷新或銷燬時用到的鎖,由於刷新或銷燬的過程是不可被打斷的.

還有一些其它的對象,有興趣的能夠看下this

接下在看到 this()裏面時,它會對這個資源解析器進行賦值

public AbstractApplicationContext() {
	this.resourcePatternResolver = getResourcePatternResolver();
}
複製代碼

而在這個getResourcePatternResolver()方法裏面是建立了一個PathMatchingResourcePatternResolver對象.

PathMatchingResourcePatternResolver主要用途就是經過給定的文件路徑或者是經過Ant風格的路徑來查找到相應的資源

AbstractXmlApplicationContext類裏面有一個名叫validating,這個我以爲也是一個要看的地方,這個是表示是否要使用XML驗證的一個標誌

private boolean validating = true;
/** * Set whether to use XML validation. Default is {@code true}. */
public void setValidating(boolean validating) {
    this.validating = validating;
}
複製代碼

在super裏面主要是作了對於一些變量進行了初始化,這些變量在後面會用到,因此你們要看下.

setConfigLocations方法是幹了些啥?

此次以前先看下ClassPathXmlApplicationContext的類關係圖,由於裏面不少的方法是直接使用的父類裏面的方法.

image-20210811092506527

進入setConfigLocations方法,其實這裏已經來到了AbstractRefreshableApplicationContext類中

/** * Set the config locations for this application context. * <p>If not set, the implementation may use a default as appropriate. */
public void setConfigLocations(@Nullable String... locations) {
    if (locations != null) {
        Assert.noNullElements(locations, "Config locations must not be null");
        this.configLocations = new String[locations.length];
        for (int i = 0; i < locations.length; i++) {
            this.configLocations[i] = resolvePath(locations[i]).trim();
        }
    }
    else {
        this.configLocations = null;
    }
}
複製代碼

這個方法會將配置文件的路徑傳入,而且將他們從新放到configLocations裏面,

  • configLocations是一個字符串類型的數組,用於存儲配置文件的路徑

這裏會調用一個名叫解析路徑的方法resolvePath(),爲啥會有這麼一個東西?

/** * Resolve the given path, replacing placeholders with corresponding * environment property values if necessary. Applied to config locations. * @param path the original file path * @return the resolved file path * @see org.springframework.core.env.Environment#resolveRequiredPlaceholders(String) */
protected String resolvePath(String path) {
    return getEnvironment().resolveRequiredPlaceholders(path);
}
複製代碼

在有的配置文件名字寫成application${username},這種名字裏面帶有佔位符的文件名,這裏能夠將這個佔位符給替換掉.

正如註釋中寫的那樣:解析給定的路徑,若有必要,用相應的環境屬性值替換佔位符。

而這裏的getEnvironment()方法就是建立了一個標準環境對象類,裏面包含了系統屬性系統環境屬性

@Override
public ConfigurableEnvironment getEnvironment() {
    if (this.environment == null) {
        this.environment = createEnvironment();
    }
    return this.environment;
}

protected ConfigurableEnvironment createEnvironment() {
    return new StandardEnvironment();
}
複製代碼

在下面的屬性裏面看到了一個SESSIONNAME的屬性,例如:有一個配置文件的名子是這樣寫的application${SESSIONNAME}當程序對這個文件名解析後就會變成applicationConsole,上面說的解析路大概也就是這個意思.

image-20210811171021917

來到resolveRequiredPlaceholders方法後,傳進來了配置文件的文件名,要對其進行解析

這裏爲了方便展現將ClassPathXmlApplicationContext裏面的參數改爲application{USERNAME{USERDOMAIN_ROAMINGPROFILE}}.xml

這裏的屬性在其它電腦上可能不存在,找一個大家電腦有的參數就好

@Override
public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
    if (this.strictHelper == null) {
        this.strictHelper = createPlaceholderHelper(false);
    }
    return doResolvePlaceholders(text, this.strictHelper);
}
複製代碼

小提示:只要是看到do開頭的方法,就是開始要進行真正的操做了

進入方法首先是判斷有沒有一個佔位符幫助器的對象,沒有的話就要建立出來,這裏沒啥好說的,就是在這個方法裏面建立了一個對象.自已能夠看下,這裏主要是看doResolvePlaceholders()方法.

private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
    return helper.replacePlaceholders(text, this::getPropertyAsRawString);
}
複製代碼

這裏就直接調用了這個幫助器裏面的replacePlaceholders方法,這個方法就直接看parseStringValue方法就行了

public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
    Assert.notNull(value, "'value' must not be null");
    return parseStringValue(value, placeholderResolver, null);
}
複製代碼

下面這個方法就開始佔位符的替換了,

再此以前咱們先來看下建立佔位符幫助器的方法,上面一開始說自已看就好,這裏我仍是說下吧(createPlaceholderHelper)

private String placeholderPrefix = SystemPropertyUtils.PLACEHOLDER_PREFIX;

private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX;

private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
    return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix,
                                         this.valueSeparator, ignoreUnresolvablePlaceholders);
}
複製代碼

還記得上面我說過在解析文件名的時候系統能夠替換** 裏面的東西 , 它是怎麼知道要替換 {}**裏面的東西,它是怎麼知道要替換** {}裏面的而不是##**裏面的東西呢?緣由就在它建立佔位符幫助器的時的構造傳參裏面.

在建立這個對象的時候,構造參數裏面有一個placeholderPrefixplaceholderSuffix而它們分別對應**${}**

在看下面parseStringValue方法的時候,它第一行代碼裏面的this.placeholderPrefix的值就是**${**

protected String parseStringValue( String value, PlaceholderResolver placeholderResolver, @Nullable Set<String> visitedPlaceholders) {

    int startIndex = value.indexOf(this.placeholderPrefix);
    if (startIndex == -1) {
        return value;
    }

    StringBuilder result = new StringBuilder(value);
    while (startIndex != -1) {
        int endIndex = findPlaceholderEndIndex(result, startIndex);
        if (endIndex != -1) {
            String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
            String originalPlaceholder = placeholder;
            if (visitedPlaceholders == null) {
                visitedPlaceholders = new HashSet<>(4);
            }
            if (!visitedPlaceholders.add(originalPlaceholder)) {
                throw new IllegalArgumentException(
                    "Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
            }
            // Recursive invocation, parsing placeholders contained in the placeholder key.
            placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
            // Now obtain the value for the fully resolved key...
            String propVal = placeholderResolver.resolvePlaceholder(placeholder);
            if (propVal == null && this.valueSeparator != null) {
                int separatorIndex = placeholder.indexOf(this.valueSeparator);
                if (separatorIndex != -1) {
                    String actualPlaceholder = placeholder.substring(0, separatorIndex);
                    String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
                    propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
                    if (propVal == null) {
                        propVal = defaultValue;
                    }
                }
            }
            if (propVal != null) {
                // Recursive invocation, parsing placeholders contained in the
                // previously resolved placeholder value.
                propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
                result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
                if (logger.isTraceEnabled()) {
                    logger.trace("Resolved placeholder '" + placeholder + "'");
                }
                startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
            }
            else if (this.ignoreUnresolvablePlaceholders) {
                // Proceed with unprocessed value.
                startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
            }
            else {
                throw new IllegalArgumentException("Could not resolve placeholder '" +
                                                   placeholder + "'" + " in value \"" + value + "\"");
            }
            visitedPlaceholders.remove(originalPlaceholder);
        }
        else {
            startIndex = -1;
        }
    }
    return result.toString();
}
複製代碼

開始替換文件名裏面的佔位符

這個目錄裏面主要是對上面parseStringValue方法裏面的代碼進行分段的解析

int startIndex = value.indexOf(this.placeholderPrefix);
if (startIndex == -1) {
    return value;
}
複製代碼

上面這段代碼是在獲取到這個文件名裏面第一次出現**${**的位置,若是沒有找到就會返回-1,這樣就直接的返回當前的文件名了

while (startIndex != -1) {
    int endIndex = findPlaceholderEndIndex(result, startIndex);
    if (endIndex != -1) {
		String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
        String originalPlaceholder = placeholder;
        if (visitedPlaceholders == null) {
            visitedPlaceholders = new HashSet<>(4);
        }
        if (!visitedPlaceholders.add(originalPlaceholder)) {
            throw new IllegalArgumentException(
                "Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
        }
        // Recursive invocation, parsing placeholders contained in the placeholder key.
        placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
        // Now obtain the value for the fully resolved key...
        String propVal = placeholderResolver.resolvePlaceholder(placeholder);
        ...
    }
}
複製代碼

在這個While裏面先經過findPlaceholderEndIndex方法來找到這個佔位符所對應的結束符(}),找到它的位置,若是找到了就進入下面的if裏面.

而後它會將這個獲取到的屬性加入到一個HashSet裏面將其保存起來

  • placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
  • 這上面這行代碼裏面是一個遞歸的調用,由於有的文件名它可能會寫成這樣:application{USERNAME{PCNAME}}
  • 因此這裏經過遞歸調用獲取到裏面嵌套的佔位符

而後會經過resolvePlaceholder方法將屬性所對應的值獲取到

if (propVal != null) {
    // Recursive invocation, parsing placeholders contained in the previously resolved placeholder value.
    propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
    result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
    if (logger.isTraceEnabled()) {
        logger.trace("Resolved placeholder '" + placeholder + "'");
    }
    //而後從新查找一次${
    startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
}
複製代碼

當獲取到的proVal結果不是空的時候就要開始對佔位符進行替換的工做了,這裏要從新的調用一次parseStringValue方法以便找到這個獲取到的屬性值裏面也包含佔位符,而後就是對原來的文件名中的佔位符進行替換了

而後從新查找一次**${**,若是這個時候沒有到的,那麼就說明文件名中的佔位符已經徹底被替換調了.

至此就完成加載配置文件以前的文件名解析的工做了

完結

相關文章
相關標籤/搜索