美文网首页
Spring源码解析(一)-BeanDefinition加载

Spring源码解析(一)-BeanDefinition加载

作者: 秋水畏寒 | 来源:发表于2020-05-02 20:46 被阅读0次

Spring版本

5.2.5.RELEASE

源码解析

1. 加载自定义bean.xml

首先看一下平时我们是怎么手动加载bean.xml文件的,示例代码如下:

ClassPathResource resource = new ClassPathResource("bean.xml");
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(resource);

可以先看到,加载BeanDefinition入口在XmlBeanDefinitionReader#loadBeanDefinitions


2. loadBeanDefinitions

    public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
        // 进行编码,不过这里只传入了resource一个参数,所以并没有指定具体的编码方式和字符集
        return loadBeanDefinitions(new EncodedResource(resource));
    }

可以看到这里对resource进行了一个资源的编码,因为我们加载BeanDefinition其实是一个解析xml文件的过程,那么读取文件就会涉及到编码。
不过这里并没有明确指定具体的编码方式和字符集:

    public EncodedResource(Resource resource) {
        this(resource, null, null);
    }
    private EncodedResource(Resource resource, @Nullable String encoding, @Nullable Charset charset) {
        super();
        Assert.notNull(resource, "Resource must not be null");
        this.resource = resource;
        this.encoding = encoding;
        this.charset = charset;
    }

编码结束后交由重载方法loadBeanDefinitions继续处理:

    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<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
        // 为空,初始化一个集合
        if (currentResources == null) {
            currentResources = new HashSet<>(4);
            this.resourcesCurrentlyBeingLoaded.set(currentResources);
        }
        // 加入缓存中
        // 增加缓存的原因:避免bean定义异常的时候导致死循环,即有可能存在这样的情况:
        // bean A开始加载,加载过程中又由于bean定义的时候有问题,再次要求加载Bean A,第二次加载Bean A走到这一步就会被拦截,避免一直死循环下去
        if (!currentResources.add(encodedResource)) {
            throw new BeanDefinitionStoreException(
                    "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
        }

        try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
            InputSource inputSource = new InputSource(inputStream);
            if (encodedResource.getEncoding() != null) {
                inputSource.setEncoding(encodedResource.getEncoding());
            }
            // 开始记载bean定义
            return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException(
                    "IOException parsing XML document from " + encodedResource.getResource(), ex);
        }
        finally {
            // 移除缓存
            currentResources.remove(encodedResource);
            if (currentResources.isEmpty()) {
                this.resourcesCurrentlyBeingLoaded.remove();
            }
        }
    }

这里的逻辑主要是利用缓存对加载流程做了一个限制,防止重复加载同一个bean导致的死循环,通过判断之后,可以看到交由doLoadBeanDefinitions来做真正的解析工作:

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

        try {
            // 加载bean定义的xml文档
            Document doc = doLoadDocument(inputSource, resource);
            int count = registerBeanDefinitions(doc, resource);
            if (logger.isDebugEnabled()) {
                logger.debug("Loaded " + count + " bean definitions from " + resource);
            }
            return count;
        }
        catch (BeanDefinitionStoreException ex) {
            throw ex;
        }
        // 其余代码均为异常处理,此处略
    }

该方法逻辑主要有俩步:

  1. 解析xml,获取一个Document对象
  2. 注册Bean

2.1 doLoadDocument:

    protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
        // getValidationModeForResource 获取bean校验模型,校验bean的定义是否符合规范
        // getEntityResolver用于获取xml文件校验规则的获取方式,比如说
        // 我们在xml文件可以看到这样的声明:http://www.springframework.org/schema/beans/spring-beans.xsd
        // 这种声明是用来寻找XSD的定义,以便对xml进行验证,而默认的方式就是通过网络下载(实际上这个声明就是一个url)
        // 而寻找XSD这个行为,就是交由EntityResolver去实现的
        return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
                getValidationModeForResource(resource), isNamespaceAware());
    }

这里通过documentLoader.loadDocument去解析xml,在调用之前,主要做了俩件事:

2.1.1. getEntityResolver

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

EntityResolver主要作用在于提供一个获取验证模型的规则,我们验证一个xml文档是否正确,简单理解就是xml语法是否正确,而我们往往都能在xml文档的开头处看到类似这样的一个声明: http://www.springframework.org/schema/beans/spring-beans.xsd
这种声明就是用来指定xml文档的校验规则的,则默认方式是通过网络方式获取规则,所以是一个url,而获取规则这么一件差事,就是交由EntityResolver去处理的

2.1.2. getValidationModeForResource

    protected int getValidationModeForResource(Resource resource) {
        // 获取用户通过set方法指定的验证模型
        int validationModeToUse = getValidationMode();
        // 如果不是自动验证模式,返回该值
        if (validationModeToUse != VALIDATION_AUTO) {
            return validationModeToUse;
        }
        // 否则,检测应该使用哪种验证模型
        // 检测方法:通过验证文档是否存在DOCTYPE节点,如果是,判断为DTD,否则XSD
        int detectedMode = detectValidationMode(resource);
        if (detectedMode != VALIDATION_AUTO) {
            return detectedMode;
        }
        // 使用XSD作为默认值
        // 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;
    }

    /**
     * Detect which kind of validation to perform on the XML file identified
     * by the supplied {@link Resource}. If the file has a {@code DOCTYPE}
     * definition then DTD validation is used otherwise XSD validation is assumed.
     * <p>Override this method if you would like to customize resolution
     * of the {@link #VALIDATION_AUTO} mode.
     */
    protected int detectValidationMode(Resource resource) {
        if (resource.isOpen()) {
            throw new BeanDefinitionStoreException(
                    "Passed-in Resource [" + resource + "] contains an open stream: " +
                    "cannot determine validation mode automatically. Either pass in a Resource " +
                    "that is able to create fresh streams, or explicitly specify the validationMode " +
                    "on your XmlBeanDefinitionReader instance.");
        }

        InputStream inputStream;
        try {
            inputStream = resource.getInputStream();
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException(
                    "Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " +
                    "Did you attempt to load directly from a SAX InputSource without specifying the " +
                    "validationMode on your XmlBeanDefinitionReader instance?", ex);
        }

        try {
            // 读取输入流,确定是否存在DOCTYPE节点,若存在,返回DTD模式,否则返回XSD模式
            // 内部代码相对简单,不扩展解析
            return this.validationModeDetector.detectValidationMode(inputStream);
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException("Unable to determine validation mode for [" +
                    resource + "]: an error occurred whilst reading from the InputStream.", ex);
        }
    }

该方法的主要逻辑在于获取xml文件的校验模型,校验模型可以是:

  • XSD
  • DTD
    具体使用哪种方式首先由用户指定,若没有指定,则通过判断xml文件是否存在DOCTYPE,若存在,使用DTD模式,否则使用XSD模式

现在回过头来看看loadDocument的具体实现逻辑:

    @Override
    public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
            ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
        
        DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
        if (logger.isTraceEnabled()) {
            logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
        }
        DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
        return builder.parse(inputSource);
    }

    /**
     * Create the {@link DocumentBuilderFactory} instance.
     * @param validationMode the type of validation: {@link XmlValidationModeDetector#VALIDATION_DTD DTD}
     * or {@link XmlValidationModeDetector#VALIDATION_XSD XSD})
     * @param namespaceAware whether the returned factory is to provide support for XML namespaces
     * @return the JAXP DocumentBuilderFactory
     * @throws ParserConfigurationException if we failed to build a proper DocumentBuilderFactory
     */
    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 (IllegalArgumentException ex) {
                    ParserConfigurationException pcex = new ParserConfigurationException(
                            "Unable to validate using XSD: Your JAXP provider [" + factory +
                            "] does not support XML Schema. Are you running on Java 1.4 with Apache Crimson? " +
                            "Upgrade to Apache Xerces (or Java 1.5) for full XSD support.");
                    pcex.initCause(ex);
                    throw pcex;
                }
            }
        }

        return factory;
    }

    /**
     * Create a JAXP DocumentBuilder that this bean definition reader
     * will use for parsing XML documents. Can be overridden in subclasses,
     * adding further initialization of the builder.
     * @param factory the JAXP DocumentBuilderFactory that the DocumentBuilder
     * should be created with
     * @param entityResolver the SAX EntityResolver to use
     * @param errorHandler the SAX ErrorHandler to use
     * @return the JAXP DocumentBuilder
     * @throws ParserConfigurationException if thrown by JAXP methods
     */
    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;
    }

代码块稍长,但还是很清晰的,代码已经很清楚地展示了对应的逻辑含义,最后解析的核心在于:

builder.parse(inputSource)

这里的实现类使用了apache的包去做了解析,不在本文探讨范围内,就不展开了,解析完毕之后,封装成一个Document对象,供后续注册使用


到这里为止,我们仅仅只是验证了xml文件是否合法,并且将输入流转化为一个Document对象,尚未解析成BeanDefinition,也还没有进行注册,对于这俩部分内容,在下一篇 《Spring源码解析(二)-BeanDefinition注册》进行讲解

相关文章

网友评论

      本文标题:Spring源码解析(一)-BeanDefinition加载

      本文链接:https://www.haomeiwen.com/subject/mchophtx.html