Spring源码解析 - IOC容器的初始化过程 (一)

引言

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容器中还是比较常用的,很多地方都用到了,所有很有必要看懂

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值