概述
上一篇我們搭建完Spring原始碼閱讀環境,spring原始碼深度解析—Spring的整體架構和環境搭建 這篇我們開始真正的閱讀Spring的原始碼,分析spring的原始碼之前我們先來簡單回顧下spring核心功能的簡單使用
容器的基本用法
bean是spring最核心的東西,spring就像是一個大水桶,而bean就是水桶中的水,水桶脫離了水也就沒有什麼用處了,我們簡單看下bean的定義,程式碼如下:
package com.chenhao.spring; /** * @author: ChenHao * @Description: * @Date: Created in 10:35 2019/6/19 * @Modified by: */ public class MyTestBean { private String name = "ChenHao"; public String getName() { return name; } public void setName(String name) { this.name = name; } }
原始碼很簡單,bean沒有特別之處,spring的的目的就是讓我們的bean成為一個純粹的的POJO,這就是spring追求的,接下來就是在配置檔案中定義這個bean,配置檔案如下:
<?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="myTestBean" class="com.chenhao.spring.MyTestBean"/> </beans>
在上面的配置中我們可以看到bean的宣告方式,在spring中的bean定義有N種屬性,但是我們只要像上面這樣簡單的宣告就可以使用了。
具體測試程式碼如下:
import com.chenhao.spring.MyTestBean; import org.junit.Test; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.xml.XmlBeanFactory; import org.springframework.core.io.ClassPathResource; /** * @author: ChenHao * @Description: * @Date: Created in 10:36 2019/6/19 * @Modified by: */ public class AppTest { @Test public void MyTestBeanTest() { BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml")); MyTestBean myTestBean = (MyTestBean) bf.getBean("myTestBean"); System.out.println(myTestBean.getName()); } }
執行上述測試程式碼就可以看到輸出結果如下圖:
其實直接使用BeanFactory作為容器對於Spring的使用並不多見,因為企業級應用專案中大多會使用的是ApplicationContext(後面我們會講兩者的區別,這裡只是測試)
功能分析
接下來我們分析2中程式碼完成的功能;
- 讀取配置檔案spring-config.xml。
- 根據spring-config.xml中的配置找到對應的類的配置,並例項化。
- 呼叫例項化後的例項
下圖是一個最簡單spring功能架構,如果想完成我們預想的功能,至少需要3個類:
ConfigReader :用於讀取及驗證自己直檔案 我們妥用配直檔案裡面的東西,當然首先 要做的就是讀取,然後放直在記憶體中.
ReflectionUtil :用於根據配置檔案中的自己直進行反射例項化,比如在上例中 spring-config.xml 出現的<bean id="myTestBean" class="com.chenhao.spring.MyTestBean"/>,我們就可以根據 com.chenhao.spring.MyTestBean 進行例項化。
App :用於完成整個邏輯的串聯。
工程搭建
spring的原始碼中用於實現上面功能的是spring-bean這個工程,所以我們接下來看這個工程,當然spring-core是必須的。
beans包的層級結構
閱讀原始碼最好的方式是跟著示例操作一遍,我們先看看beans工程的原始碼結構,如下圖所示:
- src/main/java 用於展現Spring的主要邏輯
- src/main/resources 用於存放系統的配置檔案
- src/test/java 用於對主要邏輯進行單元測試
- src/test/resources 用於存放測試用的配置檔案
核心類介紹
接下來我們先了解下spring-bean最核心的兩個類:DefaultListableBeanFactory和XmlBeanDefinitionReader
DefaultListableBeanFactory
XmlBeanFactory繼承自DefaultListableBeanFactory,而DefaultListableBeanFactory是整個bean載入的核心部分,是Spring註冊及載入bean的預設實現,而對於XmlBeanFactory與DefaultListableBeanFactory不同的地方其實是在XmlBeanFactory中使用了自定義的XML讀取器XmlBeanDefinitionReader,實現了個性化的BeanDefinitionReader讀取,DefaultListableBeanFactory繼承了AbstractAutowireCapableBeanFactory並實現了ConfigurableListableBeanFactory以及BeanDefinitionRegistry介面。以下是ConfigurableListableBeanFactory的層次結構圖以下相關類圖
上面類圖中各個類及介面的作用如下:
- AliasRegistry:定義對alias的簡單增刪改等操作
- SimpleAliasRegistry:主要使用map作為alias的快取,並對介面AliasRegistry進行實現
- SingletonBeanRegistry:定義對單例的註冊及獲取
- BeanFactory:定義獲取bean及bean的各種屬性
- DefaultSingletonBeanRegistry:預設對介面SingletonBeanRegistry各函式的實現
- HierarchicalBeanFactory:繼承BeanFactory,也就是在BeanFactory定義的功能的基礎上增加了對parentFactory的支援
- BeanDefinitionRegistry:定義對BeanDefinition的各種增刪改操作
- FactoryBeanRegistrySupport:在DefaultSingletonBeanRegistry基礎上增加了對FactoryBean的特殊處理功能
- ConfigurableBeanFactory:提供配置Factory的各種方法
- ListableBeanFactory:根據各種條件獲取bean的配置清單
- AbstractBeanFactory:綜合FactoryBeanRegistrySupport和ConfigurationBeanFactory的功能
- AutowireCapableBeanFactory:提供建立bean、自動注入、初始化以及應用bean的後處理器
- AbstractAutowireCapableBeanFactory:綜合AbstractBeanFactory並對介面AutowireCapableBeanFactory進行實現
- ConfigurableListableBeanFactory:BeanFactory配置清單,指定忽略型別及介面等
- DefaultListableBeanFactory:綜合上面所有功能,主要是對Bean註冊後的處理
XmlBeanFactory對DefaultListableBeanFactory類進行了擴充套件,主要用於從XML文件中讀取BeanDefinition,對於註冊及獲取Bean都是使用從父類DefaultListableBeanFactory繼承的方法去實現,而唯獨與父類不同的個性化實現就是增加了XmlBeanDefinitionReader型別的reader屬性。在XmlBeanFactory中主要使用reader屬性對資原始檔進行讀取和註冊
XmlBeanDefinitionReader
XML配置檔案的讀取是Spring中重要的功能,因為Spring的大部分功能都是以配置作為切入點的,可以從XmlBeanDefinitionReader中梳理一下資原始檔讀取、解析及註冊的大致脈絡,首先看看各個類的功能
ResourceLoader:定義資源載入器,主要應用於根據給定的資原始檔地址返回對應的Resource
BeanDefinitionReader:主要定義資原始檔讀取並轉換為BeanDefinition的各個功能
EnvironmentCapable:定義獲取Environment方法
DocumentLoader:定義從資原始檔載入到轉換為Document的功能
AbstractBeanDefinitionReader:對EnvironmentCapable、BeanDefinitionReader類定義的功能進行實現
BeanDefinitionDocumentReader:定義讀取Document並註冊BeanDefinition功能
BeanDefinitionParserDelegate:定義解析Element的各種方法
整個XML配置檔案讀取的大致流程,在XmlBeanDefinitionReader中主要包含以下幾步處理
(1)通過繼承自AbstractBeanDefinitionReader中的方法,來使用ResourceLoader將資原始檔路徑轉換為對應的Resource檔案
(2)通過DocumentLoader對Resource檔案進行轉換,將Resource檔案轉換為Document檔案
(3)通過實現介面BeanDefinitionDocumentReader的DefaultBeanDefinitionDocumentReader類對Document進行解析,並使用BeanDefinitionParserDelegate對Element進行解析
容器的基礎XmlBeanFactory
通過上面的內容我們對spring的容器已經有了大致的瞭解,接下來我們詳細探索每個步驟的詳細實現,接下來要分析的功能都是基於如下程式碼:
BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml"));
首先呼叫ClassPathResource的建構函式來構造Resource資原始檔的例項物件,這樣後續的資源處理就可以用Resource提供的各種服務來操作了。有了Resource後就可以對BeanFactory進行初始化操作,那配置檔案是如何封裝的呢?
配置檔案的封裝
Spring的配置檔案讀取是通過ClassPathResource進行封裝的,Spring對其內部使用到的資源實現了自己的抽象結構:Resource介面來封裝底層資源,如下原始碼:
public interface InputStreamSource { InputStream getInputStream() throws IOException; } public interface Resource extends InputStreamSource { boolean exists(); default boolean isReadable() { return true; } default boolean isOpen() { return false; } default boolean isFile() { return false; } URL getURL() throws IOException; URI getURI() throws IOException; File getFile() throws IOException; default ReadableByteChannel readableChannel() throws IOException { return Channels.newChannel(getInputStream()); } long contentLength() throws IOException; long lastModified() throws IOException; Resource createRelative(String relativePath) throws IOException; String getFilename(); String getDescription(); }
InputStreamSource封裝任何能返回InputStream的類,比如File、Classpath下的資源和Byte Array等, 它只有一個方法定義:getInputStream(),該方法返回一個新的InputStream物件 。
Resource介面抽象了所有Spring內部使用到的底層資源:File、URL、Classpath等。首先,它定義了3個判斷當前資源狀態的方法:存在性(exists)、可讀性(isReadable)、是否處於開啟狀態(isOpen)。另外,Resource介面還提供了不同資源到URL、URI、File型別的轉換,以及獲取lastModified屬性、檔名(不帶路徑資訊的檔名,getFilename())的方法,為了便於操作,Resource還提供了基於當前資源建立一個相對資源的方法:createRelative(),還提供了getDescription()方法用於在錯誤處理中的列印資訊。
對不同來源的資原始檔都有相應的Resource實現:檔案(FileSystemResource)、Classpath資源(ClassPathResource)、URL資源(UrlResource)、InputStream資源(InputStreamResource)、Byte陣列(ByteArrayResource)等,相關類圖如下所示:
在日常開發中我們可以直接使用spring提供的類來載入資原始檔,比如在希望載入資原始檔時可以使用下面的程式碼:
Resource resource = new ClassPathResource("spring-config.xml"); InputStream is = resource.getInputStream();
有了 Resource 介面便可以對所有資原始檔進行統一處理 至於實現,其實是非常簡單的,以 getlnputStream 為例,ClassPathResource 中的實現方式便是通 class 或者 classLoader 提供的底層方法進行呼叫,而對於 FileSystemResource 其實更簡單,直接使用 FileInputStream 對檔案進行例項化。
ClassPathResource.java
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); }
FileSystemResource.java
public InputStream getinputStream () throws IOException { return new FilelnputStream(this file) ; }
當通過Resource相關類完成了對配置檔案進行封裝後,配置檔案的讀取工作就全權交給XmlBeanDefinitionReader來處理了。
接下來就進入到XmlBeanFactory的初始化過程了,XmlBeanFactory的初始化有若干辦法,Spring提供了很多的建構函式,在這裡分析的是使用Resource例項作為建構函式引數的辦法,程式碼如下:
XmlBeanFactory.java
public XmlBeanFactory(Resource resource) throws BeansException { this(resource, null); } public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { super(parentBeanFactory); this.reader.loadBeanDefinitions(resource); }
上面函式中的程式碼this.reader.loadBeanDefinitions(resource)才是資源載入的真正實現,但是在XmlBeanDefinitionReader載入資料前還有一個呼叫父類建構函式初始化的過程:super(parentBeanFactory),我們按照程式碼層級進行跟蹤,首先跟蹤到如下父類程式碼:
public DefaultListableBeanFactory(@Nullable BeanFactory parentBeanFactory) { super(parentBeanFactory); }
然後繼續跟蹤,跟蹤程式碼到父類AbstractAutowireCapableBeanFactory的建構函式中:
public AbstractAutowireCapableBeanFactory(@Nullable BeanFactory parentBeanFactory) { this(); setParentBeanFactory(parentBeanFactory); } public AbstractAutowireCapableBeanFactory() { super(); ignoreDependencyInterface(BeanNameAware.class); ignoreDependencyInterface(BeanFactoryAware.class); ignoreDependencyInterface(BeanClassLoaderAware.class); }
這裡有必要提及 ignoreDependencylnterface方法,ignoreDependencylnterface 的主要功能是 忽略給定介面的向動裝配功能,那麼,這樣做的目的是什麼呢?會產生什麼樣的效果呢?
舉例來說,當 A 中有屬性 B ,那麼當 Spring 在獲取 A的 Bean 的時候如果其屬性 B 還沒有 初始化,那麼 Spring 會自動初始化 B,這也是 Spring 提供的一個重要特性 。但是,某些情況 下, B不會被初始化,其中的一種情況就是B 實現了 BeanNameAware 介面 。Spring 中是這樣介紹的:自動裝配時忽略給定的依賴介面,典型應用是邊過其他方式解析 Application 上下文註冊依賴,類似於 BeanFactor 通過 BeanFactoryAware 進行注入或者 ApplicationContext 通過 ApplicationContextAware 進行注入。
呼叫ignoreDependencyInterface方法後,被忽略的介面會儲存在BeanFactory的名為ignoredDependencyInterfaces的Set集合中:
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory { private final Set<Class<?>> ignoredDependencyInterfaces = new HashSet<>(); public void ignoreDependencyInterface(Class<?> ifc) { this.ignoredDependencyInterfaces.add(ifc); } ... }
ignoredDependencyInterfaces集合在同類中被使用僅在一處——isExcludedFromDependencyCheck方法中:
protected boolean isExcludedFromDependencyCheck(PropertyDescriptor pd) { return (AutowireUtils.isExcludedFromDependencyCheck(pd) || this.ignoredDependencyTypes.contains(pd.getPropertyType()) || AutowireUtils.isSetterDefinedInInterface(pd, this.ignoredDependencyInterfaces)); }
而ignoredDependencyInterface的真正作用還得看AutowireUtils類的isSetterDefinedInInterface方法。
public static boolean isSetterDefinedInInterface(PropertyDescriptor pd, Set<Class<?>> interfaces) { //獲取bean中某個屬性物件在bean類中的setter方法 Method setter = pd.getWriteMethod(); if (setter != null) { // 獲取bean的型別 Class<?> targetClass = setter.getDeclaringClass(); for (Class<?> ifc : interfaces) { if (ifc.isAssignableFrom(targetClass) && // bean型別是否介面的實現類 ClassUtils.hasMethod(ifc, setter.getName(), setter.getParameterTypes())) { // 介面是否有入參和bean型別完全相同的setter方法 return true; } } } return false; }
ignoredDependencyInterface方法並不是讓我們在自動裝配時直接忽略實現了該介面的依賴。這個方法的真正意思是忽略該介面的實現類中和介面setter方法入參型別相同的依賴。
舉個例子。首先定義一個要被忽略的介面。
public interface IgnoreInterface { void setList(List<String> list); void setSet(Set<String> set); }
然後需要實現該介面,在實現類中注意要有setter方法入參相同型別的域物件,在例子中就是List<String>和Set<String>。
public class IgnoreInterfaceImpl implements IgnoreInterface { private List<String> list; private Set<String> set; @Override public void setList(List<String> list) { this.list = list; } @Override public void setSet(Set<String> set) { this.set = set; } public List<String> getList() { return list; } public Set<String> getSet() { return set; } }
定義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" 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" default-autowire="byType"> <bean id="list" class="java.util.ArrayList"> <constructor-arg> <list> <value>foo</value> <value>bar</value> </list> </constructor-arg> </bean> <bean id="set" class="java.util.HashSet"> <constructor-arg> <list> <value>foo</value> <value>bar</value> </list> </constructor-arg> </bean> <bean id="ii" class="com.chenhao.ignoreDependency.IgnoreInterfaceImpl"/> <bean class="com.chenhao.autowire.IgnoreAutowiringProcessor"/> </beans>
最後呼叫ignoreDependencyInterface:
beanFactory.ignoreDependencyInterface(IgnoreInterface.class);
執行結果:
null
null
而如果不呼叫ignoreDependencyInterface,則是:
[foo, bar]
[bar, foo]
我們最初理解是在自動裝配時忽略該介面的實現,實際上是在自動裝配時忽略該介面實現類中和setter方法入參相同的型別,也就是忽略該介面實現類中存在依賴外部的bean屬性注入。
典型應用就是BeanFactoryAware和ApplicationContextAware介面。
首先看該兩個介面的原始碼:
public interface BeanFactoryAware extends Aware { void setBeanFactory(BeanFactory beanFactory) throws BeansException; } public interface ApplicationContextAware extends Aware { void setApplicationContext(ApplicationContext applicationContext) throws BeansException; }
在Spring原始碼中在不同的地方忽略了該兩個介面:
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class); ignoreDependencyInterface(BeanFactoryAware.class);
使得我們的BeanFactoryAware介面實現類在自動裝配時不能被注入BeanFactory物件的依賴:
public class MyBeanFactoryAware implements BeanFactoryAware { private BeanFactory beanFactory; // 自動裝配時忽略注入 @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } public BeanFactory getBeanFactory() { return beanFactory; } }
ApplicationContextAware介面實現類中的ApplicationContext物件的依賴同理:
public class MyApplicationContextAware implements ApplicationContextAware { private ApplicationContext applicationContext; // 自動裝配時被忽略注入 @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public ApplicationContext getApplicationContext() { return applicationContext; } }
private void invokeAwareInterfaces(Object bean) { if (bean instanceof Aware) { if (bean instanceof EnvironmentAware) { ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment()); } if (bean instanceof EmbeddedValueResolverAware) { ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver); } if (bean instanceof ResourceLoaderAware) { ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext); } if (bean instanceof ApplicationEventPublisherAware) { ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext); } if (bean instanceof MessageSourceAware) { ((MessageSourceAware) bean).setMessageSource(this.applicationContext); } if (bean instanceof ApplicationContextAware) { ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); } } }
通過這種方式保證了ApplicationContextAware和BeanFactoryAware中的容器保證是生成該bean的容器。
bean載入
在之前XmlBeanFactory建構函式中呼叫了XmlBeanDefinitionReader型別的reader屬性提供的方法this.reader.loadBeanDefinitions(resource),而這句程式碼則是整個資源載入的切入點,這個方法的時序圖如下:
我們來梳理下上述時序圖的處理過程:
(1)封裝資原始檔。當進入XmlBeanDefinitionReader後首先對引數Resource使用EncodedResource類進行封裝
(2)獲取輸入流。從Resource中獲取對應的InputStream並構造InputSource
(3)通過構造的InputSource例項和Resource例項繼續呼叫函式doLoadBeanDefinitions,loadBeanDefinitions函式具體的實現過程:
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { Assert.notNull(encodedResource, "EncodedResource must not be null"); if (logger.isTraceEnabled()) { logger.trace("Loading XML bean definitions from " + encodedResource); } Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get(); if (currentResources == null) { currentResources = new HashSet<>(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(); } } ... }
EncodedResource的作用是對資原始檔的編碼進行處理的,其中的主要邏輯體現在getReader()方法中,當設定了編碼屬性的時候Spring會使用相應的編碼作為輸入流的編碼,在構造好了encodeResource物件後,再次轉入了可複用方法loadBeanDefinitions(new EncodedResource(resource)),這個方法內部才是真正的資料準備階段,程式碼如下:
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException { try { // 獲取 Document 例項 Document doc = doLoadDocument(inputSource, resource); // 根據 Document 例項****註冊 Bean資訊 return registerBeanDefinitions(doc, resource); } ... }
核心部分就是 try 塊的兩行程式碼。
- 呼叫
doLoadDocument()
方法,根據 xml 檔案獲取 Document 例項。 - 根據獲取的 Document 例項註冊 Bean 資訊
其實在doLoadDocument()
方法內部還獲取了 xml 檔案的驗證模式。如下:
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception { return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler, getValidationModeForResource(resource), isNamespaceAware()); }
呼叫 getValidationModeForResource()
獲取指定資源(xml)的驗證模式。所以 doLoadBeanDefinitions()
主要就是做了三件事情。
1. 呼叫 getValidationModeForResource()
獲取 xml 檔案的驗證模式
2. 呼叫 loadDocument()
根據 xml 檔案獲取相應的 Document 例項。
3. 呼叫 registerBeanDefinitions()
註冊 Bean 例項。
獲取XML的驗證模式
DTD和XSD區別
DTD(Document Type Definition)即文件型別定義,是一種XML約束模式語言,是XML檔案的驗證機制,屬於XML檔案組成的一部分。DTD是一種保證XML文件格式正確的有效方法,可以通過比較XML文件和DTD檔案來看文件是否符合規範,元素和標籤使用是否正確。一個DTD文件包含:元素的定義規則,元素間關係的定義規則,元素可使用的屬性,可使用的實體或符合規則。
使用DTD驗證模式的時候需要在XML檔案的頭部宣告,以下是在Spring中使用DTD宣告方式的程式碼:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//Spring//DTD BEAN 2.0//EN" "http://www.Springframework.org/dtd/Spring-beans-2.0.dtd">
XML Schema語言就是XSD(XML Schemas Definition)。XML Schema描述了XML文件的結構,可以用一個指定的XML Schema來驗證某個XML文件,以檢查該XML文件是否符合其要求,文件設計者可以通過XML Schema指定一個XML文件所允許的結構和內容,並可據此檢查一個XML文件是否是有效的。
在使用XML Schema文件對XML例項文件進行檢驗,除了要宣告名稱空間外(xmlns=http://www.Springframework.org/schema/beans),還必須指定該名稱空間所對應的XML Schema文件的儲存位置,通過schemaLocation屬性來指定名稱空間所對應的XML Schema文件的儲存位置,它包含兩個部分,一部分是名稱空間的URI,另一部分就該名稱空間所標識的XML Schema檔案位置或URL地址(xsi:schemaLocation=”http://www.Springframework.org/schema/beans http://www.Springframework.org/schema/beans/Spring-beans.xsd“),程式碼如下:
<?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="myTestBean" class="com.chenhao.spring.MyTestBean"/> </beans>
驗證模式的讀取
在spring中,是通過getValidationModeForResource方法來獲取對應資源的驗證模式,其原始碼如下:
protected int getValidationModeForResource(Resource resource) { int validationModeToUse = getValidationMode(); if (validationModeToUse != VALIDATION_AUTO) { return validationModeToUse; } int detectedMode = detectValidationMode(resource); if (detectedMode != 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). return VALIDATION_XSD; }
方法的實現還是很簡單的,如果設定了驗證模式則使用設定的驗證模式(可以通過使用XmlBeanDefinitionReader中的setValidationMode方法進行設定),否則使用自動檢測的方式。而自動檢測驗證模式的功能是在函式detectValidationMode方法中,而在此方法中又將自動檢測驗證模式的工作委託給了專門處理類XmlValidationModeDetector的validationModeDetector方法,具體程式碼如下:
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; } 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(); } }
從程式碼中看,主要是通過讀取 XML 檔案的內容,判斷內容中是否包含有 DOCTYPE ,如果是 則為 DTD,否則為 XSD,當然只會讀取到 第一個 “<” 處,因為 驗證模式一定會在第一個 “<” 之前。如果當中出現了 CharConversionException 異常,則為 XSD模式。
獲取Document
經過了驗證模式準備的步驟就可以進行Document載入了,對於文件的讀取委託給了DocumentLoader去執行,這裡的DocumentLoader是個介面,而真正呼叫的是DefaultDocumentLoader,解析程式碼如下:
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); }
分析程式碼,首選建立DocumentBuildFactory,再通過DocumentBuilderFactory建立DocumentBuilder,進而解析InputSource來返回Document物件。對於引數entityResolver,傳入的是通過getEntityResolver()函式獲取的返回值,程式碼如下:
protected EntityResolver getEntityResolver() { if (this.entityResolver == null) { // Determine default EntityResolver to use. ResourceLoader resourceLoader = getResourceLoader(); if (resourceLoader != null) { this.entityResolver = new ResourceEntityResolver(resourceLoader); } else { this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader()); } } return this.entityResolver; }
這個entityResolver是做什麼用的呢,接下來我們詳細分析下。
EntityResolver 的用法
對於解析一個XML,SAX首先讀取該XML文件上的宣告,根據宣告去尋找相應的DTD定義,以便對文件進行一個驗證,預設的尋找規則,即通過網路(實現上就是宣告DTD的URI地址)來下載相應的DTD宣告,並進行認證。下載的過程是一個漫長的過程,而且當網路中斷或不可用時,這裡會報錯,就是因為相應的DTD宣告沒有被找到的原因.
EntityResolver的作用是專案本身就可以提供一個如何尋找DTD宣告的方法,即由程式來實現尋找DTD宣告的過程,比如將DTD檔案放到專案中某處,在實現時直接將此文件讀取並返回給SAX即可,在EntityResolver的介面只有一個方法宣告:
public abstract InputSource resolveEntity (String publicId, String systemId) throws SAXException, IOException;
它接收兩個引數publicId和systemId,並返回一個InputSource物件,以特定配置檔案來進行講解
(1)如果在解析驗證模式為XSD的配置檔案,程式碼如下:
<?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"> .... </beans>
則會讀取到以下兩個引數
- publicId:null
- systemId:http://www.Springframework.org/schema/beans/Spring-beans.xsd
(2)如果解析驗證模式為DTD的配置檔案,程式碼如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//Spring//DTD BEAN 2.0//EN" "http://www.Springframework.org/dtd/Spring-beans-2.0.dtd">
....
</beans>
讀取到以下兩個引數
- publicId:-//Spring//DTD BEAN 2.0//EN
- systemId:http://www.Springframework.org/dtd/Spring-beans-2.0.dtd
一般都會把驗證檔案放置在自己的工程裡,如果把URL轉換為自己工程裡對應的地址檔案呢?以載入DTD檔案為例來看看Spring是如何實現的。根據之前Spring中通過getEntityResolver()方法對EntityResolver的獲取,我們知道,Spring中使用DelegatingEntityResolver類為EntityResolver的實現類,resolveEntity實現方法如下:
@Override @Nullable public InputSource resolveEntity(String publicId, @Nullable String systemId) throws SAXException, IOException { if (systemId != null) { if (systemId.endsWith(DTD_SUFFIX)) { return this.dtdResolver.resolveEntity(publicId, systemId); } else if (systemId.endsWith(XSD_SUFFIX)) { return this.schemaResolver.resolveEntity(publicId, systemId); } } return null; }
不同的驗證模式使用不同的解析器解析,比如載入DTD型別的BeansDtdResolver的resolveEntity是直接擷取systemId最後的xx.dtd然後去當前路徑下尋找,而載入XSD型別的PluggableSchemaResolver類的resolveEntity是預設到META-INF/Spring.schemas檔案中找到systemId所對應的XSD檔案並載入。 BeansDtdResolver 的解析過程如下:
public InputSource resolveEntity(String publicId, @Nullable String systemId) throws IOException { if (logger.isTraceEnabled()) { logger.trace("Trying to resolve XML entity with public ID [" + publicId + "] and system ID [" + systemId + "]"); } if (systemId != null && systemId.endsWith(DTD_EXTENSION)) { int lastPathSeparator = systemId.lastIndexOf('/'); int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator); if (dtdNameStart != -1) { String dtdFile = DTD_NAME + DTD_EXTENSION; if (logger.isTraceEnabled()) { logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath"); } try { Resource resource = new ClassPathResource(dtdFile, getClass()); InputSource source = new InputSource(resource.getInputStream()); source.setPublicId(publicId); source.setSystemId(systemId); if (logger.isDebugEnabled()) { logger.debug("Found beans DTD [" + systemId + "] in classpath: " + dtdFile); } return source; } catch (IOException ex) { if (logger.isDebugEnabled()) { logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex); } } } } return null; }
從上面的程式碼中我們可以看到載入 DTD 型別的 BeansDtdResolver.resolveEntity()
只是對 systemId 進行了簡單的校驗(從最後一個 / 開始,內容中是否包含 spring-beans
),然後構造一個 InputSource 並設定 publicId、systemId,然後返回。 PluggableSchemaResolver 的解析過程如下:
public InputSource resolveEntity(String publicId, @Nullable String systemId) throws IOException { if (logger.isTraceEnabled()) { logger.trace("Trying to resolve XML entity with public id [" + publicId + "] and system id [" + systemId + "]"); } if (systemId != null) { String resourceLocation = getSchemaMappings().get(systemId); if (resourceLocation != null) { Resource resource = new ClassPathResource(resourceLocation, this.classLoader); try { InputSource source = new InputSource(resource.getInputStream()); source.setPublicId(publicId); source.setSystemId(systemId); if (logger.isDebugEnabled()) { logger.debug("Found XML schema [" + systemId + "] in classpath: " + resourceLocation); } return source; } catch (FileNotFoundException ex) { if (logger.isDebugEnabled()) { logger.debug("Couldn't find XML schema [" + systemId + "]: " + resource, ex); } } } } return null; }
首先呼叫 getSchemaMappings() 獲取一個對映表(systemId 與其在本地的對照關係),然後根據傳入的 systemId 獲取該 systemId 在本地的路徑 resourceLocation,最後根據 resourceLocation 構造 InputSource 物件。 對映表如下(部分):
解析及註冊BeanDefinitions
當把檔案轉換成Document後,接下來就是對bean的提取及註冊,當程式已經擁有了XML文件檔案的Document例項物件時,就會被引入到XmlBeanDefinitionReader.registerBeanDefinitions這個方法:
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; }
其中的doc引數即為上節讀取的document,而BeanDefinitionDocumentReader是一個介面,而例項化的工作是在createBeanDefinitionDocumentReader()中完成的,而通過此方法,BeanDefinitionDocumentReader真正的型別其實已經是DefaultBeanDefinitionDocumentReader了,進入DefaultBeanDefinitionDocumentReader後,發現這個方法的重要目的之一就是提取root,以便於再次將root作為引數繼續BeanDefinition的註冊,如下程式碼:
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { this.readerContext = readerContext; logger.debug("Loading bean definitions"); Element root = doc.getDocumentElement(); doRegisterBeanDefinitions(root); }
通過這裡我們看到終於到了解析邏輯的核心方法doRegisterBeanDefinitions,接著跟蹤原始碼如下:
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; }
我們看到首先要解析profile屬性,然後才開始XML的讀取,具體的程式碼如下:
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); } }
最終解析動作落地在兩個方法處:parseDefaultElement(ele, delegate)
和 delegate.parseCustomElement(root)
。我們知道在 Spring 有兩種 Bean 宣告方式:
- 配置檔案式宣告:<bean id="myTestBean" class="com.chenhao.spring.MyTestBean"/>
- 自定義註解方式:
<tx:annotation-driven>
兩種方式的讀取和解析都存在較大的差異,所以採用不同的解析方法,如果根節點或者子節點採用預設名稱空間的話,則呼叫 parseDefaultElement()
進行解析,否則呼叫 delegate.parseCustomElement()
方法進行自定義解析。
而判斷是否預設名稱空間還是自定義名稱空間的辦法其實是使用node.getNamespaceURI()獲取名稱空間,並與Spring中固定的名稱空間http://www.springframework.org/schema/beans進行對比,如果一致則認為是預設,否則就認為是自定義。
推薦部落格
profile的用法
通過profile標記不同的環境,可以通過設定spring.profiles.active和spring.profiles.default啟用指定profile環境。如果設定了active,default便失去了作用。如果兩個都沒有設定,那麼帶有profiles的bean都不會生成。
配置spring配置檔案最下面配置如下beans
<!-- 開發環境配置檔案 --> <beans profile="development"> <context:property-placeholder location="classpath*:config_common/*.properties, classpath*:config_development/*.properties"/> </beans> <!-- 測試環境配置檔案 --> <beans profile="test"> <context:property-placeholder location="classpath*:config_common/*.properties, classpath*:config_test/*.properties"/> </beans> <!-- 生產環境配置檔案 --> <beans profile="production"> <context:property-placeholder location="classpath*:config_common/*.properties, classpath*:config_production/*.properties"/> </beans>
配置web.xml
<!-- 多環境配置 在上下文context-param中設定profile.default的預設值 -->
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>production</param-value>
</context-param>
<!-- 多環境配置 在上下文context-param中設定profile.active的預設值 -->
<!-- 設定active後default失效,web啟動時會載入對應的環境資訊 -->
<context-param>
<param-name>spring.profiles.active</param-name>
<param-value>test</param-value>
</context-param>
這樣啟動的時候就可以按照切換spring.profiles.active的屬性值來進行切換了。