1. 程式人生 > >spring源碼閱讀(2)-- 容器啟動之加載BeanDefinition

spring源碼閱讀(2)-- 容器啟動之加載BeanDefinition

不同的 from war resource encode 空間 getname contex import

  在《spring源碼閱讀(1)-- 容器啟動之資源定位》一文中,閱讀了spring是怎麽根據用戶指定的配置加載資源,當加載完資源,接下來便是把從資源中加載BeanDefinition。

  BeanDefinition作為spring其中一個組件,spring是這樣描述BeanDefinition的:BeanDefinition描述了一個bean實例,它具有屬性值,構造函數參數值以及具體實現提供的更多信息。個人的理解是BeanDefinition保存了一個bean實例的所有元數據,下面列舉一些常用的BeanDefinition屬性,更多屬性可以通過查看spring-beans.xsd了解

    name:bean實例的別買,一個bean實例可以擁有多個別名

    class:bean實例的class,如果作為一個父bean可以為空

    parent:父bean的名稱

    scope:聲明bean實例是單例還是原型的,默認單例

    lazy-init:是否延遲加載,當是一個單例bean是,默認值是false

    init-method:設置完屬性時調用的初始化方法

    destroy-method:在bean工廠關閉時調用

  項目沿用《spring源碼閱讀(1)-- 容器啟動之資源定位》一文的,這裏就不貼工程相關的配置文件,重點貼一下spring的配置文件

 1 <?xml version="1.0" encoding="UTF-8"
?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation=" 5 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 6 "> 7 8 <bean id="springtest"
class="com.zksite.spring.test.SpringBeanTest" /> 9 10 </beans>

  通過閱讀上文,BeanDefinition的加載是由BeanDefinitionReader組件負責,而具體的實現是XmlBeanDefinitionReader。BeanDefinition的加載是由BeanDefinitionReader從Resuouce中去完成的,下面是XmlBeanDefinitionReader.loadBeanDefinitions(EncodedResource encodedResource)方法

技術分享圖片
 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         }
29         catch (IOException ex) {
30             throw new BeanDefinitionStoreException(
31                     "IOException parsing XML document from " + encodedResource.getResource(), ex);
32         }
33         finally {
34             currentResources.remove(encodedResource);
35             if (currentResources.isEmpty()) {
36                 this.resourcesCurrentlyBeingLoaded.remove();
37             }
38         }
39     }
View Code

  方法裏首先判斷一下是否循環加載,然後通過資源創建InputSource(spring解析xml是通過sax去解析的),然後調用doLoadBeanDefinitions()去解析xml和加載BeanDefinition。下面是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 (BeanDefinitionStoreException ex) {
 8             throw ex;
 9         }
10         catch (SAXParseException ex) {
11             throw new XmlBeanDefinitionStoreException(resource.getDescription(),
12                     "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
13         }
14         catch (SAXException ex) {
15             throw new XmlBeanDefinitionStoreException(resource.getDescription(),
16                     "XML document from " + resource + " is invalid", ex);
17         }
18         catch (ParserConfigurationException ex) {
19             throw new BeanDefinitionStoreException(resource.getDescription(),
20                     "Parser configuration exception parsing XML from " + resource, ex);
21         }
22         catch (IOException ex) {
23             throw new BeanDefinitionStoreException(resource.getDescription(),
24                     "IOException parsing XML document from " + resource, ex);
25         }
26         catch (Throwable ex) {
27             throw new BeanDefinitionStoreException(resource.getDescription(),
28                     "Unexpected exception parsing XML document from " + resource, ex);
29         }
30     }

  doLoadDocument方法通過配置指定的DocumentLoader和創建XmlBeanDefinitionReader時指定的EntityResolver(這裏的實現是ResourceEntityResolver)去加載documen。sax在解析文檔時,由於指定了EntityResolver,所以在校驗xml文檔時會調用ResourceEntityResolver.resolveEntity()方法去加載dtd或xsd

 1     public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
 2         InputSource source = super.resolveEntity(publicId, systemId);
 3         if (source == null && systemId != null) {
 4             String resourcePath = null;
 5             try {
 6                 String decodedSystemId = URLDecoder.decode(systemId, "UTF-8");
 7                 String givenUrl = new URL(decodedSystemId).toString();
 8                 String systemRootUrl = new File("").toURI().toURL().toString();
 9                 // Try relative to resource base if currently in system root.
10                 if (givenUrl.startsWith(systemRootUrl)) {
11                     resourcePath = givenUrl.substring(systemRootUrl.length());
12                 }
13             }
14             catch (Exception ex) {
15                 // Typically a MalformedURLException or AccessControlException.
16                 if (logger.isDebugEnabled()) {
17                     logger.debug("Could not resolve XML entity [" + systemId + "] against system root URL", ex);
18                 }
19                 // No URL (or no resolvable URL) -> try relative to resource base.
20                 resourcePath = systemId;
21             }
22             if (resourcePath != null) {
23                 if (logger.isTraceEnabled()) {
24                     logger.trace("Trying to locate XML entity [" + systemId + "] as resource [" + resourcePath + "]");
25                 }
26                 Resource resource = this.resourceLoader.getResource(resourcePath);
27                 source = new InputSource(resource.getInputStream());
28                 source.setPublicId(publicId);
29                 source.setSystemId(systemId);
30                 if (logger.isDebugEnabled()) {
31                     logger.debug("Found XML entity [" + systemId + "]: " + resource);
32                 }
33             }
34         }
35         return source;
36     }

  方法裏首先調用了父類提供的resolveEntity方法去加載,而父類是通過判斷加載的是dtd或xsd然後使用持有的EntityResolver去加載。現在配置文件指定的http://www.springframework.org/schema/beans/spring-beans.xsd,xsd是由實例schemaResolver.resolveEntity去加載(schemaResolver實例的創建發生在XmlBeanDefinitionReader設置EntityResolver時)

 1     public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
 2         if (systemId != null) {
 3             if (systemId.endsWith(DTD_SUFFIX)) {
 4                 return this.dtdResolver.resolveEntity(publicId, systemId);
 5             }
 6             else if (systemId.endsWith(XSD_SUFFIX)) {
 7                 return this.schemaResolver.resolveEntity(publicId, systemId);
 8             }
 9         }
10         return null;
11     }

  schemaResolver是PluggableSchemaResolver的實例,進入PluggableSchemaResolver的resolveEntity方法

技術分享圖片
 1     public InputSource resolveEntity(String publicId, String systemId) throws IOException {
 2         if (logger.isTraceEnabled()) {
 3             logger.trace("Trying to resolve XML entity with public id [" + publicId +
 4                     "] and system id [" + systemId + "]");
 5         }
 6 
 7         if (systemId != null) {
 8             String resourceLocation = getSchemaMappings().get(systemId);
 9             if (resourceLocation != null) {
10                 Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
11                 try {
12                     InputSource source = new InputSource(resource.getInputStream());
13                     source.setPublicId(publicId);
14                     source.setSystemId(systemId);
15                     if (logger.isDebugEnabled()) {
16                         logger.debug("Found XML schema [" + systemId + "] in classpath: " + resourceLocation);
17                     }
18                     return source;
19                 }
20                 catch (FileNotFoundException ex) {
21                     if (logger.isDebugEnabled()) {
22                         logger.debug("Couldn‘t find XML schema [" + systemId + "]: " + resource, ex);
23                     }
24                 }
25             }
26         }
27         return null;
28     }
View Code

  方法裏首先調用getSchemaMappings()方法獲取所有schema,然後再從裏面獲取指定systemId的schema,如果找到則返回一個設置好systemId的InputSource。getSchemaMappings()方法裏面主要做的事情是,通過指定的ClassLoader查找出所有META-INF下的spring.schemas文件(當需要擴展spring的配置文件時,需要編寫自定義的schema),然後再存到一個Map裏面,key為命名空間,value為schema文件的路徑。

  當加載完documen和校驗通過後,接下來的便是加載BeanDefinition,進入XmlBeanDefinitionReader.registerBeanDefinitions方法

1     public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
2         BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
3         int countBefore = getRegistry().getBeanDefinitionCount();
4         documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
5         return getRegistry().getBeanDefinitionCount() - countBefore;
6     }

  方法裏首先創建一個BeanDefinitionDocumentReader(又一個新家夥,spring的抽象能力真的非常棒),BeanDefinitionDocumentReader的主要職責是負責解析dom文檔並根據dom文檔創建BeanDefinition然後註冊到BeanDefinition註冊中心(只有當標簽是默認命名空間的,也就是http://www.springframework.org/schema/beans,當是擴展的標簽時,需要自行實現BeanDefinitionParser進行解析),這裏的實現為DefaultBeanDefinitionDocumentReader,DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions實現了BeanDefinition的加載和註冊,方法裏首先根據parentDelegate(主要目的是用來傳播默認設置)創建一個BeanDefinitionParserDelegate,然後判斷是否設置了profile,如果當前的配置沒有被激活,則會跳過解析,跳過的不是整個配置文件,有關profile的使用可以《詳解Spring中的Profile》。doRegisterBeanDefinitions源碼如下:

 1   protected void doRegisterBeanDefinitions(Element root) {
 2         BeanDefinitionParserDelegate parent = this.delegate;
 3         this.delegate = createDelegate(getReaderContext(), root, parent);
 4 
 5         if (this.delegate.isDefaultNamespace(root)) {
 6             String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
 7             if (StringUtils.hasText(profileSpec)) {
 8                 String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
 9                         profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
10                 if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
11                     if (logger.isInfoEnabled()) {
12                         logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
13                                 "] not matching: " + getReaderContext().getResource());
14                     }
15                     return;
16                 }
17             }
18         }
19 
20         preProcessXml(root);
21         parseBeanDefinitions(root, this.delegate);
22         postProcessXml(root);
23 
24         this.delegate = parent;
25     }

  當配置文件沒有跳過時,執行解析documen文檔操作,doRegisterBeanDefinitions方法裏的preProcessXml和postProcessXml是預留的擴展點,DefaultBeanDefinitionDocumentReader裏的實現為空,所以直接進入parseBeanDefinitions方法,方法裏獲取所有的子節點,然後循環遍歷解析。

 1     protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
 2         if (delegate.isDefaultNamespace(root)) {
 3             NodeList nl = root.getChildNodes();
 4             for (int i = 0; i < nl.getLength(); i++) {
 5                 Node node = nl.item(i);
 6                 if (node instanceof Element) {
 7                     Element ele = (Element) node;
 8                     if (delegate.isDefaultNamespace(ele)) {
 9                         parseDefaultElement(ele, delegate);
10                     }
11                     else {
12                         delegate.parseCustomElement(ele);
13                     }
14                 }
15             }
16         }
17         else {
18             delegate.parseCustomElement(root);
19         }
20     }

  如果是默認命名空間的標簽,直接進入parseDefaultElement,方法裏根據標簽名字,進行不同的處理,如果是“import”將加載一個資源,然後執行上面的流程;如果是“alias”,向BeanDefinitionRegistry註冊別名;如果是“bean”執行BeanDefinition的註冊;如果是“beans”遞歸調用doRegisterBeanDefinitions方法。parseDefaultElement源碼如下:

 1     private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
 2         if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
 3             importBeanDefinitionResource(ele);
 4         }
 5         else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
 6             processAliasRegistration(ele);
 7         }
 8         else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
 9             processBeanDefinition(ele, delegate);
10         }
11         else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
12             // recurse
13             doRegisterBeanDefinitions(ele);
14         }
15     }

  當解析的標簽是“bean”時,將會使用BeanDefinitionParserDelegate.parseBeanDefinitionElement去解析bean標簽。進入BeanDefinitionParserDelegate.parseBeanDefinitionElement方法

技術分享圖片
 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, MULTI_VALUE_ATTRIBUTE_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     }
View Code

  方法裏首先獲取bean標簽的id和name屬性,如果配置了id,那麽beanNama就是id。然後判斷是否配置了多個name,如果有將解析為別名。然後調用parseBeanDefinitionElement方法創建BeanDefinition,parseBeanDefinitionElement方法會根據標簽的屬性和子節點內容去設置BeanDefinition(關於BeanDefinition的屬性有什麽作用這裏先跳過),至此,已經成功解析完整個bean標簽並且創建了BeanDefinition,然後返回給上層調用者即parseBeanDefinitionElement方法。parseBeanDefinitionElement方法做最後的處理,判斷用戶是否設置了名稱,如果沒有生成一個。當這些操作都完成時,回到DefaultBeanDefinitionDocumentReader.processBeanDefinition方法進行進一步裝飾BeanDefinition然後向給定的BeanDefinitionRegistry註冊。

  如果解析的命名空間不是默認的,spring會怎麽處理呢?現在來更改一下配置文件

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3     xmlns:context="http://www.springframework.org/schema/context"
 4     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 5     xsi:schemaLocation="
 6     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
 7     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
 8     ">
 9 
10     <bean class="com.zksite.spring.test.SpringBeanTest" />
11     <context:component-scan base-package="com"></context:component-scan>
12 </beans>

  配置文件新添加了命名空間:http://www.springframework.org/schema/context並配置了schemaLocation http://www.springframework.org/schema/context/spring-context.xsd。這裏先介紹一下spring命名空間擴展機制,spring為了方便用戶擴展,提供了NamespaceHandler接口,如果用戶需要擴展spring的配置文件只需要做以下處理:

    1.編寫xml schema文件

    2.編寫spring.schemas文件,用於獲取xml schema文件路徑

    3.編寫spring.handlers文件,用戶獲取自定義標簽解析器

    4.實現NamespaceHandler接口,通常建議繼承NamespaceHandlerSupport,實現init方法

  spring在解析到用戶自定義的標簽時,通過調用BeanDefinitionParserDelegate.parseCustomElement進行處理,方法裏會通過持有的NamespaceHandlerResolver獲取用戶配置的NamespaceHandler,然後調用NamespaceHandler.parse方法去解析。NamespaceHandlerResolver是怎麽獲取到指定的NamespaceHandler的呢?進入DefaultNamespaceHandlerResolver的resolve方法

 1     public NamespaceHandler resolve(String namespaceUri) {
 2         Map<String, Object> handlerMappings = getHandlerMappings();
 3         Object handlerOrClassName = handlerMappings.get(namespaceUri);
 4         if (handlerOrClassName == null) {
 5             return null;
 6         }
 7         else if (handlerOrClassName instanceof NamespaceHandler) {
 8             return (NamespaceHandler) handlerOrClassName;
 9         }
10         else {
11             String className = (String) handlerOrClassName;
12             try {
13                 Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
14                 if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
15                     throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
16                             "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
17                 }
18                 NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
19                 namespaceHandler.init();
20                 handlerMappings.put(namespaceUri, namespaceHandler);
21                 return namespaceHandler;
22             }
23             catch (ClassNotFoundException ex) {
24                 throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" +
25                         namespaceUri + "] not found", ex);
26             }
27             catch (LinkageError err) {
28                 throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" +
29                         namespaceUri + "]: problem with handler class file or dependent class", err);
30             }
31         }
32     }

  首先調用getHandlerMappings()方法,getHandlerMappings方法會根據指定的classLoader找出META-INF下面的所有spring.handlers,然後再把spring.handlers裏面的內容存放到一個Map裏,key存放命名空間,value存放NamespaceHandler。獲取到NamespaceHandler時,先判斷一下是否已經初始化了,如果沒有,通過反射初始化,然後調用NamespaceHandler.init()方法。當找到指定的NamespaceHandler之後返回給BeanDefinitionParserDelegate.parseCustomElement,方法裏再調用獲取回來的NamespaceHandler.parse方法去解析自定義標簽。

  至此,spring已經通過資源加載了BeanDefinition,接下裏的便是向註冊中心註冊BeanDefinition。進入BeanDefinitionReaderUtils.registerBeanDefinition方法

 1     public static void registerBeanDefinition(
 2             BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
 3             throws BeanDefinitionStoreException {
 4 
 5         // Register bean definition under primary name.
 6         String beanName = definitionHolder.getBeanName();
 7         registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
 8 
 9         // Register aliases for bean name, if any.
10         String[] aliases = definitionHolder.getAliases();
11         if (aliases != null) {
12             for (String alias : aliases) {
13                 registry.registerAlias(beanName, alias);
14             }
15         }
16     }

  方法裏使用傳入的BeanDefinitionRegistry進行註冊,BeanDefinitionRegistry是通過一個Map將BeanDefinition存起來。

  最後總結一下註冊BeanDefinition用到了哪些組件

    BeanDefinitionReader:負責從配置文件加載BeanDefinition

    BeanDefinitionDocumentReader:負責解析document加載BeanDefinition並註冊

    BeanDefinitionParserDelegate:負責解析標簽並根據標簽內容構建BeanDefinition

    BeanDefinitionRegistry:負責BeanDefinition的註冊

  當註冊完BeanDefinition,接下來便是創建bean

spring源碼閱讀(2)-- 容器啟動之加載BeanDefinition