本系列所有基於 Spring 5.2.2.BUILD-SNAPSHOT
版本。由於 Spring 整個體系太過於龐大,因此只會進行關鍵部分的源碼解析。java
本篇文章主要介紹 Spring IoC 容器怎麼加載 bean
的定義元信息。node
下圖是一個大體的流程圖:git
首先定義兩個簡單的 POJO,以下:github
public class User { private Long id; private String name; private City city; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public City getCity() { return city; } public void setCity(City city) { this.city = city; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", city=" + city + '}'; } }
public class City { private Long id; private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "City{" + "id=" + id + ", name='" + name + '\'' + '}'; } }
再編寫一個 XML 文件。web
<?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="user" class="com.leisurexi.ioc.domain.User"> <property name="id" value="1"/> <property name="name" value="leisurexi"/> <property name="city" ref="city"/> </bean> <bean id="city" class="com.leisurexi.ioc.domain.City"> <property name="id" value="1"/> <property name="name" value="beijing"/> </bean> </beans>
最後再來一個測試類。spring
public class BeanDefinitionDemo { public static void main(String[] args) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); reader.loadBeanDefinitions("META-INF/bean-definition.xml"); User user = beanFactory.getBean("user", User.class); System.err.println(user); } }
上面這段代碼比較簡單,無非就是聲明 bean
工廠,而後經過指定的 XML 文件加載 bean
的定義元信息,最後經過 bean
工廠獲取 bean
。接下來介紹上面代碼中的2個核心類 DefaultListableBeanFactory
和 XmlBeanDefinitionReader
。緩存
下面是該類的類圖及層次結構:dom
alias
的簡單增刪改等操做。map
做爲 alias
的緩存,並對接口 AliasRegistry
進行實現。bean
及 bean
的各類屬性。SingletonBeanRegistry
各函數的實現。BeanFactory
,也就是在 BeanFactory
定義的功能的基礎上增長了對 parentBeanFactory
的支持。BeanDefinition
的各類增刪改操做。DefaultSingletonBeanRegistry
基礎上增長了對 FactoryBean
的特殊處理功能。BeanFactory
的各類方法。BeanFactory
提供了獲取多個 bean
的各類方法。FactoryBeanRegistrySupport
和 ConfigurableBeanFactory
的功能。bean
、自動注入、初始化以及應用 bean
的後處理器。AbstractBeanFactory
並對接口 AutowireCapableBeanFactory
進行實現。BeanFactory
配置清單,指定忽略類型及接口等。bean
註冊後的處理。能夠看到上面的接口大多數是定義了一些功能或在父接口上擴展了一些功能,DefaultListableBeanFactory
實現了全部接口,大多數默認狀況下咱們所使用的 beanFactory
就是 DefaultListableBeanFactory
。ide
下面咱們就開始分析 Spring 是如何解析 XML 文件,並讀取其中的內容的。函數
public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException { // 獲取resourceLoader,這邊是PathMatchingResourcePatternResolver ResourceLoader resourceLoader = getResourceLoader(); if (resourceLoader == null) { throw new BeanDefinitionStoreException("Cannot load bean definitions from location [" + location + "]: no ResourceLoader available"); } // 判斷resourceLoader是不是ResourcePatternResolver,咱們這邊是符合的 if (resourceLoader instanceof ResourcePatternResolver) { try { // 根據路徑獲取所欲符合的配置文件並封裝成Resource對象 Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); // 根據Resource加載bean定義,並返回數量 int count = loadBeanDefinitions(resources); if (actualResources != null) { Collections.addAll(actualResources, resources); } return count; } catch (IOException ex) { throw new BeanDefinitionStoreException( "Could not resolve bean definition resource pattern [" + location + "]", ex); } } else { // 只能經過絕對路徑加載單個資源 Resource resource = resourceLoader.getResource(location); // 根據Resource加載bean的定義,並返回數量 int count = loadBeanDefinitions(resource); if (actualResources != null) { actualResources.add(resource); } return count; } } public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException { Assert.notNull(resources, "Resource array must not be null"); int count = 0; for (Resource resource : resources) { // 調用具體實現類的方法,加載 BeanDefinition,並返回數量,見下文詳解 count += loadBeanDefinitions(resource); } return count; }
上面方法主要是將資源文件轉換爲 Resource
對象,而後調用 loadBeanDefinitions(Resource...)
加載 BeanDefinition
。
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { // 將Resource封裝成EncodedResource,也就是對資源指定編碼和字符集 return loadBeanDefinitions(new EncodedResource(resource)); } public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { Assert.notNull(encodedResource, "EncodedResource must not be null"); // 當前正在加載的EncodedResource,第一次加載的話這裏是空的 Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get(); if (currentResources == null) { currentResources = new HashSet<>(4); this.resourcesCurrentlyBeingLoaded.set(currentResources); } // 若是當前encodedResource已經存在,表明出現了循環加載,拋出異常 if (!currentResources.add(encodedResource)) { throw new BeanDefinitionStoreException( "Detected cyclic loading of " + encodedResource + " - check your import definitions!"); } try { // 獲取Resource的輸入流 InputStream inputStream = encodedResource.getResource().getInputStream(); try { // 將inputStream封裝成org.xml.sax.InputSource InputSource inputSource = new InputSource(inputStream); // 若是encodedResource的編碼不爲空,設置inputSource的編碼 if (encodedResource.getEncoding() != null) { inputSource.setEncoding(encodedResource.getEncoding()); } // 加載bean定義(方法以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中移除 currentResources.remove(encodedResource); if (currentResources.isEmpty()) { this.resourcesCurrentlyBeingLoaded.remove(); } } }
上面方法主要將 Resource
封裝成 EncodedResource
,也就是制定資源的編碼和字符集。而後獲取 Resource
的輸入流 InputStream
,並封裝成 InputSource
設置其編碼,最終調用 doLoadBeanDefinitions
開始真正的加載流程。
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException { try { // 根據 inputSource 和 resource 加載 XML 文件,並封裝成 Document Document doc = doLoadDocument(inputSource, resource); // 用 doc 去解析和註冊 bean definition,見下文詳解 int count = registerBeanDefinitions(doc, resource); return count; } // 省略異常處理 } protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception { return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,getValidationModeForResource(resource), isNamespaceAware()); } protected int getValidationModeForResource(Resource resource) { // 若是手動指定了驗證模式則使用指定的驗證模式 int validationModeToUse = getValidationMode(); if (validationModeToUse != VALIDATION_AUTO) { return validationModeToUse; } // 若是未指定則使用自動檢測,其實就是判斷文件是否包含 DOCTYPE int detectedMode = detectValidationMode(resource); if (detectedMode != VALIDATION_AUTO) { return detectedMode; } // 若是沒有找到驗證,默認使用 XSD 模式,由於 DTD 已經不維護了 return VALIDATION_XSD; } // DefaultDocumentLoader.java public Document loadDocument(InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception { // 建立DocumentBuilderFactory DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware); // 建立DocumentBuilder DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler); // 解析inputSource並返回Document對象 return builder.parse(inputSource); }
detectValidationMode()
方法其實就是讀取文件內容,判斷是否包含 DOCTYPE
,若是包含就是 DTD 不然就是 XSD。
獲取 XML 配置文件的驗證模式。XML 文件的驗證模式是用來保證 XML 文件的正確性,常見的驗證模式有 DTD 和 XSD。
DTD XML 格式示例:
XSD XML 格式示例:
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { // 獲取DefaultBeanDefinitionDocumentReader BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); // 獲取註冊中心,再靠註冊中心獲取註冊以前以及註冊過的BeanDefinition數量 int countBefore = getRegistry().getBeanDefinitionCount(); // 解析並註冊BeanDefinition,見下文詳解 documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); // 獲取註冊事後BeanDefinition數量減去註冊以前的數量,獲得的就是本次註冊的數量 return getRegistry().getBeanDefinitionCount() - countBefore; }
這裏的 getRegistry()
方法返回的就是咱們經過構造函數傳入的 DefaultListableBeanFactory
,基本上這裏都是 DefaultListableBeanFactory
,由於就只有它實現了 BeanDefinitionRegistry
接口。
DefaultListableBeanFactory
中定義了存放 BeanDefinition
的緩存,以下:
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { // 存放BeanDefinition的緩存,key爲 bean的名稱,value就是其BeanDefinition private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256); }
因此 getBeanDefinitionCount()
方法返回的就是 beanDefinitionMap
中元素的數量。
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { this.readerContext = readerContext; // 提取 root,註冊 BeanDefinition (理論上 Spring 的配置文件,root 都應該是 beans 標籤) doRegisterBeanDefinitions(doc.getDocumentElement()); } protected void doRegisterBeanDefinitions(Element root) { BeanDefinitionParserDelegate parent = this.delegate; // 專門處理解析 this.delegate = createDelegate(getReaderContext(), root, parent); // 校驗root節點的命名空間是否爲默認的命名空間(默認命名空間http://www.springframework.org/schema/beans) 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); // 校驗當前節點的 profile 是否符合當前環境定義的,若是不是則直接跳過,不解析該節點下的內容 if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) { return; } } } // 解析前處理,留給子類實現 preProcessXml(root); // 解析註冊 BeanDefinition,見下文詳解 parseBeanDefinitions(root, this.delegate); // 解析後處理,留給子類實現 postProcessXml(root); this.delegate = parent; }
profile
主要是用於多環境開發,例如:
集成到 Web 環境時,在 web.xml 中加入如下代碼:
<coontext-param> <param-name>Spring.profiles.active</param-name> <param-value>dev</param-value> </coontext-param>
preProcessXml()
和 postProcessXml()
採用的 模板方法模式,子類能夠DefaultBeanDefinitionDoucumentReader
來重寫這兩個方法,這也是解析先後的擴展點。
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { // 校驗root節點的命名空間是否爲默認的命名空間,這裏爲何再次效驗,由於調用解析前調用了preProcessXml()方法,可能會對節點作修改 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)) { // 默認命名空間節點的處理,例如<bean id="..." class="..."/> parseDefaultElement(ele, delegate); } else { // 自定義命名空間節點的處理,例如<context:compoent-scan/>、<aop:aspectj-autoproxy> delegate.parseCustomElement(ele); } } } } else { // 自定義命名空間節點的處理 delegate.parseCustomElement(root); } }
關於 默認命名空間節點 的處理 和 自定義命名空間節點 的處理,會在後續文章一一分析。
本篇文章對 Spring 解析 XML 文件具體節點前的準備工做作了簡要分析,可是對 Spring 的資源管理(Resource)沒有作過多介紹,有興趣的小夥伴能夠自行去研究一下。
我模仿 Spring 寫了一個精簡版,代碼會持續更新。地址:https://github.com/leisurexi/tiny-spring。
《Spring 源碼深度解析》—— 郝佳