前面认识了两大核心类
- DefaultListableBeanFactory
- XmlBeanDefinitionReader
对整体架构有了一定的认识,对容器功能有了大致的认识,下面就来看看容器的基础
XmlBeanFactory
XmlBeanFactory是容器的基础,继承了DefaultListableBeanFactory,并且自身组装了XmlBeanDefinitionReader,可以进行解析XML配置文件
拓展:BeanFactory其实就是我们的IOC容器,而器上层接口为ApplicationContext,也就是Spring的上下文
下
所以,我们单纯使用XmlBeanFactory就可以进行读取配置文件、获取和创建Bean了
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("xxx.xml"));
bf.getBean("beanName");
可以看到,首先使用ClassPathResource去进行加载配置文件,然后使用ClassPathResource去实例化XmlBeanFactory
配置文件封装
从上面的那一句代码我们就可以看到,配置文件的封装主要在下面这句代码里面!!!
new ClassPathResource("xxx.xml");
那么究竟这个ClassPathResource完成了什么功能
ClassPathResource实现的就是加载配置文件的功能,并且Spring实现了自己的资源加载策略,不妨可以试想一下,如果没有自己的资源加载策略,对于各种形式的文件,比如file、jar、xml等形式,需要怎样进行解析加载呢?所以,Spring干脆对其内部使用到的资源实现了自己的抽象结构
而这个资源加载策略、抽象结构的关键在于两个接口
- InputStreamSource
- Resource
InputStreamSource
可以看到这个接口十分简单,里面只定义了一个获取InputStream流的方法,用于获取文件的InputStream
Resource
从源码上就可以看到Resource接口继承了InputStreamSource接口,所以具体的配置文件抽象结构就是Resource接口
相当于Resource接口抽象了所有Spring内部将会使用到的底层资源,一个Resource对应就是一个资源,是File类型的、还是URL类型的、是否存在、是否可读、最近的修改时间等,都可以从这个抽象接口中获取,从这个接口就可以获取配置文件的资源
所以,针对不同资源类型的(File、Url、ClassPath等),对应都会有一个Resource实现类!
可以看到这个Resource家族是很大的!
下面列举几个常用的
- FileSystemResource:获取File资源
- ClassPathResource:获取ClassPath资源
- UrlResource:获取URL资源
- InputStreamResource:获取InputStream资源
- ByteArrayResource:获取Byte数组资源
有了Resource接口,我们就可以对所有资源文件进行统一处理了,每个资源文件就是一个Resource
Resource相关实现类对配置文件进行封装了之后,后面XmlBeanFactory对文件的读取工作就全部交给了XmlBeanDefinitionReader来处理了
可以看到,构造方法就是将传进来的Resource交由XmlBeanDefinitionReader去进行处理,调用的是loadBeanDefinitions方法,也就是说,this.reader.loadBeanDefinitionReader(Resource)才是资源加载的真正实现!
所以,从配置文件的读取,要从这里开始
这里要注意的一点是,XmlBeanFactory还调用了super(ParentBeanFactory方法),而XmlBeanFactory的父类是DefaultListableBeanFactory
可以看到,DefaultListableBeanFactory又直接super,其父类是AbstractAutowireCapableBeanFactory
可以看到,进来先调用AbstractAutowireCapableBeanFactory的无参构造方法
无参构造中值得注意的是ignoreDependencyInterface方法,该方法的主要作用是忽略给定接口的自动装配功能,也就是说实现了这些接口的类都不会再有自动注入功能,其底层实现是将忽略的接口存进名为ignoreDependencyInterfaces的HashSet集合中
举个栗子,假如一个实体A中的成员属性是实体B,当实体A进行自动注入的时候,实体B也会被自动实例化,这也是Spring中提供的一个重要特性,但有些时候,我们不希望成员实体被自动注入那么我们可以让成员实体B去实现BeanNameAware接口、BeanFactoryAware接口、BeanClassLoaderAware接口,但前提是,实体A必须要有B的setter方法,也就是外部注入方法是不支持的!(这也可能是官方推荐setter方法注入原因之一)
对于aware这类接口,其实还有一个重要的作用是让bean对容器有意识,一般来说,bean是根本不知道Spring的IOC容器的,所以Bean是无法获取Spring的IOC容器服务的,为了让Bean对Spring的IOC容器有意识,所以我们可以让实体去实现上面的那些aware接口,这样在容器里面的这些Bean会意识到IOC容器,并且可以获取相对应的IOC容器服务,比如实现BeanNameAware接口,可以让Bean获取自己在IOC容器里面的Name
但这里还不能理解,为什么要给这些接口忽略外部注入功能???
这里我猜测,可能是想不让Bean被外部有意识的Bean污染(setter注入),本来Bean就是一个无意识的东西,假如Bean还能注入外部有意识的Bean,那么就会越来越多的Bean对Spring的IOC容器有意识,违反了初衷
加载Bean
现在已经对配置文件进行封装了,那么下面就可以来加载Bean了
加载Bean的时候,其实是在读取配置文件里面完成的,也就是下面这句代码
这一整套流程是比较复杂的
EncodedResource
首先是封装资源文件后,对文件Resource进行EncodedResource封装
这个类是对文件的编码进行处理的
可以看到其构造器,其实起到装配作用而已,比如装配了文件Resource,定义了编码encoding和字符集charset,其主要作用体现在getReader方法上
不过先科普一下字符集和编码的关系,编码是依赖于字符集的,比如UTF-8编码就依赖于Unicode字符集,一般来说一个字符集对应一种编码
public Reader getReader() throws IOException {
//判断字符集是否为空,如果不为空,根据字符集处理文件并返回InputStreamReader
//并且优先判断字符集
if (this.charset != null) {
return new InputStreamReader(this.resource.getInputStream(), this.charset);
}
//判断编码是否为空,如果字符集不存在,那么就根据编码处理文件并返回InputStreamReader
else if (this.encoding != null) {
return new InputStreamReader(this.resource.getInputStream(), this.encoding);
}
//如果即没有编码,也没有字符集,默认方式处理文件返回InputStreamReader,默认为UTF-8编码
//从下面的源码中可以看出来
else {
return new InputStreamReader(this.resource.getInputStream());
}
}
loadBeanDefinitions方法
使用了EncodedResource对文件进行编码处理了之后,接下来就调用重载的loadBeanDefinitions,相当于将编码后的Resource交给重载的loadBeanDefinitions方法处理,具体根据配置文件去加载Bean的操作都在这个方法里面
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
//空判断
Assert.notNull(encodedResource, "EncodedResource must not be null");
//开启日志追踪
if (logger.isTraceEnabled()) {
logger.trace("Loading XML bean definitions from " + encodedResource);
}
//从当前线程的ThreadLocal去获取正在加载的Resource
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
//下面这个判断,是用来防止循环加载的!
//因为currentResource是一个HashSet集合,重复的不会进行添加
//所以,往currentResources添加失败,就表明当前文件已经在加载了
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
//获取文件的InputStream
try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
//又封装了一层,封装为了不是Spring下的InputResource
InputSource inputSource = new InputSource(inputStream);
//同样进行编码操作
//所以可以估计,Spring所说的自定义文件加载策略,其实就是基于InputResource的
if (encodedResource.getEncoding() != null) {
//如果文件自身携带编码规则,那就使用自带的编码规则
inputSource.setEncoding(encodedResource.getEncoding());
}
//进入了加载Bean的核心部分
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
//最终都会执行的操作
finally {
//当文件加载完后,无论失败与否,都要清楚在currentResources的存在
currentResources.remove(encodedResource);
//如果currentResources为空了,证明没有配置文件加载了
//当前线程的ThreadLocal直接清空这个集合
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
从源码上可以看到,这个重载的loadBeanDefinitions方法有以下功能
- 判断文件是否为空
- 使用了ThreadLocal检测、防止配置文件循环加载
- Spring的自定义资源加载策略,其实就是给InputResource又套多了一层,最后的加载文件还是依赖于InputResource
- 加载Bean的核心部分在doLoadBeanDefinitions方法
doLoadBeanDefinitions方法
这个方法就是加载Bean的核心了
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
//doLoadDocument方法就是加载XML文件并得到对应的Document
Document doc = doLoadDocument(inputSource, resource);
//根据返回的Dom去注册bean信息
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " bean definitions from " + resource);
}
return count;
}
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);
}
}
可以看到这个方法只有两个步骤
- 加载Xml文件(细节在doLoadDocument方法里面),生成Dom
- 将Dom里面的Bean进行注册