Spring启动流程-加载配置文件之前的工作解析文件名
说到Spring启动流程,还是要在加载配置主文件开始.
准备的Demo
我这里有一个小Demo,项目的结构是这样的
而它的配置文件也是很简单的
<?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");,
想必用过Spring的小伙伴对于这行代码已经很熟悉了,这行代码是加载Spring配置文件的代码。它是读取的classpath路径下面的文件,所在上面代码中的classpath前缀写不写都可
在开始加载配置文件之前做了哪些工作?
当跟着断点走到了ClassPathXmlApplicationContext类中,最后他们会走到这样的一个构造方法里面去
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的类时,这里会创建出一些对象,先了解下,后面可能会用到
- id:这个是context里面唯一的一个上下文
- startupShutdownMonitor:你可以理解这个是一个刷新或销毁时用到的锁,因为刷新或销毁的过程是不可被打断的.
还有一些其它的对象,有兴趣的可以看下
接下在看到 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的类关系图,因为里面很多的方法是直接使用的父类里面的方法.
进入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,上面说的解析路大概也就是这个意思.
来到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);
}
还记得上面我说过在解析文件名的时候系统可以替换**{}里面的而不是##**里面的东西呢?原因就在它创建占位符帮助器的时的构造传参里面.
在创建这个对象的时候,构造参数里面有一个placeholderPrefix和placeholderSuffix而它们分别对应**${和}**
在看下面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方法以便找到这个获取到的属性值里面也包含占位符,然后就是对原来的文件名中的占位符进行替换了
然后重新查找一次**${**,如果这个时候没有到的,那么就说明文件名中的占位符已经完全被替换调了.
至此就完成加载配置文件之前的文件名解析的工作了