引言
Spring框架是一种分层架构,它包含了一系列的功能,大概由20种模块组成。 这些模块分为核心容器(Core Container), 数据访问/集成(Data Access/Integration), Web, AOP, 工具(Instrumentation), 消息(Messaging), 测试用例(Test).
Spring的代码量非常庞大,对于大多数第一次看spring源码的读者来说,会感觉到一头雾水,不知从哪里看起,我在这里也是初入Spring源码,和绝大部分读者一样,但我相信这是对我自身的一个挑战,也是一个漫长的过程,我会以写博客的方式记下我自己对Spring的理解,希望能帮助到各位读者,当然为了保证文章的严谨性,如果读者发现我哪里理解错了请一定不吝指出,希望大家可以一起监督成长,更希望能听到读者的声音。
我看源码有一些我自己的小技巧,比如我要看一个框架源码的时候,先要熟悉这个框架的简单使用,了解其内部大致结构,然后找到切入点,那么看源码就会轻松很多,好了,废话不多说,我们开始吧。
程序的开始
首先我们创建一个实体类:
public class User {
private String name;
}
创建xml文件
<?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="user" class="com.xwcoding.User"></bean>
</beans>
启动起来
public static void main(String[] args) {
AbstractXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:application.xml"");
System.out.println("容器启动成功");
User bean = applicationContext.getBean(User.class);
// 这里肯定能打印出内存地址
System.out.println(bean);
}
以上是一个很简单的例子,是一个spring的入门代码,相信很多读者都对以上代码并不陌生,正是spring加载xml文件来启动容器所使用的类之一,也是我们进军容器的第一步,我们可以点进去看看…
AbstractXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:application.xml");
启动流程分析
请自行准备瓜子花生啤酒,开始枯燥的旅程(建议读者一起跟我打开源码走起来)。。。
public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
this(new String[] {configLocation}, true, null);
}
public ClassPathXmlApplicationContext(
String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
throws BeansException {
super(parent);
//设置配置地址
setConfigLocations(configLocations);
if (refresh) {
//容器刷新 ioc最重要的方法
refresh();
}
}
这是ClassPathXmlApplicationContext
的构造器方法,在这个方法内进行了设置配置地址和容器的刷新操作
在setConfigLocations
方法中我们可以看到代码中用了this.configLocations
这个数组来接收所有的配置文件的地址
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;
}
}
解析路径,为什么要解析,因为路径可能会有占位符 比如:Application-${name} .xml
protected String resolvePath(String path) {
//首先创建一个获取一个Environment,然后调用resolveRequiredPlaceholders
return getEnvironment().resolveRequiredPlaceholders(path);
}
在上述方法中 通过getEnvironment()
来获取一个 StandardEnvironment
对象,但是此处用来接收StandardEnvironment
是一个接口 名为:ConfigurableEnvironment
,那么拿到了这个接口后调用了这个接口的父级的父级的方法 resolveRequiredPlaceholders()
,其实现类是AbstractPropertyResolver
,我们看到以下这个方法其实就是拿到了一个助手接口,此处用到实现类是 PropertyPlaceholderConfigurer
,也是比较常用的占位符助手类
@Override
public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
if (this.strictHelper == null) {
//创建一个占位符助手
this.strictHelper = createPlaceholderHelper(false);
}
//真正的执行占位符助手进行解析
return doResolvePlaceholders(text, this.strictHelper);
}
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
return helper.replacePlaceholders(text, this::getPropertyAsRawString);
}
以下是真正的解析占位符的步骤,也是比较重要的,spring容器中很多地方都用到了这个方法,看懂它,相信会有大用。
我们可以看到parseStringValue()
方法,先去找占位符的开头,如果没有就直接返回,如果有就循环开始找占位符的结束,
为什么要循环,是因为可能占位符中还有占位符,找到的第一个占位符可能不是最后一个展位,所有需要循环递归去找,详细的解说我会写在代码注释中,请细读。。。
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
return parseStringValue(value, placeholderResolver, null);
}
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) {
//找到占位符中间的值 ${key} key
//那么此处的这个key 可能还会附带占位符, 比如 abc${def}g 也可能是最终的值 我们不确定
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
//创建一个Set 用来限制多层占位符相同的问题
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...
// 最里面那层会第一个走到这里
// 这里是最终获取key值的方法 ,这里会去配置文件 Properties 中获取
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
//如果拿到的值是null的话并且设置了分隔符,那么就会根据分隔符去找值
if (propVal == null && this.valueSeparator != null) {
//propVal 是null 那么就查找分隔符 :
int separatorIndex = placeholder.indexOf(this.valueSeparator);
if (separatorIndex != -1) {
//例如 key:value 拿到key
String actualPlaceholder = placeholder.substring(0, separatorIndex);
//例如 key:value 拿到value
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
//用key 再去系统中找一遍
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) {
//如果找不到就 拿到 value 值做默认
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 + "'");
}
// 走到这里就结束,继续找占位符,一般来说 是-1 所有循环结束
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 + "\"");
}
//解析成功就删除set集合中对应的占位符 key
visitedPlaceholders.remove(originalPlaceholder);
}
else {
startIndex = -1;
}
}
return result.toString();
}
我的个乖乖,总算把setConfigLocations()
方法走完了,快累倒,内部的逻辑还是挺绕的,比如说占位符那一块,但是占位符在spring容器中还是比较常用的,很多地方都用到了,所有很有必要看懂