二、Spring资源封装与资源加载具体实现

在Java中,将不同来源的资源抽象成URL,通过注册不同的handler(URLStreamHandler)来处理不同来源的读取逻辑,一般handler的类型使用不同的前缀(协议)来识别,如:“file:”、“http:”、“jar:”等,然而URL没有默认定义相对ClassPath或ServletContext等资源的handler,虽然可以注册自己的URLStreamHandler来解析特定的URL前缀(协议),比如:“classpath:”,然而这需要了解URL的实现机制,而且URL也没有提供基本的方法,如检查当前资源是否存在、检查当前资源是否可读等方法。

所以Spring对其内部使用到的资源实现了自己的抽象结构。

配置文件封装

InputStreamSource

public interface InputStreamSource {
	// 为基础资源的内容返回一个 InputStream 
	InputStream getInputStream() throws IOException;
}

InputStreamSource接口封装任何能返回InputStream的类,比如File、Classpath下的资源和ByteArray等。

它只定义了一个方法:getInputStream(),该方法返回一个新的InputStream对象。

Resource

public interface Resource extends InputStreamSource {
    boolean exists();

    default boolean isReadable() {
        return this.exists();
    }

    default boolean isOpen() {
        return false;
    }

    default boolean isFile() {
        return false;
    }

    URL getURL() throws IOException;

    URI getURI() throws IOException;

    File getFile() throws IOException;

    default ReadableByteChannel readableChannel() throws IOException {
        return Channels.newChannel(this.getInputStream());
    }

    long contentLength() throws IOException;

    long lastModified() throws IOException;

    Resource createRelative(String var1) throws IOException;

    @Nullable
    String getFilename();

    String getDescription();
}

Resource接口抽象了所有Spring内部使用到的底层资源:File、URL、Classpath等。

可以看到Resource接口继承了InputStreamSource接口,并且在此基础上进行了扩展。

首先它定义了3个判断当前资源状态的方法:存在性(exists)、可读性(isReadable)、是否处于打开状态(isOpen)。

另外还提供了不同资源到URL、URI、File类型的转换,以及获取lastModified属性、文件名(不带路径信息的文件名,getFilename())的方法。

为了便于操作,还提供了基于当前资源创建一个相对资源的方法:createRelative()。

在错误处理中需要详细地打印出错的资源文件,因而还提供了getDescription()方法用来在错误处理中打印信息。

Resource接口有许多实现类,所以不同来源的资源文件都有相应的Resource实现:文件(FileSystemResource)、Classpath资源(ClassPathResource)、URL资源(UrlResource)、InputStream资源(InputStreamResource)、Byte数组(ByteArrayResource)等。

资源文件处理相关类图

资源文件处理相关类图

在日常开发中,资源文件的加载也是经常用到的,可以直接使用Spring提供的类,例如在希望加载文件时可以这样做:

Resource resource = new ClassPathResource("beanFactoryTest.xml");
InputStream inputStream = resource.getInputStream();

得到InputStream对象后,就可以按照之前的方式进行实现了,并且也可以利用Resource及其子类为我们提供的诸多特性。

配置文件封装逻辑

有了Resource接口便可以对所有资源文件进行统一处理。

至于实现,也非常简单,以getInputStram()为例,ClassPathResource中的实现方式便是通过class或者classLoader提供的底层方法进行调用,而对于FileSystemResource的实现其实更简单,直接使用FileInputStream对文件进行实例化。

ClasspathResource

// 此实现为给定类路径资源打开输入流
public InputStream getInputStream() throws IOException {
    InputStream is;
    if (this.clazz != null) {
        is = this.clazz.getResourceAsStream(this.path);
    } else if (this.classLoader != null) {
        is = this.classLoader.getResourceAsStream(this.path);
    } else {
        is = ClassLoader.getSystemResourceAsStream(this.path);
    }

    if (is == null) {
        throw new FileNotFoundException(this.getDescription() + " cannot be opened because it does not exist");
    } else {
        return is;
    }
}

FileSystemResource

// 此实现会为基础文件打开 NIO 文件流
public InputStream getInputStream() throws IOException {
    try {
        return Files.newInputStream(this.file.toPath());
    }
    catch (NoSuchFileException ex) {
        throw new FileNotFoundException(ex.getMessage());
    }
}

当通过Resource相关类完成了对配置文件进行封装后配置文件的读取工作就全权交给XmlBeanDefinitionReader来处理了。

配置文件加载

了解了Spring中将配置文件封装为Resource类型的实例方法后,就可以继续看XmlBeanFactory的初始化过程中配置文件是如何加载的了,XmlBeanFactory的初始化有若干方法,Spring中提供了很多构造函数,但我们这里只看使用Resource实例作为构造函数参数的方法。

加载资源实现

XmlBeanFactory

@SuppressWarnings({"serial", "all"})
public class XmlBeanFactory extends DefaultListableBeanFactory {

    // 全局属性用来扫描加载配置资源文件
    private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);

    // 使用给定的资源创建一个新的 XmlBeanFactory,该资源必须可以使用 DOM 进行解析
    public XmlBeanFactory(Resource resource) throws BeansException {
        // 调用XmlBeanFactory(Resource,BeanFactory)构造方法
        this(resource, null);
    }
	// 使用给定的输入流创建一个新的 XmlBeanFactory,该输入流必须可以使用 DOM 进行解析
    public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
        super(parentBeanFactory);
        this.reader.loadBeanDefinitions(resource);
    }
}

可以看到方法中this.reader.loadBeanDefinitions(resource)才是资源加载的真正实现。

但是在XmlBeanDefinitionReader加载数据前还有一个调用父类构造方法初始化的过程:super(parentBeanFactory),我们跟踪代码进入AbstractAutowireCapableBeanFactory的构造方法中。

AbstractAutowireCapableBeanFactory

// 创建一个新的 AbstractAutowireCapableBeanFactory
public AbstractAutowireCapableBeanFactory() {
    super();
    ignoreDependencyInterface(BeanNameAware.class);
    ignoreDependencyInterface(BeanFactoryAware.class);
    ignoreDependencyInterface(BeanClassLoaderAware.class);
}

可以看到执行了若干次ignoreDependencyInterface方法。

ignoreDependencyInterface的主要功能是忽略给定接口的自动装配功能。

举例来说:

当A中有属性B,那么当Spring在获取A的Bean的时候如果其属性B还没有初始化,那么Spring会自动初始化B,这也是Spring中提供的一个重要特性。

但是,某些情况下,B不会被初始化,其中的一种情况就是B实现了BeanNameAware接口。

Spring中这样介绍:自动装配时忽略给定的依赖接口,典型应用是通过其他方式解析Application上下文注册依赖,类似于BeanFactory通过BeanFactoryAware进行注入或者ApplicationContext通过ApplicationAware进行注入。

核心方法loadBeanDefinitions

我们通过查看XmlBeanFactory以Resource实例进行构造的代码得知,在XmlBeanFactory构造方法内部通过调用XmlBeanDefinitionReader类型的reader属性提供的this.loadBeanDefinitions(resource)来进行资源文件的加载,这句代码也是整个资源加载的切入点,因此也是核心方法,那我们就先通过方法执行时序图来看看到底做了些什么事情。

loadBeanDefinitions 方法执行时序图

loadBeanDefinitions方法执行时序图

根据时序图我们看到,兜兜转转还在原地踏步,没有真真正的切入主题,一直在做准备工作。

通过时序图梳理整个的处理过程:

  1. 封装资源。当进入XmlBeanDefinitionReader后首先对参数Resource使用EncodedResource类进行封装
  2. 获取输入流。从Resource中获取对应的InputStream并构造InputStream。
  3. 通过构造的InputSource实例和Resource实例继续调用函数doLoadBeanDefinitions

接下来就让我们一起来看看loadBeanDefinition方法具体的实现过程。

EncodedReader

我们先简单的对EncodedResource做个介绍,通过名称,可以大致推断整个类主要用于对资源文件的编码进行处理。

主要逻辑体现在getReader()方法中,当设置了编码属性的时候Spring会使用相应的编码作为输入流的编码。

// 使用指定的 或编码(如果有)为指定的Charset资源打开
public Reader getReader() throws IOException {
    if (this.charset != null) {
        return new InputStreamReader(this.resource.getInputStream(), this.charset);
    }
    else if (this.encoding != null) {
        return new InputStreamReader(this.resource.getInputStream(), this.encoding);
    }
    else {
        return new InputStreamReader(this.resource.getInputStream());
    }
}

上面代码构造了一个有编码(encoding)的InputStreamReader。

XmlBeanDefinitionReader

// 从指定的 XML 文件装入 Bean 定义
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
    return loadBeanDefinitions(new EncodedResource(resource));
}

当构造好encodedResource对象后,再次转入了loadBeanDefinitions(new EncodedResource(resource))。

这个方法内部才是真正的数据准备阶段,也是时序图所描述的逻辑。

// 从指定XML文件加载bean的定义
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
    Assert.notNull(encodedResource, "EncodedResource must not be null");
    if (logger.isInfoEnabled()) {
        logger.info("Loading XML bean definitions from " + encodedResource);
    }
    // 通过属性来记录已经加载的资源
    Set<EncodedResource> 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 {
        // 从 encodedResource中获取已经封装的 Resource 对象并再次从 Resource 中获取其中的 inputStream
        InputStream inputStream = encodedResource.getResource().getInputStream();
        try {
            // InputSource 这个类不来自于 Spring ,它的全路径是 org.xml.sax.InputSource
            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();
        }
    }
}

首先对传入的resource参数做封装,目的是考虑到Resource可能存在编码要求的情况。

其次通过SAX读取XML文件的方式来准备InputSource对象。

最后将准备的数据通过参数传入真正的核心处理部分doLoadBeanDefinitions(inpuSource,encodedResource.getResource())

接下来我们来查看doLoadBeanDefinitions方法内部是怎样做的。

// 实际上从指定的 XML 文件加载 Bean 定义
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
    throws BeanDefinitionStoreException {
    try {
        Document doc = doLoadDocument(inputSource, resource);
        return registerBeanDefinitions(doc, resource);
    }
    catch (BeanDefinitionStoreException ex) {
        throw ex;
    }
    catch (SAXParseException ex) {
        throw new XmlBeanDefinitionStoreException(resource.getDescription(),
                                                  "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
    }
    catch (SAXException ex) {
        throw new XmlBeanDefinitionStoreException(resource.getDescription(),
                                                  "XML document from " + resource + " is invalid", ex);
    }
    catch (ParserConfigurationException ex) {
        throw new BeanDefinitionStoreException(resource.getDescription(),
                                               "Parser configuration exception parsing XML from " + resource, ex);
    }
    catch (IOException ex) {
        throw new BeanDefinitionStoreException(resource.getDescription(),
                                               "IOException parsing XML document from " + resource, ex);
    }
    catch (Throwable ex) {
        throw new BeanDefinitionStoreException(resource.getDescription(),
                                               "Unexpected exception parsing XML document from " + resource, ex);
    }
}

上面代码如果不考虑异常类的代码,其实只做了三件事。

  • 获取对XML文件的验证模式。
  • 加载XML文件,并得到对应的Document。
  • 根据返回的Document注册bean信息。

这三个步骤支撑着整个Spring容器部分的实现。

对配置文件的解析非常重要,逻辑也很复杂。

因此后面我们就来看看Spring中是如何获取XML文件的验证模式并进行解析的


posted @ 2023-09-17 00:28  困意总是偷袭我  阅读(49)  评论(0)    收藏  举报