SpringMVC的启动流程分析

说在前面

SpringMVC的项目是基于外部Tomcat的启动来启动的,当Tomcat启动好了,SpringMVC的项目也就启动好了,也就是说SpringMVC是借助于Tomcat提供的扩展点来完成启动的,其实SpringMVC的启动主要利用Tomcat的两个扩展点:

  1. ServletContainerInitializer的回调
  2. ServletContextListener事件回调

下面我们就这两个Tomcat的扩展点来展开分析SpringMVC的启动流程:

从Servlet的规范说起

ServletContainerInitializer的回调

Tomcat的回调逻辑

在Tomcat的启动过程中会触发ServletContainerInitializer的回调,即在StandardContext中的startInternal方法中会遍历所有的ServletContainerInitializer实现并触发其onStartup方法,代码如下:

    // Call ServletContainerInitializers
            // 回调实现了ServletContainerInitializer的实现类的onStartup方法
            // SpringMVC正式基于这一回调实现的,丢进去DispatcherServlet
            for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
                initializers.entrySet()) {
                try {
                    // SpringMVC在此注入了ContextLoaderListener监听器【创建了RootApplicationContext,并将其丢到ContextLoaderListener中】,
                    // 并创建了WebApplicationContext
                    // 同时创建了DispatcherServlet对象并且赋值到Context中
                    entry.getKey().onStartup(entry.getValue(),
                            getServletContext());
                } catch (ServletException e) {
                    log.error(sm.getString("standardContext.sciFail"), e);
                    ok = false;
                    break;
                }
            }

Tomcat是通过SPI机制来加载所有的ServletContainerInitializer的对象的,这就很方便第三方来利用这个扩展点,SpringMVC就是利用了这一点,注入了自己的SpringServletContainerInitializer,我们来看下SpringMVC中SPI的配置:

image

当Tomcat启动时回调到ServletContainerInitializers的这个点时,就会触发SpringServletContainerInitializer#onStartup方法的执行,并且Servlet3.0的规范是会扫描ServletContainerInitializer实现类上标注@HandlesTypes注解中的类做为onStartup方法的第一个参数。那么下面我们就重点分析下SpringServletContainerInitializer这个类:

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {

        List<WebApplicationInitializer> initializers = Collections.emptyList();

        if (webAppInitializerClasses != null) {
            initializers = new ArrayList<>(webAppInitializerClasses.size());
            for (Class<?> waiClass : webAppInitializerClasses) {
                // Be defensive: Some servlet containers provide us with invalid classes,
                // no matter what @HandlesTypes says... 所有非接口非抽象的WebApplicationInitializer实现类
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                        WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer)//集合负责保存满足上面条件的类
                                ReflectionUtils.accessibleConstructor(waiClass).newInstance());
                    }
                    catch (Throwable ex) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }

        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
            return;
        }
        //下面会调用所有的满足要求的WebApplicationInitializer,调用他们的onStartup方法
        servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
        AnnotationAwareOrderComparator.sort(initializers);
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);//调用所有的WebApplicationInitializer的onStartup方法
        }
    }

}

通过看代码,我们知道Tomcat启动时会将项目中所有的WebApplicationInitializer类对象作为集合参数传给onStartup方法的第一个参数,然后在onStartup方法中会循环遍历调用所有的WebApplicationInitializer对象的onStartup方法完成启动。

基于SpringMVC我们如何让Tomcat启动我们的项目

所以当我们想Tomcat启动能够成功启动我们的SpringMVC项目,我们就需要实现WebApplicationInitializer接口来完成启动流程,当然WebApplicationInitializer只是一个接口,为了方便快速开发,SpringMVC通过模板模式为我们提供了一些实现类:

image

当我们是基于注解开发时,我们只需要继承AbstractAnnotationConfigDispatcherServletInitializer方法即可,如下:

public class QuickAppStarter extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override   //根容器的配置(Spring的配置文件===配置类)
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[]{SpringConfig.class};
    }

    @Override   //web容器的配置(SpringMVC的配置文件===配置类)
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{SpringMVCConfig.class};
    }

    @Override   //Servlet的映射DispatcherServlet的映射路径
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

其中,我们需要重写几个方法:

  1. getRootConfigClasses:指定根容器的配置类,父子容器中的父容器
  2. getServletConfigClasses:指定web容器的配置类,父子容器中的子容器
  3. getServletMappings:指定DispatcherServlet的映射路径

具体的启动流程

启动起来还是十分简单的,我们来看下这个的启动流程:

因为我们继承的是AbstractAnnotationConfigDispatcherServletInitializer,所以首先会调用AbstractDispatcherServletInitializer的onStartup方法,在这个方法中也干了两件事:

  1. 调用父类AbstractContextLoaderInitializer的onStart方法

具体调用的AbstractContextLoaderInitializer#registerContextLoaderListener方法如下:

    protected void registerContextLoaderListener(ServletContext servletContext) {
        WebApplicationContext rootAppContext = createRootApplicationContext();//创建一个根容器
        if (rootAppContext != null) { // 将创建好的Spring容器放入到ContextLoaderListener监听器中,后面Tomcat启动完应用是回调这个监听器的contextInitialized方法
            ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
            listener.setContextInitializers(getRootApplicationContextInitializers());
            // 将监听器加入到上下文
            servletContext.addListener(listener);
        }
        else {
            logger.debug("No ContextLoaderListener registered, as " +
                    "createRootApplicationContext() did not return an application context");
        }
    }

主要就是往ServletContext中加入了一个监听器ContextLoaderListener,并且这个监听器中组合了通过我们指定的配置类而创建的根容器。这个监听器会在ServletContainerInitializer回调完之后被回调到,使我们后面要讲的第二个扩展点。

  1. 往ServletContext中注册DispatcherServlet

具体调用的AbstractDispatcherServletInitializer#registerDispatcherServlet方法代码逻辑如下:

    protected void registerDispatcherServlet(ServletContext servletContext) {
        String servletName = getServletName();
        Assert.hasLength(servletName, "getServletName() must not return null or empty");

        WebApplicationContext servletAppContext = createServletApplicationContext();// 创建Web容器
        Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");

        FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);//new 出来了一个DispatcherServlet,并保存了web-ioc容器
        Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
        dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

        ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
        if (registration == null) {
            throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +
                    "Check if there is another servlet registered under the same name.");
        }

        registration.setLoadOnStartup(1);
        registration.addMapping(getServletMappings());//根据我指定的DispatcherServlet的路径进行注册
        registration.setAsyncSupported(isAsyncSupported());

        Filter[] filters = getServletFilters();
        if (!ObjectUtils.isEmpty(filters)) {
            for (Filter filter : filters) {
                registerServletFilter(servletContext, filter);
            }
        }

        customizeRegistration(registration);
    }

这个方法主要也就干了一件事:将创建的DispatcherServlet添加到ServletContext中,其中DispatcherServlet中组合了根据我们指定的配置创建的Web容器。

总结

也就是说:整个ServletContainerInitializer的回调主要就干了两件事:

  1. 创建了ContextLoaderListener监听器【组合了创建的根容器】,并加入到ServletContext
  2. 创建了DispatcherServlet【组合了创建的web容器】,并加入到ServletContext

ServletContextListener事件回调

ServletContainerInitializer的回调结束之后,Tomcat会回调ServletContextListener的contextInitialized方法,即StandardContext中listenerStart方法,代码如下:

for (Object instance : instances) {
            if (!(instance instanceof ServletContextListener)) {
                continue;
            }
            ServletContextListener listener = (ServletContextListener) instance;
            try {
                fireContainerEvent("beforeContextInitialized", listener);
                if (noPluggabilityListeners.contains(listener)) {
                    listener.contextInitialized(tldEvent);
                } else {
                    // 回调了监听器:Spring的Web容器在此初始化
                    listener.contextInitialized(event); 
                }
                fireContainerEvent("afterContextInitialized", listener);
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                fireContainerEvent("afterContextInitialized", listener);
                getLogger().error(sm.getString("standardContext.listenerStart",
                        instance.getClass().getName()), t);
                ok = false;
            }
        }

所以就会触发我们在ServletContainerInitializer回调时加入的ContextLoaderListener的contextInitialized方法:

    @Override // 监听器回调【会在web启动完成的时候tomcat会触发监听器钩子】
    public void contextInitialized(ServletContextEvent event) {
        initWebApplicationContext(event.getServletContext());// 初始化ioc容器(根容器)
    }

对应的具体逻辑在initWebApplicationContext方法中:

public WebApplicationContext initWebApplicationContext(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!");
        }

        servletContext.log("Initializing Spring root WebApplicationContext");
        Log logger = LogFactory.getLog(ContextLoader.class);
        if (logger.isInfoEnabled()) {
            logger.info("Root WebApplicationContext: initialization started");
        }
        long startTime = System.currentTimeMillis();

        try {
            // Store context in local instance variable, to guarantee that
            // it is available on ServletContext shutdown.
            if (this.context == null) {
                this.context = createWebApplicationContext(servletContext);
            }
            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
                    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);
                    }
                    // 刷新容器,触发Spring的流程
                    configureAndRefreshWebApplicationContext(cwac, servletContext);
                }
            }
            // 将根容器添加到SevletContext中的application域中,后面DispatherServlet启动的时候回来拿,
            // 从而构建父子容器
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);// 将创建的容器放到tomcat的application域中

            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
            if (ccl == ContextLoader.class.getClassLoader()) {
                currentContext = this.context;
            }
            else if (ccl != null) {
                currentContextPerThread.put(ccl, this.context);
            }

            if (logger.isInfoEnabled()) {
                long elapsedTime = System.currentTimeMillis() - startTime;
                logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
            }

            return this.context;
        }
        catch (RuntimeException | Error ex) {
            logger.error("Context initialization failed", ex);
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
            throw ex;
        }
    }

在这个方法中,主要是将我们前面创建的根容器进行了刷新【Spring的刷新逻辑】,然后将根容器对象添加到了SevletContext的属性中,即应用的Application域中,为何要放进去呢?这就是和下面构建父子容器相关了。

SpringMVC父子容器启动的过程

SpringMVC是可以通过构造父子容器来进行,隔离基本组件和web组件配置的,就像我们上面的配置就是构建父子容器的。那他究竟是何时将父子容器关联起来了呢?这个就需要我们来看下DispatcherServlet初始化的过程了,关键的逻辑在FrameworkServlet#initServletBean方法中,代码如下:

    protected final void initServletBean() throws ServletException {
        getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
        if (logger.isInfoEnabled()) {
            logger.info("Initializing Servlet '" + getServletName() + "'");
        }
        long startTime = System.currentTimeMillis();

        try {//初始化web ioc容器
            this.webApplicationContext = initWebApplicationContext();
            initFrameworkServlet();
        }
        catch (ServletException | RuntimeException ex) {
            logger.error("Context initialization failed", ex);
            throw ex;
        }

        if (logger.isDebugEnabled()) {
            String value = this.enableLoggingRequestDetails ?
                    "shown which may lead to unsafe logging of potentially sensitive data" :
                    "masked to prevent unsafe logging of potentially sensitive data";
            logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
                    "': request parameters and headers will be " + value);
        }

        if (logger.isInfoEnabled()) {
            logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
        }
    }

通过调用initWebApplicationContext方法会构建好父子容器关系,并启动父容器;因为我们在构造DispatcherServlet的时候传入了创建的Web容器,然后在通过应用的Application域获取当我们前面存放的根容器,然后构建父子容器关系,即cwac.setParent(rootContext);

protected WebApplicationContext initWebApplicationContext() {
        WebApplicationContext rootContext =//先会获取之前的WebApplicationContext,这是用来构建父子容器的
                WebApplicationContextUtils.getWebApplicationContext(getServletContext());//父容器【S】
        WebApplicationContext wac = null;

        if (this.webApplicationContext != null) {
            // A context instance was injected at construction time -> use it
            wac = this.webApplicationContext;//当前的web-ioc容器
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
                if (!cwac.isActive()) {
                    // The context has not yet been refreshed -> provide services such as
                    // setting the parent context, setting the application context id, etc
                    if (cwac.getParent() == null) {
                        // The context instance was injected without an explicit parent -> set
                        // the root application context (if any; may be null) as the parent
                        cwac.setParent(rootContext);//父子容器的体现
                    }
                    configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }
    //.......省略相关代码

最后通过configureAndRefreshWebApplicationContext(cwac);来启动web容器。并且注册了监听ContextRefreshedEvent事件的监听器,当web容器刷新完成的时候会触发构建SpringMVC的九大组件的创建。

    protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
        if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
            // The application context id is still set to its original default value
            // -> assign a more useful id based on available information
            if (this.contextId != null) {
                wac.setId(this.contextId);
            }
            else {
                // Generate default id...
                wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                        ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
            }
        }

        wac.setServletContext(getServletContext());
        wac.setServletConfig(getServletConfig());
        wac.setNamespace(getNamespace());
        // 注册了一个监听器,当容器完全刷新结束的时候,会回调这个监听器,完成SpringMVC的八大组件的初始化
        wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));// 用来监听初始化全部结束,容器会发送Spring的事件

        // 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(getServletContext(), getServletConfig());
        }

        postProcessWebApplicationContext(wac);
        applyInitializers(wac);
        wac.refresh();//刷新容器
    }

最后

至此,根容器和Web容器就刷新完成了,也就是说相关的组件都加载到我们指定的容器中了,当然也包括SpringMVC的九大组件,这样整个SpringMVC项目就启动完成了,后面当请求进来的时候就会通过DispatcherServlet来进行分发请求,然后通过九大组件来完成请求的处理以及响应。

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

推荐阅读更多精彩内容