《Spring源码深度解析》第二章 容器的基本实现

时间:2021-12-18 17:17:14

  入门级别的spring配置文件

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd">
  
    <bean id="springHelloWorld"
        class="com.tutorial.spring.helloworld.impl.SpringHelloWorld"></bean>
    <bean id="strutsHelloWorld"
        class="com.tutorial.spring.helloworld.impl.StrutsHelloWorld"></bean>
  
  
    <bean id="helloWorldService"
        class="com.tutorial.spring.helloworld.HelloWorldService">
        <property name="helloWorld"ref="springHelloWorld"/>
    </bean>
</beans>

  Spring容器的基本功能简要:

  1. 读取配置文件
  2. 解析出配置文件中的类,并进行实例化
  3. 获取实例化的对象,提供使用

  对于以上第一步,读取配置文件,简单代码如下:

BeanFactory bf = new XmlBeanFactory(new ClassPathResource("config.xml"));

  在Java中,不同来源的资源被抽象成URL,这些资源具有不同的协议,如:"file:","http:","jar:"等,却没有"classpath:",所以Spring对要使用的资源文件进行了封装,相关接口如下:

public interface InputStreamSource {
    InputStream getInputStream() throws IOException;
}
public interface Resource extends InputStreamSource {
    boolean exists();
    default boolean isReadable() {
        return 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(getInputStream());
    }
    long contentLength() throws IOException;
    long lastModified() throws IOException;
    Resource createRelative(String relativePath) throws IOException;
    @Nullable
    String getFilename();
    String getDescription();
}

  以上ClassPathResource是Resource接口的实现,除此之外,还有其他的实现,如:FileSystemResource、UrlResource、InputStreamResource、ByteArrayResource等。通过这些Resource的实现类,在Spring中可以快速地定位和使用资源(getInputSteam就可以获取到对应资源的流)。

  通过Resource的实现类获取到了Spring配置文件之后,传递给了XmlBeanFactory的构造函数:

 1 public class XmlBeanFactory extends DefaultListableBeanFactory {
 2     private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);
 3 
 4     public XmlBeanFactory(Resource resource) throws BeansException {
 5         this(resource, null);
 6     }
 7     public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
 8         super(parentBeanFactory);
 9         this.reader.loadBeanDefinitions(resource);
10     }
11 }

  从18行一直点击去,代码如下:

public AbstractAutowireCapableBeanFactory() {
    super();
    ignoreDependencyInterface(BeanNameAware.class);
    ignoreDependencyInterface(BeanFactoryAware.class);
    ignoreDependencyInterface(BeanClassLoaderAware.class);
}
public void ignoreDependencyInterface(Class<?> ifc) {
    this.ignoredDependencyInterfaces.add(ifc);
}

  可以发现,有三个类被添加到了ignoredDependencyInterfaces集合中,这个集合中的类不会被自动装配(?测试发现:注入A,A中有B属性,B实现了以上接口,B属性依然被自动注入了)

  被添加的这三个类都是接口,用于bean感知spring的存在,java对象被spring容器创建并且管理期间,java对象无法感知这个过程,而如果这个对象类实现了以上三个接口,则可以获取到spring框架的相关信息,分别是:作为bean时的名称、bean容器的引用和加载这个bean的classLoader。

  回到XmlBeanFactory的源码,对资源起加载作用的是第9行:

this.reader.loadBeanDefinitions(resource);

  点进去,代码如下:

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

  通过EncodedResource对资源进行再次包装,这里简单介绍一下这个EncodedResource类:

public class EncodedResource implements InputStreamSource {

    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;
    }

    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());
        }
    }

    // ... 省略代码
}

  这个类包装了资源的字符编码相关的操作。编码后的资源被这样使用:

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();
}

  可见,资源被传递到了doLoadBeanDefinitions的另一个重载版本中(对异常的处理细度从高到低):

 1 protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
 2         throws BeanDefinitionStoreException {
 3 
 4     try {
 5         Document doc = doLoadDocument(inputSource, resource);
 6         int count = registerBeanDefinitions(doc, resource);
 7         if (logger.isDebugEnabled()) {
 8             logger.debug("Loaded " + count + " bean definitions from " + resource);
 9         }
10         return count;
11     }
12     catch (BeanDefinitionStoreException ex) {
13         throw ex;
14     }
15     catch (SAXParseException ex) {
16         throw new XmlBeanDefinitionStoreException(resource.getDescription(),
17                 "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
18     }
19     catch (SAXException ex) {
20         throw new XmlBeanDefinitionStoreException(resource.getDescription(),
21                 "XML document from " + resource + " is invalid", ex);
22     }
23     catch (ParserConfigurationException ex) {
24         throw new BeanDefinitionStoreException(resource.getDescription(),
25                 "Parser configuration exception parsing XML from " + resource, ex);
26     }
27     catch (IOException ex) {
28         throw new BeanDefinitionStoreException(resource.getDescription(),
29                 "IOException parsing XML document from " + resource, ex);
30     }
31     catch (Throwable ex) {
32         throw new BeanDefinitionStoreException(resource.getDescription(),
33                 "Unexpected exception parsing XML document from " + resource, ex);
34     }
35 }

  第5行将传入的资源转换为Document类型,方法内代码如下:

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

   这个方法中有两个注意点:1.getEntityResolver()  2.getValidationModeForResource()

getEntityResolver

  XSD和DTD的区别。SAX在解析一个XML文档变为Document期间,会下载这个xml中的文档校验模式所对应的约束文件,并进行验证。下载是一个漫长的过程,而且可能下载失败,entityResolver为此提供解决方案,对象只需实现EntityResolver接口即可,在接口方法中,根据传入的SystemId和publicId,返回对应约束文件的InputSource即可。而约束文件分为两类:DTD和XSD,所以在Spring中,对EntityResolver的实现类中需要分情况处理约束文件,对于不同的类型,接口函数所接收到的参数存在一定规律:

《Spring源码深度解析》第二章 容器的基本实现

  可以看出,在EntityResolve的实现类中(实现函数中),只需要判断systemId参数的后缀即可:

@Override
@Nullable
public InputSource resolveEntity(String publicId, @Nullable String systemId) throws SAXException, IOException {
    if (systemId != null) {
        if (systemId.endsWith(DTD_SUFFIX)) {
            return this.dtdResolver.resolveEntity(publicId, systemId);
        }
        else if (systemId.endsWith(XSD_SUFFIX)) {
            return this.schemaResolver.resolveEntity(publicId, systemId);
        }
    }
    return null;
}

   那以上是怎么根据这两个传入的参数找到对应约束文件的InputSource呢?在EntityResolver的实现类构造函数中,代码如下:

public DelegatingEntityResolver(@Nullable ClassLoader classLoader) {
    this.dtdResolver = new BeansDtdResolver();
    this.schemaResolver = new PluggableSchemaResolver(classLoader);
}

   以上两个对象也是实现了EntityResolver,也就是说,为了实现处理不同情况,创建了多个对象实现了相同的接口,其中一个实现类作为委托类,对情况进行判断后再调用其他实现类。可以看出,这个委托类就是上面的DelegateingEntityResolver,名字也符合语义。

1.BeanDtdResolver

  进去处理第一件事就是判断SystemId是否以dtd结尾,其实在开始调用这个类之前,就已经进行过判断了,进去后又判断了一次,可以说是很严谨了。然后继续处理systemId,找到最后一个斜杠的索引,接着从这个索引后面开始搜索“spring-beans”这个字符串,如果没找到则返回null,SAX解析的时候就会从网络下载约束文件了。找到的话,就认为了约束文件的名字为:“spring-beans.dtd”(这里不明白为什么忽略了版本,如这个xml文件中的dtd指定为spring-beans-2.0.dtd),然后从spring工程的classpath中查找这个文件,然后返回对应文件的InputSource【查找和获取InputSource都是通过ClassPathResource工具类来处理】。

2.PluggableSchemaResolver

  这里的处理比上面的情况稍微复杂一些。首先获取xsd的映射关系(url->文件名),使用懒加载方式,在锁住了this的情况下,再次对资源进行判空,防止竞态条件:

private Map<String, String> getSchemaMappings() {
    Map<String, String> schemaMappings = this.schemaMappings;
    if (schemaMappings == null) {
        synchronized (this) {
            schemaMappings = this.schemaMappings;
            if (schemaMappings == null) {
                if (logger.isTraceEnabled()) {
                    logger.trace("Loading schema mappings from [" + this.schemaMappingsLocation + "]");
                }
                try {
                    Properties mappings =
                            PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
                    if (logger.isTraceEnabled()) {
                        logger.trace("Loaded schema mappings: " + mappings);
                    }
                    schemaMappings = new ConcurrentHashMap<>(mappings.size());
                    CollectionUtils.mergePropertiesIntoMap(mappings, schemaMappings);
                    this.schemaMappings = schemaMappings;
                }
                catch (IOException ex) {
                    throw new IllegalStateException(
                            "Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex);
                }
            }
        }
    }
    return schemaMappings;
}

  以上所提到的schemaMappingsLocation默认值为:META-INF/spring.schemas,这个文件在spring-beans工程这里:

《Spring源码深度解析》第二章 容器的基本实现

  文件内容如下(纯粹就是一个 .properties 文件):

http\://www.springframework.org/schema/beans/spring-beans-2.0.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans-2.5.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans-3.0.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans-3.1.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans-3.2.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans-4.0.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans-4.1.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans-4.2.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans-4.3.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/beans/spring-beans.xsd=org/springframework/beans/factory/xml/spring-beans.xsd
http\://www.springframework.org/schema/tool/spring-tool-2.0.xsd=org/springframework/beans/factory/xml/spring-tool.xsd
http\://www.springframework.org/schema/tool/spring-tool-2.5.xsd=org/springframework/beans/factory/xml/spring-tool.xsd
http\://www.springframework.org/schema/tool/spring-tool-3.0.xsd=org/springframework/beans/factory/xml/spring-tool.xsd
http\://www.springframework.org/schema/tool/spring-tool-3.1.xsd=org/springframework/beans/factory/xml/spring-tool.xsd
http\://www.springframework.org/schema/tool/spring-tool-3.2.xsd=org/springframework/beans/factory/xml/spring-tool.xsd
http\://www.springframework.org/schema/tool/spring-tool-4.0.xsd=org/springframework/beans/factory/xml/spring-tool.xsd
http\://www.springframework.org/schema/tool/spring-tool-4.1.xsd=org/springframework/beans/factory/xml/spring-tool.xsd
http\://www.springframework.org/schema/tool/spring-tool-4.2.xsd=org/springframework/beans/factory/xml/spring-tool.xsd
http\://www.springframework.org/schema/tool/spring-tool-4.3.xsd=org/springframework/beans/factory/xml/spring-tool.xsd
http\://www.springframework.org/schema/tool/spring-tool.xsd=org/springframework/beans/factory/xml/spring-tool.xsd
http\://www.springframework.org/schema/util/spring-util-2.0.xsd=org/springframework/beans/factory/xml/spring-util.xsd
http\://www.springframework.org/schema/util/spring-util-2.5.xsd=org/springframework/beans/factory/xml/spring-util.xsd
http\://www.springframework.org/schema/util/spring-util-3.0.xsd=org/springframework/beans/factory/xml/spring-util.xsd
http\://www.springframework.org/schema/util/spring-util-3.1.xsd=org/springframework/beans/factory/xml/spring-util.xsd
http\://www.springframework.org/schema/util/spring-util-3.2.xsd=org/springframework/beans/factory/xml/spring-util.xsd
http\://www.springframework.org/schema/util/spring-util-4.0.xsd=org/springframework/beans/factory/xml/spring-util.xsd
http\://www.springframework.org/schema/util/spring-util-4.1.xsd=org/springframework/beans/factory/xml/spring-util.xsd
http\://www.springframework.org/schema/util/spring-util-4.2.xsd=org/springframework/beans/factory/xml/spring-util.xsd
http\://www.springframework.org/schema/util/spring-util-4.3.xsd=org/springframework/beans/factory/xml/spring-util.xsd
http\://www.springframework.org/schema/util/spring-util.xsd=org/springframework/beans/factory/xml/spring-util.xsd

  加载完成后,转到到一个map中,最后返回。这个map中映射了url到对应xsd文件在工程中的路径关系,在当前EntityResolver的实现类中,获取到了这个map之后,再根据传递的systemId,通过这个map找到对应的xsd文件路径,最后再使用ClassPathResolver来加载资源,并返回。

  以上两个实现类中,第二个稍微复杂一些,多了一个获取map的操作,之后加载资源的方式都一样,都是使用ClassPathResolver。而且所找的资源,都在这里:

《Spring源码深度解析》第二章 容器的基本实现

 

   好了,以上有了EntityResolver之后,就传递给DocumentBuilder即可(这个类存在于javax.xml.parsers中):

DocumentBuilder docBuilder = factory.newDocumentBuilder();
if (entityResolver != null) {
    docBuilder.setEntityResolver(entityResolver);
}

  从这段代码也可以推断出,EntityResolve并不是必须的,但最好设置一下,这样就不需要从网络下载文件了。

getValidationModeForResource

  获取文档的校验模式(XSD或者DTD)。

while ((content = reader.readLine()) != null) {
    content = consumeCommentTokens(content);
    if (this.inComment || !StringUtils.hasText(content)) {
        continue;
    }
    if (hasDoctype(content)) {
        isDtdValidated = true;
        break;
    }
    if (hasOpeningTag(content)) {
        // End of meaningful data...
        break;
    }
}
return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);

  按行进行读取。文档中可能存在xml的注释,通过consumeCommentTokens进行处理,这个函数可以返回当前行的非注释部分。接着判断这部分中是否存在“DOCTYPE”这个字符串,如果是,则决定校验模式为DTD,否则是XSD。以上操作持续进行,直到遇见一个开标签“<”,因为文档的模式定义必定在标签开始之前。

  确定好了验证模式之后,这个值会用在DocumentBuilderFactory的创建中。

  总结一下解析xml为Document分三步走:

DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);

注册Bean

   回顾一下函数 doLoadBeanDefinitions :

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

  前面章节中写了这么多,终于做好了一个工作:把配置文件转为Document。

  接下来就开始根据得到的Document来注册bean了。进去首先判断根节点上的profiles属性,如果当前环境变量中并没有指定这个profile,则不解析这个beans标签了:

if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
    if (logger.isDebugEnabled()) {
        logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
                "] not matching: " + getReaderContext().getResource());
    }
    return;
}

  如果要解析的话,核心代码:

preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);

  第1、3个都是桩函数,里面函数体是空的,用来提高xml(Document)的扩展性,只需要创建一个子类来重写这两个桩函数即可在里面对文档做一些额外操作了。

  第二个函数点进去,遍历根节点(beans标签是根节点)的子元素:

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    if (delegate.isDefaultNamespace(root)) {
        NodeList nl = root.getChildNodes();
        for (int i = 0; i < nl.getLength(); i++) {
            Node node = nl.item(i);
            if (node instanceof Element) {
                Element ele = (Element) node;
                if (delegate.isDefaultNamespace(ele)) {
                    parseDefaultElement(ele, delegate);
                }
                else {
                    delegate.parseCustomElement(ele);
                }
            }
        }
    }
    else {
        delegate.parseCustomElement(root);
    }
}

  判断子节点是否具有默认的命名空间:http://www.springframework.org/schema/beans。是则当做默认元素进行解析,不是则认为是自定义元素,因为两种标签的用法以及解析存在比较大差异,所以对两种情况分类解析。

默认元素:
<bean id="test" class="test.TestBean" />

自定义:
<tx:annotation-driven/>

  解析默认元素时,对四类子标签(import、alias、bean和beans)进行解析,对于beans,标签进行递归处理,重走上面的流程。

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
    if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
        importBeanDefinitionResource(ele);
    }
    else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
        processAliasRegistration(ele);
    }
    else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
        processBeanDefinition(ele, delegate);
    }
    else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
        // recurse
        doRegisterBeanDefinitions(ele);
    }
}

  对于自定义标签和默认标签的解析,放在第三章,本章完。