SpringMVC之浅析上下文初始化(一)

说明:本文所用的SpringMVC版本为4.3.4.RELEASE,应用服务器为TomCat8.0.33。下面我们先回顾一下我们在用SpringMVC进行开发时在web.xml中进行的一些配置:

    <!-- 配置Spring上下文配置文件 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
    <!-- 配置SpringMVC Servlet -->
    <servlet>
        <servlet-name>spring-miscellaneous</servlet-name>
        <!-- SpringMVC 分发器 -->
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 更改SpringMVC配置文件的名字 默认的是spring-mvc-servlet.xml -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-miscellaneous-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>spring-miscellaneous</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    <!-- 配置监听器 用来载入上下文配置文件 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

上面的代码,基本上是我们用哪个SpringMVC开发时进行的一些配置。那么这些配置都用什么用呢?下面我们来一点一点进行分析:

ContextLoaderListener

ContextLoaderListener上下文监听器。ContextLoaderListener的作用是Web容器启动的时候,自动装配ApplicationContext的配置信息,即初始化Spring IOC容器。我们看一下这个类的继承关系:

。从图中我们可以看出ContextLoaderListener实现了ServletContextListener(Servlet上下文监听器)接口。我们简单说一下ServletContextListener的作用:ServletContextListener中有两个方法,一个是contextInitialized一个是contextDestroyed。每一个应用都有ServletContext与之相关联,ServletContext中存放了一些web应用全局性的配置。当Servlet容器启动的时候,会实例化和ServletContext相关的一些类。如ApplicationContext(注意不是Spring中的那个ApplicationContext)、ApplicationContexFacade、ServletConfig、ServletContextEvent以及ServletContextListener的实现类等等,并调用contextInitialized方法进行一些上下文初始化相关的动作。当Servlet容器关闭的时候,会调用contextDestroyed销毁上下文相关的内容。即ServletContextListener是和ServletContext的生命周期相关联的。ContextLoaderListener实现了ServletContextListener接口,所以当容器启动的时候,会调用ContextLoaderListener中的contextInitialized方法,进行一系列的初始化的动作。
而<context-param>中的内容即是ServletContext中的一些配置信息。
下面我们看一下从容器启动到调用到contextInitialized方法的调用链:



从上图中我们可以看到在org.apache.catalina.core.StandardContext#listenerStart的方法中调用了contextInitialized的方法,进行上下文的初始化。PS:StandardContex是TomCat体系中很重要的一个类,可以找时间说一下TomCat的大致体系结构,网上也有很多资料。下面我们看一下在ContextLoaderListener中的contextInitialized中发生了什么。
    @Override
    public void contextInitialized(ServletContextEvent event) {
        initWebApplicationContext(event.getServletContext());
    }

ContextLoaderListener中的内容很简单,就是调用了initWebApplicationContext这个方法,并把ServletContext的实现类(ApplicationContextFacade)传进去。initWebApplicationContext这个方法在ContextLoader这个类中,ContextLoaderListener继承了ContextLoader这个类,其实主要的调用逻辑都在ContextLoader这个类中。我们就进入到initWebApplicationContext这个方法中,看看代码中是怎么写的(去掉了无关的代码):

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
        //如果已经初始化过了,则抛出异常 在ServletContext的生命周期中,只能初始化一次
        if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
            throw new IllegalStateException(
                    "Cannot initialize context because there is already a root application context present - " +
                    "check whether you have multiple ContextLoader* definitions in your web.xml!");
        }
        Log logger = LogFactory.getLog(ContextLoader.class);
        servletContext.log("Initializing Spring root WebApplicationContext");
        long startTime = System.currentTimeMillis();
        try {
            //如果context为空的话,则创建Spring上下文 因为这里有一个带WebApplicationContext参数的构造函数
            if (this.context == null) {
                this.context = createWebApplicationContext(servletContext);
            }
            //如果Spring web上下文为可配置的web上下文
            if (this.context instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
                if (!cwac.isActive()) {
                    // The context has not yet been refreshed -> provide services such as
                    // setting the parent context, setting the application context id, etc
                    //在Spring上下文还未刷新完之前  ApplicationContext是通过调用refresh方法来进行bean的加载初始化装配等一系列动作
                    if (cwac.getParent() == null) {
                        // The context instance was injected without an explicit parent ->
                        // determine parent for root web application context, if any.
                        //设置父类上下文
                        ApplicationContext parent = loadParentContext(servletContext);
                        cwac.setParent(parent);
                    }
                    configureAndRefreshWebApplicationContext(cwac, servletContext);
                }
            }
            //把Spring中的web上下文放到ServletContext中,  在程序中可以通过ServletContext获取到Spring中的web上下文
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
            //获取线程上下文加载器
            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
            //如果线程上下文加载器和当前类加载器是同一个类加载器,则当前上下文和Context是一个东西
            if (ccl == ContextLoader.class.getClassLoader()) {
                currentContext = this.context;
            }
            //如果不是同一个类加载器加载的,则把刚实例化的上下文放到currentContextPerThread中 key是线程上下文加载器 value是当前实例化的context
            else if (ccl != null) {
                currentContextPerThread.put(ccl, this.context);
            }
            return this.context;
        }
    }

假设我们的this.context为null(这里我们只考虑正常的一般场景),我们继续到createWebApplicationContext方法中,看一下是怎么创建Spring web上下文的:

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
        //查找WebApplicationContext的实现类
        Class<?> contextClass = determineContextClass(sc);
        if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {//获取到的Class必须是ConfigurableWebApplicationContext的子类
            throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
                    "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
        }
        //实例化WebApplicationContext的实现类 注意的是这个实现类一定是实现了ConfigurableWebApplicationContext的接口的
        return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
    }

上面的代码很简单:查找WebApplicationContext的实现类,然后通过反射的方式实例化。实例化的过程不用多说,我们看一下determineContextClass这个方法。

protected Class<?> determineContextClass(ServletContext servletContext) {
        //从ServletContext中获取key为contextClass的配置信息
        String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
        //如果在web.xml中配置了WebApplicationContext的实现类,则获取配置的实现类的类信息
        if (contextClassName != null) {
            try {
                return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
            }
            catch (ClassNotFoundException ex) {
                throw new ApplicationContextException(
                        "Failed to load custom context class [" + contextClassName + "]", ex);
            }
        }
        else {
            //取默认的配置信息
            contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
            try {
                return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
            }
            catch (ClassNotFoundException ex) {
                throw new ApplicationContextException(
                        "Failed to load default context class [" + contextClassName + "]", ex);
            }
        }
    }

从上面的代码中我们可以看出,我们可以配置在web.xml中配置WebApplicationContext的实现类,如果没有在web.xml中进行配置的话,则取默认的实现类。下面我们看一下是怎么去默认的实现类的:
在ContextLoader中有这样的一段代码:

static {
       //从类路径下获取ContextLoader.properties
       //Spring的Resource相关的类非常好用
       try {
           ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
           //加载ContextLoader.properties
           defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
       }
       catch (IOException ex) {
           throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
       }
   }

静态代码块,我们知道在类初始化的时候会执行静态代码块。所以在ContextLoader实例化之前已经加载过了ContextLoader.properties配置文件。我们看一下ContextLoader.properties中的内容:

org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

我们发现这里就一个配置项,默认配置了一个WebApplicationContext的实现类:XmlWebApplicationContext。所以如果我们没有在web.xml中进行配置的话,这里就会实例化XmlWebApplicationContext这个类。
下面我们回到initWebApplicationContext这个方法中继续分析。因为我们的Context是刚实例化的,而active的默认值是false,所以会进入到if中,同样cwac.getParent()==null成立,所以会执行loadParentContext()这个方法。我们看看这个方法中做了什么操作:

protected ApplicationContext loadParentContext(ServletContext servletContext) {
        ApplicationContext parentContext = null;
        //从ServletContext中加载配置项为locatorFactorySelector的信息
        String locatorFactorySelector = servletContext.getInitParameter(LOCATOR_FACTORY_SELECTOR_PARAM);
        //从ServletContext中加载配置项为parentContextKey的新
        String parentContextKey = servletContext.getInitParameter(LOCATOR_FACTORY_KEY_PARAM);
        //如果设置了parentContextKey的信息
        if (parentContextKey != null) {
            // locatorFactorySelector may be null, indicating the default "classpath*:beanRefContext.xml"
            //实例化一个单例的BeanFactoryLocator
            BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance(locatorFactorySelector);
            Log logger = LogFactory.getLog(ContextLoader.class);
            if (logger.isDebugEnabled()) {
                logger.debug("Getting parent context definition: using parent context key of '" +
                        parentContextKey + "' with BeanFactoryLocator");
            }
            //获取父上下文
            this.parentContextRef = locator.useBeanFactory(parentContextKey);
            parentContext = (ApplicationContext) this.parentContextRef.getFactory();
        }
        return parentContext;
    }

loadParentContext主要做的就是从web.xml中加载父应用上下文。
下面就到了我们最重要的一个方法了configureAndRefreshWebApplicationContext了。接下来我们的分析也就是围绕这个类了:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
        //这里判断wac有没有被初始化 设置过
        if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
            //如果ServletContext中配置了contextId,则获取配置的值,并设置为id的值
            String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
            if (idParam != null) {
                wac.setId(idParam);
            }
            else {
                // Generate default id...
                //生成一个默认的ID值
                wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                        ObjectUtils.getDisplayString(sc.getContextPath()));
            }
        }
        //把ServletContext设置到Spring的Web上下文中
        wac.setServletContext(sc);
        //获取contextConfigLocation的值, 这个值就是我们的Spring的配置文件
        String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
        if (configLocationParam != null) {
            wac.setConfigLocation(configLocationParam);
        }

        // The wac environment's #initPropertySources will be called in any case when the context
        // is refreshed; do it eagerly here to ensure servlet property sources are in place for
        // use in any post-processing or initialization that occurs below prior to #refresh
        ConfigurableEnvironment env = wac.getEnvironment();
        if (env instanceof ConfigurableWebEnvironment) {
            ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
        }
        //定制化的上下文
        customizeContext(sc, wac);
        //调用上下文的刷新的方法,这里就开始bean的加载初始化实例化装配等一系列的动作
        wac.refresh();
    }

在上面的代码中我们分析一下customizeContext这个方法,对于wac.refresh();这个方法我们这里就先不展开了。因为这个方法是SpringIOC容器初始化的入口。内容太多了,我们以后在分析Spring的源码时再详细的说明。

   //获取配置的初始化的类
       List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
               determineContextInitializerClasses(sc);
       for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
           Class<?> initializerContextClass =
                   GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
           if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
               throw new ApplicationContextException(String.format(
                       "Could not apply context initializer [%s] since its generic parameter [%s] " +
                       "is not assignable from the type of application context used by this " +
                       "context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
                       wac.getClass().getName()));
           }
           this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
       }
       //进行排序操作  即上面得到的进行初始化的类 需要实现Ordered的接口或者配置@Order注解
       AnnotationAwareOrderComparator.sort(this.contextInitializers);
       for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
           //调用初始化方法 进行初始化
           initializer.initialize(wac);
       }
   }

下面我们看一下determineContextInitializerClasses这个查找初始化类的方法

protected List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>>
            determineContextInitializerClasses(ServletContext servletContext) {

        List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> classes =
                new ArrayList<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>>();
        //从ServletContext中获取初始化参数为globalInitializerClasses的值
        String globalClassNames = servletContext.getInitParameter(GLOBAL_INITIALIZER_CLASSES_PARAM);
        if (globalClassNames != null) {
            //进行字符串的分割
            for (String className : StringUtils.tokenizeToStringArray(globalClassNames, INIT_PARAM_DELIMITERS)) {
                classes.add(loadInitializerClass(className));
            }
        }
        //从ServletContext中获取初始化参数为contextInitializerClasses的值
        String localClassNames = servletContext.getInitParameter(CONTEXT_INITIALIZER_CLASSES_PARAM);
        if (localClassNames != null) {
            //进行字符串的分割, ; 空格
            for (String className : StringUtils.tokenizeToStringArray(localClassNames, INIT_PARAM_DELIMITERS)) {
                classes.add(loadInitializerClass(className));
            }
        }

        return classes;
    }

OK,到这里我们关于Spring Web上下文的分析就先到这里了。这里初始化了一个父的IOC容器。下篇文章我们接着分析DispatcherServlet的初始化过程。其时序图如下:


资料:线程上下文:
http://blog.csdn.net/zhoudaxia/article/details/35897057

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容