容器的基本用法
bean是Spring中最核心的东西,因为Spring就像是个大水桶,而bean就像是容器中的水,水桶脱离了水便也没什么用处了,那么我们先来看看bean的定义。
1 public class MyTestBean { 2 private String testStr = "testStr"; 3 4 public String getTestStr() { 5 return testStr; 6 } 7 8 public void setTestStr(String testStr) { 9 this.testStr = testStr; 10 } 11 }
很普通,bean没有任何特别之处,的确,Spring的目的就是让我们的bean能成为一个纯粹的POJO(Plain Ordinary Java Objects),这也是Spring所追求的。再来看看Spring的配置文件:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sca="http://www.springframework.org/schema/sca" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/sca http://www.osoa.org/xmlns/sca/1.0/spring-sca.xsd"> <bean class= "bean.MyTestBean" id="myTestBean"/> </beans>
在配置文件中我们看到了bean的声明方式,尽管Spring中国bean的元素定义着N种属性来支撑我们业务的各种应用,但是我们只要声明成这样,基本上就已经可以满足我们的大多数的应用了。
功能分析
新建一个测试类来测试上面的代码:
1 @Test 2 public void testSimpleLoad(){ 3 BeanFactory bf = new XmlBeanFactory(new ClassPathResource("bean.xml")); 4 MyTestBean bean = (MyTestBean) bf.getBean("myTestBean"); 5 assertEquals("testStr",bean.getTestStr()); 6 }
我们现在来分析一下上面测试代码的功能,来探索上面的测试代码中Spring究竟帮助我们做了什么工作?无非也就下面几个动作:
(1)读取配置文件bean.xml。
(2)根据bean.xml中的配置找到对应类的配置,并实例化。
(3)调用实例化后的实例。
接下来深入分析上面测试功能的代码实现:
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("bean.xml"));
通过下面XmlBeanFactory初始化时序图,来看上面这句代码的执行逻辑:
从图中可以看出,在测试类中,首先调用了ClassPathResource 的构造函数来构造Resource资源文件的实例对象,这样后续的资源处理就可以用Resource提供的各种服务来操作了,当我们有了Resource后就可以进行XmlBeanFactory的初始化了。那么我们先来了解Resource资源是如何封装的呢?
配置文件封装
Spring的配置文件读取是通过ClassPathResource 进行封装的,如 new ClassPathResource("bean.xml"),那么ClassPathResource完成了什么功能呢?
在Java中,将不同来源的资源抽象成URL,通过注册不同的handler(URLStreamHandler)来处理不同来源的资源的读取逻辑。一般handler的类型使用不同的前缀(协议,Protocol)来识别,如“file:”、“http:”、“jar:”等,然而URL没有默认定义相对Classpath或者ServletContext等资源的handler,虽然可以注册自己的URLStreamHandler来解析特定的URL前缀(协议),比如:“Classpath:”,然而这需要了解URL的实现机制,而且URL也没有提供一些基本的方法,如检查当前资源是否存在、检查当前资源是否可读等方法,因而Spring对其内部使用到的资源实现了自己的抽象结构:Resource接口来封装底层资源。
来看一下Resource接口的源码:
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 var1) throws IOException; String getFilename(); String getDescription(); }
public interface InputStreamSource { InputStream getInputStream() throws IOException; }
InputStreamSource封装任何能返回InputStream的类,比如File、Classpath下的资源和Byte Array等。从第二个源码可以看出,它只有一个方法的定义:getInputStream(),该方法返回一个新的InputStream对象。
Resource接口抽象了所有Spring内部使用到的底层资源:File、URL、Classpath等。首先,它定义了3个判断当前资源状态的方法:存在性(exists)、可读性(isReadable)、是否处于打开状态(isOpen)。另外,Resource接口还提供了不同资源到URL、URI、File类型的转换,以及获取lastModified属性、文件名(不带路径信息的文件名,getFilename()的方法)。为了便于操作,Resource还提供了基于当前资源创建一个相对资源的方法:createRelative()。在错误处理中需要详细地打印出错的资源文件,因而Resource还提供了getDescription()方法用于在错误处理中的打印信息。
对不同来源的资源文件都有相应的Resource实现:文件(FileSystemResource)、Classpath资源(ClasspathResource)、URL资源(UrlResource)、InputStream资源(InputStreamResource)、Byte数组(ByteArrayResource)等。相关的类图如下:
在日常的开发工作中,资源文件的加载也是也是经常用到的,可以直接使用Spring提供的类,比如在需要加载资源文件时可以使用以下代码:
Resource resource = new ClassPathResource("bean.xml"); InputStream inputStream = resource.getInputStream();
得到InputStream后,我们就可以按照以前的开发方式去进行实现了,并且我们还可以利用Resource及其子类为我们提供好的诸多特性。
有了Resource接口便可以对所有的资源文件进行统一处理。至于实现,其实很简单,以getInputStream为例,ClassPathResource中的实现方式便是通过class或者classLoader提供的底层方法进行调用,而对于FileSystemResource的实现更简单,直接使用FileInputStream对文件进行实例化。下面看一下相应的源码:
ClassPathResource:
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(this.getDescription() + " cannot be opened because it does not exist"); } else { return is; } }
FileSystemResource:
public InputStream getInputStream() throws IOException { return new FileInputStream(this.file); }
上述就是Resource相关类对配置文件的封装就讲述完了。
当通过Resource相关类完成了对配置文件进行封装后配置文件的读取工作就全权交给XmlBeanDefinitionReader来处理了。
了解了Spring中将配置文件封装为Resource类型的实例方法后,接下来就探寻XmlBeanFactory的初始化过程了,XmlBeanFactory的初始化有若干办法,Spring中提供了很多的构造函数,在这里分析的是使用Resource实例作为构造函数参数的办法,源码如下:
public XmlBeanFactory(Resource resource) throws BeansException { this(resource, (BeanFactory)null); } public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { super(parentBeanFactory); this.reader = new XmlBeanDefinitionReader(this); this.reader.loadBeanDefinitions(resource); }
上面的函数中的代码 this.reader.loadBeanDefinitions(resource)才是资源加载的真正实现,也是我们分析的重点之一。从上面的时序图中提到的XMLBeanDefinitionReader加载数据就是在这里完成的,但是在XMLBeanDefinitionReader加载数据前还有一个调用父类构造函数初始化的过程:super(parentBeanFactory),跟踪代码到父类AbstractAutowireCapableBeanFactory的构造函数中:
/** * Create a new AbstractAutowireCapableBeanFactory. */ public AbstractAutowireCapableBeanFactory() { super(); ignoreDependencyInterface(BeanNameAware.class); ignoreDependencyInterface(BeanFactoryAware.class); ignoreDependencyInterface(BeanClassLoaderAware.class); }
这里需要提及一下ignoreDependencyInterface()方法,ignoreDependencyInterface的主要功能是忽略给定接口的自动装配功能,那么,这么做是为什么呢?做了会有什么样的效果呢?
举个例子来说明,当A中有属性B,那么当Spring在获取A的Bean的时候如果其属性B还没有初始化,那么Spring会自动初始化B,这也是Spring中提供的一个重要特性。但是,某些情况下,B不会被初始化,其中的一种情况就是B实现了BeanNameAware接口。Spring中是这样介绍的:自动装配时忽略给定的依赖接口,典型应用是通过其他方式解析Application上下文注册依赖,类似于BeanFactory 通过 BeanFactoryAware 进行注入或者ApplicationContext 通过ApplicationContextAware 进行注入。
下篇文章会继续讲解容器的基础 XmlBeanFactory
参考:《Spring源码深度解析》 郝佳 编著: