前言
前面的两篇文章,已经讲述了加载Bean的前两个步骤,这篇文章接着上面的文章来讲述加载Bean的第三个步骤,就是根据第二个步骤得到的Document对象注册Bean信息。
注册BeanDefinitions
获取Document对象后,会根据此对象和Resource资源对象调用registerBeanDefinitions()方法,开始注册BeanDefinitions之旅了,该方法源码如下:
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); int countBefore = getRegistry().getBeanDefinitionCount(); documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); return getRegistry().getBeanDefinitionCount() - countBefore; }
先说一下方法的逻辑:首先调用createBeanDefinitionDocumentReader()方法实例化BeanDefinitionDocumentReader对象,然后获取统计前BeanDefinition的个数,最后调用registerBeanDefinitions()注册BeanDefinition。
实例化BeanDefinitionDocumentReader对象的方法源码如下:
protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() { return BeanUtils.instantiateClass(this.documentReaderClass); }
注册BeanDefinition的方法registerBeanDefinitions()方法是在接口 BeanDefinitionDocumentReader中,源码如下:
void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) throws BeanDefinitionStoreException;
从给定的Document对象中解析定义BeanDefinition并将它们注册到注册表中。该方法接收两个参数,待解析的Document对象,以及解析器的当前上下文,包括目标注册表和被解析的资源。其中readerContext是根据Resource来创建的,源码如下:
public XmlReaderContext createReaderContext(Resource resource) { return new XmlReaderContext(resource, this.problemReporter, this.eventListener, this.sourceExtractor, this, getNamespaceHandlerResolver()); }
在DefaultBeanDefinitionDocumentReader中对注册BeanDefinition的方法registerBeanDefinitions()方法进行了实现:
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { this.readerContext = readerContext; doRegisterBeanDefinitions(doc.getDocumentElement()); }
这个方法的重要目的之一就是提取root(doc.getDocumentElement()),以便于再次将root作为参数继续BeanDefinition的注册。可以说历经千难险阻,终于到了核心逻辑的底部doRegisterBeanDefinitions(doc.getDocumentElement())。如果说之前一直是XML加载解析的准备阶段,那么doRegisterBeanDefinitions(doc.getDocumentElement())算是真正开始解析了,核心部分真正开始了:
protected void doRegisterBeanDefinitions(Element root) { BeanDefinitionParserDelegate parent = this.delegate; this.delegate = createDelegate(getReaderContext(), root, parent); if (this.delegate.isDefaultNamespace(root)) { //处理profile属性 String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); if (StringUtils.hasText(profileSpec)) { String[] specifiedProfiles = StringUtils.tokenizeToStringArray( profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS); if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) { if (logger.isDebugEnabled()) { logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec + "] not matching: " + getReaderContext().getResource()); } return; } } } //解析前处理,空方法,留给子类实现 preProcessXml(root); //解析 parseBeanDefinitions(root, this.delegate); //解析后处理,空方法,留给子类实现 postProcessXml(root); this.delegate = parent; }
逻辑:程序首先处理profile属性,profile主要用于我们切换环境,比如开发、测试、生产环境,非常方便。然后调用parseBeanDefinitions()进行解析,不过在解析方法之前分别调用preProcessXml()和postProcessXml()方法来进行解析前、后处理,目前这两个方法是空实现,交由子类来实现:
protected void preProcessXml(Element root) { } protected void postProcessXml(Element root) { }
既然是空的,那么还写着有什么用呢?
就像面向对象设计方法学中常说的一句话,一个类要么是面向继承设计的,要么就用final修饰。在DefaultBeanDefinitionDocumentReader中并没有final修饰,所以它是面向继承设计的。这也就是设计模式中的模板方法模式。
解析方法parseBeanDefinitions()的源码实现如下:
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { 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)) { parseDefaultElement(ele, delegate); } else { delegate.parseCustomElement(ele); } } } } else { delegate.parseCustomElement(root); } }
可以看出,最终解析动作落地在两个方法上:parseDefaultElement(ele,delegate)和delegate.parseCustomElement(root)。我们知道在Spring中有两种Bean的声明方式:
(1)配置文件声明:<bean id="classId" class="com.joe.classname"/>
(2)自定义注解方式:<tx:annotation-driven>
两种方式的读取和解析都存在较大的差异,所以采用不同的解析方法,如果根节点或者子节点采用默认命名空间的话,则调用parseDefaultElement(ele,delegate)进行解析,否则调用delegate.parseCustomElement(root)方法进行自定义解析。而判断是否默认命名空间还是自定义命名空间的办法其实是使用node.getNamespaceURI()获取命名空间,并与Spring中固定的命名空间 http://www.Springframework.org/schema/beans 进行对比,如果一致则认为是默认的,否则就是自定义的。
profile属性的使用
在上面解析BeanDefinition中注意到,最开始是对PROFILE_ATTRIBUTE属性进行解析,可能对于我们来说,profile属性并不是很常用。我们先来了解一下这个属性。
先来看一下profile的用法:
<?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:dubbo="http://code.alibabatech.com/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"> <bean id="userService" class="org.springframework.example.service.impl.UserServiceImpl" lazy-init="true"></bean> <beans profile="dev"> <bean id="testService" class="org.springframework.example.service.impl.TestServiceImpl" scope="prototype" lazy-init="true"> <property name="userService" ref="userService"></property> </bean> </beans> <beans profile="test"> </beans>
集成到Web环境中时,在web.xml中加入以下代码:
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"> <display-name>Archetype Created Web Application</display-name> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath*:/applicationContext*.xml </param-value> </context-param> <!-- 在上下文context-param中设置profile.default的默认值 --> <context-param> <param-name>spring.profiles.default</param-name> <param-value>development</param-value> </context-param> <!-- 在上下文context-param中设置profile.active的默认值 --> <!-- 设置active后default失效,web启动时会加载对应的环境信息 --> <context-param> <param-name>spring.profiles.active</param-name> <param-value>dev</param-value> </context-param> </web-app>
有了这个特性我们就可以同时在配置文件中部署两套来适用于生产环境和开发环境,这样可以很方便的进行环境的切换,其中最常用的是数据库的更换。
总结:在了解了profile的使用再来分析代码会清晰很多,首先程序会去获取beans节点是否定义了profile属性,如果定义了则会需要到环境变量中去寻找,所以这里首先断言environment不可能为空,因为profile是可以同时指定多个的,需要程序对其拆分,并解析每个profile是都符合环境变量中所定义的,不定义则不会浪费性能去解析。
参考:《Spring源码深度解析》 郝佳 编著: