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就可以放给客户端使用了。
整个资源的加载过程很复杂,参考下面的时序图
整个流程大致要经过以下几个步骤:
- 封装资源文件。进入XmlBeanDefinitionReader后,首先使用EncodedResource类对resource进行封装。
- 获取输入流,从Resource中获取对应的InputStream并构造。
- 通过构造的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); } } }
处理逻辑如下:
- 首选检查资源路径是否以classpath:前缀打头,如果是,则尝试构造ClassPathResource类型资源并返回
- 否则,尝试通过url,根据资源路径来定位资源
- 如果还没有,则委派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所提供的信息实例化请求对象,并为其注入依赖,如果该对象实现了某些回调接口,也会根据回调接口的要求来装配它。当该对象装配完成之后,容器就会立即将其返回请求方使用。