天天看點

【死磕 Spring】----- IOC 之 IOC 初始化總結

前面 13 篇博文從源碼層次分析了 IOC 整個初始化過程,這篇就這些内容做一個總結将其連貫起來。

在前文提過,IOC 容器的初始化過程分為三步驟:Resource 定位、BeanDefinition 的載入和解析,BeanDefinition 注冊。

【死磕 Spring】----- IOC 之 IOC 初始化總結

  • Resource 定位。我們一般用外部資源來描述 Bean 對象,是以在初始化 IOC 容器的第一步就是需要定位這個外部資源。
  • BeanDefinition 的載入和解析。裝載就是 BeanDefinition 的載入。BeanDefinitionReader 讀取、解析 Resource 資源,也就是将使用者定義的 Bean 表示成 IOC 容器的内部資料結構:BeanDefinition。在 IOC 容器内部維護着一個 BeanDefinition Map 的資料結構,在配置檔案中每一個都對應着一個BeanDefinition對象。
  • BeanDefinition 注冊。向IOC容器注冊在第二步解析好的 BeanDefinition,這個過程是通過 BeanDefinitionRegistery 接口來實作的。在 IOC 容器内部其實是将第二個過程解析得到的 BeanDefinition 注入到一個 HashMap 容器中,IOC 容器就是通過這個 HashMap 來維護這些 BeanDefinition 的。在這裡需要注意的一點是這個過程并沒有完成依賴注入,依賴注冊是發生在應用第一次調用

    getBean()

    向容器索要 Bean 時。當然我們可以通過設定預處理,即對某個 Bean 設定 lazyinit 屬性,那麼這個 Bean 的依賴注入就會在容器初始化的時候完成。

還記得在部落格

【死磕 Spring】----- IOC 之 加載 Bean

中提供的一段代碼嗎?這裡我們同樣也以這段代碼作為我們研究 IOC 初始化過程的開端,如下:

  1. ClassPathResource resource = new ClassPathResource("bean.xml");

  2. DefaultListableBeanFactory factory = new DefaultListableBeanFactory();

  3. XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);

  4. reader.loadBeanDefinitions(resource);

剛剛開始的時候可能對上面這幾行代碼不知道什麼意思,現在應該就一目了然了。

  • ClassPathResourceresource=newClassPathResource("bean.xml");

    : 根據 Xml 配置檔案建立 Resource 資源對象。ClassPathResource 是 Resource 接口的子類,bean.xml 檔案中的内容是我們定義的 Bean 資訊。
  • DefaultListableBeanFactoryfactory=newDefaultListableBeanFactory();

    建立一個 BeanFactory。DefaultListableBeanFactory 是 BeanFactory 的一個子類,BeanFactory 作為一個接口,其實它本身是不具有獨立使用的功能的,而 DefaultListableBeanFactory 則是真正可以獨立使用的 IOC 容器,它是整個 Spring IOC 的始祖,在後續會有專門的文章來分析它。
  • XmlBeanDefinitionReaderreader=newXmlBeanDefinitionReader(factory);

    :建立 XmlBeanDefinitionReader 讀取器,用于載入 BeanDefinition 。
  • reader.loadBeanDefinitions(resource);

    :開啟 Bean 的載入和注冊程序,完成後的 Bean 放置在 IOC 容器中。
Resource 定位

Spring 為了解決資源定位的問題,提供了兩個接口:Resource、ResourceLoader,其中 Resource 接口是 Spring 統一資源的抽象接口,ResourceLoader 則是 Spring 資源加載的統一抽象。關于Resource、ResourceLoader 的更多知識請關注

【死磕 Spring】----- IOC 之 Spring 統一資源加載政策

Resource 資源的定位需要 Resource 和 ResourceLoader 兩個接口互相配合,在上面那段代碼中

newClassPathResource("bean.xml")

為我們定義了資源,那麼 ResourceLoader 則是在什麼時候初始化的呢?看 XmlBeanDefinitionReader 構造方法:

  1. public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {

  2. super(registry);

  3. }

直接調用父類 AbstractBeanDefinitionReader :

  1. protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {

  2. Assert.notNull(registry, "BeanDefinitionRegistry must not be null");

  3. this.registry = registry;

  4. // Determine ResourceLoader to use.

  5. if (this.registry instanceof ResourceLoader) {

  6. this.resourceLoader = (ResourceLoader) this.registry;

  7. }

  8. else {

  9. this.resourceLoader = new PathMatchingResourcePatternResolver();

  10. }

  11. // Inherit Environment if possible

  12. if (this.registry instanceof EnvironmentCapable) {

  13. this.environment = ((EnvironmentCapable) this.registry).getEnvironment();

  14. }

  15. else {

  16. this.environment = new StandardEnvironment();

  17. }

  18. }

核心在于設定 resourceLoader 這段,如果設定了 ResourceLoader 則用設定的,否則使用 PathMatchingResourcePatternResolver ,該類是一個集大成者的 ResourceLoader。

BeanDefinition 的載入和解析

reader.loadBeanDefinitions(resource);

開啟 BeanDefinition 的解析過程。如下:

  1. public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {

  2. return loadBeanDefinitions(new EncodedResource(resource));

  3. }

在這個方法會将資源 resource 包裝成一個 EncodedResource 執行個體對象,然後調用

loadBeanDefinitions()

方法,而将 Resource 封裝成 EncodedResource 主要是為了對 Resource 進行編碼,保證内容讀取的正确性。

  1. public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {

  2. // 省略一些代碼

  3. try {

  4. // 将資源檔案轉為 InputStream 的 IO 流

  5. InputStream inputStream = encodedResource.getResource().getInputStream();

  6. try {

  7. // 從 InputStream 中得到 XML 的解析源

  8. InputSource inputSource = new InputSource(inputStream);

  9. if (encodedResource.getEncoding() != null) {

  10. inputSource.setEncoding(encodedResource.getEncoding());

  11. }

  12. // 具體的讀取過程

  13. return doLoadBeanDefinitions(inputSource, encodedResource.getResource());

  14. }

  15. finally {

  16. inputStream.close();

  17. }

  18. }

  19. // 省略一些代碼

  20. }

從 encodedResource 源中擷取 xml 的解析源,調用

doLoadBeanDefinitions()

執行具體的解析過程。

  1. protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)

  2. throws BeanDefinitionStoreException {

  3. try {

  4. Document doc = doLoadDocument(inputSource, resource);

  5. return registerBeanDefinitions(doc, resource);

  6. }

  7. // 省略很多catch代碼

在該方法中主要做兩件事:1、根據 xml 解析源擷取相應的 Document 對象,2、調用

registerBeanDefinitions()

開啟 BeanDefinition 的解析注冊過程。

轉換為 Document 對象

調用

doLoadDocument()

會将 Bean 定義的資源轉換為 Document 對象。

  1. protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {

  2. return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,

  3. getValidationModeForResource(resource), isNamespaceAware());

  4. }

loadDocument()

方法接受五個參數:

  • inputSource:加載 Document 的 Resource 源
  • entityResolver:解析檔案的解析器
  • errorHandler:處理加載 Document 對象的過程的錯誤
  • validationMode:驗證模式
  • namespaceAware:命名空間支援。如果要提供對 XML 名稱空間的支援,則為true

對于這五個參數,有兩個參數需要重點關注下:entityResolver、validationMode。這兩個參數分别在

【死磕Spring】----- IOC 之 擷取 Document 對象

【死磕Spring】----- IOC 之 擷取驗證模型

中有詳細的講述。

loadDocument()

在類 DefaultDocumentLoader 中提供了實作,如下:

  1. public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,

  2. ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

  3. // 建立檔案解析工廠

  4. DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);

  5. if (logger.isDebugEnabled()) {

  6. logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");

  7. }

  8. // 建立文檔解析器

  9. DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);

  10. // 解析 Spring 的 Bean 定義資源

  11. return builder.parse(inputSource);

  12. }

這到這裡,就已經将定義的 Bean 資源檔案,載入并轉換為 Document 對象了,那麼下一步就是如何将其解析為 Spring IOC 管理的 Bean 對象并将其注冊到容器中。這個過程有方法

registerBeanDefinitions()

實作。如下:

  1. public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {

  2. // 建立 BeanDefinitionDocumentReader 來對 xml 格式的BeanDefinition 解析

  3. BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();

  4. // 獲得容器中注冊的Bean數量

  5. int countBefore = getRegistry().getBeanDefinitionCount();

  6. // 解析過程入口,這裡使用了委派模式,BeanDefinitionDocumentReader隻是個接口,

  7. // 具體的解析實作過程有實作類DefaultBeanDefinitionDocumentReader完成

  8. documentReader.registerBeanDefinitions(doc, createReaderContext(resource));

  9. return getRegistry().getBeanDefinitionCount() - countBefore;

  10. }

首先建立 BeanDefinition 的解析器 BeanDefinitionDocumentReader,然後調用

documentReader.registerBeanDefinitions()

開啟解析過程,這裡使用的是委派模式,具體的實作由子類 DefaultBeanDefinitionDocumentReader 完成。

  1. public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {

  2. // 獲得XML描述符

  3. this.readerContext = readerContext;

  4. logger.debug("Loading bean definitions");

  5. // 獲得Document的根元素

  6. Element root = doc.getDocumentElement();

  7. // 解析根元素

  8. doRegisterBeanDefinitions(root);

  9. }

對 Document 對象的解析

從 Document 對象中擷取根元素 root,然後調用

doRegisterBeanDefinitions()

開啟真正的解析過程。

  1. protected void doRegisterBeanDefinitions(Element root) {

  2. BeanDefinitionParserDelegate parent = this.delegate;

  3. this.delegate = createDelegate(getReaderContext(), root, parent);

  4. // 省略部分代碼

  5. preProcessXml(root);

  6. parseBeanDefinitions(root, this.delegate);

  7. postProcessXml(root);

  8. this.delegate = parent;

  9. }

preProcessXml()

postProcessXml()

為前置、後置增強處理,目前 Spring 中都是空實作,

parseBeanDefinitions()

是對根元素 root 的解析注冊過程。

  1. protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {

  2. // Bean定義的Document對象使用了Spring預設的XML命名空間

  3. if (delegate.isDefaultNamespace(root)) {

  4. // 擷取Bean定義的Document對象根元素的所有子節點

  5. NodeList nl = root.getChildNodes();

  6. for (int i = 0; i < nl.getLength(); i++) {

  7. Node node = nl.item(i);

  8. // 獲得Document節點是XML元素節點

  9. if (node instanceof Element) {

  10. Element ele = (Element) node;

  11. // Bean定義的Document的元素節點使用的是Spring預設的XML命名空間

  12. if (delegate.isDefaultNamespace(ele)) {

  13. // 使用Spring的Bean規則解析元素節點(預設解析規則)

  14. parseDefaultElement(ele, delegate);

  15. }

  16. else {

  17. // 沒有使用Spring預設的XML命名空間,則使用使用者自定義的解析規則解析元素節點

  18. delegate.parseCustomElement(ele);

  19. }

  20. }

  21. }

  22. }

  23. else {

  24. // Document 的根節點沒有使用Spring預設的命名空間,則使用使用者自定義的解析規則解析

  25. delegate.parseCustomElement(root);

  26. }

  27. }

疊代 root 元素的所有子節點,對其進行判斷,若節點為預設命名空間,則ID調用

parseDefaultElement()

開啟預設标簽的解析注冊過程,否則調用

parseCustomElement()

開啟自定義标簽的解析注冊過程。

标簽解析

若定義的元素節點使用的是 Spring 預設命名空間,則調用

parseDefaultElement()

進行預設标簽解析,如下:

  1. private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {

  2. // 如果元素節點是<Import>導入元素,進行導入解析

  3. if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {

  4. importBeanDefinitionResource(ele);

  5. }

  6. // 如果元素節點是<Alias>别名元素,進行别名解析

  7. else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {

  8. processAliasRegistration(ele);

  9. }

  10. // 如果元素節點<Bean>元素,則進行Bean解析注冊

  11. else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {

  12. processBeanDefinition(ele, delegate);

  13. }

  14. // // 如果元素節點<Beans>元素,則進行Beans解析

  15. else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {

  16. // recurse

  17. doRegisterBeanDefinitions(ele);

  18. }

  19. }

對四大标簽:import、alias、bean、beans 進行解析,其中 bean 标簽的解析為核心工作。關于各個标簽的解析過程見如下文章:

對于預設标簽則由

parseCustomElement()

負責解析。

  1. public BeanDefinition parseCustomElement(Element ele) {

  2. return parseCustomElement(ele, null);

  3. }

  4. public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {

  5. String namespaceUri = getNamespaceURI(ele);

  6. if (namespaceUri == null) {

  7. return null;

  8. }

  9. NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);

  10. if (handler == null) {

  11. error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);

  12. return null;

  13. }

  14. return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));

  15. }

擷取節點的 namespaceUri,然後根據該 namespaceuri 擷取相對應的 Handler,調用 Handler 的

parse()

方法即完成自定義标簽的解析和注入。想了解更多參考:

【死磕Spring】----- IOC 之解析自定義标簽 注冊 BeanDefinition

經過上面的解析,則将 Document 對象裡面的 Bean 标簽解析成了一個個的 BeanDefinition ,下一步則是将這些 BeanDefinition 注冊到 IOC 容器中。動作的觸發是在解析 Bean 标簽完成後,如下:

  1. protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {

  2. BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);

  3. if (bdHolder != null) {

  4. bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);

  5. try {

  6. // Register the final decorated instance.

  7. BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());

  8. }

  9. catch (BeanDefinitionStoreException ex) {

  10. getReaderContext().error("Failed to register bean definition with name '" +

  11. bdHolder.getBeanName() + "'", ele, ex);

  12. }

  13. // Send registration event.

  14. getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));

  15. }

  16. }

BeanDefinitionReaderUtils.registerBeanDefinition()

注冊,其實這裡面也是調用 BeanDefinitionRegistry 的

registerBeanDefinition()

來注冊 BeanDefinition ,不過最終的實作是在 DefaultListableBeanFactory 中實作,如下:

  1. @Override

  2. public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)

  3. throws BeanDefinitionStoreException {

  4. // 省略一堆校驗

  5. BeanDefinition oldBeanDefinition;

  6. oldBeanDefinition = this.beanDefinitionMap.get(beanName);

  7. // 省略一堆 if

  8. this.beanDefinitionMap.put(beanName, beanDefinition);

  9. }

  10. else {

  11. if (hasBeanCreationStarted()) {

  12. // Cannot modify startup-time collection elements anymore (for stable iteration)

  13. synchronized (this.beanDefinitionMap) {

  14. this.beanDefinitionMap.put(beanName, beanDefinition);

  15. List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);

  16. updatedDefinitions.addAll(this.beanDefinitionNames);

  17. updatedDefinitions.add(beanName);

  18. this.beanDefinitionNames = updatedDefinitions;

  19. if (this.manualSingletonNames.contains(beanName)) {

  20. Set<String> updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames);

  21. updatedSingletons.remove(beanName);

  22. this.manualSingletonNames = updatedSingletons;

  23. }

  24. }

  25. }

  26. else {

  27. // Still in startup registration phase

  28. this.beanDefinitionMap.put(beanName, beanDefinition);

  29. this.beanDefinitionNames.add(beanName);

  30. this.manualSingletonNames.remove(beanName);

  31. }

  32. this.frozenBeanDefinitionNames = null;

  33. }

  34. if (oldBeanDefinition != null || containsSingleton(beanName)) {

  35. resetBeanDefinition(beanName);

  36. }

  37. }

這段代碼最核心的部分是這句

this.beanDefinitionMap.put(beanName,beanDefinition)

,是以注冊過程也不是那麼的高大上,就是利用一個 Map 的集合對象來存放,key 是 beanName,value 是 BeanDefinition。

至此,整個 IOC 的初始化過程就已經完成了,從 Bean 資源的定位,轉換為 Document 對象,接着對其進行解析,最後注冊到 IOC 容器中,都已經完美地完成了。現在 IOC 容器中已經建立了整個 Bean 的配置資訊,這些 Bean 可以被檢索、使用、維護,他們是控制反轉的基礎,是後面注入 Bean 的依賴。最後用一張流程圖來結束這篇總結之文。

【死磕 Spring】----- IOC 之 IOC 初始化總結

更多閱讀:

原文釋出時間為:2018-09-9

本文作者:

Java技術驿站

本文來自雲栖社群合作夥伴“

”,了解相關資訊可以關注“

”。