1. 程式人生 > >spring4.2.9 java專案環境下ioc原始碼分析(四)——refresh之obtainFreshBeanFactory方法(@2處理Resource、載入Document及解析前準備)

spring4.2.9 java專案環境下ioc原始碼分析(四)——refresh之obtainFreshBeanFactory方法(@2處理Resource、載入Document及解析前準備)

接上篇文章,上篇文章講到載入完返回Rescouce。先找到要解析的程式碼位置,在AbstractBeanDefinitionReader類的

loadBeanDefinitions(String location, Set<Resource> actualResources)的方法中,要繼續的是

int loadCount = loadBeanDefinitions(resources);
@Override
	public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException {
		Assert.notNull(resources, "Resource array must not be null");
		int counter = 0;
		for (Resource resource : resources) {
			counter += loadBeanDefinitions(resource);
		}
		return counter;
	}
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
		return loadBeanDefinitions(new EncodedResource(resource));
	}

這裡再次對Resource進行了封裝,封裝成了EncodedResource,這個類是是對檔案資源的編碼處理,如果指定了編碼格式,在得到Reader時會給幾編碼格式返回。這裡暫時沒用到。繼續看程式碼

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());
		}
		//resourcesCurrentlyBeingLoaded是個ThreadLocal<Set<EncodedResource>>變數
		//得到變數中存放的值,注意每個執行緒得到的都是一個拷貝
		Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
		//第一次載入是null
		if (currentResources == null) {
			currentResources = new HashSet<EncodedResource>(4);
			this.resourcesCurrentlyBeingLoaded.set(currentResources);
		}
		//把資源放進set中,紀錄已載入內容,不能載入重複內容
		if (!currentResources.add(encodedResource)) {
			throw new BeanDefinitionStoreException(
					"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
		}
		try {
			//得到該Resource的InputStream
			InputStream inputStream = encodedResource.getResource().getInputStream();
			try {
				//用InputSource封裝InputStream,此類是org.xml.sax包下的
				InputSource inputSource = new InputSource(inputStream);
				//如果encodedResource的編碼格式不為空,設定InputSource編碼格式
				if (encodedResource.getEncoding() != null) {
					inputSource.setEncoding(encodedResource.getEncoding());
				}
				//do..開始真正的解析
				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();
			}
		}
	}

基本邏輯很簡單,註釋已經很詳細了這裡不多說。

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
			throws BeanDefinitionStoreException {
		try {
			//載入Xml得到Document物件
			Document doc = doLoadDocument(inputSource, resource);
			//根據Document註冊BeanDefinitions
			return registerBeanDefinitions(doc, resource);
		}
	}
這裡就幹了兩件事。真正的做事要開始了。咱們一個一個分析。先看doLoadDocument,這是根據上面得到的InputSource和Resource去生成Document物件。即把XML檔案解析為Document。
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
		return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
				getValidationModeForResource(resource), isNamespaceAware());
	}
這裡具體的解析交給了DefaultDocumentLoader物件。這裡有兩個比較重要的方法getEntityResolver與getValidationModeForResource

getEntityResolver返回的是EntityResolver。那什麼是EntityResolver呢?作用是什麼呢?

首先說下xml解析,一般情況下,SAX先去讀取XML上的文件說明做一個驗證,一般情況都是從網上下載,但是如果沒有網會報錯,這就是為什麼在沒有網的時候我們自己配置xsd或dtd檔案。

EntityResolver做了一個本地化,就是先在本地找約束檔案,找不到再在網上去找。

先說一下大體驗證流程。

以xsd檔案為例,首先為什麼要找xsd檔案,要知道我們要解析XML要按照一定模式去解析,xsd就是模式,他告訴你有哪些標籤可以用,標籤的屬性是什麼,子標籤是什麼等等。。這樣才可以去解析。但是如果你自己寫的標籤不屬於xsd定義的,則驗證失敗,造成無法解析的後果。

spring所有的xsd約束檔案都在META-INF/spring.schemas下(DTD這裡不說了)

getValidationModeForResource是獲取驗證模式,如果只有XSD那也就不分什麼模式了,但是XSD之前還有個DTD約束。我們要保證約束的匹配性,我不能用XSD約束去驗證DTD標籤是吧?

下面看下程式碼

protected int getValidationModeForResource(Resource resource) {
		//得到指定的驗證模式,當然這裡沒有指定預設是VALIDATION_AUTO
		int validationModeToUse = getValidationMode();
		if (validationModeToUse != VALIDATION_AUTO) {
			//如果指定了就用指定的
			return validationModeToUse;
		}
		//檢查驗證模式,根據xml文件頭進行檢驗
		int detectedMode = detectValidationMode(resource);
		if (detectedMode != VALIDATION_AUTO) {
			//如果不等於VALIDATION_AUTO,就用檢驗後的
			return detectedMode;
		}
		// Hmm, we didn't get a clear indication... Let's assume XSD,
		// since apparently no DTD declaration has been found up until
		// detection stopped (before finding the document's root tag).
		//直接用VALIDATION_XSD
		return VALIDATION_XSD;
	}
protected int detectValidationMode(Resource resource) {
		

		InputStream inputStream;
		try {
			inputStream = resource.getInputStream();
		}
		//檢測
		try {
			return this.validationModeDetector.detectValidationMode(inputStream);
		}
		
	}
public int detectValidationMode(InputStream inputStream) throws IOException {
		// Peek into the file to look for DOCTYPE.
		BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
		try {
			boolean isDtdValidated = false;
			String content;
			//一行一行讀取
			while ((content = reader.readLine()) != null) {
				content = consumeCommentTokens(content);
				//為空的話直接跳過
				if (this.inComment || !StringUtils.hasText(content)) {
					continue;
				}
				//是否包含DOCTYPE
				if (hasDoctype(content)) {
					isDtdValidated = true;
					break;
				}
				//讀取開始的符號
				if (hasOpeningTag(content)) {
					// End of meaningful data...
					break;
				}
			}
			return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
		}
		catch (CharConversionException ex) {
			// Choked on some character encoding...
			// Leave the decision up to the caller.
			return VALIDATION_AUTO;
		}
		finally {
			reader.close();
		}
	}
private boolean hasDoctype(String content) {
		return content.contains(DOCTYPE);
	}
上面可以看到如果包含DOCTYPE內容就是DTD.然後看XSD的
private boolean hasOpeningTag(String content) {
		if (this.inComment) {
			return false;
		}
		int openTagIndex = content.indexOf('<');
		return (openTagIndex > -1 && (content.length() > openTagIndex + 1) &&
				Character.isLetter(content.charAt(openTagIndex + 1)));
	}

上面這個程式碼是啥?如果是註釋就一直返回false.

重點看程式碼

Character.isLetter(content.charAt(openTagIndex + 1))
這裡的意思是<開始符號的下一個字元是不是字母,如果是返回true,如果不是比如!?就返回false.

那麼看下DTD和XSD的區別是什麼?

這是DTD

<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
			"http://www.springframework.org/dtd/spring-beans-2.0.dtd">

這是XSD

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
注意:只要不是包含DOCTYPE的都是XSD!!我只是舉了個例子,XSD約束,讀取第二行是以<開始但是第二個位置是字母。

好了接下來就是為Document了

public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
			ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

		DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
		if (logger.isDebugEnabled()) {
			logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
		}
		DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
		return builder.parse(inputSource);
	}
上面程式碼就不說了,解析XML的固定方法。接下來看registerBeanDefinitions方法。程式碼如下
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
		//例項化BeanDefinitionDocumentReader的實現類DefaultBeanDefinitionDocumentReader
		BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
		//記錄註冊之前的數值
		int countBefore = getRegistry().getBeanDefinitionCount();
		//執行註冊
		documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
		//註冊的數值
		return getRegistry().getBeanDefinitionCount() - countBefore;
	}
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
		this.readerContext = readerContext;
		logger.debug("Loading bean definitions");
		//獲取rootElement
		Element root = doc.getDocumentElement();
		//do。。真正進行註冊
		doRegisterBeanDefinitions(root);
	}
	protected void doRegisterBeanDefinitions(Element root) {
		//專用來解析的類
		BeanDefinitionParserDelegate parent = this.delegate;
		this.delegate = createDelegate(getReaderContext(), root, parent);
		//判斷root的名稱空間是不是預設的
		if (this.delegate.isDefaultNamespace(root)) {
			//處理profile屬性
			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)) {
					return;
				}
			}
		}
		//擴充套件,解析前處理
		preProcessXml(root);
		//解析
		parseBeanDefinitions(root, this.delegate);
		//擴充套件,解析後處理
		postProcessXml(root);

		this.delegate = parent;
	}
上面三段程式碼重點看下第三段,這裡判斷root的名稱空間是不是預設的。也就是beans的預設名稱空間,預設名稱空間是什麼呢。
xmlns="http://www.springframework.org/schema/beans"

上面就是預設的名稱空間。這裡判斷相同後進行profile屬性處理。首先說下在日常中我們怎麼用這個屬性。這個屬性可以配置多個一般是不同環境配置對應的,比如在web環境中我們配置spring.profiles.active屬性為dev,那麼在查詢beans的時候如果制定了profile屬性,只加載其屬性值為dev的beans.下面根據程式碼去看

if (this.delegate.isDefaultNamespace(root)) {
			//取得屬性值
			String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
			//判斷是否存在
			if (StringUtils.hasText(profileSpec)) {
				//profile可以指定多個
				String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
						profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
				//去系統環境中去找是否匹配設定,不存跳過解析,存在就解析
				if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
					return;
				}
			}
		}
public boolean acceptsProfiles(String... profiles) {
		Assert.notEmpty(profiles, "Must specify at least one profile");
		for (String profile : profiles) {
			if (StringUtils.hasLength(profile) && profile.charAt(0) == '!') {
				if (!isProfileActive(profile.substring(1))) {
					return true;
				}
			}
			//如果系統包含profile屬性返回true
			else if (isProfileActive(profile)) {
				return true;
			}
		}
		return false;
	}
protected boolean isProfileActive(String profile) {
		validateProfile(profile);
		//獲取系統環境下配置的profile屬性
		Set<String> currentActiveProfiles = doGetActiveProfiles();
		return (currentActiveProfiles.contains(profile) ||
				(currentActiveProfiles.isEmpty() && doGetDefaultProfiles().contains(profile)));
	}
protected Set<String> doGetActiveProfiles() {
		synchronized (this.activeProfiles) {
			//只獲取一次即可,系統配置的屬性profile載入一次即可
			if (this.activeProfiles.isEmpty()) {
				//從系統中獲取ACTIVE_PROFILES_PROPERTY_NAME==spring.profiles.active
				String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME);
				if (StringUtils.hasText(profiles)) {
					setActiveProfiles(StringUtils.commaDelimitedListToStringArray(
							StringUtils.trimAllWhitespace(profiles)));
				}
			}
			return this.activeProfiles;
		}
	}

說下大體流程,第一:系統配置的spring.profiles.active只會載入一次,如果有多個beans直接取到比對即可。

第二:一個xml檔案中可包含多個beans標籤,如果都配置了profile,拿到profile的屬性與系統得到的比對,不同則跳過解析,相同進行解析。

繼續看parseBeanDefinitions方法

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);
		}
	}
這裡就開始解析了。具體解析下長再將