1. 程式人生 > >【死磕 Spring】----- IOC 之 IOC 初始化總結

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

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

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

spring-201805281001

  • 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 初始化過程的開端,如下:

ClassPathResource resource = new ClassPathResource("bean.xml");
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(resource);

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

  • ClassPathResource resource = new ClassPathResource("bean.xml");: 根據 Xml 配置檔案建立 Resource 資源物件。ClassPathResource 是 Resource 介面的子類,bean.xml 檔案中的內容是我們定義的 Bean 資訊。
  • DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); 建立一個 BeanFactory。DefaultListableBeanFactory 是 BeanFactory 的一個子類,BeanFactory 作為一個介面,其實它本身是不具有獨立使用的功能的,而 DefaultListableBeanFactory 則是真正可以獨立使用的 IOC 容器,它是整個 Spring IOC 的始祖,在後續會有專門的文章來分析它。
  • XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(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 兩個介面互相配合,在上面那段程式碼中 new ClassPathResource("bean.xml") 為我們定義了資源,那麼 ResourceLoader 則是在什麼時候初始化的呢?看 XmlBeanDefinitionReader 構造方法:

	public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
		super(registry);
	}

直接呼叫父類 AbstractBeanDefinitionReader :

	protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {
		Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
		this.registry = registry;

		// Determine ResourceLoader to use.
		if (this.registry instanceof ResourceLoader) {
			this.resourceLoader = (ResourceLoader) this.registry;
		}
		else {
			this.resourceLoader = new PathMatchingResourcePatternResolver();
		}

		// Inherit Environment if possible
		if (this.registry instanceof EnvironmentCapable) {
			this.environment = ((EnvironmentCapable) this.registry).getEnvironment();
		}
		else {
			this.environment = new StandardEnvironment();
		}
	}

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

BeanDefinition 的載入和解析

reader.loadBeanDefinitions(resource); 開啟 BeanDefinition 的解析過程。如下:

	public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
		return loadBeanDefinitions(new EncodedResource(resource));
	}

在這個方法會將資源 resource 包裝成一個 EncodedResource 例項物件,然後呼叫 loadBeanDefinitions() 方法,而將 Resource 封裝成 EncodedResource 主要是為了對 Resource 進行編碼,保證內容讀取的正確性。

   public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
        // 省略一些程式碼
        try {
            // 將資原始檔轉為 InputStream 的 IO 流
            InputStream inputStream = encodedResource.getResource().getInputStream();
            try {
                // 從 InputStream 中得到 XML 的解析源
                InputSource inputSource = new InputSource(inputStream);
                if (encodedResource.getEncoding() != null) {
                    inputSource.setEncoding(encodedResource.getEncoding());
                }

                // 具體的讀取過程
                return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
            }
            finally {
                inputStream.close();
            }
        }
        // 省略一些程式碼
    }

從 encodedResource 源中獲取 xml 的解析源,呼叫 doLoadBeanDefinitions() 執行具體的解析過程。

	protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
			throws BeanDefinitionStoreException {
		try {
			Document doc = doLoadDocument(inputSource, resource);
			return registerBeanDefinitions(doc, resource);
		}
		// 省略很多catch程式碼

在該方法中主要做兩件事:1、根據 xml 解析源獲取相應的 Document 物件,2、呼叫 registerBeanDefinitions() 開啟 BeanDefinition 的解析註冊過程。

轉換為 Document 物件

呼叫 doLoadDocument() 會將 Bean 定義的資源轉換為 Document 物件。

	protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
		return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
				getValidationModeForResource(resource), isNamespaceAware());
	}

loadDocument() 方法接受五個引數:

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

loadDocument() 在類 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);
		// 解析 Spring 的 Bean 定義資源
		return builder.parse(inputSource);
	}

這到這裡,就已經將定義的 Bean 資原始檔,載入並轉換為 Document 物件了,那麼下一步就是如何將其解析為 Spring IOC 管理的 Bean 物件並將其註冊到容器中。這個過程有方法 registerBeanDefinitions() 實現。如下:

	public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
	   // 建立 BeanDefinitionDocumentReader 來對 xml 格式的BeanDefinition 解析
		BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
		// 獲得容器中註冊的Bean數量
		int countBefore = getRegistry().getBeanDefinitionCount();
		
		// 解析過程入口,這裡使用了委派模式,BeanDefinitionDocumentReader只是個介面,
		// 具體的解析實現過程有實現類DefaultBeanDefinitionDocumentReader完成
		documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
		return getRegistry().getBeanDefinitionCount() - countBefore;
	}

首先建立 BeanDefinition 的解析器 BeanDefinitionDocumentReader,然後呼叫 documentReader.registerBeanDefinitions() 開啟解析過程,這裡使用的是委派模式,具體的實現由子類 DefaultBeanDefinitionDocumentReader 完成。

	public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
	   // 獲得XML描述符
		this.readerContext = readerContext;
		logger.debug("Loading bean definitions");
		
		// 獲得Document的根元素
		Element root = doc.getDocumentElement();
		
		// 解析根元素
		doRegisterBeanDefinitions(root);
	}

對 Document 物件的解析

從 Document 物件中獲取根元素 root,然後呼叫 doRegisterBeanDefinitions() 開啟真正的解析過程。

	protected void doRegisterBeanDefinitions(Element root) {
		BeanDefinitionParserDelegate parent = this.delegate;
		this.delegate = createDelegate(getReaderContext(), root, parent);

	   // 省略部分程式碼

		preProcessXml(root);
		parseBeanDefinitions(root, this.delegate);
		postProcessXml(root);

		this.delegate = parent;
	}

preProcessXml()postProcessXml() 為前置、後置增強處理,目前 Spring 中都是空實現, parseBeanDefinitions() 是對根元素 root 的解析註冊過程。

	protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
	   // Bean定義的Document物件使用了Spring預設的XML名稱空間
		if (delegate.isDefaultNamespace(root)) {
		  // 獲取Bean定義的Document物件根元素的所有子節點
			NodeList nl = root.getChildNodes();
			for (int i = 0; i < nl.getLength(); i++) {
				Node node = nl.item(i);
				// 獲得Document節點是XML元素節點 
				if (node instanceof Element) {
					Element ele = (Element) node;
					// Bean定義的Document的元素節點使用的是Spring預設的XML名稱空間
					if (delegate.isDefaultNamespace(ele)) {
					   // 使用Spring的Bean規則解析元素節點(預設解析規則)
						parseDefaultElement(ele, delegate);
					}
					else {
					   // 沒有使用Spring預設的XML名稱空間,則使用使用者自定義的解析規則解析元素節點 
						delegate.parseCustomElement(ele);
					}
				}
			}
		}
		else {
		  // Document 的根節點沒有使用Spring預設的名稱空間,則使用使用者自定義的解析規則解析
			delegate.parseCustomElement(root);
		}
	}

迭代 root 元素的所有子節點,對其進行判斷,若節點為預設名稱空間,則ID呼叫 parseDefaultElement() 開啟預設標籤的解析註冊過程,否則呼叫 parseCustomElement() 開啟自定義標籤的解析註冊過程。

標籤解析

若定義的元素節點使用的是 Spring 預設名稱空間,則呼叫 parseDefaultElement() 進行預設標籤解析,如下:

	private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
	   // 如果元素節點是<Import>匯入元素,進行匯入解析
		if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
			importBeanDefinitionResource(ele);
		}
		// 如果元素節點是<Alias>別名元素,進行別名解析 
		else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
			processAliasRegistration(ele);
		}
		// 如果元素節點<Bean>元素,則進行Bean解析註冊
		else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
			processBeanDefinition(ele, delegate);
		}
		
		// // 如果元素節點<Beans>元素,則進行Beans解析
		else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
			// recurse
			doRegisterBeanDefinitions(ele);
		}
	}

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

對於預設標籤則由 parseCustomElement() 負責解析。

	public BeanDefinition parseCustomElement(Element ele) {
		return parseCustomElement(ele, null);
	}

	public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
		String namespaceUri = getNamespaceURI(ele);
		if (namespaceUri == null) {
			return null;
		}
		NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
		if (handler == null) {
			error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
			return null;
		}
		return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
	}

獲取節點的 namespaceUri,然後根據該 namespaceuri 獲取相對應的 Handler,呼叫 Handler 的 parse() 方法即完成自定義標籤的解析和注入。想了解更多參考:【死磕Spring】----- IOC 之解析自定義標籤

註冊 BeanDefinition

經過上面的解析,則將 Document 物件裡面的 Bean 標籤解析成了一個個的 BeanDefinition ,下一步則是將這些 BeanDefinition 註冊到 IOC 容器中。動作的觸發是在解析 Bean 標籤完成後,如下:

	protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
		BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
		if (bdHolder != null) {
			bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
			try {
				// Register the final decorated instance.
				BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
			}
			catch (BeanDefinitionStoreException ex) {
				getReaderContext().error("Failed to register bean definition with name '" +
						bdHolder.getBeanName() + "'", ele, ex);
			}
			// Send registration event.
			getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
		}
	}

呼叫 BeanDefinitionReaderUtils.registerBeanDefinition() 註冊,其實這裡面也是呼叫 BeanDefinitionRegistry 的 registerBeanDefinition()來註冊 BeanDefinition ,不過最終的實現是在 DefaultListableBeanFactory 中實現,如下:

	@Override
	public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
			throws BeanDefinitionStoreException {
        
      // 省略一堆校驗
      
		BeanDefinition oldBeanDefinition;

		oldBeanDefinition = this.beanDefinitionMap.get(beanName);
		  // 省略一堆 if
			this.beanDefinitionMap.put(beanName, beanDefinition);
		}
		else {
			if (hasBeanCreationStarted()) {
				// Cannot modify startup-time collection elements anymore (for stable iteration)
				synchronized (this.beanDefinitionMap) {
					this.beanDefinitionMap.put(beanName, beanDefinition);
					List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
					updatedDefinitions.addAll(this.beanDefinitionNames);
					updatedDefinitions.add(beanName);
					this.beanDefinitionNames = updatedDefinitions;
					if (this.manualSingletonNames.contains(beanName)) {
						Set<String> updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames);
						updatedSingletons.remove(beanName);
						this.manualSingletonNames = updatedSingletons;
					}
				}
			}
			else {
				// Still in startup registration phase
				this.beanDefinitionMap.put(beanName, beanDefinition);
				this.beanDefinitionNames.add(beanName);
				this.manualSingletonNames.remove(beanName);
			}
			this.frozenBeanDefinitionNames = null;
		}

		if (oldBeanDefinition != null || containsSingleton(beanName)) {
			resetBeanDefinition(beanName);
		}
	}

這段程式碼最核心的部分是這句 this.beanDefinitionMap.put(beanName, beanDefinition) ,所以註冊過程也不是那麼的高大上,就是利用一個 Map 的集合物件來存放,key 是 beanName,value 是 BeanDefinition。

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

spring-201807201001

更多閱讀: