Spring原始碼解析-applicationContext.xml載入和bean的註冊

架構師springboot發表於2019-03-29

applicationContext檔案載入和bean註冊流程

​ Spring對於從事Java開發的boy來說,再熟悉不過了,對於我們這個牛逼的框架的介紹就不在這裡複述了,Spring這個大雜燴,怎麼去使用怎麼去配置,各種百度谷歌都能查到很多大牛教程,但是,當我們按著教程一步步的把spring的開發框架搭建起來的時候,有沒有一種想搞明白spring的衝動,萬事開頭難,就要從開頭開始,而我認為spring開頭就是如何載入配置檔案,並初始化配置檔案裡面的bean當然也包括了我們用註解Service、Component等註解註解的bean,spring在容器啟動的時候就要去載入這些內容,然後統一管理這些bean(統一管理的是他們的bean definition),這也就是spring的一個重要概念bean的容器。

​ applicationContext.xml到底是如何載入的呢?我把他簡化成以下流程,當然了每個環節裡Spring的實現都是錯綜複雜的,也是很佩服寫Spring的大神。

applicationContext解析和bean註冊流程

Spring初始化

​ 當我們初學Spring的教程的時候,教程裡面肯定會有這樣的一步操作,就是新建一個applicationContext.xml檔案,當然了這是Spring裡必須要有的一個檔案,在這個檔案裡面我們可以進行bean的配置等等工作,讓Spring來管理我們的Bean。然後,這個檔案放在哪裡也是個比較講究的事情,可能對於初學者來說可額能會往WEB-INF資料夾一放就了事了,確實這樣是可以的,因為Spring預設的位置就是這個,但是我們一般不這麼做,一般會把這個檔案放在resource裡面,那這樣子做的話,你就要指定位置,讓Spring知道你這個檔案的位置,這就有了下面一段程式碼,我們的Spring專案都會在web.xml配置這樣的程式碼:

<context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
</context-param>
複製程式碼

那問題來了,當專案啟動的時候,spring是怎麼去初始化應用的上下文的呢?答案就在類ContextLoader.java裡面。當Tomcat啟動時候會呼叫該類裡面的一個方法public WebApplicationContext initWebApplicationContext(ServletContext servletContext),這個方法主要完成,根據我們在web.xml裡面配置的contextConfigLocation初始化spring的web的應用上下文。具體看下改方法的實現(非完整程式碼,PS:由於太長了):

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
	......
    this.context = createWebApplicationContext(servletContext);//主要程式碼,建立web應用上下文  
    ......
    configureAndRefreshWebApplicationContext(cwac, servletContext);//配置引數並呼叫初始化方法
    ......  
}
複製程式碼

在這個方法裡面有兩句重要程式碼,第一句createWebApplicationContext(servletContext),這個會根據你配置的contextClass建立一個WebApplicationContext物件,但是我們一般不會配置這個引數,所以Spring預設會建立一個XMLWebApplicationContext物件,而這個就是後續操作的的重要物件,然後接下來一句重要程式碼configureAndRefreshWebApplicationContext(cwac, servletContext)這個就會去讀取我們在web.xml裡面配置的引數並set到變數裡頭去,這樣Spring就能找到我們專案的applicationContext.xml檔案了,到底如何找到下面會講。接下來我們來看下configureAndRefreshWebApplicationContext方法的實現如下:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
		if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
			// The application context id is still set to its original default value
			// -> assign a more useful id based on available information
			String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
			if (idParam != null) {
				wac.setId(idParam);
			}
			else {
				// Generate default id...
				wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
						ObjectUtils.getDisplayString(sc.getContextPath()));
			}
		}

		wac.setServletContext(sc);
		String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
		if (configLocationParam != null) {
			wac.setConfigLocation(configLocationParam);
		}

		// The wac environment's #initPropertySources will be called in any case when the context
		// is refreshed; do it eagerly here to ensure servlet property sources are in place for
		// use in any post-processing or initialization that occurs below prior to #refresh
		ConfigurableEnvironment env = wac.getEnvironment();
		if (env instanceof ConfigurableWebEnvironment) {
			((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
		}

		customizeContext(sc, wac);
		wac.refresh();
	}
複製程式碼

在這個方法中我們只要關注兩個地方,第一個:

String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
		if (configLocationParam != null) {
			wac.setConfigLocation(configLocationParam);
		}
複製程式碼

這塊程式碼塊就是,講我們配置在web.xml裡面的引數set到我們的變數中去。第二個地方就是:

wac.refresh();
複製程式碼

呼叫這個執行後續的載入檔案操作等後續操作。

Spring是如何找到applicationContext.xml檔案

​ 其實,從refresh到Spring裡去查詢配置檔案路徑之間,有很多步驟,這些也都要花點時間去理解的,在這裡不展開講,我們只要知道,XmlWebApplicationContext會委託給XmlBeanDefinitionReader類去解析配置檔案,在XmlWebApplicationContext類裡面有個方法loadBeanDefinitions如下:

protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
		String[] configLocations = getConfigLocations();
		if (configLocations != null) {
			for (String configLocation : configLocations) {
				reader.loadBeanDefinitions(configLocation);
			}
		}
	}
複製程式碼

該方法就是將一個個的配置檔案委託給XmlBeanDefinitionReader去解析配置檔案,但是解析之前有句程式碼String[] configLocations = getConfigLocations();這個就是查詢我們的配置的檔案的方法,

	protected String[] getConfigLocations() {
		return (this.configLocations != null ? this.configLocations : getDefaultConfigLocations());
	}
複製程式碼

實現很簡單,就是我們有配置該位置地址就會去讀我們配置的路徑,否則就會去讀預設的配置檔案路徑,這就是開篇說到的要是沒配置路徑也能讀取到配置檔案,前提就是要跟Spring預設定義好的檔案路徑及檔名保持一致才行。getDefaultConfigLocations函式的實現也很簡單:

/** Default config location for the root context */
public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";

/** Default prefix for building a config location for a namespace */
public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/";

/** Default suffix for building a config location for a namespace */
public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml";

protected String[] getDefaultConfigLocations() {
		if (getNamespace() != null) {
			return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX};
		}
		else {
			return new String[] {DEFAULT_CONFIG_LOCATION};
		}
	}
複製程式碼

如果配置了namespace就會去找這個名字的xml配置檔案,如果沒有配置就去找預設的配置檔案。所以不管如何,這個配置檔案是必須在spring專案中的。至此,配置檔案基本將完,接下來就是重頭戲了,就是解析xml以及xml裡面的節點,並註冊到spring的bean容器中去。

將xml檔案轉成Document處理物件

如何將xml轉成Document物件,這個也是很複雜的操作,首先將resource讀取InputStream流,在將InputStream流包裝成InputSource物件,在處理成Document物件,直接上程式碼:

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
		Assert.notNull(encodedResource, "EncodedResource must not be null");
		if (logger.isInfoEnabled()) {
			logger.info("Loading XML bean definitions from " + encodedResource.getResource());
		}

		Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
		if (currentResources == null) {
			currentResources = new HashSet<EncodedResource>(4);
			this.resourcesCurrentlyBeingLoaded.set(currentResources);
		}
		if (!currentResources.add(encodedResource)) {
			throw new BeanDefinitionStoreException(
					"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
		}
		try {
			InputStream inputStream = encodedResource.getResource().getInputStream();//獲取流
			try {
				InputSource inputSource = new InputSource(inputStream);
				if (encodedResource.getEncoding() != null) {
					inputSource.setEncoding(encodedResource.getEncoding());
				}
				return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
			}
			finally {
				inputStream.close();
			}
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException(
					"IOException parsing XML document from " + encodedResource.getResource(), ex);
		}
		finally {
			currentResources.remove(encodedResource);
			if (currentResources.isEmpty()) {
				this.resourcesCurrentlyBeingLoaded.remove();
			}
		}
	}
複製程式碼

接下來又到doLoadBeanDefinitions(inputSource, encodedResource.getResource());方法去了,該方法就是生成Doucument物件的,然後就是解析具體的節點了,部分原始碼如下:

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
			throws BeanDefinitionStoreException {
		
			Document doc = doLoadDocument(inputSource, resource);//這就是解析成Document物件的操作
			return registerBeanDefinitions(doc, resource);
			......
}
複製程式碼

解析Document不展開講了,不是本篇的重點,重點是下面的,spring如何解析xml檔案的bean及註解的bean然後註冊到容器中去,registerBeanDefinitions(doc, resource)是下面的重點。

解析Document裡面的節點

XmlBeanDfinitionReader本身又不是直接取解析document的,他是委託給了DefaultBeanDefinitionDocumentReader類去實現,原始碼中,會去建立DefaultBeanDefinitionDocumentReader物件例項,然後呼叫例項的註冊方法,程式碼如下:

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
		BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
		int countBefore = getRegistry().getBeanDefinitionCount();
		documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
		return getRegistry().getBeanDefinitionCount() - countBefore;
	}
複製程式碼

首先,我們必須知道,spring的xml檔案裡面有兩種型別的節點,一種是預設節點,相對於預設節點之外的節點統稱自定義節點,這可以從原始碼裡面知道,而預設節點有以下幾個:beans、import、alias、bean這幾個節點是預設節點,而相對於這幾個節點之外的都是預設節點,applicationContext裡面有幾個自定義節點,如下:property-placeholder、property-override、annotation-config、component-scan、load-time-weaver、spring-configured、mbean-export、mbean-server,這裡面常見的有component-scan等,為什麼spring要分成預設和自定義節點呢,是因為自定義節點都有特定的業務,比如component-scan,他是去掃描程式包,載入用註解定義的bean,例如開發中的service等bean,所以這些自定義節點都配備瞭解析器,這些解析器預先初始化好的,解析到什麼節點就去獲取相應的解析器去處理相應的業務,自定義節點解析器配置如下:

@Override
	public void init() {
		registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
		registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
		registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
		registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
		registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
		registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
		registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
		registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
	}
複製程式碼

從以上原始碼分析,我們可以得到一個推論:

我們自己可以自定義xml的節點,spring可以去解析我們自定義的xml節點。

其實這個推論明顯成立,我們可以看到spring裡面到處都是這種自定義的節點的。

這裡又引申出一個問題:spring怎麼去區分預設節點和自定義節點的呢?答案是通過節點的namespaceUri屬性去判斷,namespaceUri是什麼東東?我們來看下,預設節點的namespaceUri是怎麼樣的,原始碼是這樣定義的:

public static final String BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans";
複製程式碼

是不是很熟悉,這貨就是我們配置檔案裡面的beans根節點會寫的東西,如下:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
	   					   http://www.springframework.org/schema/beans/spring-beans.xsd 
						   http://www.springframework.org/schema/context
						   http://www.springframework.org/schema/context/spring-context.xsd">
</beans>
複製程式碼

但是問題又來了,子節點上我們根本沒配置這貨,但是也能讀取到,以下是個人推論:

子節點會繼承父節點的屬性,這就說的通,子節點即使沒配置那一堆東西也能判斷為預設節點。

接下來,就是解析Document的元素,從root元素開始解析,這時候spring是建立了一個解析類的代理類,所有的比較和解析操作都有該類完成,我們來看下spring的原始碼實現:

protected void doRegisterBeanDefinitions(Element root) {		
		BeanDefinitionParserDelegate parent = this.delegate;
		this.delegate = createDelegate(getReaderContext(), root, parent);

		if (this.delegate.isDefaultNamespace(root)) {
			String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
			if (StringUtils.hasText(profileSpec)) {
				String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
						profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
				if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
					if (logger.isInfoEnabled()) {
						logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
								"] not matching: " + getReaderContext().getResource());
					}
					return;
				}
			}
		}

		preProcessXml(root);
		parseBeanDefinitions(root, this.delegate);
		postProcessXml(root);

		this.delegate = parent;
	}
複製程式碼

解析節點的過程是個遞迴的過程,每次都要記錄節點的父節點,首先會建立一個delegate物件,然後再去解析節點,呼叫parseBeanDefinitions(root, this.delegate);這個方法進行解析操作;

繼續來看下parseBeanDefinitions(root, this.delegate);的實現:

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
		if (delegate.isDefaultNamespace(root)) {
			NodeList nl = root.getChildNodes();
			for (int i = 0; i < nl.getLength(); i++) {
				Node node = nl.item(i);
				if (node instanceof Element) {
					Element ele = (Element) node;
					if (delegate.isDefaultNamespace(ele)) {
						parseDefaultElement(ele, delegate);
					}
					else {
						delegate.parseCustomElement(ele);
					}
				}
			}
		}
		else {
			delegate.parseCustomElement(root);
		}
	}
複製程式碼

很簡單,可以很清晰的看出,解析是分預設節點和自定義節點分開解析的,而自定義的節點的解析其實就是找到對應的解析器各自處理對應的業務,如component-scan會找到ComponentScanBeanDefinitionParser類來處理對應的掃描包註冊bean的操作,而預設的節點的處理有如下幾種,程式碼如下:

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
		//處理import
		if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
			importBeanDefinitionResource(ele);
		}
		//處理alias
		else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
			processAliasRegistration(ele);
		}
		//處理bean
		else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
			processBeanDefinition(ele, delegate);
		}
		//處理beans
		else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
			// recurse
			doRegisterBeanDefinitions(ele);
		}
	}
複製程式碼

import的處理相對其他幾種比較複雜點,但最終還是處理變成其他3種的處理,而beans的處理就重新遞迴上面提到的方法,最重要的是bean的處理,bean的處理其實就是下面要講的內容,解析bean並註冊bean definition的過程。

註冊bean

終於到了最後一個內容了,也是最重要的一個內容,上面講的所有都是為了這個而服務的,讀取配置檔案也是為了載入bean,然後註冊到spring的容器裡面,讓spring統一管理我們定義的bean。大家都很明白,spring的bean的容器,但是如果沒有去看原始碼的話,是不是都認為spring,是把每個例項物件註冊到容器裡面然後統一管理的?其實,spring其實不是這樣的做的,spring註冊的bean最終是個bean的定義,即BeanDefinition這個例項,並不是一個個類的具體例項。我們可以簡單理解這些註冊的bean definition是為了方便後續的例項化bean進行的一步準備操作。所謂的註冊,其實就是把各種這些例項用一個Map來管理,所以,spring的bean的容器的底層儲存其實是用Map來實現的(這個之前面試被問過)。接下來,看看原始碼的實現:

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
		BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
		if (bdHolder != null) {
		    //這個是對bean definition進行修改如果有必要,如配置了代理的bean等
			bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
			try {
				// Register the final decorated instance.
				BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
			}
			catch (BeanDefinitionStoreException ex) {
				getReaderContext().error("Failed to register bean definition with name '" +
						bdHolder.getBeanName() + "'", ele, ex);
			}
			// Send registration event.
			getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
		}
	}
複製程式碼

從原始碼裡可以看出,bean的解析類代理會去解析ele元素,並返回一個BeanDefinitionHolder的例項,而這個BeanDefinitionHolder我們可以簡單理解為BeanDefinition物件的持有物件。然後,通過呼叫BeanDefinitionReaderUtils工具類去執行具體的註冊操作。繼續看BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry())這個的實現如下:

public static void registerBeanDefinition(
			BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
			throws BeanDefinitionStoreException {

		// Register bean definition under primary name.
		String beanName = definitionHolder.getBeanName();
		registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

		// Register aliases for bean name, if any.
		String[] aliases = definitionHolder.getAliases();
		if (aliases != null) {
			for (String alias : aliases) {
				registry.registerAlias(beanName, alias);
			}
		}
	}
複製程式碼

從上面程式碼中,spring註冊bean其實註冊的是BeanDfinition,註冊bean其實就是繫結bean的name和BeanDfinition的關係。那麼,我們繼續看看bean的具體註冊過程,程式碼如下:

public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
			throws BeanDefinitionStoreException {

		Assert.hasText(beanName, "Bean name must not be empty");
		Assert.notNull(beanDefinition, "BeanDefinition must not be null");

		if (beanDefinition instanceof AbstractBeanDefinition) {
			try {
				((AbstractBeanDefinition) beanDefinition).validate();
			}
			catch (BeanDefinitionValidationException ex) {
				throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
						"Validation of bean definition failed", ex);
			}
		}

		BeanDefinition oldBeanDefinition;

		oldBeanDefinition = this.beanDefinitionMap.get(beanName);
		if (oldBeanDefinition != null) {
			if (!isAllowBeanDefinitionOverriding()) {
				throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
						"Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
						"': There is already [" + oldBeanDefinition + "] bound.");
			}
			else if (oldBeanDefinition.getRole() < beanDefinition.getRole()) {
				// e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
				if (this.logger.isWarnEnabled()) {
					this.logger.warn("Overriding user-defined bean definition for bean '" + beanName +
							"' with a framework-generated bean definition: replacing [" +
							oldBeanDefinition + "] with [" + beanDefinition + "]");
				}
			}
			else if (!beanDefinition.equals(oldBeanDefinition)) {
				if (this.logger.isInfoEnabled()) {
					this.logger.info("Overriding bean definition for bean '" + beanName +
							"' with a different definition: replacing [" + oldBeanDefinition +
							"] with [" + beanDefinition + "]");
				}
			}
			else {
				if (this.logger.isDebugEnabled()) {
					this.logger.debug("Overriding bean definition for bean '" + beanName +
							"' with an equivalent definition: replacing [" + oldBeanDefinition +
							"] with [" + beanDefinition + "]");
				}
			}
			this.beanDefinitionMap.put(beanName, beanDefinition);
		}
		else {
			if (hasBeanCreationStarted()) {
				// Cannot modify startup-time collection elements anymore (for stable iteration)
				synchronized (this.beanDefinitionMap) {
					this.beanDefinitionMap.put(beanName, beanDefinition);
					List<String> updatedDefinitions = new ArrayList<String>(this.beanDefinitionNames.size() + 1);
					updatedDefinitions.addAll(this.beanDefinitionNames);
					updatedDefinitions.add(beanName);
					this.beanDefinitionNames = updatedDefinitions;
					if (this.manualSingletonNames.contains(beanName)) {
						Set<String> updatedSingletons = new LinkedHashSet<String>(this.manualSingletonNames);
						updatedSingletons.remove(beanName);
						this.manualSingletonNames = updatedSingletons;
					}
				}
			}
			else {
				// Still in startup registration phase
				this.beanDefinitionMap.put(beanName, beanDefinition);
				this.beanDefinitionNames.add(beanName);
				this.manualSingletonNames.remove(beanName);
			}
			this.frozenBeanDefinitionNames = null;
		}

		if (oldBeanDefinition != null || containsSingleton(beanName)) {
			resetBeanDefinition(beanName);
		}
	}
複製程式碼

這段程式碼還是比較容易理解的,首先先判斷容器裡面有沒這個bean,沒有的話判斷是否在建立過程,如果不是直接將該bean註冊到容器裡並設定其他資訊。簡單的說,其實就是將一個個的bean的定義跟bean的名稱繫結起來,存放到map裡面。至此,spring載入applicationContext.xml的大致流程已經說清楚了,不過這裡面涉及很多比較細又難懂的類並沒有體現出來,最終要的是搞清楚spring載入配置檔案的過程和註冊bean的過程。要想深入,可以繼續研讀原始碼。

總結

通過該篇文章,我們弄清楚了spring的applicationContext.xml檔案的載入和bean的註冊過程。可以說配置檔案解析只是spring為了後續的bean的例項化操作的準備階段,即為需要例項化的bean準備bean definition。

覺得不錯請點贊支援,歡迎留言或進我的個人群855801563領取【架構資料專題目合集90期】、【BATJTMD大廠JAVA面試真題1000+】,本群專用於學習交流技術、分享面試機會,拒絕廣告,我也會在群內不定期答題、探討。


相關文章