Spring容器那点事

Spring 容器启动

对于一个web应用,部署在web容器中,web容器为其提供一个全局的上下文环境即ServletContext,其为SpringIOC容器提供宿主环境。
启动web项目后,会去加载web.xml中的内容,其中包括以下内容

1
2
3
4
5
6
7
8
9
10
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:spring-application.xml,
</param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

由于 ContextLoaderListener 实现了 ServletContextListener 接口,故在web容器启动的时候,ContextLoaderListener 会监听到这个事件,contextInitialized(event) 方法被调用。


image

跟踪该方法会发现它初始化一个上下文 WebApplicationContext,默认实现类为 XmlWebApplicationContext,也就是 SpringIOC 容器。在 ContextLoader 创建如下:


image

跟踪 determineContextClass(servletContext) 方法,由于我们未配置 contextClass,
所以走else走


image

一开始我就知道创建了一个 WebApplicationContext 没仔细看创建的是什么,后来打断点发现 contextClassName 为 XmlWebApplicationContext,383行的 defaultStrategies 原来在静态块中,真是大意了。(你去看下 org.springframework.web.context 包下的 ContextLoader.properties 就知道了)


image

然后返回看 initWebApplicationContext(servletContext),给 IOC 容器设置父容器,并把它作为属性设置进web容器。


image

我们主要看下306行的 configureAndRefreshWebApplicationContext(cwac,servletContext) 方法。源码不贴了,主要就是给设置一些web容器中的配置信息(包括将 ServletContext 设置成 XmlWebApplicationContext 的属性,这样 Spring 就能在上下文里轻松拿到 ServletContext 了)

Spring 加载 class 文件

看最后一行的 refresh()方法即可,在这个方法里,会完成资源文件的加载、配置文件解析、Bean定义的注册、组件的初始化等核心工作。该方法来自于 XmlWebApplicationContext 的父父父类 AbstractApplicationContext(调皮一下,我并不是个结巴==),其实不管是 web 容器装载的 XmlWebApplicationContext 还是直接 ClassPathXmlApplicationContext 都会调用这个方法。


image

讲道理我现在一眼看到 synchronized,我都不把它翻译成锁,我都翻译成安全哈哈哈。至于这为什么不是锁住整个 refresh() 方法,由于不属于今天所讲的范围就不说了,感兴趣的自己去查一下咯。
我们来看下初始化 BeanFactory,obtainFreshBeanFactory 是整个 refresh() 方法的核心,其中完成了配置文件的加载、解析、注册。


image

后面你看到的都是 getBeanFactory 的代码,也就是已经初始化好了,这个 refreshBeanFactory 方法是 AbstractRefreshableApplicationContext 中的方法,它是 AbstractApplicationContext 的子类,同样不论是 XmlWebApplicationContext 还是 ClassPathXmlApplicationContext 都继承了它,因此都能调用到这个一样的初始化方法。


image

createBeanFactory 就是实例化一个 beanFactory 没别的,我们要看的是 bean 在哪里加载的,现在貌似还没看到重点,继续跟踪 loadBeanDefinitions(DefaultListableBeanFactory) 方法,此处web项目中将会由类: XmlWebApplicationContext 来实现


image

这里有一个 XmlBeanDefineitionReader,是读取 XML 中 spring 的相关信息(也就是解析 spring-application.xml 的),这里通过 getConfigLocations() 获取到的就是这个或多个文件的路径,会循环,通过 XmlBeanDefineitionReader 来解析,跟踪到 loadBeanDefinitions 方法里面,会发现方法实现体在 XmlBeanDefineitionReader 的父类 AbstractBeanDefinitionReader 中,代码如下:


image

解析spring-application.xml

我们目前只解析到我们的 spring-application.xml 在哪里,但是还没解析到spring-application.xml 的内容是什么,可能有多个 spring 的配置文件,这里会出现多个 Resource,所以是一个数组。
XmlBeanDefinitionReader.loadBeanDefinitions(Resource) 和上面这个类是父子关系,接下来会做:doLoadBeanDefinitions、registerBeanDefinitions 的操作,在注册 beanDefinitions 的时候,其实就是要真正开始解析XML了
它调用了 DefaultBeanDefinitionDocumentReader 类的 registerBeanDefinitions 方法,然后调用了 doRegisterBeanDefinitions 方法,如下图所示:


image

这里创建了一个 BeanDefinitionParserDelegate 实例,解析 XML 的过程就是委托它完成的,我们先不管它是怎样解析XML的,我们看下它怎么加载类的,所以主要看 parseBeanDefinitions 这个方法,里面会调用到 BeanDefinitionParserDelegate 类的 parseCustomElement 方法,用来解析bean的信息:


image

这里解析了 XML 的信息,跟踪进去,会发现用了 NamespaceHandlerSupport 的 parse 方法,它会根据节点的类型,找到一种合适的解析 BeanDefinitionParser(接口),他们预先被 spring 注册好了,放在一个 HashMap 中,例如我们在 spring 中通常会配置:

1
2
3
<context:component-scan base-package="com.lb.supervision.service" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service" />
</context:component-scan>

此时根据名称“component-scan”就会找到对应的解析器来解析,而与之对应的就是 ComponentScanBeanDefinitionParser 的 parse 方法,这地方已经很明显有扫描 bean 的概念在里面了,这里的 parse 获取到后,中间有一个非常非常关键的步骤那就是定义了 ClassPathBeanDefinitionScanner 来扫描类的信息,它扫描的是什么?是加载的类还是class文件呢?答案是后者,为什么?,因为有些类在初始化化时根本还没被加载, ClassLoader 根本还没加载,只是 ClassLoader 可以找到这些 class 的路径而已:


image

咦,看到这个doScan有种似曾相识的感觉呢,啊,之前说 mybatis 的 mapper 接口代理的时候看到过。先不说这个了,跟进去看看。


image

这么多代码看着很难受,要说把每行代码都搞懂估计现在还有点困难,直接看重点 findCandidateComponents,也就是通过每个 basePackage 去获取匹配的 class 文件路径。


image

image

此处的 packageSearchPath = classpath*:com/lb/supervision/service/**/*.class,如果我们配置的是 * ,那么将会被组装为 classpath*:*/**/*.class,这个也能获取到? Spring 还有这种操作的?我们点进 getResources(packageSearchPath)方法看下:


image

这里会先判断表达式是否以 classpath*: 开头。前面我们看到 Spring 已经给我们添加了这个头,这里当然符合条件了。接着会进入 findPathMatchingResources 方法。在这里又把 **/*.class 去掉了,然后在调用 getResources 方法,然后再进入 findAllClassPathResources 方法。这里的参数只剩下包名了例如 com/lb/supervision/service/ 。,先看下 findPathMatchingResources 方法:


image

这里有一个 rootDirPath,这个地方有个容易出错的,是如果你配置的是 com.lb.supervision.service ,那么 rootDirPath 部分应该是: classpath*:com/lb/supervision/service , 而如果配置是 * 那么就是 classpath*: 只有这个结果,而不是 classpath*:* (这里我就不说截取字符串的源码了),回到上一段代码,这里再次调用了 getResources(String) 方法,又回到前面一个方法,这一次,依然是以 classpath*: 开头,所以第一层 if 语句会进去,而第二层不会。所以我们再看一下 findAllClassPathResources(location)方法里的 doFindAllClassPathResources(path):


image

image

真相大白了, Spring 也是用的 ClassLoader 加载的 class 文件。一路追踪,原始的 ClassLoader 是 Thread.currentThread().getContextClassLoader()。
到此为止,就拿到class文件了。
然后我们回到 ClassPathBeanDefinitionScanner 的 doScan() 方法,跟踪 registerBeanDefinition() 方法直到 DefaultListableBeanFactory 类中的registerBeanDefinition() 方法。 Spring 会将 class 信息封装成的 BeanDefinition 放进 DefaultListableBeanFactory 的 beanDefinitionMap 中。源码就不贴了,感兴趣的旁友自己看下。

Spring 实例化 非懒加载单例bean

我们回到 refresh() 方法中看 finishBeanFactoryInitialization(beanFactory) 方法,此方法主要的任务就是实例化非懒加载的单例 bean。来看下这个方法:


image

由于代码长没有截全,该方法首先将加载进来的 beanDefinitionNames 循环分析,如果是我们自己配置的 bean 就会走 else 中的 getBean(beanName)。


image

getBean(beanName) 中调用 doGetBean() 方法,二话不说点进去看看,卧槽这么长!唉,没办法一点点看吧。


image

我们先看下这里的 getSingleton() 方法:


image

这里能看到, Spring 会把实例化好的 bean 存入 singletonObjects,这是一个 ConcurrentHashMap 。当然这里我们 bean 并未实例化过,所以这里应该也不能 get 出什么东西来,也就是返回 null 了。 doGetBean() 中的第一个 if 子句也就不会执行了。那么接着看 else 子句的内容。


image

在这里拿到 RootBeanDefinition 并 check,并获得 bean 的依赖,并循环迭代实例化 bean。例如class A 依赖于 class B,就会先实例化B。下面的 if … else …就是真正实例化 bean 的地方。其实真正实例化 bean 的方法是 createBean(beanName, mbd, args),只是区分了 isSingleton 或 isPrototype ,两者的区别在于单例的(Singleton)被缓存起来,而 Prototype 是不用缓存的。首先看一下 createBean()。 createBean 方法中除了做了一些实例化 bean 前的检查准备工作外,最核心的方法就是

1
beanInstance = this.doCreateBean(beanName, mbd, args);

这里面代码那就多了,就不贴出所有代码了

1
2
3
4
5
6
7
8
9
10
11
12
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, Object[] args) {
BeanWrapper instanceWrapper = null;
if(mbd.isSingleton()) {
instanceWrapper = (BeanWrapper)this.factoryBeanInstanceCache.remove(beanName);
}

if(instanceWrapper == null) {
instanceWrapper = this.createBeanInstance(beanName, mbd, args);
}

final Object bean = instanceWrapper != null?instanceWrapper.getWrappedInstance():null;
Class beanType = instanceWrapper != null?instanceWrapper.getWrappedClass():null;

首先就是创建一个bean的实例且封装到BeanWrapper中,在这里bean已经实例化了。具体的实现方法是在org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(RootBeanDefinition beanDefinition, String beanName, BeanFactory owner) 中。里面不难看出分两种情况,如果没有无参构造器是就生成CGLIB子类,否则就直接反射成实例。

既然已经有了实例对象了,那么, Spring 是如何将 bean 的属性注入到 bean 的呢?返回到上面的 doCreateBean 方法中。往下看找到 populateBean(beanName, mbd, instanceWrapper) ,内幕就在这里:


image

这里是调用 InstantiationAwareBeanPostProcessor 的具体子类的 ibp.postProcessPropertyValues 方法注入属性。当我们使用 @Resource 注解的时候,具体的子类是 CommonAnnotationBeanPostProcessor;如果使用的是 @Autowired 注解,则具体的子类是 AutowiredAnnotationBeanPostProcessor。此方法内是委托 InjectionMetadata 对象来完成属性注入。以 AutowiredAnnotationBeanPostProcessor 为例:


image

findAutowiringMetadata 方法能拿到使用了特定注解的属性(Field)、方法(Method)及依赖的关系保存到 checkedElements 集合里,然后再执行自己的 inject 方法。


image

喏,终于快要完事了,来看 InjectedElement 的 inject 方法:


image

喏,还是用JDK反射完成的咯。讲道理,以前只知道反射能干嘛,但是不知道能用在什么地方。现在看看源码好多地方都用的反射。。。

Spring容器看到这里差不多了,有空再看下SpringMVC的。。。