Spring源码解读——容器是如何初始化的

Posted BaldWinf

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring源码解读——容器是如何初始化的相关的知识,希望对你有一定的参考价值。

为什么要读源码

  • Spring是一群优秀的框架组成的社区、现在已经非常丰富了。当我们享受着Spring带来的便利同时,有时也想一探究竟。
  • 人人都说Spring好,难免有人趋之若鹜,如果让你说出个究竟,你能说出多少来?就我而言,除了能拽两AOP、DI等耳熟能详的洋词以外,就很难有高深的见解了。
  • 不得不说,选择先读Spring源码,是受到人云亦云的影响,既然都说他好,我们就要一探究竟。如果好,就要说出好的道理,如果不好,还要指出缺陷和不足。做不到这点,就算不上实事求是,就是“皇帝的新衣”了。
  • 有点儿好奇
  • 想写写东西。除了敲代码,偶尔写点别的东西,可以让我放松,也能装出很认真的样子-_-!

怎么读呢

当你下载好源码,也编译了,接下来就是欣赏源码了。可是突然发现,那画面太美以至于无法直视。怎么看,从哪里看?像对待一位亭亭玉立的处女一样,手足无措。

一切从实际出发

如果真有一位美女在你面前,你会做什么?答案是:在你没有搞清楚你和她的关系之前,你什么也不敢做。
对待源码也是这样。我用Spring源码,最多的地方莫过于启动项目的时候注入实例,包括Spring测试的时候常写的那句代码

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("bean.xml");

所以对我而言,Spring怎么启动并完成初始化,是我首先感兴趣的。这是我和Spring的直接关系

ClassPathXmlApplicationContext是什么

先看一张类图

看一下这张图,上图最底部的ClassPathXmlApplicationContext就是我关注的Context,Context是什么意思?一般翻译为“上下文”,我认为这点比较恶心,明显词不达意。我认为就叫“容器”更准确,这点我持保留意见,不一定有人认同。

顾名思义,ClassPathXmlApplicationContext的意思就是“类路径下的Xml形式的应用容器”。这个容器辈分很低,有很多的祖先。对于客户端来说,第一种初始化容器的方式,就是创建ClassPathXmlApplicationContext的实例,并且传入一个xml格式的配置文件名称,这个配置文件需要放在classpath路径下。

 new ClassPathXmlApplicationContext("bean.xml");

这里可以大胆猜测,ClassPathXmlApplicationContext的构造方法是一个复杂的过程,这个过程就是开启我们spring源码探险的征程。

ClassPathXmlApplicationContext的构造方法

我怀着激动无比的心情,ctrl+鼠标左键,点进了ClassPathXmlApplicationContext的构造方法中,看到了这个

    /**
     * Create a new ClassPathXmlApplicationContext, loading the definitions
     * from the given XML file and automatically refreshing the context.
     * @param configLocation resource location
     * @throws BeansException if context creation failed
     */
    public ClassPathXmlApplicationContext(String configLocation) throws BeansException 
        this(new String[] configLocation, true, null);
    

构造函数的重载,是常见的复用手段,这个技巧我也经常使用。上述代码值得注意的细节如下:

1、复用的构造函数有3个参数,我们的xml配置文件作为String数组的一个元素继续传递。是不是可以猜测 “spring的配置文件其实是支持多个的”。

2、第二个参数的是true,如果用idea的话,可以看到这个参数的名字叫“refresh”。一个boolean类型的变量叫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, ApplicationContext parent)
            throws BeansException 

        super(parent);
        setConfigLocations(configLocations);
        if (refresh) 
            refresh();
        
    

从注释中可以看到,我们一些猜想得到的验证。

1、配置文件路径是能支持多个的
2refresh定义:是否自动刷新容器,加载所有定义并创建所有单例。否则,在更多其他配置操作后,手动刷新容器。

疑惑也多了

parent context 父容器是什么东西?
简单猜测下,应该是一个父类,这个类也是容器。

进一步看代码,共三部分

    // 调用父类构造器
    super(parent);
    // 配置文件相关
    setConfigLocations(configLocations);
    // 如果需要,刷新容器
    if (refresh) 
        refresh();
    

这三部分就是ClassPathXmlApplicationContext构造器的全部内容,虽然简短,但是深远。从这里我们可以看到spring源码的一个特点,复用率高,而且可读性非常好。这一点我非常佩服,我始终认为,写代码应该像写小说一样,每一个方法、变量的名字,都应该发挥它最大的作用,让看代码的人脑海中充满画面感。

supper(parent)指向何方

我在大海中,充满迷茫,这时海神波塞冬出现了,他希望我能完成一件伟大的使命,这个使命很模糊,但是第一件事情就是追寻,追寻一个起源。
supper(parent),到底指向什么?还想象不到。还有parent是什么?还不清楚,充满疑惑,我继续点击进去,竟然

    /**
     * Create a new AbstractXmlApplicationContext with the given parent context.
     * @param parent the parent context
     */
    public AbstractXmlApplicationContext(ApplicationContext parent) 
        super(parent);
    

离开了ClassPathXmlApplicationContext的怀抱,进入了AbstractXmlApplicationContext 。这个Context是什么,还记得那张图吗?先回头看看,然后再继续。

public abstract class AbstractXmlApplicationContext extends AbstractRefreshableConfigApplicationContext 
    ...

他是ClassPathXmlApplicationContext的父类,抽象的。抽象类给我的感觉两种
1、抽象类维护一种逻辑上的结构关系。
2、抽象类通过模板方法,提高复用。
这两点,我认为第一点更重要,为了复用使用抽象类,是不明智甚至愚蠢的。

supper(parent)带着parent找到了这个抽象容器,可是它什么也不做,继续supper(parent)。对于这种做法,我渐渐有种认同:一条父子链上的所有类,往往越往上,功能越简单,甚至是个空实现。越往下,往往越具体,方法的特征也更有“具体问题具体分析”的感觉。往上,更哲学,往下,更接地气。我们要做的就是,将适度具体的方法,放在适度的层级,放的对放的准,代码的复用率就会更高。

显然,spring的作者认为,这个parent太深奥了,在AbstractXmlApplicationContext容器中做处理,有点“小材大用”了。

继续往下点击,没想到

    /**
     * Create a new AbstractRefreshableConfigApplicationContext with the given parent context.
     * @param parent the parent context
     */
    public AbstractRefreshableConfigApplicationContext(ApplicationContext parent) 
        super(parent);
    

又是个过手卡油的家伙,AbstractRefreshableConfigApplicationContext可以翻看前面的类图,他算是ClassPathXmlApplicationContext的爷爷了。

继续点

public AbstractRefreshableApplicationContext(ApplicationContext parent) 
        super(parent);
    

不解释,继续

    /**
     * Create a new AbstractApplicationContext with the given parent context.
     * @param parent the parent context
     */
    public AbstractApplicationContext(ApplicationContext parent) 
        this();
        setParent(parent);
    

这里需要停一下,AbstractApplicationContext通过类图可以看到,它的地位有点特殊了,家族的人脉关系,在这里有了一个极大的丰富。
一个重要的发现是(看图):

AbstractApplicationContext往下的子孙容器,首要一个身份是ResourceLoader,即资源加载器。而Context的身份来源居然是通过接口实现的。

这一点在逻辑上是很难理解的。就好比,一天,日昏黄,你饭后坐在门口,思绪飞扬,浮想你的祖先会是什么样的一个人呢?是诗人、商人、英雄、还是普通人呢?但最终你发现,原来你的祖先不是人,而是别的什么东西?是不是感觉到了“惊悚”,我对这样的继承关系,是不太认同的。

而我对这样的继承关系,唯一能想到的解释就是——“历史原因”。如果有识之士知道其中原委,不吝赐教。
这点我还是想多说两句,我始终认为继承是要谨慎的东西。好的继承关系,就像一部家庭伦理喜剧,而不好的继承关系,就是一部乱伦史。
为了复用而继承,就好比你的同学新买了一个iphonex,而你为了玩儿上它,竟然认他做“爹”,虽然iphonex得到了复用,但是伦理丧失、逻辑全无。后人如果不知其里,定会觉得“悬疑”无比,非常的“烧脑”,科学家们也会趋之若鹜。

AbstractApplicationContext的意义在于,super(parent)止于此,我们一定要关注一下。

    this();
    setParent(parent);

点开this(),看到

    /**
     * Create a new AbstractApplicationContext with no parent.
     */
    public AbstractApplicationContext() 
        this.resourcePatternResolver = getResourcePatternResolver();
    

注释很清楚:创建一个没有父容器的抽象构造容器。
然后初始化了一个resourcePatternResolver(资源模板解析器)
这里用到的资源模板解析器具体实例是:

    protected ResourcePatternResolver getResourcePatternResolver() 
        return new PathMatchingResourcePatternResolver(this);
    

叫PathMatchingResourcePatternResolver(路径匹配资源模板解析器)。而这个解析器需要一个类加载器的实例,我们给的就是this。这个this,可以理解就是我们创建的ClassPathXmlApplicationContext实例。

然后这个解析器持有了我们的资源加载器。

    /**
     * Create a new PathMatchingResourcePatternResolver.
     * <p>ClassLoader access will happen via the thread context class loader.
     * @param resourceLoader the ResourceLoader to load root directories and
     * actual resources with
     */
    public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) 
        Assert.notNull(resourceLoader, "ResourceLoader must not be null");
        this.resourceLoader = resourceLoader;
    

看完AbstractApplicationContext的this()之后,我们接着看下一行

setParent(parent);

看名字可以知道,这是添加父容器,这也是之前挖的一个坑,什么是父容器?需要点进去

    /**
     * @inheritDoc
     * <p>The parent @linkplain ApplicationContext#getEnvironment() environment is
     * @linkplain ConfigurableEnvironment#merge(ConfigurableEnvironment) merged with
     * this (child) application context environment if the parent is non-@code null and
     * its environment is an instance of @link ConfigurableEnvironment.
     * @see ConfigurableEnvironment#merge(ConfigurableEnvironment)
     */
    @Override
    public void setParent(ApplicationContext parent) 
        this.parent = parent;
        if (parent != null) 
            Environment parentEnvironment = parent.getEnvironment();
            if (parentEnvironment instanceof ConfigurableEnvironment) 
                getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
            
        
    

这里没有看懂这个setParent到底干了什么,而且我们用的测试方法是

    @Test
    public void testSingleConfigLocation() 
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(FQ_SIMPLE_CONTEXT);
        assertTrue(ctx.containsBean("someMessageSource"));
        ctx.close();
    

这里没有涉及到parent,所以这个坑暂时也是没有办法填上了。
这样回顾ClassPathXmlApplicationContext,这三行中的第一行代码就讲完了

        super(parent);
        setConfigLocations(configLocations);
        if (refresh) 
            refresh();
        

总结下,基本上就是两部分
1、创建一个解析器,解析器拥有容器自己的实例,而容器实际上也是个资源加载器。
2、添加父容器,这里没有涉及到

setConfigLocations(configLocations)

super(parent)之后,是设置配置文件路径的功能,点进去
其实ClassPathXmlApplicationContext的setConfigLocations(configLocations)方法是继承自AbstractRefreshableConfigApplicationContext的

    /**
     * Set the config locations for this application context.
     * <p>If not set, the implementation may use a default as appropriate.
     */
    public void setConfigLocations(String... locations) 
        if (locations != null) 
            Assert.noNullElements(locations, "Config locations must not be null");
            this.configLocations = new String[locations.length];
            for (int i = 0; i < locations.length; i++) 
                this.configLocations[i] = resolvePath(locations[i]).trim();
            
        
        else 
            this.configLocations = null;
        
    

注释告诉了一个重要信息:为应用容器设置“配置路径”,如果不设置,将使用一个默认的实现。
我的设置路径是

/org/springframework/context/support/simpleContext.xml

扩展:
1、可变参数可以传入数组,这是我刚知道的事情。
2、对于数组,可以通过Assert.noNullElements方法判断是否含有元素

AbstractRefreshableConfigApplicationContext将“locations”一个个地加入到自己的configLocations当中,configLocations是“AbstractRefreshableConfigApplicationContext”的一个私有变量

private String[] configLocations;

可能正是因为这个变量,才有的AbstractRefreshableConfigApplicationContext这个容器。

在加入之前,有一个resolvePath的动作,跟进一下

    /**
     * Resolve the given path, replacing placeholders with corresponding
     * environment property values if necessary. Applied to config locations.
     * @param path the original file path
     * @return the resolved file path
     * @see org.springframework.core.env.Environment#resolveRequiredPlaceholders(String)
     */
    protected String resolvePath(String path) 
        return getEnvironment().resolveRequiredPlaceholders(path);
    

大概意思就是用相应的环境属性,替换占位符。

getEnvironment方法的实现在AbstractApplicationContext中,是AbstractRefreshableConfigApplicationContext的爷爷了,点进getEnvironment()方法看看:

    /**
     * @inheritDoc
     * <p>If @code null, a new environment will be initialized via
     * @link #createEnvironment().
     */
    @Override
    public ConfigurableEnvironment getEnvironment() 
        if (this.environment == null) 
            this.environment = createEnvironment();
        
        return this.environment;
    

有一个createEnvironment()方法,点进去

    /**
     * Create and return a new @link StandardEnvironment.
     * <p>Subclasses may override this method in order to supply
     * a custom @link ConfigurableEnvironment implementation.
     */
    protected ConfigurableEnvironment createEnvironment() 
        return new StandardEnvironment();
    

注释大意:
创建并返回一个标准环境(StandardEnvironment)
子类如果想提供一个自定义的“配置环境(ConfigurableEnvironment)”,继承这个方法。

那么StandardEnvironment()是什么?还是不太清楚,看一张图

原来StandardEnvironment是一个PropertyResolver,可以看下这个PropertyResolver是什么

/**
 * Interface for resolving properties against any underlying source.
 *
 * @author Chris Beams
 * @author Juergen Hoeller
 * @since 3.1
 * @see Environment
 * @see PropertySourcesPropertyResolver
 */
public interface PropertyResolver 
    ...

叫做属性解析器。

返回的PropertyResolver立即调用了方法

    /**
     * Resolve $... placeholders in the given text, replacing them with corresponding
     * property values as resolved by @link #getProperty. Unresolvable placeholders with
     * no default value will cause an IllegalArgumentException to be thrown.
     * @return the resolved String (never @code null)
     * @throws IllegalArgumentException if given text is @code null
     * or if any placeholders are unresolvable
     * @see org.springframework.util.SystemPropertyUtils#resolvePlaceholders(String, boolean)
     */
    String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;

大概意思就是解析配置文件中的占位符,如果解析不了抛出异常

以上是关于Spring源码解读——容器是如何初始化的的主要内容,如果未能解决你的问题,请参考以下文章

Spring-IOC源码解读2-容器的初始化过程

Spring源码解读之核心容器下节

Spring-IOC源码解读3-依赖注入

Spring源码解读之核心容器上节

Spring:源码解读Spring IOC原理

Spring:源码解读Spring IOC原理