概述
上一篇我們搭建完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的屬性值來進行切換了。