spring源码分析(二)- 容器基础

 

1.基本用法

用过Spring的都知道,bean是Spring中最基础也是最核心的。首先看一个简单的例子。

一个类和一个配置文件

package bean;
public class MyBean {
    private String name = "test";
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }}

这就是实现Spring的bean最基本代码。然后写个测试类测试下

public class BeanFactoryTest {
    @Test  
    public void test(){
        BeanFactory bf = new XmlBeanFactory(new ClassPathResource("application.xml"));
        MyBean bean = (MyBean) bf.getBean("myBean");
        Assert.assertEquals("test", bean.getName());
    } 
}

 

最后出现绿条。当然,这是个非常简单的例子,只有几行代码,整个流程分为3部:读取配置文件,根据配置实例化类,调用实例。但是在Spring中执行了很多的逻辑代码,我学习的是源码而不是去怎么使用它,相信有工作经验的人都会使用Spring。BeanFactory作为容器的话在企业级的应用中还是比较少见,一般都用ApplicationContext,之后再去学习他们的区别。 

2.XmlBeanFactory

接下来详细分析下每个步骤的实现,深入的分析下功能代码实现:

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

很明显首先调用ClassPathResource的构造函数构造了一个Resource资源文件的对象,后续资源处理就可以用Resource提供各种的服务才操作,然后传入XmlBeanFactory中进行初始化。

封装Resource资源到底做了哪些操作?

2.1 配置文件封装

Spring对其内部使用到的资源实现了自己的抽象结构:Resource接口来封装底层资源。

public interface InputStreamSource {
    InputStream getInputStream() throws IOException;
 
}
 
public interface Resource extends InputStreamSource {
    //存在
    boolean exists();
    //可读性
    boolean isReadable();
    //是否打开状态
    boolean isOpen();
    URL getURL() throws IOException;
    URI getURI() throws IOException;
    File getFile() throws IOException;
    long contentLength() throws IOException;
    long lastModified() throws IOException;
    //创建相对资源
    Resource createRelative(String relativePath) throws IOException;
    String getFilename();
    //错误处理打印信息
    String getDescription();<span style="white-space:pre">  </span>
 
}

  

对于不同来源的资源都有相应的Resource实现:文件(FileSystemResource),ClassPath资源(ClassPathResource),URL资源(URLResource),InputStream资源(InputStreamResource)等。

以getInputStream为例,ClassPathResource中的实现方式通过class或者classLoader提供的底层方法进行调用,对于FileSystemResource直接用FileInputStream对文件进行实例化。

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(getDescription() + " cannot be opened because it does not exist");
        }
        return is;
    }
public InputStream getInputStream() throws IOException {
        return new FileInputStream(this.file);
    }

 

对配置文件进行封装成Resource后,就进行XMLBeanFactory的初始化了。查看构造函数:

public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
        super(parentBeanFactory);
        this.reader.loadBeanDefinitions(resource);
    }

this.reader.loadBeanDefinitions(resource);这句代码是资源加载的真正实现。首先先看super(parentBeanFactory);跟踪到

public AbstractAutowireCapableBeanFactory() {
        super();
        ignoreDependencyInterface(BeanNameAware.class);
        ignoreDependencyInterface(BeanFactoryAware.class);
        ignoreDependencyInterface(BeanClassLoaderAware.class);
    }

ignoreDependencyInterface的主要功能是忽略给定接口的自动装配功能。引用别人的解释:

 

2.2加载bean

this.reader.loadBeanDefinitions(resource); 其中reader是XMLBeanFactory中的XmlBeanDefinitionReader对象,实现了个性化的BeanDefinitionReader读取。

主要功能就是对资源文件读取,解析和注册。进入loadBeanDefinitions方法:

public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
        return loadBeanDefinitions(new EncodedResource(resource));//对资源文件的编码进行处理,设置编码属性
    }

EncodedResource的作用是对资源文件的编码进行处理。继续深入loadBeanDefinitions:

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.getResource());
        }
        //通过属性来记录已经加载的资源
        Set<encodedresource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
        if (currentResources == null) {
            currentResources = new HashSet<encodedresource>(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 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();
            }
        }
    }</encodedresource></encodedresource>

首先对传入的Resource参数做封装,目的是考虑对Resource可能存在编码要求的情况,其次,通过SAX读取XML文件的方式准备InputSource对象,最后将准备的数据通过参数传入真正的核心处理方法doLoadBeanDefinitions。

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
            throws BeanDefinitionStoreException {
        try {
            int validationMode = getValidationModeForResource(resource);//获取对XML文件的验证模式
            Document doc = this.documentLoader.loadDocument(
                    inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware());//加载XML文件,并得到对应的Document
            return registerBeanDefinitions(doc, resource);//根据返回的Document注册Bean信息
        }
        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);
        }
    }

无视掉异常代码,其实就只有三行是核心代码,做了三件事:

 

1.获取XML文件的验证模式

2.加载XML,并得到对应的Document

3.根据得到的Document注册Bean信息

 

3.XML的验证模式

包括2种:DTD和XSD。

DTD即文档类型定义,是一种XML约束模式语言,是XML文件的验证机制,属于XML文件组成的一部分。
DTD 是一种保证XML文档格式正确的有效方法,可以通过比较XML文档和DTD文件来看文档是否符合规范,元素和标签使用是否正确。 一个 DTD文档包含:元素的定义规则,元素间关系的定义规则,元素可使用的属性,可使用的实体或符号规则。

XML Schema语言也就是XSD。XML Schema描述了XML文档的结构。
可以用一个指定的XML Schema来验证某个XML文档,以检查该XML文档是否符合其要求。文档设计者可以通过XML Schema指定一个XML文档所允许的结构和内容,并可据此检查一个XML文档是否是有效的。XML Schema本身是一个XML文档,它符合XML语法结构。可以用通用的XML解析器解析它。
一个XML Schema会定义:文档中出现的元素、文档中出现的属性、子元素、子元素的数量、子元素的顺序、元素是否为空、元素和属性的数据类型、元素或属性的默认 和固定值。
XSD是DTD替代者的原因,一是据将来的条件可扩展,二是比DTD丰富和有用,三是用XML书写,四是支持数据类型,五是支持命名空间。
DTD和XSD相比:DTD 是使用非 XML 语法编写的。
DTD 不可扩展,不支持命名空间,只提供非常有限的数据类型 .

验证模式的读取:

protected int getValidationModeForResource(Resource resource) {
        int validationModeToUse = getValidationMode();
        //如果手动指定了验证模式则使用指定的验证模式
        if (validationModeToUse != VALIDATION_AUTO) {
            return validationModeToUse;
        }
        //如果未指定则使用自动检测
        int detectedMode = detectValidationMode(resource);
        if (detectedMode != VALIDATION_AUTO) {
            return detectedMode;
        }
        return VALIDATION_XSD;
    }

自动检测:

public int detectValidationMode(InputStream inputStream) throws IOException {
        // Peek into the file to look for DOCTYPE.
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        try {
            boolean isDtdValidated = false;
            String content;
            while ((content = reader.readLine()) != null) {
                content = consumeCommentTokens(content);
                //如果读取的行是空或者是注释则略过
                if (this.inComment || !StringUtils.hasText(content)) {
                    continue;
                }
                //读取到DOCTYPR就是DTD,否则就是XSD
                if (hasDoctype(content)) {
                    isDtdValidated = true;
                    break;
                }
                //读取到<开始符号,验证模式一定会在开始符号之前
                if (hasOpeningTag(content)) {
                    // End of meaningful data...
                    break;
                }
            }
            return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
        }
        catch (CharConversionException ex) {
            // Choked on some character encoding...
            // Leave the decision up to the caller.
            return VALIDATION_AUTO;
        }
        finally {
            reader.close();
        }
    }

原理就是判断是否包含DOCTYPE,如果包含就是DTD,否则就是XSD。

4.获取Document

在loadDocument 方法中包含一个EntityResolver参数。

验证文件默认加载方式是通过URL进行网络下载获取,这样有延迟,用户体验不好,一般做法就是把验证文件放置在自己的工程里,问题是如何把URL转换为自己工程下对应的文件呢。

DelegatingEntityResolver.java

public InputSource resolveEntity(String publicId, 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)) {
                //通过调用META-inf/Spring.schemas 解析
                return this.schemaResolver.resolveEntity(publicId, systemId);
            }
        }
        return null;
    }

对于不同的验证模式,Spring使用了不同的解析器解析。方式都比较简单,DTD直接截取systemId的最后xx.dtd,然后去当前路径下寻找,XSD默认到META-INF/Spring.schemas文件中找到systemid所对应的XSD文件并加载。

 

 

4.解析及注册BeanDefinitions

现在已经拿到Document。

XmlBeanDefinitionReader.java

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
        //使用DefaultBeanDefinitionDocumentReader 实例化 BeanDefinitionDocumentReader
        BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
        //将环境变量设置其中
        documentReader.setEnvironment(getEnvironment());
        //记录统计前BeanDefinition的加载个数
        int countBefore = getRegistry().getBeanDefinitionCount();
        //加载及注册bean
        documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
        //记录本次加载的BeanDefinition个数
        return getRegistry().getBeanDefinitionCount() - countBefore;
    }

注意,createBeanDefinitionDocumentReader方法后documentReader已经是DefaultBeanDefinitionDocumentReader类型。

 

DefaultBeanDefinitionDocumentReader.java

protected void doRegisterBeanDefinitions(Element root) {
        //处理profile属性
        String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
        if (StringUtils.hasText(profileSpec)) {
            String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
                    profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
            if (!getEnvironment().acceptsProfiles(specifiedProfiles)) {
                return;
            }
        }
 
        //专门处理解析
        BeanDefinitionParserDelegate parent = this.delegate;
        this.delegate = createDelegate(this.readerContext, root, parent);
        //解析前处理,留给子类实现
        preProcessXml(root);
        parseBeanDefinitions(root, this.delegate);
        //解析后处理,留给子类实现
        postProcessXml(root);
 
        this.delegate = parent;
    }

 

首先是对profile的出力,然后才开始解析。它的作用主要是可以同时在配置文件中部署两套配置来适用于生产和开发环境,方便切换环境,最常用的就是更换不同的数据库,具体百度。继续查看parseBeanDefinitions: 

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

代码看起来非常清晰,在Spring的XML配置中有两大类Bean声明,一个是默认的: 另一类就是自定义,例如:

这两种方式的读取和解析差别很大,默认的话有默认的方式,自定义的话需要用户实现一些接口和配置。具体详细的之后再分析。

 

 转载于 https://www.2cto.com/kf/201605/510664.html

 

posted @ 2017-10-25 23:03  剑神西门吹雪  阅读(251)  评论(0编辑  收藏  举报