阅读 85
Spring源码阅读-加载配置文件(一)

Spring源码阅读-加载配置文件(一)

Spring启动流程-加载配置文件之前的工作解析文件名

说到Spring启动流程,还是要在加载配置主文件开始.

准备的Demo

我这里有一个小Demo,项目的结构是这样的

image-20210811082808487

而它的配置文件也是很简单的

<?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的类关系图,因为里面很多的方法是直接使用的父类里面的方法.

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方法以便找到这个获取到的属性值里面也包含占位符,然后就是对原来的文件名中的占位符进行替换了

然后重新查找一次**${**,如果这个时候没有到的,那么就说明文件名中的占位符已经完全被替换调了.

至此就完成加载配置文件之前的文件名解析的工作了

完结

文章分类
后端