资讯专栏INFORMATION COLUMN

Spring源码一(容器的基本实现1)

awokezhou / 1400人阅读

摘要:下面跟踪代码到这个实现中看看是怎么做的在实例化的过程中,在构造函数中调用了其超类的构造函数,而在超类中对其所处换环境进行的判断,所谓的环境呢,事实上指得就是是通过,还是通过加载的上下文,这也就意味着不同方式加载可能存在某些不同。

前言

本文基于《Spring源码深度解析》学习, 《Spring源码深度解析》讲解的Spring版本低于Spring3.1,当前阅读的版本为Spring5.x,所以在文章内容上会有所不同。
这篇文章基于有一定Spring 基础的人进行讲解,所以有些问题并不做详细的实现, 如有分析不合理或是错误的地方请指教指正,不胜感激。

一、在非ApplicationContext下使用

在《Spring源码深度解析》中有这样一个实例:

public class BeanFactoryTest {
    @test
    public void testSimpleLoad() {
        BeanFactory bf = new XmBeanFactory(new ClassPathResource("beanFactoryTest.xml");
        MyTestBean bean = (MyTestBean) bf.getBean("myTestBean");
        assertEquals("testStr", bean.getTestStr());        
    }
}

当然在这里会有一个Spring的配置文件 beanFactoryTest.xml, 当使用xml文件的时候,会发现文件头有一些
这样的标签, 建议学习一下DOM,DOM2, DOM3结构, 以便更加清晰的了解xml文件头中的内容的真正意义。
这里的配置文件只写一个相关的bean

这段代码的作用就是以下几点:

读取配置文件。

在Spring的配置中找到bean,并实例化。

使用断言判断实例的属性。

@deprecated as of Spring 3.1 in favor of {@link DefaultListableBeanFactory} and {@link XmlBeanDefinitionReader}
这是该类在当前版本的部分注释,在Spring3.1以后这个类被弃用了,Spring官方不建议使用这个类。建议使用以上这两个类。XmlBeanDefinitionReader本就是以前定义在这个类中的一个final的实例,而DefaultListableBeanFactory则是该类的超类。加载配置文件可以这样使用:

 Resource resource = new ClassPathResource("beanFactoryTest.xml");
 BeanFactory beanFactory = new DefaultListableBeanFactory();
 BeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader((BeanDefinitionRegistry) beanFactory);
 beanDefinitionReader.loadBeanDefinitions(resource);
 MyTestBean bean = (MyTestBean) bf.getBean("myTestBean");
 assertEquals("testStr", bean.getTestStr());

这个过程和上面的过程实际上的实现只用一点不同、前者是在创建时就直接实例化了bean, 后者则是在加载的时候才实例化bean:

读取配置文件。

创建BeanFactory。

创建BeanDefinitionReader。

加载resource资源。

获取bean实例(实例化bean)。

使用断言判断实例的属性。

事实上在实际的使用中,绝大多数时候都会通过以下这种ApplicationContext的方式来加载Spring的配置文件并进行解析, 以后会写到这里的实现:

ApplicationContext sc = new ClassPathXmlApplicationContext("applicationContext.xml");

二、加载并解析配置文件
 Resource resource = new ClassPathResource("beanFactoryTest.xml");

通过ClassPathResource 加载配置文件,并构建该实例的时候,是使用Resource接口进行定义的, 这也就说明了创建的实际上是Resource的实例,通过查看Resource 的源码不难发现,Resource对Java中将要使用的资源进行了抽象,Spring的设计中几乎所有可以加载资源的
类需要直接或间接的实现Resource 这个接口。下面可以看一下这个接口:

boolean exists();    // 判断是否资源是否存在
default boolean isReadable() {  // 判断资源是否可读
    return exists();
}
default boolean isOpen() { // 判断文件是否打开
    return false;
}
default boolean isFile() { // 判断文件是否是文件系统中的文件,Spring5.0后加入的
    return false;
}
URL getURL() throws IOException; // 获取文件的URL

URI getURI() throws IOException; // 获取文件的URI
File getFile() throws IOException; // 获取文件
default ReadableByteChannel readableChannel() throws IOException {  // 返回一个Channel, 拥有最大效率的读操作
    return Channels.newChannel(getInputStream());
}
long contentLength() throws IOException; // 返回资源解析后的长度
long lastModified() throws IOException; // 最后一次休干时间
Resource createRelative(String relativePath) throws IOException; // 基于当前资源创建一个相对资源
@Nullable
String getFilename(); // 获取文件名  for example, "myfile.txt"
String getDescription(); // 获取资源描述, 当发生错误时将被打印


通过查看源码,还有一点可以发现, Resource接口继承了InputStreamSource 接口,下面来看下这个接口:

public interface InputStreamSource {

    /**
     * Return an {@link InputStream} for the content of an underlying resource.
     * 

It is expected that each call creates a fresh stream. *

This requirement is particularly important when you consider an API such * as JavaMail, which needs to be able to read the stream multiple times when * creating mail attachments. For such a use case, it is required * that each {@code getInputStream()} call returns a fresh stream. * @return the input stream for the underlying resource (must not be {@code null}) * @throws java.io.FileNotFoundException if the underlying resource doesn"t exist * @throws IOException if the content stream could not be opened */ InputStream getInputStream() throws IOException; }

这个接口的作用非常简单并且是顶层接口,它的作用就是返回一个InputStream, 最简单的作用却提供了最大的方便, 因为所有资源加载类几乎都直接或间接的实现了Resource, 这也就意味着, 几乎所有的资源加载类都可以得到一个InputStream, 这将使得在资源加载之后能轻易地得到一个InputStream, 这非常重要。通过InputStream, Spring使所有的资源文件都能进行统一的管理了, 优点是不言而喻的。至于实现是非常简单的, ClassPathResource 中的实现方式便是通过class或者classLoader提供的底层方法进行调用的, 对于FileSystemResource的实现其实更加简单, 就是直接使用FileInputStream对文件进行实例化。

三、DefaultListableBeanFactory、XmlBeanDefinitionReader和BeanDefinitionRegistry

配置文件加载完成后进行的下一步操作是这样的,这和3.1之前的版本不太一样,至于为什么要弃用XmlBeanFactory(我猜是为了对其他部分进行设计,从而让这部分代码更加充分的进行解耦):

 BeanFactory beanFactory = new DefaultListableBeanFactory();
 BeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader((BeanDefinitionRegistry) beanFactory);
 beanDefinitionReader.loadBeanDefinitions(resource);

配置文件加载完成后,创建了一个BeanFactory的实例。DefaultListableBeanFactory: 见名知意,这是一个默认可列的bean工厂类,注释用说道,典型的一个应用就是在第一次定义时注册所有bean。接下来的操作是利用多态将上一步创建的BeanFactory的实例转成BeanDefinitionRegistry, 因为下一步需要读取xml文件中定义的内容,这也就是XmlBeanDefinitionReader的作用,而XmlBeanDefinitionReader在实例化的时候需要一个bean的定义注册机,所以就进行了以上操作, 事实上:在创建BeanFactory实例时,同样可以定义为BeanDefinitionRegistry类型。下面详细说下一这三个类的作用:

DefaultListableBeanFactory:在定义时通过当前类的类加载器(如果不存在就向上级加载器寻找直到系统加载器)下的所有的bean进行注册,注意这里只是进行注册而已。

BeanDefinitionRegistry: 为了注册Spring持有的bean的一个接口,是在BeanFactory,AbstractBeanDefinition中间的一层接口。

XmlBeanDefinitionReader:注释中这样写道

    /**
     * Bean definition reader for XML bean definitions.
     * Delegates the actual XML document reading to an implementation
     * of the {@link BeanDefinitionDocumentReader} interface.
     *
     * 

Typically applied to a * {@link org.springframework.beans.factory.support.DefaultListableBeanFactory} * or a {@link org.springframework.context.support.GenericApplicationContext}. * *

This class loads a DOM document and applies the BeanDefinitionDocumentReader to it. * The document reader will register each bean definition with the given bean factory, * talking to the latter"s implementation of the * {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} interface. */ 只说最重要的一个部分, 在这里它需要委托一个真正的xml文档读取器来读取文档内容,也就是BeanDefinitionDocumentReader,而这个文档读取器将读取所有的bean注册内容,而这些资源正是1、2中所得到的。

接下来就是重要的一步,beanDefinitionReader.loadBeanDefinitions(resource); 在解析了配置文件中的bean后,事实上配置文件中bean并没有被真正的加载,并且上面的步骤也只是对所有的bean进行了一次注册, 所以,这个时候load了resoure中的内容, 在编码没有问题以后,并且resource中bean可以在类加载器下找到这些类,这时就对这些bean进行加载,实例化。下面跟踪代码到这个实现中看看Spring 是怎么做的:

/**
     * Create new XmlBeanDefinitionReader for the given bean factory.
     * @param registry the BeanFactory to load bean definitions into,
     * in the form of a BeanDefinitionRegistry
     */
    public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
        super(registry);
    }


protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) {
        Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
        this.registry = registry;

        // Determine ResourceLoader to use.
        if (this.registry instanceof ResourceLoader) {
            this.resourceLoader = (ResourceLoader) this.registry;
        }
        else {
            this.resourceLoader = new PathMatchingResourcePatternResolver();
        }

        // Inherit Environment if possible
        if (this.registry instanceof EnvironmentCapable) {
            this.environment = ((EnvironmentCapable) this.registry).getEnvironment();
        }
        else {
            this.environment = new StandardEnvironment();
        }
    }

在实例化XmlBeanDefinitionReader 的过程中,在构造函数中调用了其超类的构造函数,而在超类中对其所处换环境进行的判断,所谓的环境呢,事实上指得就是是通过BeanFactory, 还是通过ApplicationContext加载的上下文,这也就意味着不同方式加载可能存在某些不同。写这些的目的其实是为了引出这里的一个我们十分关注的东西, 就是自动装配。在AbstractAutowireCapableBeanFactory这个抽象类的构造方法中实现了相关的自动装配,在BeanDefinitionRegistry 和DefaultListableBeanFactory中都继承了这个抽象类, 并在其构造函数内直接调用了其超类的构造函数也就是:

/**
     * Create a new AbstractAutowireCapableBeanFactory.
     */
    public AbstractAutowireCapableBeanFactory() {
        super();
        ignoreDependencyInterface(BeanNameAware.class);
        ignoreDependencyInterface(BeanFactoryAware.class);
        ignoreDependencyInterface(BeanClassLoaderAware.class);
    }
这里有必要提及一下ignoreDependencyInterface();这个方法。它的主要功能就是忽略接口中的自动装配, 那么这样做的目的是什么呢?会产生什么样的效果呢?举例来说, 当A中有属性B, 那么Spring在获取A的时候就会去先去获取B, 然而有些时候Spring不会这样做,就是Spring通过BeanNameAware、BeanFactoryAware和BeanClassLoaderAware进行注入的, 也就是根据环境的不同, Spring会选择相应的自从装配的方式。在不是当前环境中的注入,Spring并不会再当前环境对Bean进行自动装配。类似于,BeanFactory通过BeanFactoryAwar进行注入或者ApplicationContext通过ApplicationContextAware进行注入。

经过了这么长时间的铺垫,终于应该进入正题了, 就是进入通过loadBeanDefinitions(resource)方法加载这个文件。这个方法这样实现:

 public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
        Assert.notNull(encodedResource, "EncodedResource must not be null");
        if (logger.isTraceEnabled()) {
            logger.trace("Loading XML bean definitions from " + encodedResource);
        }

        Set currentResources = this.resourcesCurrentlyBeingLoaded.get();
        if (currentResources == null) {
            currentResources = new HashSet<>(4);
            this.resourcesCurrentlyBeingLoaded.set(currentResources);
        }
        if (!currentResources.add(encodedResource)) {
            throw new BeanDefinitionStoreException(
                    "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
        }
        try {
            InputStream inputStream = encodedResource.getResource().getInputStream();
            try {
                InputSource inputSource = new InputSource(inputStream);
                if (encodedResource.getEncoding() != null) {
                    inputSource.setEncoding(encodedResource.getEncoding());
                }
                return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
            }
            finally {
                inputStream.close();
            }
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException(
                    "IOException parsing XML document from " + encodedResource.getResource(), ex);
        }
        finally {
            currentResources.remove(encodedResource);
            if (currentResources.isEmpty()) {
                this.resourcesCurrentlyBeingLoaded.remove();
            }
        }
    }

这段代码很容易理解,不过真正实现的核心代码是在return doLoadBeanDefinitions(inputSource, encodedResource.getResource());这里实现的。下面是这个方法的核心代码:

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
            throws BeanDefinitionStoreException {

        try {
            Document doc = doLoadDocument(inputSource, resource);
            int count = registerBeanDefinitions(doc, resource);
            if (logger.isDebugEnabled()) {
                logger.debug("Loaded " + count + " bean definitions from " + resource);
            }
            return count;
        }
}

这段处理可以说非常容易了,对resource流进行再封装,封装为Docment对象,然后解析并注册这个doc中的bean对象,返回定义的bean的个数。在Spring3.1之前上面这个方法中还要验证加载Xml是否符合规范。而Spring5.x之后Spring将验证的工作放到了获取Document中。

三、获取Document

看一下Document doc = doLoadDocument(inputSource, resource);这个方法的源码:


protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
        return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
                getValidationModeForResource(resource), isNamespaceAware());
    }

在这个方法中做了三件事:

getEntityResolver();这个方法将根据当前的resource创建一个ResourceLoader实例,然后根据这个对ResourceLoader进行封装,封装为EntityResolver实例, 这个EntityResolver的作用是进行处理实体映射。

getValidationModeForResource(); 这个方法的作用是获取资源的验证模式,通过自动或手动的方式对已经加载到的资源进行检验。这里是真正对xml文件进行验证的地方。

isNamespaceAware(); 这个方法用来判断加载的xml文件是否支持明明空间。

实现上面方法的类继承了这个接口:DocumentLoader,并且实现了这个接口中的唯一的抽象:

/**
 * Load a {@link Document document} from the supplied {@link InputSource source}.
 * @param inputSource the source of the document that is to be loaded
 * @param entityResolver the resolver that is to be used to resolve any entities
 * @param errorHandler used to report any errors during document loading
 * @param validationMode the type of validation
 * {@link org.springframework.util.xml.XmlValidationModeDetector#VALIDATION_DTD DTD}
 * or {@link org.springframework.util.xml.XmlValidationModeDetector#VALIDATION_XSD XSD})
 * @param namespaceAware {@code true} if support for XML namespaces is to be provided
 * @return the loaded {@link Document document}
 * @throws Exception if an error occurs
 */

Document loadDocument(

    InputSource inputSource, EntityResolver entityResolver,
    ErrorHandler errorHandler, int validationMode, boolean namespaceAware)
    throws Exception;

那么详细讲一下上面提及的EntityResolver, 如果SAX(Simple API for XML:简单的来讲,它的作用就是不去构建DOM,而是以应用程序的方式以最有效率的方式实现XML与应用实体之间的映射;当然还有一种方式是解析DOM,具体两种方式,我也没有做过相应深入探究)应用驱动程序需要实现自定义的处理外部实体,在必须实现此接口并通过某种方式向SAX驱动器注册一个实例。这需要根据XML头部的DTD中的网络地址下载声明并认证,而EntityResolver实际上就是在提供一个寻dtd找声明的方法,这样就可以在项目中直接定义好声明,而通过本地寻找的方式避免了网络寻找的过程,编译器也避免了在网络延迟高或没有网络的情况下报错。

四、解析及注册BeanDefinitions
当文件转换为Document后,接下来提取及注册Bean就是我们后头的重头戏。事实上,这一部分内容并不会在我们的使用中出现了。

同样在XmlBeanDefinitionReader这个类中,可以发现随着Docment的获取完成后,直接做的是下面的这个事情registerBeanDefinitions();

/**
 * Register the bean definitions contained in the given DOM document.
 * Called by {@code loadBeanDefinitions}.
 * 

Creates a new instance of the parser class and invokes * {@code registerBeanDefinitions} on it. * @param doc the DOM document * @param resource the resource descriptor (for context information) * @return the number of bean definitions found * @throws BeanDefinitionStoreException in case of parsing errors * @see #loadBeanDefinitions * @see #setDocumentReaderClass * @see BeanDefinitionDocumentReader#registerBeanDefinitions */ 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; }

作用在注释中写的很清楚,注册DOM文档(Spring的配置信息中. 也就是解析后的xml)中包含的bean。

首先创建一个bean定义文档读取器,这个对象是根据DefaultBeanDefinitionDocumentReader的class通过反射的方式来创建的,

DefaultBeanDefinitionDocumentReader实现了BeanDefinitionDocumentReader这个接口,这里唯一的抽象方法就是registerBeanDefinitions(Document doc, XmlReaderContext readerContext);,这也就意味着上面的第三行代码是一个自然的应用。

提示:这里的doc参数就是通过之前的doLoadDocument方法获得的,而这很好的应用了面向对象的单一职责原则, 将转换为Docment的复杂过程交给一个单一的类处理,而这个类就是BeanDefinitionDocumentReader, 事实上这是一个接口,而具体的实例化是在createBeanDefinitionDocumentReader这个方法中完成的。

getRegistry();的作用实际上是获得一个BeanDefinitionRegistry对象。下面的图片是程序最开始时,容器开始实现时候的代码,这里可以看到BeanDefinitionRegistry这个接口。注意这里创建的BeanDefinitionRegistry是final的,也就是这里获取的是Spring发现的所有的bean个数,是不许改变的, 熟悉设计模式的同学肯定知道,这个BeanDefinitionRegistry是一个单例的

而接下来做的就是就是,记录所有对我们的资源文件进行加载,这里是真正解析xml文件并加载的地方,而这个逻辑就是那么简单了, 先统计当前的bean defintions个数然后加载一些bean定义进来,然后在统计bean 的个数,然后用后来的减去开始的就是加载的。没错了,就是学前班加减法。

到这里我已经不想探究xml文件是如何读取的了,如果想看的话,可以去看下一篇《Spring源码一(容器的基本实现2)》!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72023.html

相关文章

  • Spring IOC 容器源码分析系列文章导读

    摘要:本文是容器源码分析系列文章的第一篇文章,将会着重介绍的一些使用方法和特性,为后续的源码分析文章做铺垫。我们可以通过这两个别名获取到这个实例,比如下面的测试代码测试结果如下本小节,我们来了解一下这个特性。 1. 简介 Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本。经过十几年的迭代,现在的 Spring 框架已经非常成熟了...

    NSFish 评论0 收藏0
  • Spring源码容器基本实现2)

    摘要:进一步解析其他所有属性并统一封装至类型的实例中。是一个接口,在中存在三种实现以及。通过将配置文件中配置信息转换为容器的内部表示,并将这些注册到中。容器的就像是配置信息的内存数据库,主要是以的形式保存。而代码的作用就是实现此功能。 前言:继续前一章。 一、porfile 属性的使用 如果你使用过SpringBoot, 你一定会知道porfile配置所带来的方便, 通过配置开发环境还是生产...

    yagami 评论0 收藏0
  • Java深入-框架技巧

    摘要:从使用到原理学习线程池关于线程池的使用,及原理分析分析角度新颖面向切面编程的基本用法基于注解的实现在软件开发中,分散于应用中多出的功能被称为横切关注点如事务安全缓存等。 Java 程序媛手把手教你设计模式中的撩妹神技 -- 上篇 遇一人白首,择一城终老,是多么美好的人生境界,她和他历经风雨慢慢变老,回首走过的点点滴滴,依然清楚的记得当初爱情萌芽的模样…… Java 进阶面试问题列表 -...

    chengtao1633 评论0 收藏0
  • 起来读Spring源码吧(容器初始化

    摘要:对于开发者来说,无疑是最常用也是最基础的框架之一。概念上的东西还是要提一嘴的用容器来管理。和是容器的两种表现形式。定义了简单容器的基本功能。抽象出一个资源类来表示资源调用了忽略指定接口的自动装配功能委托解析资源。 对于Java开发者来说,Spring无疑是最常用也是最基础的框架之一。(此处省略1w字吹Spring)。相信很多同行跟我一样,只是停留在会用的阶段,比如用@Component...

    libxd 评论0 收藏0

发表评论

0条评论

awokezhou

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<