簡單看Spring原始碼--對xml檔案解析
Spring如何解析xml配置檔案?
xml配置檔案是Spring中極其重要的一部分,讓我們一起看一下spring解析xml檔案的。
以下是一段簡單的通過類路徑下的test.xml檔案載入bean獲得BeanFactory的程式碼:
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("test.xml"));
一行程式碼,spring做的事情極其的複雜,主要分為以下幾步:
1, 把資原始檔進行封裝,封裝為Resource,有了Resource就可以對所有的資原始檔進行統一處理
ClassPathResource的核心邏輯: (其實是簡單的用class或classLoader讀取類路徑的資原始檔)
InputStream is;
if (this.clazz != null) {
is = this.clazz.getResourceAsStream(this.path);
}
else if (this.classLoader != null) {
is = this.classLoader.getResourceAsStream(this.path);
}
else {
is = ClassLoader.getSystemResourceAsStream(this .path);
}
if (is == null) {
throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
}
return is;
2,在XmlBeanFactory構造方法中呼叫XmlBeanDefinitionReader開始Bean的載入
this.reader.loadBeanDefinitions(resource);
這句程式碼是整個資源載入的切入點。
3, EncodedResource對Resoure進行編碼的處理,設定了編碼屬性的時候Spring會使用相應的編碼作為輸入流的編碼
public Reader getReader() throws IOException {
if (this.charset != null) {
return new InputStreamReader(this.resource.getInputStream(), this.charset);
}
else if (this.encoding != null) {
return new InputStreamReader(this.resource.getInputStream(), this.encoding);
}
else {
return new InputStreamReader(this.resource.getInputStream());
}
}
這段程式碼設定了輸入流的編碼,但是我在看原始碼的過程中並沒有發現其有被呼叫的情況,
在註釋上@see getInputStream()
@Override
public InputStream getInputStream() throws IOException {
return this.resource.getInputStream();
}
發現直接就是呼叫resource方法。
到底怎麼回事???我看的時候一臉矇蔽0。0
4,構造InputSource,InputSource並不來自於Spring,全路徑是org.xml.sax,SAX解析xml的時候會使用InputSource來決定如何讀取xml檔案
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
原來encode在這裡實現了,那麼上面的EncodeResource只是起到了簡單的提供encode或者charset的作用,
這樣的話那麼設計的時候是不是可以把EncodeResource去掉呢,直接在InputSource.setEncoding即可。
5,現在到達了核心處理部分
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
首先getValidationModeForResource
獲取xml檔案的驗證模式(DTD或者XSD),可以自己設定驗證方式,否則預設是開啟VALIDATION_AUTO
即自動獲取驗證模式的,底層實現是InputStream讀取xml檔案看xml檔案是否包含DOCTYPE
單詞,包含的話就是DTD,否則返回XSD。看一下關鍵邏輯:
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;
}
if (hasDoctype(content)) {
isDtdValidated = true;
break;
}
if (hasOpeningTag(content)) {
// End of meaningful data...
break;
}
}
return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
接著獲取Document,Spring並沒有進行特殊的對xml文件的處理,使用了SAX解析xml文件,三步走:先建立DocumentBuilderFactory,接著獲取DocumentBuilder,最後解析InputStream返回Document物件。
6, 可以看一下EntityResolver
類,EntityResolver是什麼呢,對於一個xml的驗證,XSD或者DTD檔案預設是從網上下載的,可以的話一般都是把DTD檔案放在工程之中,而EntityResolver就是提供了一個如何尋找DTD檔案的宣告。
對於xsd模式:
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
對於DTD模式驗證:
<!DOCTYPE beans PUBLIC "-//Spring//DTD BEAN 2.0//EN"
"http://www.springframework.org/dtd/spring-beans-2.0.dtd">
預設spring對於兩種驗證方式提供了不同的解析器。
this.dtdResolver = new BeansDtdResolver();
this.schemaResolver = new PluggableSchemaResolver(classLoader);
XSD解析:
預設的XSD解析PluggableSchemaResolver使用了private volatile Map<String, String> schemaMappings;
來儲存schemaURL->local schema path的對映,預設的schemaMappingLocation位於META-INF/spring.schemas ,通過getSchemaMapping來獲取這個對映,其中使用雙重校驗鎖的方式來實現單例模式:
if (this.schemaMappings == null) {
synchronized (this) {
if (this.schemaMappings == null) {
...
}
}
}
有了map,接下來獲取InputSource就是使用systemId獲取resourceLocation,實現比較簡單。
DTD解析:
DTD解析是直接擷取systemId的最後的xx.dtd去當前路徑下面尋找。
7,快了,快了再堅持一下,最後解析及註冊bean
通過DocumentBuilder獲取Document之後,就剩下
return registerBeanDefinitions(doc, resource);
方法的返回值是發現的定義的bean的數目,方法主要內容:
//建立DocumentReader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
//註冊beanDefinition
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
//獲得已有的beanDefinition數目
int countBefore = getRegistry().getBeanDefinitionCount();
return getRegistry().getBeanDefinitionCount() - countBefore;
註冊bean的時候首先使用一個BeanDefinitionParserDelegate
類來判斷是否是預設名稱空間,實現是通過判斷namespace uri 是否和預設的uri相等:
public static final String BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans";
public boolean isDefaultNamespace(String namespaceUri) {
return (!StringUtils.hasLength(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri));
}
對於預設的名稱空間,首先開始的是對profile屬性解析,profile用得最多的是DataSource在不同環境下使用不同的bean,spring使用StringTokenizer來進行字串的分割,但是jdk為了相容性是推薦使用String.split()方法的:
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);
接下來的解析使用了模板模式,preProcessXml和postProcessXml都是空方法,是為了方便之後的子類在解析之前之後進行一些處理。只需要覆寫這兩個方法即可。
在parseBeanDefinitions方法中spring對不同名稱空間的元素的解析使用不同的方法:
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
對於不同的bean宣告,spring的處理方法我先看一下,下次再寫了。。
為了首尾呼應,回到開始的
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("test.xml"));
我們已經進入到解析bean的比較深入的步驟了,接下來
<bean id="myTestBean" class="MyTestBean.class"/>
經過預設名稱空間的解析,接下來就是對bean標籤的解析以及註冊。
下次再寫了。。。