relativeClass, String... resourceNames) {
load(relativeClass, resourceNames);
refresh();
从构造函数可以看出GenericXmlApplicationContext 能从不同的输入中加载bean定义
四、细节分析
从前面的分析,已经对Spring IOC的整体由哪些部分组成有一个了解了,下面就深入去分析各个部分是怎么实现的
1. Bean定义加载和解析
ApplicationContext如何加载、解析Bean定义的。读源码,我们不光是了解一下这个过程,更重要的是看它是如何设计接口、类来配合解决这个问题的,以及有哪些扩展点、灵活之处。
1.1 xml文件的bean配置的加载和解析
xml配置方式的bean定义获取过程:
首先,你自己一定要思考清楚这个。然后才去看源码,不然你都不知道它在干嘛。
第一次来学习spring的源码,该怎么来看源码呢?
一行一行看,理解每一行?这肯定行不通。
对于未知的代码,我们并不清楚它的接口、类结构,方法设计。那怎么看呢?
首先找准一个你熟悉的过程点,比如上面的过程中,我们比较熟悉xml解析,那xml解析的代码你是可以看懂的。
然后以debug的方式开启发现之旅,从入口处开始一步一步往里跟,直到看到找准的点的代码。这时一定要记住这个点在哪个类中。下次就可以直接在这里打断点了。然后要把一路跟过来的调用栈截图存下来。这个调用栈将是我们重要的分析作者是如何设计接口、类的源泉。
第一次总是困难的,但之后就轻松了
开始吧,从这里出发,断点这行,调试运行
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
通过艰辛之旅,我们得到了调用栈:
在这里我们也可以有捷径获得这个调用栈,如果你够细心,会联想:
我们知道IOC容器的工作过程是加载xml、解析xml得到bean定义、把bean定义注册到bean工厂里面、bean工厂根据bean定义创建bean实例,最终目的是注册bean定义到bean工厂创建bean实例,那么我们就根据ClassPathXmlApplicationContext的继承体系先找到哪个类里面持有bean工厂,找到持有bean工厂的地方以后先看有没有注册bean定义相关的方法,根据继承体系寻找,最终发现在父类AbstractRefreshableApplicationContext里面持有Bean工厂DefaultListableBeanFactory:
/** Bean factory for this context. */
@Nullable
private DefaultListableBeanFactory beanFactory;
通过在AbstractRefreshableApplicationContext里面查找,没有找到注册bean定义相关的方法,那么我们就看DefaultListableBeanFactory的里面有没有注册bean定义相关的方法,最终发现DefaultListableBeanFactory里面果然有注册bean定义的方法registerBeanDefinition
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
通过查看DefaultListableBeanFactory继承体系,我们可以看到DefaultListableBeanFactory实现了BeanDefinitionRegistry这个接口来实现bean定义注册
那么我们就在registerBeanDefinition(String beanName, BeanDefinition beanDefinition)方法里面打个断点,然后debug运行前面的示例代码到这里:
这样我们就能快速的拿到整个调用栈,而不用一步一步的去debug代码了:
那么怎么来具体分析调用栈呢?
主要看调用栈(看从开始到注册bean定义这件事情)用到了哪些类的哪些方法,看传参。工作是如何分配、分工协作完成的。
看传参重点是看输入的参数(如xml配置文件的路径)在这些类中是怎么变化的(代码的本质其实就是对输入数据的各种处理得到最终想要的结果),从而知道每一个类是干什么用的。分析完整个调用栈以后,想要了解哪一部分就点击对应的调用栈去分析就行了。
从调用栈可以看到要加载xml、解析xml获取bean定义、注册bean定义到bean工厂这些事由以下4个部分组成:
方法里面含有<init>的表示是在构造函数进行初始化,方法里面带有(AbstractApplicationContext).refresh()的表示调用的是AbstractApplicationContext父类的refresh()方法,其他的类似
从方法和参数上我们可以看出这4部分分别是做什么:
第一部分:初始化IOC容器,创建了内部的BeanFactory,然后将加载xml的工作委托给了XmlBeanDefinitionReader。
第二部分:XmlBeanDefinitionReader完成了对输入数据:字符串——>Resource——>EncodedResource——>InputSource——>Document的转变。
第三部分:DefaultBeanDefinitionDocumentReader完成对Document中元素的解析,获得Bean定义等信息。
第四部分:就是简单的完成bean定义的注册。
接下来,你就可以针对每一部分去看你感兴趣的处理逻辑、数据结构了。
比如:你可能对第三部分Document中元素的解析很感兴趣,想搞清楚它是如何解析xml文档中的各种标签元素的。
1.1.1 xml元素的解析
从第三部分的调用栈上,我们可以看到如下变化:
1——>2 :Document ——>Element
看 doRegisterBeanDefinitions 方法:
接下来看在 parseBeanDefinitions 方法中是如何来处理里面<beans>的元素的:
至于它是如何解析bean的不重要,很简单的。我们重点关心的是它是如何处理其他名称空间元素的,因为这里是个变化点:其他模块所需要的标签各异,表示的信息也不同,它也不知道其他模块会有哪些标签。
它是如何做到以不变应万变的?看下面的xml配置示例:
就来看 parseCustomElement方法:
先来看一下 NamespaceHandler 的继承体系:
NamespaceHandler 里面定义的方法:
请详细看NamespaceHandler的接口注释,方法注释说明了方法的用法。
问题:名称空间对应的处理器在不同的模块实现,这里是如何加载到的?如事务处理的根本就不在现在的这里。那就要去看图中的这条语句的方法调用了:
进入resolve 方法
它是一个接口,那这里用的是它的什么实现类对象呢?
我们看到有两个属性classLoader,handlerMappingsLocation。从handlerMappingsLocation这个名字能知道这是处理器与名称空间的映射的配置所在的地址。从前两个构造方法,我们看到它给入了一个常量的地址值:
可大胆推测它是要到类目录下去找这个文件。看下我们的 spring 的模块 jar 包下有没有这个文件
接下来来看下 resolve 方法:
请把NamespaceHandler、NamespaceHandlerResolver的类图画出来。思考一下这里有用到什么设计原则、设计模式?【很重要】
扩展:
1)如果你需要加一个自己开发的模块(含自定义的bean定义标签)到spring中,你是否可以做到了。
请看我的文章:
dubbo系列三:dubbo源码分析(dubbo框架是如何跟spring进行整合的,消费者获取的代理实例是如何创建的呢、生产者怎么把内容注册到注册中心的,然后消费者是如何获取这个信息的、dubbo负载均衡策略)
中的dubbo框架是如何跟spring进行整合的
2)spring标签处理这里的设计:模块之间可以灵活组合,配置在各自的模块中,即插即用。你是否可以把它应用到你的系统设计上。
策略模式跟工厂模式的组合使用
那么通用的实现 GenericXmlApplicationContext 加载xml、获取bean定义、注册bean定义的调用栈是否也是一样的呢?
ClassPathXmlApplicationContext加载xml、获取bean定义、注册bean定义:
//类路径下加载xml配置文件创建bean定义
ApplicationContext context1 = new ClassPathXmlApplicationContext("application.xml");
断点在DefaultListableBeanFactory中的注册方法上:
GenericXmlApplicationContext 加载xml、获取bean定义、注册bean定义:
//通用的xml方式加载xml配置文件创建bean定义
ApplicationContext context3 = new GenericXmlApplicationContext("file:e:/study/application.xml");
断点还是在DefaultListableBeanFactory中的注册方法上:
经过对比:
ClassPathXmlApplicationContext和GenericXmlApplicationContext 加载xml、获取bean定义、注册bean定义的过程是一样的
1.2 bean工厂DefaultListableBeanFactory
经过前面的分析,我们发现bean定义都是注册到DefaultListableBeanFactory中。接下来就来认识一下它
在ApplicationContext的两类实现中,通过查看继承体系我们都可以看到ApplicationContext中持有DefaultListableBeanFactory:
xml配置方式的实现的父类:
通用实现:
下面我们来看一下ApplicationContext和BeaFatory的关系
1.3 注解方式的bean配置的加载和解析
// 扫描注解的方式创建bean定义
AnnotationConfigApplicationContext context4 = new AnnotationConfigApplicationContext("com.study.leesmall.spring.service");
断点还是在DefaultListableBeanFactory中的注册方法上:
debug到上面的断点以后拿到的调用栈:
在这个调用栈中,我们并发没有看到它做包扫描的相关工作。从下往上看这个执行栈,点第2个执行栈看代码:
它现在是在做一些初始化的准备处理,从这里我们获知,它做了registerAnnotationConfigProcessors。从名字上理解就是注册了一些注解配置的处理器。到底是一些什么processors,点方法进去看看:
注册各种processor
它注册了很多的processor,都是些什么Processor?点第一个的类名进去看看
BeanDefinitionRegistryPostProcessor扩展了BeanFactoryPostProcessor,增加了BeanDefinitionRegistry位置的处理,即它可以提前对注册好的BeanDefinitionRegistry进行前置处理。
下面我们来看一下BeanFactoryPostProcessor:
来看看BeanFactoryPostProcessor有哪些实现:
这是springIOC中给我们提供的又一个【扩展点】,让我们可以在beanFactory开始创建Bean实例前对beanFactory进行一些处理!!!
使用示例如下:
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations" value="classpath:application.properties"/>
</bean>
扩展一个自己的BeanFactoryPostProcessor:
package com.study.leesamll.spring.ext;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
System.out.println(this + " 扩展一个自己的BeanFactoryPostProcessor");
// 扫描注解的方式创建bean定义
AnnotationConfigApplicationContext context4 = new AnnotationConfigApplicationContext("com.study.mike.spring.service");
context4.close();
BeanFactoryPostProcessor的类图
1.3.1 扫描包获取bean定义的过程是怎样的?
前面我们分析到,注解方式是在scan方法开始进行扫描的
那么我们就在这个方法的这里再打个断点,记住之前的bean工厂注册方法里面的断点保留,debug调试看一下调用栈
从调用栈上我们可看到有哪些类参与进来,在哪里发生的什么。
下面来看一下是怎么扫描的:
下面来看一下扫描候选组件的方法:
类图如下:
参考文章:
Spring Java-based容器配置
完整代码获取地址:https://github.com/leeSmall/FrameSourceCodeStudy/tree/master/spring-source-study