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

扫码关注公众号:Java 技术驿站

发送:vip
将链接复制到本浏览器,永久解锁本站全部文章

【公众号:Java 技术驿站】 【加作者微信交流技术,拉技术群】
免费领取 2000+ 道 Java 面试题

为什么要读源码

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

怎么读呢

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

一切从实际出发

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

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

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

ClassPathXmlApplicationContext是什么

先看一张类图

20191123100125\_1.png

看一下这张图,上图最底部的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、配置文件路径是能支持多个的
    2、refresh定义:是否自动刷新容器,加载所有定义并创建所有单例。否则,在更多其他配置操作后,手动刷新容器。

疑惑也多了

    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()是什么?还是不太清楚,看一张图

20191123100125\_2.png

原来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;

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


来源:http://ddrv.cn/a/88268

赞(0) 打赏
版权归原创作者所有,任何形式的转载请联系博主:daming_90:Java 技术驿站 » Spring源码解读(一)——容器是如何初始化的

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏