Spring原始碼解讀(1)-IOC容器BeanDefinition的載入

ALivn發表於2019-05-06

1、概述

    spring的兩大核心:IOC(依賴注入)和AOP(面向切面),IOC本質上就是一個執行緒安全的hashMap,put和get方法就對應IOC容器的bean的註冊和獲取,spring通過讀取xml或者使用註解配置的類生成一個BeanDefinition放入到容器中,獲取的時候通過BeanDefinition的配置通過asm、反射等技術完成屬性的注入最終獲取一個bean,獲取bean的方法就getBean(),我們無需關心實現細節,直接按照spring提供的註解或者xml配置方式使用即可。

2、IOC容器

    雖然IOC本質上是一個執行緒安全的hashMap,使用時直接通過getBean()獲取(@Autowired本質也是通過getBean()獲取),這樣在使用bean例項的時候,就不用關心bean的建立,只管用就行了,IOC會在程式啟動時,自動將依賴的物件注入到目標物件中,非常簡單,省心。但是如果不瞭解IOC中bean的註冊和獲取原理,當使用Spring無法獲取一個bean的時候,針對丟擲的異常可能一頭霧水。

IOC容器的實現包含了兩個非常重要的過程:

  • xml的讀取生成BeanDefinition註冊到IOC中
  • 通過BeanDefinition例項化bean,並從IOC中通過getBean()獲取
//spring原始碼中將一個bean的BeanDefinition放入到IOC中
this.beanDefinitionMap.put(beanName, beanDefinition);
複製程式碼
//Spring原始碼中通過beanName獲取一個bean
public Object getBean(String name) throws BeansException {
		assertBeanFactoryActive();
		return getBeanFactory().getBean(name);
	}
複製程式碼

這兩個過程還對應了兩個非常核心的介面:

    BeanDefinitionRegistry和BeanFactory,一個向IOC中註冊BeanDefinition,一個從IOC獲取Bean例項物件。

    讀IOC原始碼必須從瞭解BeanDefinition開始,BeanDefinition是一個介面,無論是通過xml宣告還是通過註解定義一個bean例項,在IOC容器中第一步總是為其對應生成一個BeanDefinition,裡面包含了類的所有基本資訊。

    其中AbstractBeanDefinition實現BeanDefinition,這個介面有兩個子介面GenericBeanDefinition和RootBeanDefinition,再來看看BeanDefinition中的一些方法

  • getBeanClassName()獲取bean的全限定名,

  • getScope()獲取該類的作用域,

  • isLazyInit()該類是否為懶載入,

  • getPropertyValues()獲取配置的屬性值列表(用於setter注入),

  • getConstructorArgumentValues()獲取配置的建構函式值(用於構造器注入)等bean的重要資訊,如果通過註解的方式,還會包含一些註解的屬性資訊,

    總而言之 ,BeanDefinition包含了我們定義的一個類的所有資訊,然後通過 BeanDefinitionRegistry介面的registerBeanDefinition註冊到IOC容器中,最後通過BeanDefinition結合asm,反射等相關技術,通過BeanFactory介面的getBean()獲取一個例項物件好像也不是什麼困難的事情了。當然這只是表層的大致原理,實際上spring在實現IOC的時候,用了大量的設計模式,比如:單例模式、模板方法、工廠模式、代理模式(AOP基本上全是)等,此外物件導向的基本原則中的單一職責、開放封閉原則等隨處可見,具體的原始碼解讀還是在之後的筆記裡介紹。

3、讀取xml配置檔案

    讀取原始碼需要通過除錯去看,Spring啟動時首先會讀取xml配置檔案,xml檔案可以從當前類路徑下讀,也可以從檔案系統下讀取,以下是用於除錯的簡單案例:

    @Test
    public void testSpringLoad() {
        ClassPathXmlApplicationContext application = new ClassPathXmlApplicationContext("spring/spring-context.xml");
        BankPayService bankPayService = (BankPayService) application.getBean("bankPayService");
        Assert.assertNotNull(bankPayService);
    }
複製程式碼

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-lazy-init="true">
    <bean id="bankPayService" class="com.yms.manager.serviceImpl.BankPayServiceImpl"/>
    <context:property-placeholder location="classpath*:app-env.properties"/>
    <context:component-scan base-package="com.yms.market"/>
</beans>
複製程式碼

    執行測試案例,肯定是成功的,打斷點開始除錯,首先會進入ClassPathXmlApplicationContext的建構函式中:

public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent){

		super(parent);
                //獲取當前環境 裝飾傳入的路徑
		setConfigLocations(configLocations);
		if (refresh) {
                       //程式入口
			refresh();
		}
	}
複製程式碼

    建構函式中最關鍵的部分是refresh()方法,該方法用於重新整理IOC容器資料,該方法由AbstractApplicationContext實現。

@Override
public void refresh() {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// 讓子類去重新整理beanFactory 進入這裡檢視
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) {
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}}}
複製程式碼

    在refresh方法中主要完成了載入xml檔案的環境配置、xml檔案讀取,註冊BeanFactoryPostProcessor處理器、註冊監聽器等工作,其中比較核心的是第二行程式碼ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
	  //載入xml配置檔案,生成BeanDefinition並註冊到IOC容器中	
          refreshBeanFactory();
                //獲取載入完xml檔案之後的beanFactory物件
		ConfigurableListableBeanFactory beanFactory = getBeanFactory();
		if (logger.isDebugEnabled()) {
			logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
		}
		return beanFactory;
	}
複製程式碼

    refreshBeanFactory和getBeanFactory都是由AbstractApplicationContext的子類AbstractRefreshableApplicationContext實現的,

	@Override
	protected final void refreshBeanFactory() throws BeansException {
               //如果beanFactory不為空 ,清除老的beanFactory
		if (hasBeanFactory()) {
			destroyBeans();
			closeBeanFactory();
		}
		try {
                        //建立一個beanFactory
			DefaultListableBeanFactory beanFactory = createBeanFactory();
			beanFactory.setSerializationId(getId());
                        //設定bean是否允許覆蓋 是否允許迴圈依賴
			customizeBeanFactory(beanFactory);
                        //載入beans宣告,即讀取xml或者掃描包 生成BeanDefinition註冊到IOC
			loadBeanDefinitions(beanFactory);
			synchronized (this.beanFactoryMonitor) {
				this.beanFactory = beanFactory;
			}
		}
		catch (IOException ex) {
			throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
		}
	}
複製程式碼

    在Spring中DefaultListableBeanFactory是一個非常重要的類,它實現了BeanDefinitionRegistry和BeanFactory介面,並且完成了這兩個介面的具體實現,DefaultListableBeanFactory的類圖如下:

Spring原始碼解讀(1)-IOC容器BeanDefinition的載入

    我們已經知道BeanDefinitionRegistry完成了BeanDefinition的註冊,BeanFactory完成了getBean()中bean的建立,其中xml讀取和bean的 註冊的入口就是loadBeanDefinitions(beanFactory)這個方法,loadBeanDefinitions是一個抽象方法,由類AbstractXmlApplicationContext實現。loadBeanDefinitions在AbstractXmlApplicationContext有很多個過載方法,在不通階段方法使用的引數值不同,接下來看看各個loadBeanDefinitions的呼叫順序:

建立XmlBeanDefinitionReader物件

	@Override
	protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
		//Spring將讀取xml操作委託給了XmlBeanDefinitionReader物件
        //並且傳入DefaultListableBeanFactory將生成的beandefinition註冊到IOC中
		XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
		//設定Spring中bean的環境
		beanDefinitionReader.setEnvironment(this.getEnvironment());
		beanDefinitionReader.setResourceLoader(this);
		beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
		// 設定beanDefinitionReader驗證屬性,子類可以重寫該方法使用自定義reader物件
		initBeanDefinitionReader(beanDefinitionReader);
        //通過beanDefinitionReader讀取xml
		loadBeanDefinitions(beanDefinitionReader);
	}
複製程式碼

    Spring對於讀取xml檔案。並不是由DefaultListableBeanFactory親力親為,而是委託給了XmlBeanDefinitionReader,在該類內部會將xml配置檔案轉換成Resource,Spring封裝了xml檔案獲取方式,我們使用ClassPathXmlApplicationContext讀取xml,因此Spring會通過ClassLoader獲取當前專案工作目錄,並在該目錄下查詢spring-context.xml檔案,當然我們還可以使用FileSystemXmlApplicationContext從檔案系統上以絕對路徑的方式讀取檔案

繼續檢視第二個loadBeanDefinitions(beanDefinitionReader):

獲取配置檔案路徑集合

	protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
                //預設返回為空  子類可以實現該方法 讀取指定檔案
		Resource[] configResources = getConfigResources();
		if (configResources != null) {
			reader.loadBeanDefinitions(configResources);
		}
                //獲取我們配置的xml檔案路徑集合
		String[] configLocations = getConfigLocations();
		if (configLocations != null) 
        {
			reader.loadBeanDefinitions(configLocations);
		}
	}
複製程式碼

    在這個方法中,最終獲取到了我們通過ClassPathXmlApplicationContext物件傳進來的xml配置檔案路徑,然後由進入委託物件XmlBeanDefinitionReader的loadBeanDefinitions方法中:

	@Override
	public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
		Assert.notNull(locations, "Location array must not be null");
		int counter = 0;
		for (String location : locations) {
                        //迴圈讀取配置的所有配置檔案路徑
			counter += loadBeanDefinitions(location);
		}
                //返回此次載入的BeanDefinition個數
		return counter;
	}
複製程式碼

    在XmlBeanDefinitionReader中,會迴圈讀取配置的所有配置檔案路徑,並將讀取到的bean的宣告建立成BeanDefinition,並將此次生成的數量返回,繼續檢視loadBeanDefinitions:

	public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {
		ResourceLoader resourceLoader = getResourceLoader();
		if (resourceLoader == null) {
			throw new BeanDefinitionStoreException(
					"Cannot import bean definitions from location [" + location + "]: no ResourceLoader available");
		}

		if (resourceLoader instanceof ResourcePatternResolver) {
			// Resource pattern matching available.
			try {
				Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
                                //實際執行到這裡
				int loadCount = loadBeanDefinitions(resources);
				if (actualResources != null) {
					for (Resource resource : resources) {
						actualResources.add(resource);
					}
				}
				if (logger.isDebugEnabled()) {
					logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]");
				}
				return loadCount;
			}
			catch (IOException ex) {
				throw new BeanDefinitionStoreException(
						"Could not resolve bean definition resource pattern [" + location + "]", ex);
			}
		}
		else {
			// Can only load single resources by absolute URL.
			Resource resource = resourceLoader.getResource(location);
			int loadCount = loadBeanDefinitions(resource);
			if (actualResources != null) {
				actualResources.add(resource);
			}
			if (logger.isDebugEnabled()) {
				logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]");
			}
			return loadCount;
		}
	}
複製程式碼

    這個方法就是上面所說的Spring將xml配置檔案封裝成Resourse,最終獲取到Resourse的過程,這部分程式碼沒什麼好看的,就是找到ClassPathResource將xml路徑放進去,然後呼叫loadBeanDefinitions(resources),再來看這個方法:

	@Override
	public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
		return loadBeanDefinitions(new EncodedResource(resource));
	}
複製程式碼

    這個方法將Resource封裝城了EncodedResource物件,這個物件有一個屬性encoding,如果設定了xml檔案的編碼,在這裡讀取xml檔案的時候會根據該編碼進行讀取,繼續往下看:

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
		Assert.notNull(encodedResource, "EncodedResource must not be null");
		if (logger.isInfoEnabled()) {
			logger.info("Loading XML bean definitions from " + encodedResource.getResource());
		}
                 //獲取前面載入到的xml配置資原始檔
		Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
		if (currentResources == null) {
			currentResources = new HashSet<EncodedResource>(4);
			this.resourcesCurrentlyBeingLoaded.set(currentResources);
		}
		if (!currentResources.add(encodedResource)) {
			throw new BeanDefinitionStoreException(
					"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
		}
		try {
                         //之所以將路徑都封裝到Resource裡面,就是使其提供一個統一的getInputStream方法
                        //獲取檔案流物件,XmlBeanDefinitionReader無需關心xml檔案怎麼來的
			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();
			}
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException(
					"IOException parsing XML document from " + encodedResource.getResource(), ex);
		}
		finally {
			currentResources.remove(encodedResource);
			if (currentResources.isEmpty()) {
				this.resourcesCurrentlyBeingLoaded.remove();
			}
		}
	}
複製程式碼

    spring的程式碼風格好像有一個特點,凡是真正開始做事的方法入口都會以do為字首,經過前面一系列對xml配置檔案的設定,終於來到了doLoadBeanDefinitions(inputSource, encodedResource.getResource()),在這個方法裡Spring會讀取每一個Element標籤,並根據名稱空間找到對應的NameSpaceHandler去讀取解析Node生成BeanDefinition物件。經過一系列操作Resouse最終會被轉換成InputSource物件,這個類也沒什麼特別的,只是除了檔案流之外多了一些引數而已,比如XSD,DTD的publicId,systemId約束,檔案流的編碼等,最重要的還是InputStream,然後來看看這個方法:

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) {
                         //主要是校驗檔案 通過查詢DTD檔案約束 校驗檔案格式
                        //讀取xml檔案生成DOC
			Document doc = doLoadDocument(inputSource, resource);
			return registerBeanDefinitions(doc, resource);
	}
複製程式碼

這個方法完成兩件事情:

  • 通過inputSource生成Document物件

  • 解析Document並將生成BeanDefinition註冊到IOC中

4、解析DOC生成BeanDefinition

    檔案校驗和生成DOC文件都是一些校驗操作,如果想自定義DTD文件讓Spring載入,後面還會細說這部分內容,暫且放下,現在主要是看看IOC的BeanDefinition的生成過程,接下來進入registerBeanDefinitions:

	public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
		BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
		documentReader.setEnvironment(this.getEnvironment());
		//獲取載入之前IOC容器中的BeanDefinition數量
		int countBefore = getRegistry().getBeanDefinitionCount();
		//具體解析 註冊 
		documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
		//返回本次載入的BeanDefinition數量
		return getRegistry().getBeanDefinitionCount() - countBefore;
	}
複製程式碼

    在這個方法裡,首先建立BeanDefinitionDocumentReader,這是個介面用於完成BeanDefinition向IOC容器註冊的功能,Spring只提供了唯一的實現DefaultBeanDefinitionDocumentReader,檢視registerBeanDefinitions:

	@Override
	public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
		this.readerContext = readerContext;
		logger.debug("Loading bean definitions");
                 //root 在這個測試裡就是<beans></beans>
		Element root = doc.getDocumentElement();
		doRegisterBeanDefinitions(root);
	}
複製程式碼

    該方法第一步首先回去xml的根節點,在這個測試xml裡就是標籤了,然後將根節點作為引數傳入到下面的方法中解析doRegisterBeanDefinitions:

	protected void doRegisterBeanDefinitions(Element root) {
                //獲取profile環境變數
		String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
		if (StringUtils.hasText(profileSpec)) {
			Assert.state(this.environment != null, "Environment must be set for evaluating profiles");
			String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
					profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
	      //判斷該root下的bean是否是前面通過web.xml或者前面設定的bean的環境值
             //如果不是  不需要解析當前root標籤
            if (!this.environment.acceptsProfiles(specifiedProfiles)) {
				return;
			}
		}
		BeanDefinitionParserDelegate parent = this.delegate;
		this.delegate = createDelegate(this.readerContext, root, parent);
                //解析bean所要執行的方法,該方法實際是空的 允許子類擴充套件 去讀取自定義的node
                //屬於模板方法
		preProcessXml(root);
                //真正解析beans的方法
		parseBeanDefinitions(root, this.delegate);
                //beans解析完之後需要執行的方法,實際也是通過子類擴充套件 是模板方法
		postProcessXml(root);

		this.delegate = parent;
	}
複製程式碼

這個方法看起來很多,其實真正核心的只有兩部分:

  • 讀取beans的profile屬性,判斷是否屬於被啟用的組,如果不是則不解析

  • 建立BeanDefinitionParserDelegate,委託該類執行beans解析工作。

最後通過parseBeanDefinitions(root, this.delegate)方法將beans的解析交給BeanDefinitionParserDelegate

	protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
                 //判斷是否是預設的名稱空間 也就是beans
		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)) {
                                               //判斷是否是xml配置的bean,如果是則呼叫該方法解析
						parseDefaultElement(ele, delegate);
					}
					else {
                                                //否則按照自定義方式解析
						delegate.parseCustomElement(ele);
					}
				}
			}
		}
		else {
             //否則按照自定義方式解析
			delegate.parseCustomElement(root);
		}
	}
複製程式碼

    這個方法很重要,這裡已經開始解析了,首先會判斷,要解析的root是否是beans標籤,如果是再判斷子元素是否是元素,正常來講,我們使用spring的時候都會再標籤下配置,所以不出意外都會走到for迴圈裡,然後在for迴圈裡判斷是否是預設名稱空間的時候就會發生變化:

  • 如果是則走parseDefaultElement(ele, delegate);

  • 如果是<mvc:annotation-driven>、 <context:component-scan base-package="***"/>等則會走到自定義元素解析delegate.parseCustomElement(ele)裡

    自定義解析載入到最後還是會跟載入預設名稱空間的bean一樣,所以在這裡只分析自定義名稱空間的解析,不過值得提一下的是自定義解析方法裡會首先根據Element的名稱空間找到NamespaceHandler,然後由該NamespaceHanler去解析

	public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
		 //獲取元素的名稱空間
                 String namespaceUri = getNamespaceURI(ele);
                //獲取名稱空間解析器
		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));
	}
複製程式碼

    由於後面會自己實現一個NamespaceHandler解析自定義的標籤,會專門說明Spring如何查詢NamespaceHandler以及如何解析自定義元素,這裡只是瞭解下NamespaceHandler的概念即可,接著看Spring解析

檢視parseDefaultElement:

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
		if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
                        //解析 import
			importBeanDefinitionResource(ele);
		}
		else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
                        //解析alias
			processAliasRegistration(ele);
		}
		else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
                        //解析bean
			processBeanDefinition(ele, delegate);
		}
		else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
			// 如果還是beans  遞迴呼叫
			doRegisterBeanDefinitions(ele);
		}
	}
複製程式碼

    這個方法裡面就是幾個if判斷,用於解析對應的標籤,其中import alias相當於是去讀取另一個xml檔案,最後還是會呼叫解析bean,所以在這裡只看解析bean的方法processBeanDefinition(ele, delegate):

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
                //將bean的屬性都讀取到到BeanDefinitionHolder上
		BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
		if (bdHolder != null) {
                        //如果bean裡面有自定義標籤 來決定是否再次解析
			bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
			try {
		         	// 將生成的BeanDefinitionHolder註冊到IOC中
				BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
			}
			catch (BeanDefinitionStoreException ex) {
				getReaderContext().error("Failed to register bean definition with name '" +
						bdHolder.getBeanName() + "'", ele, ex);
			}
			// 傳送註冊事件
			getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
		}
	}
複製程式碼

在這個方法裡 ,之前分析的邏輯才逐漸清晰起來,程式碼的條例也很清晰

  • BeanDefinitionParserDelegate將bean標籤的屬性讀取到BeanDefinitionHolder物件中

  • 如果beans下還有其他自定義標籤決定是否有必要再次解析

  • 將BeanDefinition註冊到IOC中

  • 傳送註冊事件

首先來看第一步,讀取node屬性到BeanDefinitionParserDelegate中

	public AbstractBeanDefinition parseBeanDefinitionElement(
			Element ele, String beanName, BeanDefinition containingBean) {
		this.parseState.push(new BeanEntry(beanName));
		String className = null;
		if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
                        //獲取class全限定名
			className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
		}
		try {
			String parent = null;
			if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
				parent = ele.getAttribute(PARENT_ATTRIBUTE);
			}
                        //設定beanClass或者beanClassName
			AbstractBeanDefinition bd = createBeanDefinition(className, parent);
                        //讀取node屬性 將配置的屬性 塞入合適的欄位中
			parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
			bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
			parseMetaElements(ele, bd);
                        //記錄lookup-method配置
			parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
                       //記錄replaced-method配置
			parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
                       //解析建構函式(構造器注入)
			parseConstructorArgElements(ele, bd);
                        //解析屬性(setter注入)
			parsePropertyElements(ele, bd);
			parseQualifierElements(ele, bd);
			bd.setResource(this.readerContext.getResource());
			bd.setSource(extractSource(ele));
			return bd;
		}
		finally {
			this.parseState.pop();
		}
		return null;
	}

複製程式碼

    這個方法完成了讀取一個bean包括將node屬性讀入到BeanDefinition,讀取bean的建構函式配置(是構造器注入的前提),讀取bean的屬性配置(是setter注入的前提),其實將node屬性讀取到BeanDefinition很簡單,僅僅是一一對應而已,真正的複雜點在於讀取建構函式引數、讀取屬性值引數。

5、構造器和屬性引數解析

來看下面一段配置:

    <bean id="userDao" class="spring.road.beans.models.UserDao"/>
     <!--setter注入-->
    <bean id="beanService" class="spring.road.beans.models.BeanService">
        <property name="mapper" ref="userDao"/>
        <property name="name" value="lijinpeng"/>
        <property name="sex" value="false"/>
    </bean>
    <!--構造器注入-->
    <bean id="person" class="spring.road.beans.models.Person">
        <constructor-arg name="age" value="26"/>
        <constructor-arg name="name" value="dangwendi"/>
        <constructor-arg name="userDao" ref="userDao"/>
        <constructor-arg name="sex" value="true"/>
    </bean>
複製程式碼

這段配置使用了兩種注入方式:

property屬性解析

    setter注入就是我們通過為屬性賦值,如果屬性值都是string型別的還很好解決,如果pojo類的屬性值不是String,而是比如像Boolean、int、Date等這些資料的時候,必須要進行資料轉換操作才可以在getBean()的時候將property配置的屬性通過反射注入到對應的欄位裡,這好像也不是什麼困難的事情,但是如果是ref引用型別呢,這個問題該如何解決呢?Spring很巧妙的解決了這個問題,用RuntimeBeanReference來表示ref引用的資料,用TypedStringValue表示普通String字串。既然一個pojo類的所有配置都會讀取到BeanDefinition,所以在xml中配置的屬性必然也會儲存到BeanDefinition中,繼續看原始碼會發現BeanDefinition中用MutablePropertyValues類表示屬性集合,該類中propertyValueList就是property集合資料,Spring用PropertyValue儲存了property的name value資訊。

//在BeanDefinition類中
MutablePropertyValues getPropertyValues();
//在MutablePropertyValues類中的屬性
private final List<PropertyValue> propertyValueList;
//在PropertyValue中的屬性
private final String name;
private final Object value;
複製程式碼

    根據上面xml配置可以得知value可能需要型別轉換,也可能是引用ref,鑑於getBean階段無法直接賦值,所以需要一箇中間類儲存資料,在getBean()反射階段根據型別去轉換成物件,再次檢視parsePropertyElements方法:

	public void parsePropertyElements(Element beanEle, BeanDefinition bd) {
		NodeList nl = beanEle.getChildNodes();
		for (int i = 0; i < nl.getLength(); i++) {
			Node node = nl.item(i);
                         //解析bean下的property屬性節點
			if (isCandidateElement(node) && nodeNameEquals(node, PROPERTY_ELEMENT)) {
				parsePropertyElement((Element) node, bd);
			}
		}
	}
複製程式碼

parsePropertyElement解析bean下的property屬性節點

public void parsePropertyElement(Element ele, BeanDefinition bd) {
                //獲取property的name  這個很簡單
		String propertyName = ele.getAttribute(NAME_ATTRIBUTE);
		if (!StringUtils.hasLength(propertyName)) {
			error("Tag 'property' must have a 'name' attribute", ele);
			return;
		}
		this.parseState.push(new PropertyEntry(propertyName));
		try {
			if (bd.getPropertyValues().contains(propertyName)) {
				error("Multiple 'property' definitions for property '" + propertyName + "'", ele);
				return;
			}
                       //獲取獲取property的value 這個需要用中間類表示
			Object val = parsePropertyValue(ele, bd, propertyName);
			PropertyValue pv = new PropertyValue(propertyName, val);
			parseMetaElements(ele, pv);
			pv.setSource(extractSource(ele));
			bd.getPropertyValues().addPropertyValue(pv);
		}
		finally {
			this.parseState.pop();
		}
	}
複製程式碼

山重水複疑無路,柳暗花明又一村,下面方法即是實現過程:

	public Object parsePropertyValue(Element ele, BeanDefinition bd, String propertyName) {
		String elementName = (propertyName != null) ?
						"<property> element for property '" + propertyName + "'" :
						"<constructor-arg> element";

		// Should only have one child element: ref, value, list, etc.
		NodeList nl = ele.getChildNodes();
		Element subElement = null;
		for (int i = 0; i < nl.getLength(); i++) {
			Node node = nl.item(i);
			if (node instanceof Element && !nodeNameEquals(node, DESCRIPTION_ELEMENT) &&
					!nodeNameEquals(node, META_ELEMENT)) {
				// Child element is what we're looking for.
				if (subElement != null) {
					error(elementName + " must not contain more than one sub-element", ele);
				}
				else {
					subElement = (Element) node;
				}
			}
		}
                 //是否有ref屬性
		boolean hasRefAttribute = ele.hasAttribute(REF_ATTRIBUTE);
                //是否有value屬性
		boolean hasValueAttribute = ele.hasAttribute(VALUE_ATTRIBUTE);
                //ref和value只能存在一個
		if ((hasRefAttribute && hasValueAttribute) ||
				((hasRefAttribute || hasValueAttribute) && subElement != null)) {
			error(elementName +
					" is only allowed to contain either 'ref' attribute OR 'value' attribute OR sub-element", ele);
		}

		if (hasRefAttribute) {
			String refName = ele.getAttribute(REF_ATTRIBUTE);
			if (!StringUtils.hasText(refName)) {
				error(elementName + " contains empty 'ref' attribute", ele);
			}
                       //如果是ref 則轉換成RuntimeBeanReference
			RuntimeBeanReference ref = new RuntimeBeanReference(refName);
			ref.setSource(extractSource(ele));
			return ref;
		}
		else if (hasValueAttribute) {
                        //如果是String則轉換成TypedStringValue
			TypedStringValue valueHolder = new TypedStringValue(ele.getAttribute(VALUE_ATTRIBUTE));
			valueHolder.setSource(extractSource(ele));
			return valueHolder;
		}
		else if (subElement != null) {
			return parsePropertySubElement(subElement, bd);
		}
		else {
			// Neither child element nor "ref" or "value" attribute found.
			error(elementName + " must specify a ref or value", ele);
			return null;
		}
	}
複製程式碼

    看有註釋行的程式碼,結合上面的分析,大概能瞭解setter注入的第一階段BeanDefinition儲存屬性資料的方式了,在呼叫BeanFactory的getBean()方法時,在反射階段獲取到值物件時可以根據型別去獲取值,如果是TypedStringValue則只需校驗值是否應該轉換,如果需要轉換即可,至於如何轉換,如果是RuntimeBeanReference更簡單了,直接通過getBean()獲取就好了,請記住這兩個型別,在分析getBean()階段屬性值解析的時候就會用到他們。

建構函式constructor-arg解析

     構造器注入其實比setter注入要稍微麻煩一點,之所以說麻煩其實就是要藉助相關技術去實現,因為構造器可能會有很多過載,在xml配置中如果引數順序不同可能會呼叫不同的建構函式,導致注入失敗,所以如果要儲存構造引數值,必須匹配到唯一合適的建構函式,並且在xml配置的constructor-arg必須按照一定規則與匹配的建構函式一一對應,才可以在getBean()階段注入成功。

    當我自己嘗試去寫的時候,以為只需要通過反射獲取建構函式的引數名即可,但是很不幸,通過反射拿到的引數名是諸如arg1 arg2 這樣的name,所以只能通過讀取類的位元組碼檔案了, 以前看過《深入瞭解java虛擬機器》這本書,知道可以通過讀取位元組碼檔案的方式獲取引數名,但是裡面的各種索引,欄位表集合啊什麼的想記住真的好難,而且我的水平還遠遠達不到那個高度,所以就用現成的吧, 我當時是用javassite實現的,看了spring的原始碼,發現spring是用asm實現的,當然這個階段是在getBean()階段實現的,之所以介紹是因為必須要先了解為什麼Spring要這麼儲存構造引數,後面的getBean在分析這塊原始碼,還是先來看看建構函式引數在BeanDefinition的儲存吧。

    spring允許在xml中通過index、name、type來指定一個引數,在BeanDefinition中使用ConstructorArgumentValues儲存建構函式引數集合,在ConstructorArgumentValues包含了兩個集合一個配置了索引的indexedArgumentValues引數集合,另一個沒有配置索引的genericArgumentValues建構函式引數集合,然後建構函式引數值用內部類ValueHolder表示,這個類裡包含了引數的value,型別,引數名等。

//在BeanDefinition類中    
ConstructorArgumentValues getConstructorArgumentValues();
//在ConstructorArgumentValues類中
//使用了索引的建構函式引數值集合
private final Map<Integer, ValueHolder> indexedArgumentValues = new LinkedHashMap<Integer,          ValueHolder>(0);  
//未使用索引的建構函式引數值集合
private final List<ValueHolder> genericArgumentValues = new LinkedList<ValueHolder>();
//在ValueHolder中的屬性
private Object value;
private String type;
private String name;
private Object source;
複製程式碼

建構函式引數的儲存結構分析完了,接下來看看程式碼吧,其實儲存和屬性值的儲存是一樣的 ,這裡只看關鍵的程式碼:

	public void parseConstructorArgElements(Element beanEle, BeanDefinition bd) {
		NodeList nl = beanEle.getChildNodes();
		for (int i = 0; i < nl.getLength(); i++) {
			Node node = nl.item(i);
			if (isCandidateElement(node) && nodeNameEquals(node, CONSTRUCTOR_ARG_ELEMENT)) {
				parseConstructorArgElement((Element) node, bd);
			}
		}
	}
複製程式碼

解析constructor-arg標籤parseConstructorArgElement

public void parseConstructorArgElement(Element ele, BeanDefinition bd) {
                //獲取index
		String indexAttr = ele.getAttribute(INDEX_ATTRIBUTE);
                //獲取type
		String typeAttr = ele.getAttribute(TYPE_ATTRIBUTE);
                 //獲取name
		String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
                //如果index不為空 儲存到indexedArgumentValues集合中
		if (StringUtils.hasLength(indexAttr)) {
			try {
				int index = Integer.parseInt(indexAttr);
				if (index < 0) {
					error("'index' cannot be lower than 0", ele);
				}
				else {
					try {
						this.parseState.push(new ConstructorArgumentEntry(index));
                                               //將value轉換成RuntimeBeanReference或者TypedStringValue
						Object value = parsePropertyValue(ele, bd, null);
						ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value);
                                               //儲存type
						if (StringUtils.hasLength(typeAttr)) {
							valueHolder.setType(typeAttr);
						}
                                               //儲存name
						if (StringUtils.hasLength(nameAttr)) {
							valueHolder.setName(nameAttr);
						}
						valueHolder.setSource(extractSource(ele));
						if (bd.getConstructorArgumentValues().hasIndexedArgumentValue(index)) {
							error("Ambiguous constructor-arg entries for index " + index, ele);
						}
						else {
                                                      //儲存建構函式引數值
							bd.getConstructorArgumentValues().addIndexedArgumentValue(index, valueHolder);
						}
					}
					finally {
						this.parseState.pop();
					}
				}
			}
			catch (NumberFormatException ex) {
				error("Attribute 'index' of tag 'constructor-arg' must be an integer", ele);
			}
		}
                //index未配置 儲存到普通集合中genericArgumentValues
		else {
			try {
				this.parseState.push(new ConstructorArgumentEntry());
				Object value = parsePropertyValue(ele, bd, null);
				ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value);
				if (StringUtils.hasLength(typeAttr)) {
					valueHolder.setType(typeAttr);
				}
				if (StringUtils.hasLength(nameAttr)) {
					valueHolder.setName(nameAttr);
				}
				valueHolder.setSource(extractSource(ele));
                                //儲存建構函式引數值
				bd.getConstructorArgumentValues().addGenericArgumentValue(valueHolder);
			}
			finally {
				this.parseState.pop();
			}
		}
	}

複製程式碼

    至此,一個xml配置的bean被完全存放到了BeanDefinition中,其實基於掃描註解配置也是一樣的,只不過在做很多清理工作,針對下面配置簡要說明下基於註解的處理:

 <context:component-scan base-package="com.yms.market"/>
複製程式碼
  • 首先spring讀取到這個node,會查詢該node的NameSpaceHandler,然後呼叫parse方法解析

  • 然後讀取到屬性base-package,轉換成對應路徑後查詢該路徑下所有的class檔案

  • 讀取class檔案的註解,檢視是否實現了特定註解,如果實現了註解則處裡方式與xml配置的處理相同,否則不處理。

    真實的處理過程比較複雜,也是用了很多設計模式,用了很多類來處理,但是我想說的是,無論是用過註解還是通過xml,最終的處理方式都是一樣的,都是先生成一個BeanDefinition註冊到IOC中,然後通過getBean()獲取。

6、BeanDefinitionRegistry註冊

BeanDefinition建立完成了,還差最後一步,將生成的BeanDefinition註冊到IOC中,這就必須往回看了。

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
               //將bean的屬性都讀取到到BeanDefinitionHolder上
		BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
		if (bdHolder != null) {
                       //如果bean裡面有自定義標籤 來決定是否再次解析
			bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
			try {
				// 將生成的BeanDefinitionHolder註冊到IOC中
				BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
			}
			catch (BeanDefinitionStoreException ex) {
				getReaderContext().error("Failed to register bean definition with name '" +
						bdHolder.getBeanName() + "'", ele, ex);
			}
			// 傳送註冊事件
			getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
		}
	}
複製程式碼

這段程式碼應該還很熟悉,這次看最後一步registerBeanDefinition

public static void registerBeanDefinition(
			BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
			throws BeanDefinitionStoreException {

	      	// 獲取beanName
		String beanName = definitionHolder.getBeanName();
               //註冊BeanDefinition,key為beanName,value是BeanDefinition
		registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
		// 如果配置別名的話獲取別名
		String[] aliases = definitionHolder.getAliases();
		if (aliases != null) {
			for (String aliase : aliases) {
                                //註冊別名
				registry.registerAlias(beanName, aliase);
			}
		}
	}
複製程式碼

該方法完成了以下事情

  • 將BeanDefinition用beanName作為key註冊到IOC容器中
  • 如果配置了別名,將beanName與別名對映起來。

     再來看具體的註冊過程registry.registerBeanDefinition,註冊是呼叫BeanDefinitionRegistry的registerBeanDefinition方法,在剛開始的分析說過DefaultListableBeanFactory實現了BeanDefinitionRegistry和BeanFactory,而且實現了具體邏輯,下面的內容就是Spring註冊的過程,為了看的清晰我省去了很多異常和無用的程式碼:

@Override
	public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
{
		if (beanDefinition instanceof AbstractBeanDefinition) {
			try {
                                //驗證
				((AbstractBeanDefinition) beanDefinition).validate();
			}
			catch (BeanDefinitionValidationException ex) {
			}
		}
               //這裡保證了執行緒安全
		synchronized (this.beanDefinitionMap) {
			BeanDefinition oldBeanDefinition = this.beanDefinitionMap.get(beanName);
			if (oldBeanDefinition != null) {
				if (!this.allowBeanDefinitionOverriding) {
			       //不允許覆蓋丟擲異常
				}
				else if (oldBeanDefinition.getRole() < beanDefinition.getRole()) {
					if (this.logger.isWarnEnabled()) {
	
				}
				else {
					if (this.logger.isInfoEnabled()) {
					
					}
				}
			}
			else {
				this.beanDefinitionNames.add(beanName);
				this.frozenBeanDefinitionNames = null;
			}
                         //註冊進去嘍
			this.beanDefinitionMap.put(beanName, beanDefinition);
		}
		resetBeanDefinition(beanName);
	}
複製程式碼

    到此為止,IOC容器的第一步為bean生成BeanDefinition並註冊到IOC容器中完成,接下來就是第二步,通過BeanFactory實現依賴注入了。

相關文章