Spring源码阅读(一)如何阅读Spring源码?

Posted by Lain on 05-02,2020

前言

Spring源码,永远滴神!
本来不是很想写这个系列的,因为我觉得阅读源码,学习源码是一件很基础的事情,程序员在面对一个神奇的黑盒的时候,研究其的输入输出只是一个基本的学习手段,我们只能做到推测它内部发生了什么,却不能将其利用,扩展,以及自己实现,这种程度的学习是很难让人尽兴的,这个时候对源代码的阅读和学习就很有必要了,因此阅读源代码的能力我一直认为是基础中的基础。
但是Spring的代码确实很庞大,不分门别类记录一下,很容易忘记,就当是记个笔记吧。(才不是因为其他什么原因呢,哼
从Spring源码中我们能学到什么?

  1. 华丽到极致的面向对象的编程思想
  2. Spring框架的熟练扩展运用
  3. 开拓视野,认清楚Spring框架的本质

有的人说学习Java就是学习Spring全家桶,我觉得此言差矣,越是深入了解Spring,你就会发现Spring只是一个优秀的IOC容器框架而已,它确实相当优秀,值得我们学习和研究,但是如果将学习框架作为学习编程的终极目标的话,那就有些本末倒置了。身为一个有远大理想的开发者,应该想办法让自己从看魔法的人变成变魔法的人。

由于我个人能力有限,文中有错误理解之处在所难免,欢迎各位读者交流斧正。

从最简单的ApplicationContext开始

Spring源码之所以很多人觉得难以阅读,是因为Spring框架对于设计模式以及面向对象的编程思想的灵活运用,由于层层封装,使得很多源码阅读者追源代码追着追着就不知道到看到哪里了,很容易云里雾里。老爹说过,要用魔法打败魔法,对于面向对象编程的源码,就要用面向对象的思维来看它。

相信很多初学者都是从一个简单的ClassPathXmlApplicationContext开始接触Spring的,通过ClassPathXmlApplicationContext我们可以快速的指定一个XML配置文件,在里面定义我们的Bean,然后通过getBean()方法将它从容器中拿出来,并利用它,就让我们从它开始吧。

前期准备:
新建一个Maven项目,引入依赖:

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.5.RELEASE</version>
        </dependency>
public interface LainService {
    void test();
}
public class LainServiceImpl implements LainService{

    public void test(){
        System.out.println("Hello,Lain!");
    }
}
public class SpringTestApplication {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("classpath:/spring/lain-test.xml");
        LainService lainService = classPathXmlApplicationContext.getBean(LainService.class);
        lainService.test();
    }
}

XML配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="lainService" class="fun.lain.LainServiceImpl"></bean>
</beans>

看到这里肯定有人会说,都什么年代了,还搁这xml文件呢?溜了溜了。实际上不管是哪种配置方式,归根结底都是一样的,在这方面没有什么优劣之分,后面我会演示为什么这么说。

好了,第一个例子就这样完成了,运行后我们可以看见控制台打印出了"Hello,Lain!",说明Spring成功完成了一个完整的Bean的注册获取的过程。别看它简单,光是这个例子就够我们喝一壶了。

接下来我们来分析代码。
但是请不要现在就开始追踪运行过程,之前我说过,要用面向对象的思维去看它,在这章我并不会具体分析任何原理,我只想说明如何去看Spring的源码,具体的分析我会在后续章节中慢慢推出。

观察UML图

让我们来看一下ClassPathXmlApplicationContext的继承树:
image.png
我们可以看到,ClassPathXmlApplicationContext继承实现了一连串的Abstract类(抽象类),说明它具有上述抽象类所具有的全部功能,而抽象类只能借由实现类进行实例化,我们来依次看看这些抽象类分别代表着怎样的特性,而它们又有哪些具体的实现。
这一步极其重要,善用idea等工具,获得了UML图,我们就能得知Spring是如何封装细节的,按层次依次向上阅读,可以获得一个由简单到丰富的过程,不至于一下子就在代码的漩涡里打圈儿。

AbstractXmlApplicationContext

实现类:
image.png
可以看到,除了我们的ClassPathXmlApplicationContext以外,还有一个FileSystemXmlApplicationContext的实现,通过注释我们可以知道,两者的区别在于前者是从classpath下获取文件,支持"classpath:"前缀的路径描述,但不支持通过系统的绝对路径来获取文件,比如"C:\spring\lain-test.xml" ,而后者支持从文件系统的任意一个路径获取,支持"file:"前缀的路径描述。
大家可以自己设计实验来验证这一点,这里就不赘述了。
为什么两者会有这种差异呢?通过对比源码我们可以知道,FileSystemXmlApplicationContext 重写了getResourceByPath方法,源代码如下:


protected Resource getResourceByPath(String path) {
	if (path.startsWith("/")) {
		path = path.substring(1);
	}
	return new FileSystemResource(path);
}

ClassPathXmlApplicationContext没有重写,而是使用的默认实现,我们可以很快的定位到默认实现位于DefaultResourceLoader

/**
    * Return a Resource handle for the resource at the given path.
    * <p>The default implementation supports class path locations. This should
    * be appropriate for standalone implementations but can be overridden,
    * e.g. for implementations targeted at a Servlet container.
    * @param path the path to the resource
    * @return the corresponding Resource handle
    * @see ClassPathResource
    * @see org.springframework.context.support.FileSystemXmlApplicationContext#getResourceByPath
    * @see org.springframework.web.context.support.XmlWebApplicationContext#getResourceByPath
    */
protected Resource getResourceByPath(String path) {
    return new ClassPathContextResource(path, getClassLoader());
}

可以看到,一个返回了ClassPathContextResource,另一个返回了FileSystemResource,正是对资源文件描述的不同的实现,影响到了不同的ApplicationContext对配置文件加载的逻辑。

上述两个实现类的差异,仅仅体现在加载XML文件的方式不同,而其作为应用上下文的其他功能,都被封装在了AbstractXmlApplicationContext 及其父类之中,让我们继续回到这个抽象类里来。
点开它的源代码,先看结构。
image.png
可以看到,除去构造方法以外,其余的方法好像都和BeanDefinition有关?而其中重写自父类的方法只有一个

/**
 * Loads the bean definitions via an XmlBeanDefinitionReader.
 * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader
 * @see #initBeanDefinitionReader
 * @see #loadBeanDefinitions
 */
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
    // Create a new XmlBeanDefinitionReader for the given BeanFactory.
    XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

    // Configure the bean definition reader with this context's
    // resource loading environment.
    beanDefinitionReader.setEnvironment(this.getEnvironment());
    beanDefinitionReader.setResourceLoader(this);
    beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

    // Allow a subclass to provide custom initialization of the reader,
    // then proceed with actually loading the bean definitions.
    initBeanDefinitionReader(beanDefinitionReader);
    loadBeanDefinitions(beanDefinitionReader);
}

这就是决定了AbstractXmlApplicationContext及其子类和其他的ApplicationContext不一样的地方,它在加载Bean定义的时候,创建了一个XmlBeanDefinitionReader 实例,并使用这个实例加载了XML配置中的BeanDefinition,并将其注册进容器。

扩展点

那么如果我们稍微研究一下ResourceLoader相关的源码,是不是可以自己定义一个MyApplicationContext,通过Http,FTP,或者消息队列,各种稀奇古怪的方式来获取配置文件?或者自己定义BeanDefinationReader,比如通过Excel来读取Bean信息(当然不推荐这么做)这就是一些扩展点,由于不是本章要讲的内容,大家可以脑洞大开,自己实现一下。

AbstractRefreshableConfigApplicationContext

还是老样子,直接看它的结构:
image.png
同时我们发现,它还实现了两个接口:BeanNameAware, InitializingBean,这也是需要重点关注的地方之一,本文先不做扩展。
可以发现,这个抽象类主要是为ApplicationContext提供了设置Bean配置文件路径的功能,没有什么好讲的,继续点开它的父类。

AbstractRefreshableApplicationContext

重头戏终于来了,这个类里面出现了一些很令人感兴趣的东西,还是老样子,先看结构:
image.png

再看重写自父类的方法,我们会发现,这些方法都和BeanFactory有关。先排除掉其中getBeanFactory(),hasBeanFactory(),以及closeBeanFactory()这些一眼就能看出来是干什么的方法,我们发现下面这个方法很令人在意:

/**
 * This implementation performs an actual refresh of this context's underlying
 * bean factory, shutting down the previous bean factory (if any) and
 * initializing a fresh bean factory for the next phase of the context's lifecycle.
 */
@Override
protected final void refreshBeanFactory() throws BeansException {
    if (hasBeanFactory()) {
        destroyBeans();
        closeBeanFactory();
    }
    try {
        DefaultListableBeanFactory beanFactory = createBeanFactory();
        beanFactory.setSerializationId(getId());
        customizeBeanFactory(beanFactory);
        loadBeanDefinitions(beanFactory);
        synchronized (this.beanFactoryMonitor) {
            this.beanFactory = beanFactory;
        }
    }
    catch (IOException ex) {
        throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
    }
}

通过阅读源码和注释我们可以得知,这是一个刷新BeanFactory的方法,会将此context底层的beanfactory关闭,并创建一个全新的BeanFactory供下一个生命周期使用。
这是什么意思呢?我们带着疑问,查看一下它是在哪调用的:
image.png
找到了!是在它的父类AbstractApplicationContext中调用的,点进去继续观察:

/**
 * Tell the subclass to refresh the internal bean factory.
 * @return the fresh BeanFactory instance
 * @see #refreshBeanFactory()
 * @see #getBeanFactory()
 */
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
    refreshBeanFactory();
    return getBeanFactory();
}

看不出什么,继续往上追朔:
image.png
终于,我们来到了世界的尽头(大雾
回过头去看最开始的UML类图,在这之后,除了一个DefaultResourceLoader,已经没有父类可以封装细节了,AbstractApplicationContext就是所有ApplicationContext的基类,其余的ApplicationContext都是在其基础上,进行扩展重写得到的,它就是最底层的细节封装。
事不宜迟,让我们来看看它做了些什么。

AbstractApplicationContext

在这里,我们可以看见调用是来自一个refresh()方法,要探明整个流程,就必须要知道这个refresh()是在何处被调用的,我们继续追朔:
image.png
不愧是世界的尽头,万物的终点,这里出现的是一大堆它的子类们,在里面我们好像发现了一个熟悉的身影:
image.png
这不就是我们的ClassPathXmlApplicationContext吗?
赶紧点进去看一下,发现对于refresh的调用全是出自构造器:

/**
 * Create a new ClassPathXmlApplicationContext with the given parent,
 * loading the definitions from the given XML files.
 * @param configLocations array of resource locations
 * @param refresh whether to automatically refresh the context,
 * loading all bean definitions and creating all singletons.
 * Alternatively, call refresh manually after further configuring the context.
 * @param parent the parent context
 * @throws BeansException if context creation failed
 * @see #refresh()
 */
public ClassPathXmlApplicationContext(
        String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
        throws BeansException {

    super(parent);
    setConfigLocations(configLocations);
    if (refresh) {
        refresh();
    }
}
...........................
/**
 * Create a new ClassPathXmlApplicationContext with the given parent,
 * loading the definitions from the given XML files and automatically
 * refreshing the context.
 * @param paths array of relative (or absolute) paths within the class path
 * @param clazz the class to load resources with (basis for the given paths)
 * @param parent the parent context
 * @throws BeansException if context creation failed
 * @see org.springframework.core.io.ClassPathResource#ClassPathResource(String, Class)
 * @see org.springframework.context.support.GenericApplicationContext
 * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader
 */
public ClassPathXmlApplicationContext(String[] paths, Class<?> clazz, @Nullable ApplicationContext parent)
        throws BeansException {

    super(parent);
    Assert.notNull(paths, "Path array must not be null");
    Assert.notNull(clazz, "Class argument must not be null");
    this.configResources = new Resource[paths.length];
    for (int i = 0; i < paths.length; i++) {
        this.configResources[i] = new ClassPathResource(paths[i], clazz);
    }
    refresh();
}

可以看到这些构造器本质上只做了两件事:

  1. 加载配置文件路径
  2. 调用refresh()刷新BeanFactory

联想到之前的AbstractRefreshableConfigApplicationContext,这个父类是不是提供设置配置文件路径的功能?这样的话,如果我们用默认构造器构建ClassPathXmlApplicationContext,是不是可以手动执行这两个步骤?
于是我们的代码还可以这样写:

public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext();
    context.setConfigLocations("classpath:/spring/lain-test.xml");
    context.refresh();
    LainService lainService = context.getBean(LainService.class);
    lainService.test();
}

执行之后,控制台输出"Hello,Lain!",说明容器工作正常。
那么问题来了,这个refresh()到底有什么用?如今我们可以手动决定是否调用它了,试着不调用它,看看会有什么后果。将它注释掉之后执行:

Exception in thread "main" java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext

可以看到,Spring内部报了一个错,上面写着"BeanFactory 没有被初始化,或已经被关闭,在获取bean之前请先通过ApplicationContext执行refresh"。

到这里我们就明白了,refresh相当于是一个初始化的过程,对于AbstractRefreshableConfigApplicationContext它会重新创建新的BeanFactory,并重新初始化所有bean定义。

因此,如果我们在容器初始化之后,如果还要向里面注册新的Bean,比如添加新的配置文件地址,或者修改了配置文件中的Bean定义,都需要调用一次refresh方法,进行Bean的重新加载。比如这个例子里面,我们将refresh和setConfigLocations的执行顺序互换,就会报出另一个大家都很熟悉的错误:

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'fun.lain.LainService' available

理解了这一点,我们接下来的研究就很好办了。比如Bean是如何被注册进容器的?容器内部是用什么数据结构存储的Bean?在执行Bean的初始化时,Spring有哪些骚操作?Spring是在哪里处理注解的?探究这些问题就有了思路和方向。

总结

这篇文章只是个引子,想告诉大家的是,阅读Spring源码是一个怎么样的思路,面对面向对象的层层封装,必须要有一个正确的阅读方式,在把握了整体框架的情况下,单点突破,层层细究,善用java doc、idea等工具,结合断点,用最简单的例子去做剖析,才能让你的头脑在阅读源码时不至于迷糊。