Spring 源碼學習(一)-容器的基礎結構

關注公衆號,你們能夠在公衆號後臺回覆「博客園」,免費得到做者 Java 知識體系/面試必看資料java


 

展現的代碼摘取了一些核心方法,去掉一些默認設置和日誌輸出,還有大多數錯誤異常也去掉了,小夥伴想看詳細代碼,註釋和 demo,能夠下載我上傳的筆記項目📒node

碼雲 Gitee 地址 https://gitee.com/vip-augus/spring-analysis-note.gitGithub 地址 https://github.com/Vip-Augus/spring-analysis-note

經過閱讀源碼的過程,瞭解設計者的設計思路和從中學習,對 spring 有個基礎的瞭解。git

ClassPathXmlApplicationContext

ClassPathXmlApplicationContext 的繼承體系結構圖:github

這種結構圖是經過 IDEA 編輯器的 Diagrams 功能展現的,對當前類右鍵選擇,能夠看到繼承體系,繼承了哪些類和引用了哪些接口,方便咱們去了解~ClassPathXmlApplicationContext 繼承自 AbstractApplicationContext,而AbstractRefreshableApplicationContext 是 AbstractApplicationContext 的抽象子類,使用的類註冊工廠是 DefaultListableBeanFactory,這個註冊工廠也很重要,後面會有它的介紹。web

簡單來講,DefaultListableBeanFactory 是 Spring 註冊及加載 bean 的默認實現,它會將註冊的 bean放入 beanDefinitionMap 進行 key-value 形式存儲。面試

 在圖片的右上角能看到,ResourceLoader 是它的頂層接口,表示這個類實現了資源加載功能。構造器的代碼:spring

public ClassPathXmlApplicationContext( String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) throws BeansException { super(parent); // 註釋 1.1 獲取資源文件 setConfigLocations(configLocations); if (refresh) { refresh(); }}

 

設置配置文件路徑數據庫

org.springframework.context.support.AbstractRefreshableConfigApplicationContext設計模式

public void setConfigLocations(@Nullable String... locations) { if (locations != null) { Assert.noNullElements(locations, "Config locations must not be null"); // 註釋 1.2 將配置資源路徑放入 configLocations 數組中 this.configLocations = new String[locations.length]; for (int i = 0; i < locations.length; i++) { this.configLocations[i] = resolvePath(locations[i]).trim(); } } else { this.configLocations = null; }}數組

resolvePath

用途是:解析給定的路徑,用對應的佔位符(placeholder)替換佔位符

例如 new ClassPathXmlApplicationContext("classpath:config.xml");,就須要解析classpath,變成正確路徑。

protected String resolvePath(String path) { return getEnvironment().resolveRequiredPlaceholders(path);}

咱們有不一樣的運行環境,devtest 或者 prod,這個時候加載的配置文件和屬性應該有所不一樣,這個時候就須要使用到 Environment 來進行區分。

Spring 環境和屬性是由四個部分組成:

  • Environment : 環境,由 Profile 和 PropertyResolver 組合。

  • Profile : 配置文件,能夠理解爲,容器裏多個配置組別的屬性和 bean,只有激活的 profile,它對應的組別屬性和 bean 纔會被加載

  • PropertySource : 屬性源, 使用 CopyOnWriteArrayList 數組進行屬性對 key-value形式存儲

  • PropertyResolver :屬性解析器,這個用途就是解析屬性

Profile

經過這個屬性,能夠同時在配置文件中部署兩套配置,用來適用於生產環境和開發環境,這樣能夠方便的進行切換開發、部署環境,經常使用來更換不一樣的數據庫或者配置文件。

demo:(引用自參考資料第四條)

<!-- 測試環境配置文件 --><beans profile="test"> <context:property-placeholder location="classpath:test/*.properties, classpath:common/*.properties" /></beans><!-- 生產環境配置文件 --><beans profile="production"> <context:property-placeholder location="classpath:production/*.properties, classpath:common/*.properties" /></beans><!-- 開發環境配置文件 --><beans profile="development"> <context:property-placeholder location="classpath:dev/*.properties, classpath:common/*.properties" /></beans>

 

有兩種方式能夠設置選擇使用哪套配置:

① 在 web.xml 中設置

<context-param> <param-name>spring.profiles.active</param-name> <param-value>test</param-value></context-param>

② 在代碼啓動時設置

context.getEnvironment().setActiveProfiles("test");

 

PropertySource 接口

繼承體系如圖:

 

從 PropertySource 繼承體系來看,customizePropertySources 方法的調用鏈路是從子類一直往上調用 :

 AbstractEnvironment->StandardServletEnvironment-> StandardEnvironment

最終在 StandardEnvironment 使用 CopyOnWriteArrayList 數組進行屬性存儲

protected void customizePropertySources(MutablePropertySources propertySources) { propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties())); propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));}

 

例如從上面能夠看出,propertySourceList 將會存儲系統的參數:

到時這些參數就能在啓動的應用中,經過上下文 context 進行獲取

((MutablePropertySources)((StandardEnvironment)context.environment).propertySources).propertySourceList

 

Bean 的解析和註冊

Spring bean 的解析和註冊有一個重要的方法 refresh()

AbstractApplicationContext.refresh()
public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // Prepare this context for refreshing. (爲更新準備上下文,設定一些標誌) prepareRefresh(); // Tell the subclass to refresh the internal bean factory. (告訴子類去更新它們的 bean factory) // 類的註冊到 bean factory 也是在這一步 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context. prepareBeanFactory(beanFactory); try { // Allows post-processing of the bean factory in context subclasses. postProcessBeanFactory(beanFactory); // Invoke factory processors registered as beans in the context. invokeBeanFactoryPostProcessors(beanFactory); // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); // Initialize message source for this context. initMessageSource(); // Initialize event multicaster for this context. initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses. onRefresh(); // Check for listener beans and register them. registerListeners(); // Instantiate all remaining (non-lazy-init) singletons. finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event. finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex); } // Destroy already created singletons to avoid dangling resources. destroyBeans(); // Reset 'active' flag. cancelRefresh(ex); // Propagate exception to caller. throw ex; } finally { // Reset common introspection caches in Spring's core, since we // might not ever need metadata for singleton beans anymore... resetCommonCaches(); } }}

下面會圍繞這個方法進行跟蹤和分析。

 

具體校驗的方法

org.springframework.core.env.AbstractPropertyResolver#validateRequiredProperties
public void validateRequiredProperties() { MissingRequiredPropertiesException ex = new MissingRequiredPropertiesException(); for (String key : this.requiredProperties) { if (this.getProperty(key) == null) { ex.addMissingRequiredProperty(key); } } if (!ex.getMissingRequiredProperties().isEmpty()) { throw ex; }}

能夠看到,校驗邏輯是遍歷 requiredProperties,它是一個字符 Set,默認狀況下是空,表示不須要校驗任何元素,若是列表中有值,而後根據 key 獲取對應的環境變量爲空,將會拋出異常,致使 Spring 容器初始化失敗。

 

獲取 bean 容器

在這行代碼中 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

具體調用的是 :

org.springframework.context.support.AbstractRefreshableApplicationContext#refreshBeanFactory

 

protected final void refreshBeanFactory() throws BeansException { // 在更新時,若是發現已經存在,將會把以前的 bean 清理掉,而且關閉老 bean 容器 if (hasBeanFactory()) { destroyBeans(); closeBeanFactory(); } try { DefaultListableBeanFactory beanFactory = createBeanFactory(); beanFactory.setSerializationId(getId()); customizeBeanFactory(beanFactory); // 註釋 1.3 開始加載 (bean 註冊) loadBeanDefinitions(beanFactory); synchronized (this.beanFactoryMonitor) { this.beanFactory = beanFactory; } } catch (IOException ex) { throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex); }}

 

這個入口方法很重要,在這一步新建了 bean 容器和解析 bean,並將 bean 註冊到容器中。

BanFactory 自定義

具體方法以下,經過這個方法,能夠對工廠進行定製化設置,讓子類進行自由配置:

org.springframework.context.support.AbstractRefreshableApplicationContext#customizeBeanFactory

 

protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) { if (this.allowBeanDefinitionOverriding != null) { // 默認是 false,不容許覆蓋 beanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); } if (this.allowCircularReferences != null) { // 默認是 false,不容許循環引用 beanFactory.setAllowCircularReferences(this.allowCircularReferences); }}
EntityResolver

 

接口全路徑是:org.xml.sax.EntityResolver,具體解析使用的方法是:

org.springframework.beans.factory.xml.ResourceEntityResolver#resolveEntity

 該方法是用於解析 schema 和 dtd,具體深究的話也很複雜,但解析 xml 不是我想了解的點,因此先跳過~

 

配置文件加載

入口方法:(因爲有多個重名方法,因此複製路徑時,將參數的類型也拷貝了)

org.springframework.beans.factory.support.AbstractBeanDefinitionReader#loadBeanDefinitions(java.lang.String, java.util.Set<org.springframework.core.io.Resource>)

 核心方法是這兩行

public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException { // 獲取資源文件(資源加載器從路徑識別資源文件) Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location) // 註釋 1.6 根據資源文件加載 bean int count = loadBeanDefinitions(resources);  ···}

 

獲取資源文件後,開始解析資源文件(也就是一開始傳參的 config.xml),將它轉換成 Document

跟蹤代碼能夠看到,進行解析的資源文件從 Resource 包裝成 EncodeResouce,爲輸入流添加了字符編碼(默認爲 null),體現了設計模式裝飾器模式。

遍歷資源文件,進行轉換,核心方法是如下兩行:

org.springframework.beans.factory.xml.XmlBeanDefinitionReader#loadBeanDefinitions(org.springframework.core.io.support.EncodedResource)

 

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { // 註釋 1.7 從資源文件中獲取輸入流 InputStream inputStream = encodedResource.getResource().getInputStream(); InputSource inputSource = new InputSource(inputStream); return doLoadBeanDefinitions(inputSource, encodedResource.getResource());}

默認標籤解析

這部分不會細說,以後再寫一篇進行補充,因此簡單的過下代碼中,是如何解析默認標籤的

  • IMPORT:導入標籤

  • ALIAS:別名標籤

  • BEAN:bean 標籤

  • NESTED_BEANS:beans 標籤(嵌套的 beans)

org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader#parseDefaultElement

 

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) { importBeanDefinitionResource(ele); } else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) { processAliasRegistration(ele); } else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { processBeanDefinition(ele, delegate); } else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { // recurse doRegisterBeanDefinitions(ele); }}

讓咱們來看下如何解析 bean 標籤

獲取 id 和 name

org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parseBeanDefinitionElement(org.w3c.dom.Element, org.springframework.beans.factory.config.BeanDefinition)

 

public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { // 獲取 ID 屬性 String id = ele.getAttribute(ID_ATTRIBUTE); // 獲取 NAME 屬性 String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); List<String> aliases = new ArrayList<>(); if (StringUtils.hasLength(nameAttr)) { // 名稱按照 , ; 進行分割 String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS); aliases.addAll(Arrays.asList(nameArr)); } String beanName = id; if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) { // 若是沒有指定 id,將 name 的第一個值做爲 id beanName = aliases.remove(0); } // 默認 null if (containingBean == null) { // 檢查名字是否惟一,若是 id 重複了,將拋出錯誤 // 內部 usedNames 是一個 HashSet,將會存儲加載過的 name 和 aliases checkNameUniqueness(beanName, aliases, ele); } // 將公共屬性放入 AbstractBeanDefinition,具體實如今子類 GenericBeanDefinition AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); if (beanDefinition != null) { if (!StringUtils.hasText(beanName)) { if (containingBean != null) { // 若是 id 和 name 都是空,那個 spring 會給它生成一個默認的名稱 beanName = BeanDefinitionReaderUtils.generateBeanName( beanDefinition, this.readerContext.getRegistry(), true); } else { beanName = this.readerContext.generateBeanName(beanDefinition); String beanClassName = beanDefinition.getBeanClassName(); if (beanClassName != null && beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() && !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) { aliases.add(beanClassName); } } } } String[] aliasesArray = StringUtils.toStringArray(aliases); return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); } return null;}

獲取 id 和 name 屬性的流程,按照代碼註釋一步一步往下走就清晰了

該方法主要工做流程以下:

  • 提取元素中的 id name 屬性

  • 進一步解析其它全部屬性並統一封裝到 GenericBeanDefinition 類型的實例中

  • 檢測到 bean 沒有指定 beanName 使用默認規則生成 beanName

  • 將獲取到的信息封裝到 BeanDefinitionHolder 的實例中

對標籤中其它屬性的解析

org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parseBeanDefinitionElement(org.w3c.dom.Element, java.lang.String, org.springframework.beans.factory.config.BeanDefinition)

 

public AbstractBeanDefinition parseBeanDefinitionElement( Element ele, String beanName, @Nullable BeanDefinition containingBean) { AbstractBeanDefinition bd = createBeanDefinition(className, parent); parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT)); parseMetaElements(ele, bd); parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); parseConstructorArgElements(ele, bd); parsePropertyElements(ele, bd); parseQualifierElements(ele, bd); bd.setResource(this.readerContext.getResource()); bd.setSource(extractSource(ele)); return bd;}

 

初始化 BeanDefiniton 在這個方法中:(具體實現是它的子類 GenericBeanDefinition 噢~)

BeanDefinitionReaderUtils.createBeanDefinition(parentName, className, this.readerContext.getBeanClassLoader())

 

public static AbstractBeanDefinition createBeanDefinition( @Nullable String parentName, @Nullable String className, @Nullable ClassLoader classLoader) throws ClassNotFoundException { GenericBeanDefinition bd = new GenericBeanDefinition(); bd.setParentName(parentName); if (className != null) { if (classLoader != null) { bd.setBeanClass(ClassUtils.forName(className, classLoader)); } else { bd.setBeanClassName(className); } } return bd;}

後面就是解析其它標籤的內容,以後會補坑~

BeanDefinitionHolder 修飾

org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#decorateBeanDefinitionIfRequired(org.w3c.dom.Element, org.springframework.beans.factory.config.BeanDefinitionHolder, org.springframework.beans.factory.config.BeanDefinition)

 

public BeanDefinitionHolder decorateBeanDefinitionIfRequired( Element ele, BeanDefinitionHolder definitionHolder, @Nullable BeanDefinition containingBd) { // 方法中的第三個參數是父類 bean // 當對某個嵌套配置進行分析時,這裏須要傳遞,是爲了使用父類的 scope 屬性,以備子類沒設定 scope,可使用父類的 scope 屬性 BeanDefinitionHolder finalDefinition = definitionHolder; // Decorate based on custom attributes first. NamedNodeMap attributes = ele.getAttributes(); // 遍歷全部的屬性,進行屬性的修飾 for (int i = 0; i < attributes.getLength(); i++) { Node node = attributes.item(i); finalDefinition = decorateIfRequired(node, finalDefinition, containingBd); } // Decorate based on custom nested elements. NodeList children = ele.getChildNodes(); // 遍歷全部的子節點,修飾子元素 for (int i = 0; i < children.getLength(); i++) { Node node = children.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { finalDefinition = decorateIfRequired(node, finalDefinition, containingBd); } } return finalDefinition;}

在以前的常規屬性解析後,在這一步操做中,主要用來完成自定義標籤元素的解析,這裏繼續留個坑~

prepareBeanFactory

準備類加載器的環境,對前面獲取到的 beanFactory(ConfigurationListableBeanFactory) 進行相關的設置,包括 ClassLoaderpost-processors

invokeBeanFactoryPostProcessors

實例化並調用全部註冊的 BeanFactoryPostProcessorBean,這些是後處理器,處理類型是BeanFactorySpring 容器容許在實例化 bean 前,讀取 bean 信息和修改它的屬性。

至關於在實例化前,給用戶最後一次機會去修改 bean 信息。

還有一點,執行也能夠有前後順序,依據這些處理器是否實現 PriorityOrdered 、Order 接口,根據 order 值進行排序。

initMessageSource

初始化此上下文的消息源

onRefresh

模板方法,可被重寫以添加特定於上下文的刷新工做。

在實例化單例以前調用特殊 bean 的初始化。(霧,不知道是啥特殊 bean ,留個坑=-=)

此實現爲空。

finishBeanFactoryInitialization

完成 bean 容器的初始化,實例化全部剩餘的(非惰性初始化)單例

resetCommonCaches

是真註冊的最後一步,用來清除緩存

重置 Spring 核心中的公共內省緩存,由於咱們可能不再須要單例 bean 的元數據了

 

踩坑記錄

Javadoc 編譯錯誤

Javadoc generation failed. Generated Javadoc options file (useful for troubleshooting)

 

在編譯時,發現沒法成功,提示 Javadoc 的錯誤,解決方法是在 gradle 文件中添加如下配置:

tasks.withType(Javadoc) { options.addStringOption('Xdoclint:none', '-quiet') options.addStringOption('encoding', 'UTF-8')}

 

參考資料

一、spring-analysis/note/Spring.md

二、Spring Framework 5.0.0.M3中文文檔

三、Spring 源碼深度解析 / 郝佳編著. – 北京 : 人民郵電出版社

四、使用Spring3.1後的的Profile配置使不一樣環境加載不一樣配置文件

五、spring4.1.8擴展實戰之一:自定義環境變量驗證


 

Java 極客技術公衆號,是由一羣熱愛 Java 開發的技術人組建成立,專一分享原創、高質量的 Java 文章。若是您以爲咱們的文章還不錯,請幫忙讚揚、在看、轉發支持,鼓勵咱們分享出更好的文章。

 

相關文章
相關標籤/搜索