Spring系列之IOC容器

一、概述

  IOC容器就是具有依赖注入功能的容器,IOC容器负责实例化、定位、配置应用程序中的对象及建立这些对象之间的依赖。应用程序无需直接在代码中new 相关的对象,应用程序由IOC容器进行组装。在Spring中BeanFactory是IOC容器的实际代表者。

  由IOC容器管理的那些组成你应用程序的对象我们就叫它Bean,Bean就是由Spring容器初始化、装配及管理的对象。

  Spring提供了两种容器:BeanFactory和ApplicationContext。

  BeanFactory:基础类型IOC容器,提供完整的IoC服务,默认采用延迟初始化策略,也就是只有客户端对象需要访问容器中的某个受管理对象的时候,才对该受管理对象进行初始化以及依赖注入操作。

  ApplicationContext:是在BeanFactory的基础上构建,除了拥有BeanFactory的所有支持,还提供了其他高级特性,例如事件发布、国际化支持等。ApplicationContext所管理的对象,在容器启动后默认全部初始化并绑定完成,所以相对于BeanFactory,它要求更多的资源。

二、BeanFactory的对象注册与依赖绑定

  XML配置格式是Spring支持的最完整、功能最强大的配置方式。

  1、配置文件封装

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

  public interface InputStreamSource
  {
    InputStream getInputStream() throws IOException;
  }

  InputStreamResource封装任何返回InputStream的类,比如File、ClassPath下的资源和Byte Array等。

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

  Resource接口抽象了所有Spring内部使用到的底层资源:File、URL、classPath等。对不同来源的资源文件都有相应的Resource实现:文件(FileSystemResource)、ClassPath资源(ClassPathResource)、URL资源(URLResource)等。相关类图如下:

  

  在日常的开发中,我们可以直接使用Spring提供的类来进行资源的加载,比如:

public class Test
{
    public static void main(String[] args) throws IOException
    {
        BeanFactory beanFactory=new XmlBeanFactory(new ClassPathResource("xmlBeanFactory.xml"));
        MyTestBean myTestBean=(MyTestBean) beanFactory.getBean("myTestBean");
        myTestBean.fun();
        //加载ClassPath资源
        Resource resource=new ClassPathResource("test.txt"); 
        InputStream inputStream=resource.getInputStream();
        BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream));
        System.out.println(reader.readLine());
        //加载File文件资源
        Resource resource=new FileSystemResource("D:\\fileResourceTest.txt");
        InputStream inputStream=resource.getInputStream();
        BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream));
        String str;
        while((str=reader.readLine())!=null)
            System.out.println(str);    
    }
}

   2、加载Bean

  当通过Resource相关类完成了对配置文件封装后,配置文件的读取工作就全权交给XmlBeanDifinitionReader来处理了。XmlBeanDifinitionReader负责读取Spring指定格式的XML配置文件并解析,之后将解析后的文件内容映射到相应的BeanDefinition,并加载到相应的BeanDefinitionRegistry中。这时,整个BeanFactory就可以放给客户端使用了。

  

  整个资源的加载过程很复杂,参考下面的时序图

  

  整个流程大致要经过以下几个步骤:

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

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

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
            throws BeanDefinitionStoreException {
        try {
            /*
             * 加载XML文件,并得到相应的、Document
             * 根据返回的Document注册Bean信息
             */
            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);
        }
    }

  在获取Document的代码中,执行下列代码

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

  DocumentLoader只是一个接口,这里真正调用的是DefaultDocumentLoader。

@Override
    public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
            ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

        DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
        if (logger.isDebugEnabled()) {
            logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
        }
        DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
        return builder.parse(inputSource);
    }

  与SAX解析XML文档的思路一致,这里首先创建DocumentBuilderFactory,再通过DocumentBuilderFactory创建DocumentBuilder,进而解析InputSource来返回Document对象。DocumentLoader还涉及到验证模式的读取。

  备注:

  验证模式,XML文件的验证模式保证了XML文件的正确性,比较常用的有两种,DTD和XSD。

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

  XSD:即XML schema。描述了XML文档的结构,可以使用一个指定的XML schema来验证某个XML文档,以坚持该XML文档是否符合其要求。

  Spring通过getValidationModeForResource方法来获取对对应资源的验证模式:

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

  当把文件转换为Document后,接下来就是提取并注册bean了,也就是执行registerBeanDefinitions(doc, resource)方法。

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

  进入registerBeanDefinitions方法:

@Override
    public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
        this.readerContext = readerContext;
        logger.debug("Loading bean definitions");
        Element root = doc.getDocumentElement();
        doRegisterBeanDefinitions(root);
    }

  doRegisterBeanDefinitions()方法就是真正开始解析了。

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

  处理了profile后就进行XML读取了,跟踪代码进入parseBeanDefinitions方法。

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
        //对beans的处理
        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)) {
                        //对bean的处理
                        parseDefaultElement(ele, delegate);
                    }
                    else {
                        delegate.parseCustomElement(ele);
                    }
                }
            }
        }
        else {
            delegate.parseCustomElement(root);
        }
    }

三、ApplicationContext

  Spring为基本的BeanFactory类型容器提供了XMLBeanFactory实现,相应地,它也为ApplicationContext类型容器提供了以下几个实现:

  • FileSystemApplicationContext:从文件系统加载bean定义以及相关资源的实现
  • ClassPathXmlApplicationContext:从CLASSPATH加载bean定义以及相关资源的实现
  • XMLWebApplicationContext:用于Web应用程序的实现

  1、统一资源加载策略

  Spring框架内部使用Resource接口作为所有资源的抽象和访问接口,上面已有接触,其中ClassPathResource就是Resource的一个特定类型的实现,代表位于Classpath中的资源。有了资源,还需要ResourceLoader去查找和定位这些资源。

  ResourceLoader有一个默认的实现类DefaultResourceLoader,实现代码如下:

@Override
    public Resource getResource(String location) {
        Assert.notNull(location, "Location must not be null");
        if (location.startsWith("/")) {
            return getResourceByPath(location);
        }
        else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
        }
        else {
            try {
                // Try to parse the location as a URL...
                URL url = new URL(location);
                return new UrlResource(url);
            }
            catch (MalformedURLException ex) {
                // No URL -> resolve as resource path.
                return getResourceByPath(location);
            }
        }
    }

  处理逻辑如下:

  1. 首选检查资源路径是否以classpath:前缀打头,如果是,则尝试构造ClassPathResource类型资源并返回
  2. 否则,尝试通过url,根据资源路径来定位资源
  3. 如果还没有,则委派getResourceByPath方法来定位

  ResourcePatternResolver是ResourceLoader的扩展,ResourceLoader每次只能根据资源路径返回确定的单个Resource实例,而ResourcePatternResolver则可以根据指定的资源路径匹配模式,每次返回多个Resource实例。

  

  ApplicationContext继承了ResourcePatternResolver,也就间接实现了ResourceLoader接口,这就是ApplicationContext支持Spring内统一资源加载策略的真相。

四、IOC容器功能实现

  综上,IOC容器会以某种方式加载Configuration Metadata(通常是XMl格式的配置信息),然后根据这些信息绑定整个系统的对象,最终组装成一个可用的基于轻量级容器的应用系统。

  基本上可以分为两个阶段,容器的启动阶段和Bean实例化阶段。

  1、容器启动阶段

  容器启动伊始,首先会通过某种途径加载Configuration Metadata,大部分情况下需要依赖某些工具类(BeanDefinitionReader)对加载的Configuration Metadata进行解析和分析,并将分析后的信息编组为相应的BeanDefinition,最后把这些保存了bean定义必要信息的BeanDefinition,注册到相应的BeanDefinitionRegistry,这样容器的启动工作就完成了。

  2、Bean实例化阶段

  经过第一阶段,现在所有的bean定义信息都通过BeanDefinition的方式注册到BeanDefinitionRegistry中,当某个请求方通过容器的getBean方法明确的请求某个对象,或者因依赖关系容器需要隐式的调用getBean方法时,就会触发第二个阶段的活动。

  该阶段,容器会首先检查所请求的对象之前是否已经初始化,如果没有,则会根据注册的BeanDefinition所提供的信息实例化请求对象,并为其注入依赖,如果该对象实现了某些回调接口,也会根据回调接口的要求来装配它。当该对象装配完成之后,容器就会立即将其返回请求方使用。

posted @ 2016-03-07 11:25  温布利往事  阅读(878)  评论(0编辑  收藏  举报