天天看點

Spring源碼解析二:IOC容器初始化過程詳解

IOC容器初始化分為三個步驟,分别是:

1、Resource定位,即BeanDefinition的資源定位。

2、BeanDefinition的載入

3、向IOC容器注冊BeanDefinition

下面我們來詳細展開這三部分的内容

一、Resource定位

 以ApplicationContext的具體實作FileSystemXmlApplicationContext來介紹Resource定位的過程:

Spring源碼解析二:IOC容器初始化過程詳解

 IOC容器初始化類比為用木桶來裝水,Resource的定位過程就是尋找水的過程。

它是由ResourceLoader通過統一的Resource接口來完成的,Resource對各種形式的BeanDefinition的使用都提供了統一的接口。

Spring源碼解析二:IOC容器初始化過程詳解

Resource接口有許多實作類,針對不同的BeanDefinition,如:

在檔案系統中的Bean定義資訊可以使用FileSystemResource來進行抽象。

在類路徑中的Bean定義資訊可以使用ClassPathResource來進行抽象。

我們來看下FileSystemXmlApplicationContext其中的一個構造函數:

public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
            throws BeansException {

        super(parent);
        setConfigLocations(configLocations);
        if (refresh) {
            refresh();
        }
    }      

這裡有個refresh()方法,這個方法非常重要,可以說是IOC容器啟動的入口,該方法的實作在AbstractApplicationContext中。

在AbstractApplicationContext的refresh方法中,最終對調用到AbstractRefreshableApplicationContext的refreshBeanFactory,在這裡會建立一個基礎的IOC容器供ApplicationContext使用,這個基礎的BeanFactory就是DefaultListableBeanFactory。

protected final void refreshBeanFactory() throws BeansException {
        if (hasBeanFactory()) {
            destroyBeans();
            closeBeanFactory();
        }
        try {
            DefaultListableBeanFactory beanFactory = createBeanFactory();
            beanFactory.setSerializationId(getId());
            customizeBeanFactory(beanFactory);
            loadBeanDefinitions(beanFactory);
            synchronized (this.beanFactoryMonitor) {
                this.beanFactory = beanFactory;
            }
        }
        catch (IOException ex) {
            throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
        }
    }      

再根據loadBeanDefinitions方法一直往下查找,最終會找到DefaultResourceLoader類的getResource方法:

public Resource getResource(String location) {
        Assert.notNull(location, "Location must not be null");
        if (location.startsWith(CLASSPATH_URL_PREFIX)) {
            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
        }
        else {
            try {
                // Try to parse the location as a URL...
                URL url = new URL(location);
                return new UrlResource(url);
            }
            catch (MalformedURLException ex) {
                // No URL -> resolve as resource path.
                return getResourceByPath(location);
            }
        }
    }      

這裡的getResourceByPath方法最終被子類重寫,該場景下重寫該方法的類就是FileSystemXmlApplicationContext

protected Resource getResourceByPath(String path) {
        if (path != null && path.startsWith("/")) {
            path = path.substring(1);
        }
        return new FileSystemResource(path);
    }      

這樣我們從FileSystemXmlApplicationContext的構造函數中的refresh方法開始,最終到getResourceByPath結束,完成了資源定位的解析。但是這個隻是容器啟動中的一部分,還有載入和解析也是糅合在一起的,隻是為了思路清晰,故意對這些地方視而不見。

二、BeanDefinition的載入和解析

在第一部分資源的定位中,我們是以getResourceByPath()結束的,接下來我們要通過該方法傳回的Resource對象來進行BeanDefinition的載入了。現在我們已經知道水在哪裡了,就可以現在打水和裝水之旅了。

這個過程就是将定義的BeanDefinition在IOC容器中轉化成一個Spring内部表示的資料結構的過程。我們緊接着前面的源碼往下梳理:

AbstractRefreshableApplicationContext的方法refreshBeanFactory中調用loadBeanDefinitions(),往下梳理找到類AbstractXmlApplicationContext,源碼片段如下;

1 protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
 2         // Create a new XmlBeanDefinitionReader for the given BeanFactory.
 3         XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
 4 
 5         // Configure the bean definition reader with this context's
 6         // resource loading environment.
 7         beanDefinitionReader.setResourceLoader(this);
 8         beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
 9 
10         // Allow a subclass to provide custom initialization of the reader,
11         // then proceed with actually loading the bean definitions.
12         initBeanDefinitionReader(beanDefinitionReader);
13         loadBeanDefinitions(beanDefinitionReader);
14     }      

其中第7行是為讀取器設定ResourceLoader,為後面的資源讀取做準備;第13行就是啟動Bean定義資訊載入的開始。

1 protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
 2         Resource[] configResources = getConfigResources();
 3         if (configResources != null) {
 4             reader.loadBeanDefinitions(configResources);
 5         }
 6         String[] configLocations = getConfigLocations();
 7         if (configLocations != null) {
 8             reader.loadBeanDefinitions(configLocations);
 9         }
10     }      

第2行是以Resource的方式擷取配置檔案的資源位置,而第6行是以String的形式擷取配置檔案的位置。

reader.loadBeanDefinitions()的實作見XmlBeanDefinitionReader的父類AbstractBeanDefinitionReader

1 public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
2         Assert.notNull(locations, "Location array must not be null");
3         int counter = 0;
4         for (String location : locations) {
5             counter += loadBeanDefinitions(location);
6         }
7         return counter;
8     }      

這裡如果Resource為空,則停止BeanDefinition的載入。在啟動BeanDefinition的過程中,會周遊整個Resource集合所包含的BeanDefinition的資訊。

這裡面調用的loadBeanDefinitions()方法具體實作并不在AbstractBeanDefinitionReader,而是在子類裡:

1 public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
 2         Assert.notNull(encodedResource, "EncodedResource must not be null");
 3         if (logger.isInfoEnabled()) {
 4             logger.info("Loading XML bean definitions from " + encodedResource.getResource());
 5         }
 6 
 7         Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
 8         if (currentResources == null) {
 9             currentResources = new HashSet<EncodedResource>(4);
10             this.resourcesCurrentlyBeingLoaded.set(currentResources);
11         }
12         if (!currentResources.add(encodedResource)) {
13             throw new BeanDefinitionStoreException(
14                     "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
15         }
16         try {
17             InputStream inputStream = encodedResource.getResource().getInputStream();
18             try {
19                 InputSource inputSource = new InputSource(inputStream);
20                 if (encodedResource.getEncoding() != null) {
21                     inputSource.setEncoding(encodedResource.getEncoding());
22                 }
23                 return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
24             }
25             finally {
26                 inputStream.close();
27             }
28         }      

在這裡可以拿到代表XML檔案的Resource,讀取器在打開I/O流後就可以得到XML的檔案對象,接下來的事情就是按照Spring的Bean定義規則對這個XML檔案做解析并封裝了。而這個解析的過程是在類BeanDefinitionParserDelegate

在調用doLoadBeanDefinitions過程中,代碼會走到類XmlBeanDefinitionReader中的doLoadBeanDefinitions的方法

1 protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
2             throws BeanDefinitionStoreException {
3         try {
4             int validationMode = getValidationModeForResource(resource);
5             Document doc = this.documentLoader.loadDocument(
6                     inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware());
7             return registerBeanDefinitions(doc, resource);
8         }      

這裡第5行就是Document對象生成的入口,具體實作在DefaultDocumentLoader中。

第7行就是BeanDefinition解析的入口,我們接着往下走,最終的解析過程在類BeanDefinitionParserDelegate中,源碼如下:

1 public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) {
 2         String id = ele.getAttribute(ID_ATTRIBUTE);
 3         String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
 4 
 5         List<String> aliases = new ArrayList<String>();
 6         if (StringUtils.hasLength(nameAttr)) {
 7             String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, BEAN_NAME_DELIMITERS);
 8             aliases.addAll(Arrays.asList(nameArr));
 9         }
10 
11         String beanName = id;
12         if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
13             beanName = aliases.remove(0);
14             if (logger.isDebugEnabled()) {
15                 logger.debug("No XML 'id' specified - using '" + beanName +
16                         "' as bean name and " + aliases + " as aliases");
17             }
18         }
19 
20         if (containingBean == null) {
21             checkNameUniqueness(beanName, aliases, ele);
22         }
23 
24         AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
25         if (beanDefinition != null) {
26             if (!StringUtils.hasText(beanName)) {
27                 try {
28                     if (containingBean != null) {
29                         beanName = BeanDefinitionReaderUtils.generateBeanName(
30                                 beanDefinition, this.readerContext.getRegistry(), true);
31                     }
32                     else {
33                         beanName = this.readerContext.generateBeanName(beanDefinition);
34                         // Register an alias for the plain bean class name, if still possible,
35                         // if the generator returned the class name plus a suffix.
36                         // This is expected for Spring 1.2/2.0 backwards compatibility.
37                         String beanClassName = beanDefinition.getBeanClassName();
38                         if (beanClassName != null &&
39                                 beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
40                                 !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
41                             aliases.add(beanClassName);
42                         }
43                     }
44                     if (logger.isDebugEnabled()) {
45                         logger.debug("Neither XML 'id' nor 'name' specified - " +
46                                 "using generated bean name [" + beanName + "]");
47                     }
48                 }
49                 catch (Exception ex) {
50                     error(ex.getMessage(), ele);
51                     return null;
52                 }
53             }
54             String[] aliasesArray = StringUtils.toStringArray(aliases);
55             return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
56         }
57 
58         return null;
59     }      

其中 第24行會觸發對Bean元素的詳細解析,解析完成後會傳回一個AbstractBeanDefinition對象,可以看下這個對象的屬性,基本上都是Spring配置檔案中的常用屬性。

Spring源碼解析二:IOC容器初始化過程詳解

解析方法的具體内容如下:

1 public AbstractBeanDefinition parseBeanDefinitionElement(
 2             Element ele, String beanName, BeanDefinition containingBean) {
 3 
 4         this.parseState.push(new BeanEntry(beanName));
 5 
 6         String className = null;
 7         if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
 8             className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
 9         }
10 
11         try {
12             String parent = null;
13             if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
14                 parent = ele.getAttribute(PARENT_ATTRIBUTE);
15             }
16             AbstractBeanDefinition bd = createBeanDefinition(className, parent);
17 
18             parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
19             bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
20 
21             parseMetaElements(ele, bd);
22             parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
23             parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
24 
25             parseConstructorArgElements(ele, bd);
26             parsePropertyElements(ele, bd);
27             parseQualifierElements(ele, bd);
28 
29             bd.setResource(this.readerContext.getResource());
30             bd.setSource(extractSource(ele));
31 
32             return bd;
33         }      

其中第18行-27行,都是對Spring配置檔案中不同<Bean>節點的解析,可以每個都深入去研究下。最終傳回一個AbstractBeanDefinition對象。

到這裡,XML檔案中定義的BeanDefinition就被整個載入到IOC容器中,建立了對應的資料結構,可以看成是POJO在容器中的抽象。但是這個時候容器中還隻是有一些靜态的配置資訊,完成了管理Bean對象的資料準備工作,但是容器還沒有完全發揮作用。

三、BeanDefinition注冊過程

Spring源碼解析二:IOC容器初始化過程詳解

這個源碼中方法調用過程比較簡單,我們直接來看注冊的核心代碼:

1 public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
 2             throws BeanDefinitionStoreException {
 3 
 4         Assert.hasText(beanName, "Bean name must not be empty");
 5         Assert.notNull(beanDefinition, "BeanDefinition must not be null");
 6 
 7         if (beanDefinition instanceof AbstractBeanDefinition) {
 8             try {
 9                 ((AbstractBeanDefinition) beanDefinition).validate();
10             }
11             catch (BeanDefinitionValidationException ex) {
12                 throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
13                         "Validation of bean definition failed", ex);
14             }
15         }
16 
17         synchronized (this.beanDefinitionMap) {
18             Object oldBeanDefinition = this.beanDefinitionMap.get(beanName);
19             if (oldBeanDefinition != null) {
20                 if (!this.allowBeanDefinitionOverriding) {
21                     throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
22                             "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
23                             "': There is already [" + oldBeanDefinition + "] bound.");
24                 }
25                 else {
26                     if (this.logger.isInfoEnabled()) {
27                         this.logger.info("Overriding bean definition for bean '" + beanName +
28                                 "': replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]");
29                     }
30                 }
31             }
32             else {
33                 this.beanDefinitionNames.add(beanName);
34                 this.frozenBeanDefinitionNames = null;
35             }
36             this.beanDefinitionMap.put(beanName, beanDefinition);
37 
38             resetBeanDefinition(beanName);
39         }
40     }      

最關鍵的一步就是第36行,所謂的注冊就是将解析得到的BeanDefinition設定到hashMap中去。至此就完成了整個IOC容器初始化的工作,這樣一來就可以在beanDefinition裡檢索和使用已經配置好的Bean了。ICO容器的作用就是維護和處理裡面的Bean。

最後,我們來回答下這個問題,spring 容器中bean是什麼時候被初始化的?

通過前面的分析,我們知道bean的執行個體化是伴随着容器的初始化而同時進行的(但是也有特例),并且預設情況下是單例的。

繼續閱讀