Spring源码解析1——容器的基本实现

一、Spring整体架构

spring-overview
clipboard

1.1、Core Container

  Core Container(核心容器)包含有Core、Beans、Context、Expression Language模块。Core和Bean模块是框架的基础部分,提供Ioc(控制反转)和DI(依赖注入)。这里的基础概念是BeanFactory,它提供对Factory模式的经典实现来消除对程序中单例模式的需要。并真正允许你从程序逻辑中分离出依赖关系配置

1.1.1、Beans模块

  Beans模块是所有应用都要用到的,它包含访问配置文件,创建和管理Bean以及进行Inversion of Control/Dependency Injection(IOC/DI)操作相关的所有类。

1.1.2、Context模块

  Context模块构建于Core和Bean模块基础之上,提供了一种类似于JNDI注册器的框架式的对象访问方法。Context模块继承了Beans的特性,为Spring核心提供了大量的扩展,添加了对国际化(例如资源绑定)、事件传播、资源加载和对Context的透明创建支持。Context模块同时也支持J2EE的一些特性,例如EJB、JMX和基础的远程处理。ApplicationContext接口是Context模块的关键。

2.1、Data Access/Integration

  Data Access/Integration层包含JDBC、ORM、OXM、JMS和Transaction模块。

2.1.1、JDBC模块

  JDBC模块提供了一个JDBC抽象层,它可以消除冗长的JDBC编码和解析数据库厂商特有的错误代码。这个模块包含了Spring对JDBC数据访问进行封装的所有类。

2.1.2、ORM模块

  ORM模块为流行的对象-关系映射API,如JPA、JDO、Hibernate、iBatis等,提供了一个交互层。利用ORM封装包,可以混合使用spring提供的特性进行O/R映射。
  Spring框架插入了若干个ORM框架,从而提供了ORM的对象关系工具,其中包括JDO、Hibernate和iBatisSQL Map。所有这些都遵从Spring的通用事务和DAO异常层次结构。

2.1.3、OXM模块

  OXM模块提供了一个对Object/XML映射实现的抽象层,Object/XML映射实现包括JAXB、Castor、XMLBeans、JiBX和XStream。

2.1.4、JMS模块

  JMS模块主要包含了一些制造和消费消息的特性。

2.1.5、Transaction模块

  Transaction模块支持编程和声明性的事务管理,这些事务必须实现特定的接口,并且对所有POJO都适用
clipboard

3.1、Web

  Web上下文模块建立在用用程序上下文模块之上,为基于Web的应用程序提供了上下文。所以,Spring框架支持与Jakarta Struts 的集成。Web模块还简化了处理大部分请求以及将请求参数绑定到域对象的工作。Web层包含了Web、Web-Servlet、Web-Socket和Web-Porlet模块。

3.1.1、Web模块

  提供了基础的面向Web的集成特性。例如:1、多文件上传、使用servlet listeners初始化Ioc容器;2、面向Web的应用上下文;3、Spring远程支持Web的相关部分。

3.1.2、Web-Servlet模块

  该模块包含Spring的model-veiw-controller(MVC)实现。Spring的MVC使模型代码和web forms之间能够清楚的分离开。

3.1.3、Web-Portlet模块

  提供了用于Portlet环境和Web-Servlet模块的MVC的实现。
clipboard

4.1、AOP

  AOP模块提供了一个符合AOP标准的面向切面编程的实现,它让你可以定义方法拦截器的切入点,从而将逻辑代码分开,降低它们之间的耦合性。利用source-level的元数据功能,将各种行为信息合并到你的代码中。

4.1.1、Aspects模块

  Aspects模块提供了对AspectJ的集成支持。

4.1.2、Instrumentation模块

  提供了class instrumentation支持和classloader实现,一般用在特定的服务器。

二、容器的基本实现

1、核心类介绍

clipboard

  • AliasRegistry:定义对alias的简单增、删、改等操作。

  • SimpleAliasRegistry:主要使用map作为alias缓存,并对接口AliasRegistry进行实现。

  • BeanFactory:定义获取Bean以及Bean的各种属性。

  • SingletonBeanRegistry:定义对单例的注册及获取。

  • BeanDefinitionRegistry:定义对BeanDefinition的各种增、删、改、查操作。

  • ListableBeanFactory:根据各种条件获取bean的配置清单。

  • HierarchicalBeanFactory:继承BeanFactory,具体是在BeanFactory定义的功能基础上增加了对parentFactory的支持。

  • DefaultSingletonBeanRegistry:对接口SingletonBeanRegistry实现。

  • ConfigurableBeanFactory:提供了配置Factory的各种方法。

  • FactoryBeanRegistrySupport:在DefaultSingletonBeanRegistry基础上增加了对FactoryBean的特殊处理功能。

  • AutowireCapableBeanFactory:提供了创建bean、自动注入、初始化以及应用bean的后置处理器。

  • AbstractBeanFactory:综合FactoryBeanRegistrySupport和ConfigurableBeanFactory的功能。

  • ConfigurableListableBeanFactory:BeanFactory配置清单,指定忽略类型及接口等

  • AbstractAutowireCapableBeanFactory:综合AbstractBeanFactory,并对AutowireCapableBeanFactory进行实现。

  • DefaultListableBeanFactory:综合上面所有功能,主要是对bean注册后的处理。

  • XmlBeanFactory:对DefaultListableBeanFactory类进行了扩展,主要用于从XML文档读取BeanDefinition,对于注册及获取Bean都是使用从父类DefaultListableBeanFactory继承的方法实现,唯一区别就是:增加了XmlBeanDefinitionReader类型的变量reader。reader主要用于读取和注册资源文件。
    clipboard

2、XmlBeanDefinitionReader

  XML 配置文件的读取是Spring 中重要的功能,因为Spring 的大部分功能都是以配置作为切入点的,那么我们可以从XmlBeanDefinitionReader 中梳理一下资源文件读取、解析及注册的大致脉络。
clipboard
注:双向箭头1:实心箭头指向的类存在1个非实心箭头类型的变量,灰色虚线:间接引用,绿色虚线:接口实现,深蓝色单向箭头:继承关系。

  • ResourceLoader :定义资源加载器,主妥应用于根据给定的资源文件地址返回对应的
    Resource 。
  • BeanDefinitionReader 接口:主要定义资源文件读取并转换为BeanDefinition 的各个功能。
  • EnvironmentCapable 接口:定义获取Environment 方法。
  • DocumentLoader 接口:定义从资源、文件加载到转换为Document 的功能。
  • AbstractBeanDefinitionReader 抽象类:对EnvironmentCapabl e 、BeanDefinitionReader 类定义的功能进行实现。
  • BeanDefinitionDocumentReader :定义读取Docuemnt 并注册BeanDefinition 功能。
  • BeanDefinitionParserDelegate :定义解析Element 的各种方法。
  • DefaultBeanDefinitionDocumentReader:实现了BeanDefinitionDocumentReader 并封装了BeanDefinitionParserDelegate 。

业务逻辑:
①、通过继承向AbstractBeanDefinitionReader 中的方法,来使用ResourceLoader 将资源文件路径转换为对应的Resource 文件。
②、通过DocumentLoader 对Resource 文件进行转换,将Resource 文件转换为Document文件。
clipboard

③、通过实现接口BeanDefinitionDocumentReader 的DefaultBeanDefinitionDocumentReader 类对Document 进行解析,并使用BeanDefinitionParserDelegate 对Element 进行解析。

3、容器的基础XmlBeanDefinitionReader

3.1、初始化时序图(下面代码中的new ClassPathResource("spring-bean.xml")中初始化了XmlBeanFactory)

clipboard
clipboard

3.2、配置文件封装

  Spring的配置文件读取是通过ClassPathResource进行封装的。ClassPathResource的根是Resource。
  在Java 中,将不同来源的资源抽象成URL ,通过注册不同的handler ( URLStreamHandler )来处理不同来源的资源的读取逻辑,一般handler 的类型使用不同前缀(协议, Protocol )来识别,如“file :”、“ http :” 、“jar:”等,然而URL 没有默认定义相对Classpath 或ServletContext 等资源的handler,虽然可以注册自己的URLStreamHandler来解析特定的URL前缀(协议),比如"ClassPath:",而URL的实现机制中,没有提供基本的方法,如:检查当前资源是否存在,检查当前资源是否可读等方法。因而Spring对其资源读取实现了自己的抽象结构:Resource接口封装底层资源。
clipboard
clipboard

  InputStreamSource 封装任何能返回 InputStream 的类,比如 File 、 Classpath 下的资源和Byte Array 等。 它只有一个方法定义 : getlnputStream(),该方法返回一个新的 InputStream 对象。
  Resource 接口抽象了所有 Spring 内部使用到的底层资源: File 、 URL 、 Classpath 等,定义的规则如下:
①、exists():存在性。
②、isReadable():可读性。
③、isOpen():是否处于打开状态 。
④、getURL():不同资源转换到URL。
⑤、getURI:不同资源转换到URI。
⑥、getFile():不同资源转换到File。
⑦、createRelative():基于当前资源创建一个相对资源的方法。
⑧、getFilename():获取不带路径信息的文件名。
⑨、getDescription():用来在错误处理中打印信息。

  对不同来源的资源文件都有相应的 Resource 实现: 文件( FileSystemResource ) 、 Classpath资源( ClassPathResource )、 URL 资源( Ur!Resource )、 InputStream 资源( InputStreamResource ) 、Byte 数组( ByteArrayResource )等。 相关类图如下。

  有了 Resource 接口便可以对所有资源文件进行统一处理。 至于实现,其实是非常简单的,以 getlnputStream 为例,ClassPathResource 中的实现方式便是通过 class 或者 classLoader 提供的底层方法进行调用。
clipboard

  当通过 Resource 相关类完成了对配置文件进行封装后配置文件的读取工作就全权交给XmlBeanDefinitionReader 来处理了。XmlBeanFactory 的初始化有若干办法 , Spring 中提供了很多的构造函数,在这里分析的是使用 Resource 实例作为构造两数参数的办法:
clipboard
clipboard

  ignoreDependencyInterface 的主要功能是:忽略给定接口的向动装配功能,那么,这样做的目的是什么呢?会产生什么样的效果呢?
  举例来说,当 A 中有属性 B,那么当 Spring 在获取 A 的 Bean 的时候如果其属性 B 还没有初始化,那么 Spring 会自动初始化 B,这也是 Spring中提供的一个重要特性。 但是,某些情况下, B 不会被初始化,其中的一种情况就是 B 实现了 BeanNameAware 接口 。 Spring 中是这样介绍的:正常装配时忽略给定的依赖接口,典型应用是通过其他方式解析 Application 上下文注册依赖,类似于 BeanFactory 通过 BeanFactoryAware 进行注入或者 ApplicationContext 通过ApplicationContextAware 进行注入。

3.3、加载Bean

  之前提到的在 XmlBeanFactory 构造函数中调用了 XmlBeanDefinitionReader 类型的 reader属性提供的方法 this.reader. loadBeanDefinitions(resource),这段代码是整个资源加载的切入点。
ru5erkjggg==
① . 封装资源文件。当进入 XmlBeanDefinitionReader 后首先对参数 Resource 使用EncodedResource 类进行封装。
suvork5cyii=
注:EncodedResource 的作用是什么呢?通过名称,可以大致推断这个类主要是用于
对资服文件的编码进行处理的。其中的主要逻辑体现在 getReader() 方法中:
clipboard

②. 获取输入流。 从 Resource 中获取对应的 InputStrearn 并构造 lnputSource 。考虑到Resource 可能存在编码要求的情况,读取过程是通过 SAX 读取 XML 文件的方式来准备lnputSource对象
③. 通过构造的 lnputSource 实例和 Resource 实例继续调用函数 doLoadBeanDefinitions()。
aelftksuqmcc
suvork5cyii=

当获取到org.xml.sax.InputSource后,执行doLoadBeanDefinitions()函数,进行核心逻辑:
①、获取对 XML 文件的验证模式。
②、加载 XML 文件,并得到对应的 Document。
③、根据返回的 Document 注册 Bean 信息。
ru5erkjggg==

4、获取XML的验证模式

4.1、DTD 与 XSD 区别

  DTD ( Document Type Definition )即文挡类型定义,是一种 XML 约束模式语言,是 XML文件的验证机制,属于 XML 文件组成的一部分。 DTD 是一种保证 XML 文档格式正确的有效方法,可以通过比较 XML 文档和 DTD 文件来看文档是否符合规范,元素和标签使用是否正确。一个 DTD 文档包含:元素的定义规则,元素间关系的定义规则,元素可使用的属性, 可使用的实体或符号规则。
  要使用 DTD 验证模式的时候需要在 XML 文件的头部声明, 以下是在 Spring 中使用 DTD声明方式的代码:
clipboard

  XML Schema 语言就是 XSD ( XML Schemas Definition )。 XML Schema 描述了 XML 文档的结构。 可以用一个指定的 XML Schema 来验证某个 XML 文档 , 以检查该 XML 文档是否符合其要求。 文档设计者可以通过 XML Schema 指定 XML 文档所允许的结构和内容,并可据此检查 XML 文档是否是有效的。 XML Schema 本身是 XML 文档 , 它符合 XML 语法结构。 可以用通用的 XML 解析器解析它。
  在使用XML Schema 文档对XML 实例文档进行检验,除了要声明名称空间外 ( xmlns=http://www.Springframework.org/schema/beans ),还必须指定该名称空间所对应的 XML Schema 文档的存储位置。 通过 schemaLocation 属性来指定名称空间所对应的 XML Schema 文档的存储位置,它包含两个部分, 一部分是名称空间的 URI,另一部分就是该名称空间所标识的 XML Schema文件位置或URL地址(xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.Springframework.org/schema/beans/Spring-beans.xsd
clipboard

4.2、验证模式的读取

  了解了 DTD 与 XSD 的区别后,再去分析 Spring 中对于验证模式的提取就有概念了。

  无非是如果设定了验证模式则使用设定的验证模式(可以通过对调用 XmlBeanDefinitionReader中的 setValidationMode 方法进行设定),否则使用自动检测的方式 。
clipboard

  而自动检测验证模式的功能是在函数 detectValidationMode 方法中实现的,在detectValidationMode() 函数中又将自动检测验证模式的工作委托给了专门处理类 XmlValidationModeDetector的validationModeDetector() 方法,具体代码如下:
clipboard
clipboard

5、获取Document

  同样,XmlBeanDefinitionReader 类对于文档读取并没有亲力亲为,而是委托给了 DocumentLoader 去执行, 这里的 DocumentLoader 是个接口,而真正调用的是DefaultDocumentLoader,解析代码如下:
①、首先创建 DocumentBuilderFactory;
②、再通过 DocumentBuilderFactory 创建 DocumentBuilder
③、进而解析 InputSource 来返回 Document 对象。
clipboard

  这里有必要提及一下 EntityResolver,对于参数entity Resolver,传入的是通过 getEntityResolver()函数获取的返回值,如下代码:
clipboard
clipboard

为什么要向SAX驱动器注册一个实例?
  对于解析一个 XML, SAX 首先读取该 XML 文档上的声明,根据声明去寻找相应的 DTD定义,以便对文档进行一个验证。 默认的寻找规则,即通过网络(实现上就是声明的 DTD 的 URI 地址)来下载相应的 DTD 声明,并进行认证。下载的过程是一个漫长的过程,而且当网络中断或不可用时,这里会报错。
  EntityResolve的作用是项目本身就可以提供一个如何寻找 DTD 声明的方法,即由程序来实现寻找 DTD 声明的过程,比如我们将 DTD 文件放到项目中某处,在实现时直接将此文档读取并返回给 SAX 即可。 这样就避免了通过网络来寻找相应的声明。
clipboard
clipboard

  DelegatingEntityResolver是EntityResolver的实现类,spring中通过DelegatingEntityResolver向SAX驱动器注册一个实例。
clipboard
clipboard

  我们可以看到,对不同的验证模式, Spring 使用了不同的解析器解析。 这里简单描述一下原理,比如加载DTD类型的 BeansDtdResolver 的 resolveEntity()是直接截取systemld最后的 xx.dtd,然后去当前路径下寻找,而加载 XSD 类型的 PluggableSchemaResolver 类的 resolveEntity() 是默认到 META-INF/Spring.schemas 文件中找到 systemid 所对应的 XSD 文件并加载。
clipboard

6、解析及注册BeanDefinitions

  当文件转换为document之后,接下来就是注册bean,当程序已经拥有XML文档文件的Document实例对象时,就会被引入下面这个方法。
clipboard
clipboard
clipboard

/**
 * Register each bean definition within the given root {@code <beans/>} element.
 */
protected void doRegisterBeanDefinitions(Element root) {
   // Any nested <beans> elements will cause recursion in this method. In
   // order to propagate and preserve <beans> default-* attributes correctly,
   // keep track of the current (parent) delegate, which may be null. Create
   // the new (child) delegate with a reference to the parent for fallback purposes,
   // then ultimately reset this.delegate back to its original (parent) reference.
   // this behavior emulates a stack of delegates without actually necessitating one.
   /**
   * 在这个方法中,任何嵌套的<beans>元素都将导致递归。
   * 为了正确地传播和保存<beans> default-*属性,
   * 请跟踪当前(父)委托,该委托可能为null。
   * 创建新的(子)委托,并使用对父委托的引用进行回退,
   * 然后最终将this.delegate重置为其原始的(父)引用。
   * 此行为模拟委托堆栈,而不实际需要一个委托。
   **/
   BeanDefinitionParserDelegate parent = this.delegate;
   this.delegate = createDelegate(getReaderContext(), root, parent);

   if (this.delegate.isDefaultNamespace(root)) {
      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.isInfoEnabled()) {
               logger.info("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;
}

  preProcessXml(root)或者postProcessXml(root)发现代码是空的:面向对象设计方法学中有一个概念, 一个类要么是面向继承的设计的,要么就用final 修饰。然而DefaultBeanDefinitionDocumentReader中并没有用final修饰,因此,这里的两个方法都是面向子类设计的,这个是模板方法模式。如果继承自DefaultBeanDefinitionDocumentReader的子类需要在Bean解析前后做一些处理的话,那么只需要重写这两个方法就可以了。

6.1、profile属性的使用

  注册Bean 的最开始是对PROFILE_ATTRIBUTE 属性的解析,可能对于我们来说,profile属性并不是很常用。
  官方代码如下:

<beans xmlns= "http://www.Springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:jdbc="http://www.Springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi : schemaLocation= "...">
    .........
<beans profile= "dev">
    ... ...
</beans>
    <beans profile ="production" >
</beans>
</beans>

  集成到Web环境中时,在web.xml中加入以下代码:

<context-param>
<param-name>Spring.profiles.active</param-name>
<param-value>dev</param-value>
</context param>

  有了这个特性我们就可以同时在配置文件中部署两套配置来适用于生产环境和开发环境,这样可以方便的进行切换开发、部署环境。
  了解了profile 的使用再来分析代码会清晰得多, 首先程序会获取beans 节点是否定义了profile 属性,如果定义了则会需要到环境变量中去寻找,所以这里首先断言environment 不可能为空, 因为profile 是可以同时指定多个的,需要程序对其拆分,并解析每个profile 是都符合环境变量中所定义的,不定义则不会浪费性能去解析。

6.2、解析并注册BeanDefinition

clipboard
默认Bean标签:

<bean id="test" class="test.TestBean"/>

自定义Bean标签:

<tx:annotation-driven/>

  对于根节点或者子节点如果是默认命名空间的话则采用parseDefaultElement() 函数进行解析,否则使用 delegate.parseCustomElement() 函数对自定义命名空间进行解析。而判断是否默认命名空间还是自定义命名空间的办法其实是使用node.getNamespaceURI()函数获取命名空间,并与Spring 中固定的命名空间http://www.springframework.org/scherna/beans 进行比对:如果一致,则默认为时默认命名空间,否则就认为是自定义。

posted @ 2025-12-23 20:22  Carey_ccl  阅读(13)  评论(0)    收藏  举报