Spring 资源的定位与解析

开胃菜

一些基础的名词解释

BeanDefinition

BeanDefiniton是Spring内部对Bean数据的抽象,一个Bean在Spring中的表现形式即为一个BeanDefiniton

BeanFactory

BeanFactory是Spring中容器的最基本形态,BeanFactory接口定义了一个容器最基本的功能

若我们对水桶进行抽象,最基本的水桶需要的功能就是可以装水,可以有一个把手供我们提起,BeanFactory就类似这个水桶的定义

FactoryBean

FactoryBean很容易和BeanFactory混淆,BeanFactory代表容器本身而FactoryBean代表容器中存储的Bean

也就是BeanFactory是水桶,FactoryBean是水桶中的水

ApplicationContext

ApplicationContext是Spring中容器的高级形态,在BeanFactory的基础上新增了很多功能

BeanFactory是水桶的定义,ApplicationContext就可是桶装水可以装水且拥有更高级的功能

DefaultListableBeanFactory

DefaultListableBeanFactory是Spring中对容器最基本的实现,它间接实现了BeanFactory接口

BeanFactory是水桶的规格参数定义了水桶是什么,DefaultListableBeanFactory就是个木桶,真正的产品

Ioc的流程介绍

测试类编写

编译项目的最后编写了一组简单的测试类,使用了ClassPathXmlApplicationContext类加载xml文件,很显然这个类属于ApplicationContext是一种高级的容器。这次我们用一种低级的方案来编写一组测试类

可以看到代码多了一些,简单分析一下这组代码

public class TestUserMessage {
	@Test
	public void TestDemo01() {
		DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
		XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(defaultListableBeanFactory);
		xmlBeanDefinitionReader.loadBeanDefinitions(new ClassPathResource("ApplicationContext.xml"));
		UserMessage jelly = defaultListableBeanFactory.getBean(UserMessage.class);
		System.out.println(jelly);
    }
}
  • 首先使用DefaultListableBeanFactory的实例作为容器的基础实现,也就是水桶
  • 新建XmlBeanDefinitionReader类将defaultListableBeanFactory绑定
  • 使用XmlBeanDefinitionReader的实例读取xml配置文件,将加载的Bean(水)放入DefaultListableBeanFactory(水桶)
  • 使用defaultListableBeanFactory的getBean方法从容器中获取Bean

执行测试类代码,查看结果,和之前相同

1.png

IOC的过程

根据上述的例子可以分析出,SpringIoc的过程大致分三个操作

  • 资源的加载
  • 资源的解析
  • Bean的加载

首先资源的加载,指的是程序通过地址获取到配置文件或者其他资源,将其变为可被解析的类型,交给解析的程序进行解析

资源的解析,通过解析加载好的资源将其解析为BeanDefinition维护到容器当中

Bean的加载,通过容器的getBean方法去容器中查找Bean找到后返回,找不到就通过BeanDefinition创建

本篇就先来挑软柿子捏,介绍资源的定位和加载

ClassPathResource

ClassPathResource是Spring对资源的抽象,通过ClassPathResource可以更方便的操作资源

xmlBeanDefinitionReader.loadBeanDefinitions(new ClassPathResource("ApplicationContext.xml"));

梦开始的地方

Spring将从xml文件中读取BeanDefinition的功能委托给了XmlBeanDefinitionReader类,

进入XmlBeanDefinitionReader类的loadBeanDefinitions方法,本方法即为XmlBeanDefinitionReader读取BeanDefinition的入口方法

本方法的作用仅将Resource使用EncodedResource又包装了一层,通过名字也可以猜到大致含义,EncodedResource是一个带编码类型的资源抽象

@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
    return loadBeanDefinitions(new EncodedResource(resource));
}

进入重载的loadBeanDefinitions(EncodedResource encodedResource)方法,为了方便查找核心代码之后的代码片段会减除不重要的代码(断言、校验、异常处理等)

显然的逻辑还在下层方法doLoadBeanDefinitions中,Spring的一个方法命名习惯是:正常的方法名一般用来做数据校验等非实质性工作,实际进行操作的方法一般以do开头

例如这里的:loadBeanDefinitions做了一些数据有效性的校验,doLoadBeanDefinitions才是真正加载BeanDefinition的地方

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
    // ...
    try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
        InputSource inputSource = new InputSource(inputStream);
        if (encodedResource.getEncoding() != null) {
            inputSource.setEncoding(encodedResource.getEncoding());
        }
        // doLoadBeanDefinitions是加载BeanDefinition的入口
        return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
    }
    // ...
}

进入doLoadBeanDefinitions方法,开头这两个方法就很重量级了,doLoadDocument方法就是资源加载的入口了,registerBeanDefinitions是资源解析的入口。本章节先解析doLoadDocument方法至于资源的解析就留给下章节,记住这个位置的代码,下章的开头要用

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
    throws BeanDefinitionStoreException {
    try {
        // 读取Document的操作通过doLoadDocument方法来执行
        Document doc = doLoadDocument(inputSource, resource);
        // 注册BeanDefinition方法
        int count = registerBeanDefinitions(doc, resource);
        if (logger.isDebugEnabled()) {
            logger.debug("Loaded " + count + " bean definitions from " + resource);
        }
        return count;
    }
    // ...
}

进入doLoadDocument方法,本方法其实还是存在于XmlBeanDefinitionReader类中,在方法中可以看到读取Document的操作是交给了本类的状态documentLoader来做的,这个状态初始化的值是一个实现了DocumentLoader接口的类DefaultDocumentLoader的实例,在前往DefaultDocumentLoader类前我们先解析一下传入loadDocument方法的4个参数的含义

// ...
private DocumentLoader documentLoader = new DefaultDocumentLoader();
// ...
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
    return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
            getValidationModeForResource(resource), isNamespaceAware());
}
  • inputSource,资源本身不用多做解释
  • getEntityResolver()方法,下面小节单独解读
  • getValidationModeForResource()方法,下面小节单独解读
  • isNamespaceAware()方法,namespaceAware在xml中指的是对命名空间的感知,该方法取得是当前类的namespaceAware状态,关于命名空间感知的相关问题,可以查阅《Java核心技术第二卷第三章 3.5节》

GetEntityResolver() 方法

getEntityResolver方法会返回EntityResolver接口的实例,EntityResolver接口用于提供一个查找验证文件的方案,先看一下getEntityResolver方法

通常情况下xml的验证文件需要从网络流中获取,网络首先会有延迟,而且有些程序需要在无网络的情况下启动,为xml解析程序设置一个EntityResolver可以通过EntityResolver中的方法将验证文件指向本地文件,更多相关EntityResolver的知识可以查看《Java核心技术第二卷第三章 3.3.1节》

此处咱们先忽略resourceLoader对该方法的影响,观察ResourceEntityResolver和DelegatingEntityResolver,就能发现ResourceEntityResolver继承了DelegatingEntityResolver,可以预见的是ResourceEntityResolver增强了某些功能,我们暂且不去理会增强了啥,先看DelegatingEntityResolver类做的事情

protected EntityResolver getEntityResolver() {
    if (this.entityResolver == null) {
        // Determine default EntityResolver to use.
        ResourceLoader resourceLoader = getResourceLoader();
        if (resourceLoader != null) {
            this.entityResolver = new ResourceEntityResolver(resourceLoader);
        }
        else {
            this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
        }
    }
    return this.entityResolver;
}

DelegatingEntityResolver实现了EntityResolver接口,此接口是xml解析中规定的规范,上述一直提到的entityResolver实际上指的就是指实现了EntityResolver接口类的实例,EntityResolver接口只有一个方法resolveEntity,此方法规定了去哪里获取验证文件 可以看到spring将获取解析文件的工作交给了两个新的EntityResolver,分别是dtdResolver和schemaResolver它们两个都是EntityResolver的实例,从构造方法中可以看出两个状态实际类型分别是BeansDtdResolver和PluggableSchemaResolver 这里就不再贴代码分析这两个类了,简单的说下结论,对于DTD类型的验证文件,只会去根目录下找spring-beans.dtd这个文件,对于XSD类型的文件会去META-INF/spring.schemas文件中去查找配置的XSD文件位置,记住XSD文件查找的方法和位置,后面自定义标签解析时会用到

public class DelegatingEntityResolver implements EntityResolver {
    // ..
    public DelegatingEntityResolver(@Nullable ClassLoader classLoader) {
        this.dtdResolver = new BeansDtdResolver();
        this.schemaResolver = new PluggableSchemaResolver(classLoader);
    }
    
    @Override
    @Nullable
    public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
            throws SAXException, IOException {

        if (systemId != null) {
            if (systemId.endsWith(DTD_SUFFIX)) {
                // DTD 从这里解析
                return this.dtdResolver.resolveEntity(publicId, systemId);
            } else if (systemId.endsWith(XSD_SUFFIX)) {
                // XSD 这里解析
                return this.schemaResolver.resolveEntity(publicId, systemId);
            }
        }

        // Fall back to the parser's default behavior.
        return null;
    }
    // ...
}

getValidationModeForResource()方法

getValidationModeForResource方法的主要作用是判断xml验证模式,由于spring是支持DTD和XSD的验证模式的,需要根据两种验证模式做一些不同的处理,该方法就是判断验证模式为哪一种,直接进入方法 逻辑也比较简单,设置了就用设置的,没设置就自动判断,判断不出来就默认XSD

protected int getValidationModeForResource(Resource resource) {
    int validationModeToUse = getValidationMode();
    // 若设定了验证模式,则直接使用设定好的
    if (validationModeToUse != VALIDATION_AUTO) {
        return validationModeToUse;
    }
    // 未设定验证模式,自动判断
    int detectedMode = detectValidationMode(resource);
    if (detectedMode != VALIDATION_AUTO) {
        return detectedMode;
    }
    // Hmm, we didn't get a clear indication... Let's assume XSD,
    // since apparently no DTD declaration has been found up until
    // detection stopped (before finding the document's root tag).
    return VALIDATION_XSD;
}

接着跟入自动判断方法detectValidationMode看一眼,spring如何自动判断xml文档的验证模式。将解析文件判断验证模式的工作交给了validationModeDetector状态,该状态为XmlValidationModeDetector类的实例

protected int detectValidationMode(Resource resource){
    // ...
    return this.validationModeDetector.detectValidationMode(inputStream);
    // ...
}

接着进入detectValidationMode方法,很清晰的结构,只要存在DTD的声明DOCTYPE就判断为DTD如果都不存在就判断为XSD,很简单粗暴的判断方式

public int detectValidationMode(InputStream inputStream) throws IOException {
    this.inComment = false;
    // Peek into the file to look for DOCTYPE.
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
        boolean isDtdValidated = false;
        String content;
        while ((content = reader.readLine()) != null) {
            content = consumeCommentTokens(content);
            // 读取到的行为注释跳过
            if (!StringUtils.hasText(content)) {
                continue;
            }
            // 只要有一行中包括DOCTYPE就能确定是DTD验证模式,反之则是XSD
            if (hasDoctype(content)) {
                isDtdValidated = true;
                break;
            }
            if (hasOpeningTag(content)) {
                // End of meaningful data...
                break;
            }
        }
        return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
    }
}

至此对loadDocument方法的入参就分析完毕,接下来就直接进入loadDocument方法当中一探究竟

神明陨落之处

上文说过loadDocument实际上调用的是DefaultDocumentLoader类的实例方法,使用createDocumentBuilderFactory构建DocumentBuilderFactory然后使用createDocumentBuilder构建DocumentBuilder,最后用DocumentBuilder解析资源

public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
        ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
    DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
    DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
    // 用生成的DocumentBuilder解析资源
    return builder.parse(inputSource);
}

createDocumentBuilderFactory()方法

此方法我原称之为神陨落的地方,因为spring解析xml用了dom解析方案,在这里我们看到了最原始的代码 就好比核电站这种高级的发电方式,最终的电能转化方案也只能是烧开水;就算spring是神也得用最基础的xml解析方案,此处就是把spring拉入凡间的第一步 这里就用到入参传入的validationMode也就是验证模式,根据验证模式是否为VALIDATION_NONE来确定是否进行验证,比较有意思的是若验证模式为XSD会将命名空间感知强制设置为true

protected DocumentBuilderFactory createDocumentBuilderFactory(int validationMode, boolean namespaceAware)
        throws ParserConfigurationException {
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(namespaceAware);
    if (validationMode != XmlValidationModeDetector.VALIDATION_NONE) {
        factory.setValidating(true);
        if (validationMode == XmlValidationModeDetector.VALIDATION_XSD) {
            // Enforce namespace aware for XSD...
            factory.setNamespaceAware(true);
            try {
                factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE);
            }
            // catch ...
        }
    }
    return factory;
}

createDocumentBuilder()方法

如果解析器或异常处理器不为空,也就是用户自己设置了,就会在这里设置给DocumentBuilder

protected DocumentBuilder createDocumentBuilder(DocumentBuilderFactory factory,
        @Nullable EntityResolver entityResolver, @Nullable ErrorHandler errorHandler)
        throws ParserConfigurationException {
    DocumentBuilder docBuilder = factory.newDocumentBuilder();
    if (entityResolver != null) {
        docBuilder.setEntityResolver(entityResolver);
    }
    if (errorHandler != null) {
        docBuilder.setErrorHandler(errorHandler);
    }
    return docBuilder;
}

至此对于资源的加载就结束了,我们已经拿到了配置文件的Document,接下来就会回到doLoadBeanDefinitions方法当中,研究它当中第二个方法registerBeanDefinitions,也就是解析xml注册BeanDefinition。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值