Spring 作为成熟的开源框架,已被开发人员广泛使用于日常开发中。Spring 自带的参考文档给开发人员提供了详细的使用介绍。而作为开源框架的 Spring 源码,更为开发人员提供了许多优雅的设计思想和具体实现参考。
文章开始前,我们定义一个名词:Bean Definition:即 Bean 定义,对应于 Spring 框架对一个 Bean 的定义,包括各种不同的属性参数,每个 Bean 都有一个或多个相关的 Bean Definition。 为了文章的可读性,在此我使用斜体表示,不将其翻译。
与 Java 程序中一个对象的执行大概经历定义、实例化和使用等阶段相似。Spring 容器中对象更加明确的经历了定义、实例化和使用等阶段:
本文主要分析 Spring IoC 容器解析基于 XML 配置方式的 Bean Definition的源码实现,给开发人员提供一个知其然,并知其所以然的途径。
Spring 简介及 Bean Definition 配置方式
Spring 是 2003 年兴起的一个轻量级 Java 开发框架,经过十多年的不断积累和演进,如今已成为功能相同或相似解决方案最为流行的开源框架。Spring IoC 改变了传统的对象定义和使用方式,Spring AOP 为开发人员提供了一种简单但功能强大的面向切面的编程方式,Spring MVC 简化了 WEB 开发的流程,使得开发人员更能专注于业务逻辑代码的开发。 Spring 可以用在各种类型的应用开发上。对于 Web Project, 开发人员只要在 web.xml 加入下列代码,并将相关 Spring 依赖文件引入就可以在开发中使用 Spring。
|
这是一个典型的 ServletContextListener,Servlet 容器(如 Tomcat 等)在启动时会找到 ContextLoaderListener 并执行其 contextInitialized(ServletContextEvent event) 方法。从这里开始,Spring 将会进行 Bean Definition的解析、Bean Processors 设置和处理、Beans 实例化等工作。从而将程序会用到的 Java 对象定义并根据该定义创建好,提供给开发人员去使用。这里 Spring 首先需要处理的就是 Bean 的定义。经过不断的发展和演化,Bean 的定义方式有:
回页首
基于 XML 配置 Bean Definition 的源码解读
XML 配置 root WebApplicationContext
前文介绍,Servlet 容器启动时如果 web.xml 配置了 ContextLoaderListener,则会调用该对象的初始化方法。根据 Java 语法规定,ContextLoaderListener 的父类 ContextLoader 有一段 static 的代码会更早被执行,这段代码配置了 XML 默认使用的 Context 为 org.springframework.web.context.WebApplicationContext = org.springframework.web.context.support.XmlWebApplicationContext
根据该定义,如果开发人员没有从 web.xml 指定 contextClass 参数,则默认使用 XmlWebApplicationContext 作为 root WebApplicationContext 工具类。
XML 命名空间及 XML 配置文件解析
Spring 采用 XML 命名空间的方式,为框架的扩展提供了极大的方便。
|
清单 2 展示在 XML 配置文件中引入命名空间的配置范例。
展开 Spring 的依赖 jar 文件,可以看到 Spring 各个模块对应的 jar 包都包含一个 META-INF 目录,如果使用了命名空间,则该目录包含了 spring.schemas 和 spring.handlers 两个文件,如图 1 所示。然后 Spring 代码通过 Properties 工具类扫描 project 的 classpath 以及其使用的所有依赖包中 META-INF 目录来得到对应参数值供后续使用。
PropertiesLoaderUtils.loadAllProperties(“META-INF/spring.schemas”, this.classLoader); PropertiesLoaderUtils.loadAllProperties(“META-INF/spring.handlers”, this.classLoader);
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException { try{ int validationMode = getValidationModeForResource(resource); // 使用 JAXP 方式,将 XML 配置文件解析为 Java 对象。 Document doc = this.documentLoader.loadDocument(inputSource, getEntityResolver(),this.errorHandler, validationMode, isNamespaceAware()); return registerBeanDefinitions(doc, resource); } |
清单 3 表示 Spring 源码会创建一些必须的属性或对象,读入配置文件(通常为 XML 文件),使用 JAXP 方式将 XML 元素转换为 Java 对象(Document doc)。 这样 Spring 对 XML 配置文件的处理,都可以看作对 Java 对象的处理。
图 2 展示 Spring 框架如何根据 XML 配置文件,创建辅助类,从而实现对用户自定义的 Bean 解析的过程。详细分析如下:
public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocumentReader { public static final String BEAN_ELEMENT=BeanDefinitionParserDelegate.BEAN_ELEMENT; public static final String NESTED_BEANS_ELEMENT = "beans"; public static final String ALIAS_ELEMENT = "alias"; public static final String NAME_ATTRIBUTE = "name"; public static final String ALIAS_ATTRIBUTE = "alias"; public static final String IMPORT_ELEMENT = "import"; public static final String RESOURCE_ATTRIBUTE = "resource"; /** @see org.springframework.context.annotation.Profile */ public static final String PROFILE_ATTRIBUTE = "profile"; protected final Log logger = LogFactory.getLog(getClass()); private XmlReaderContext readerContext; private Environment environment; private BeanDefinitionParserDelegate delegate; |
从上述清单以及 XML 配置对比可知,该对象定义了
readerContext 对象定义了一些如 resource,eventListener 和 namespaceHandlerResolver 等上下文信息。通过这些属性,Spring 框架获得需要解析的 XML 文件,命名空间解析辅助类以及特定事件的监听器等。
public class BeanDefinitionParserDelegate { ... public static final String TRUE_VALUE = "true"; public static final String FALSE_VALUE = "false"; public static final String DEFAULT_VALUE = "default"; public static final String DESCRIPTION_ELEMENT = "description"; public static final String AUTOWIRE_NO_VALUE = "no"; public static final String AUTOWIRE_BY_NAME_VALUE = "byName"; public static final String AUTOWIRE_BY_TYPE_VALUE = "byType"; public static final String AUTOWIRE_CONSTRUCTOR_VALUE = "constructor"; public static final String AUTOWIRE_AUTODETECT_VALUE = "autodetect"; public static final String DEPENDENCY_CHECK_ALL_ATTRIBUTE_VALUE = "all"; public static final String DEPENDENCY_CHECK_SIMPLE_ATTRIBUTE_VALUE = "simple"; public static final String DEPENDENCY_CHECK_OBJECTS_ATTRIBUTE_VALUE = "objects"; public static final String NAME_ATTRIBUTE = "name"; public static final String BEAN_ELEMENT = "bean"; public static final String META_ELEMENT = "meta"; public static final String ID_ATTRIBUTE = "id"; public static final String PARENT_ATTRIBUTE = "parent"; public static final String CLASS_ATTRIBUTE = "class"; public static final String ABSTRACT_ATTRIBUTE = "abstract"; public static final String SCOPE_ATTRIBUTE = "scope"; public static final String SINGLETON_ATTRIBUTE = "singleton"; public static final String LAZY_INIT_ATTRIBUTE = "lazy-init"; public static final String AUTOWIRE_ATTRIBUTE = "autowire"; public static final String AUTOWIRE_CANDIDATE_ATTRIBUTE = "autowire-candidate"; public static final String PRIMARY_ATTRIBUTE = "primary"; public static final String DEPENDENCY_CHECK_ATTRIBUTE = "dependency-check"; public static final String DEPENDS_ON_ATTRIBUTE = "depends-on"; public static final String INIT_METHOD_ATTRIBUTE = "init-method"; public static final String DESTROY_METHOD_ATTRIBUTE = "destroy-method"; public static final String FACTORY_METHOD_ATTRIBUTE = "factory-method"; public static final String FACTORY_BEAN_ATTRIBUTE = "factory-bean"; public static final String CONSTRUCTOR_ARG_ELEMENT = "constructor-arg"; public static final String INDEX_ATTRIBUTE = "index"; public static final String TYPE_ATTRIBUTE = "type"; public static final String VALUE_TYPE_ATTRIBUTE = "value-type"; public static final String KEY_TYPE_ATTRIBUTE = "key-type"; public static final String PROPERTY_ELEMENT = "property"; public static final String REF_ATTRIBUTE = "ref"; public static final String VALUE_ATTRIBUTE = "value"; public static final String LOOKUP_METHOD_ELEMENT = "lookup-method"; public static final String REPLACED_METHOD_ELEMENT = "replaced-method"; public static final String REPLACER_ATTRIBUTE = "replacer"; public static final String ARG_TYPE_ELEMENT = "arg-type"; public static final String REF_ELEMENT = "ref"; ... public static final String DEFAULT_LAZY_INIT_ATTRIBUTE = "default-lazy-init"; public static final String DEFAULT_MERGE_ATTRIBUTE = "default-merge"; public static final String DEFAULT_AUTOWIRE_ATTRIBUTE = "default-autowire"; public static final String DEFAULT_DEPENDENCY_CHECK_ATTRIBUTE = "default-dependency-check"; public static final String DEFAULT_AUTOWIRE_CANDIDATES_ATTRIBUTE = "default-autowire-candidates"; public static final String DEFAULT_INIT_METHOD_ATTRIBUTE = "default-init-method"; public static final String DEFAULT_DESTROY_METHOD_ATTRIBUTE = "default-destroy-method"; ... private final XmlReaderContext readerContext; private final DocumentDefaultsDefinition defaults = new DocumentDefaultsDefinition(); private final ParseState parseState = new ParseState(); private Environment environment; |
由上述清单可见,这就是我们熟悉的
由图 2 所示,该对象会根据读入 XML 文件的属性及其 parent 属性,给对象自身属性设置一个默认值。
XML 默认命名空间 bean 元素解析
上一步描述了 Spring 如何读入一个 XML 配置文件,并设置一系列工具类,通过 JAXB 方法将 XML 文件转换为 Java 对 Document 对象的处理过程。本节具体描述 Spring 框架是如何解析 XML 配置元素为一个 Bean Definition对象的处理过程。
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate){ // 如果 XML 根元素为默认命名空间 (xmlns="http://www.springframework.org/schema/beans"), // 则直接解析 XML 配置文件的各元素。 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; // 如果 XML 的元素是默认命名空间,调用 parseDefaultElement 方法解析 . if (delegate.isDefaultNamespace(ele)) { parseDefaultElement(ele, delegate); } else { // 如果 XML 的元素不是默认命名空间,如使用 P,content 前缀等, // 调用 delegate 的 parseCustomElement 方法解析 . delegate.parseCustomElement(ele); } } } } else { // 如果 XML 根元素为非默认命名空间,直接调用 delegate 的 parserCustomElement // 处理 XML 配置文件各元素。 delegate.parseCustomElement(root); } } |
清单 6 根据将要解析节点的命名空间选择对应的解析函数。如果为默认命名空间,则使用当前 reader 的 parseDefaultElement() 函数,传入参数为该节点元素 (ele) 和前文创建的 delegate 对象。反之,则使用前文创建的 delegate 对象进行解析。根据 Spring 相关文档及源码可知,ele 元素可能为 import、alias、beans 和 bean 其中一种,为简单起见,详细解读 bean 元素源码如下。
|
一个典型的
图 4 为
由类图可知,GenericBeanDefinition 是一个 AbstractBeanDefinition 对象,该对象在创建时会初始化一些默认属性值如 scope="", singleton=true, lazyInit=false等。同时还通过构造函数的 setConstructorArgumentValues(cargs); 和 setPropertyValues(pvs); 方法初始化 Constructor 和 Property 属性值的存放容器。然后根据传入参数设置 bd 的 parentName 以及 beanClass 或者 beanClassName 值。最终,将得到的 AbstractBeanDefinition 对象返回。
... // 根据配置文件设置 bd 的 abstract 属性值 if (ele.hasAttribute(ABSTRACT_ATTRIBUTE)) { bd.setAbstract(TRUE_VALUE.equals(ele.getAttribute(ABSTRACT_ATTRIBUTE))); } // 根据配置文件设置 bd 的 lazy-init 属性值 String lazyInit = ele.getAttribute(LAZY_INIT_ATTRIBUTE); if (DEFAULT_VALUE.equals(lazyInit)) { lazyInit = this.defaults.getLazyInit(); } bd.setLazyInit(TRUE_VALUE.equals(lazyInit)); // 根据配置文件设置 bd 的 autowire 属性值 String autowire = ele.getAttribute(AUTOWIRE_ATTRIBUTE); bd.setAutowireMode(getAutowireMode(autowire)); // 根据配置文件设置 bd 的 dependency-check 属性值 String dependencyCheck = ele.getAttribute(DEPENDENCY_CHECK_ATTRIBUTE); bd.setDependencyCheck(getDependencyCheck(dependencyCheck)); ... |
// 解析 “meta”元素 parseMetaElements(ele, bd); // 解析 “lookup-method”元素 parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); // 解析 “replaced-method”元素 parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); // 解析 “constructor-arg”元素 parseConstructorArgElements(ele, bd); // 解析 “property”元素 parsePropertyElements(ele, bd); // 解析 “qualifier”元素 parseQualifierElements(ele, bd); |
以 parsePropertyElements(ele,bd) 为例,其解析步骤如下:
注: 如果解析出来的 property 包含 “meta”元素,则循环解析该元素。
经过前述步骤,我们已经得到一个 BeanDefinitionHolder 对象,其包含 beanDefinition 属性,描述了 bean 配置的属性和子节点值。但是根据前文介绍,Spring 引入了命名空间。因此一个属性可能配置形式如 p:name-ref="nameValue" 属性,这是一个非默认命名空间的属性。Spring 容器会做进一步处理(如图 4 中两个阴影部分 Loop 所示)。简介如下:
回页首
小结
本文介绍了一个典型的基于 Spring 的 web project 的配置和启动过程。详细分析了 Spring 框架解析开发人员基于 XML 文件定义的 Bean 到 Spring 容器的 Bean Deifinition对象的处理过程。该方式是最原始也是使用最普遍的一种方法,其优点在于将配置集中在一起(XML 配置文件中),且与 JAVA 代码分离,方便管理。对于配置文件的改变,不需要重新编译源代码,极大的提高了开发效率。其缺点在于对大型基于 Spring 配置的项目,冗余 XML 配置较多,增加了开发的工作量和维护成本。后续文章会详细分析 Spring 容器基于 Annotation 和 Java Code 方式解析 Bean Definition的处理流程,并简要分析这些配置方式所适用的场景,希望对读者有所帮助。
参考资料
学习
讨论
关于作者
秦天杰是 CSTL 的 Cloud team 的一名软件工程师,具有 6 年从业经验,主要使用 JavaEE 相关技术从事云计算和 Web 开发相关工作。