[Spring MVC]Spring MVC与Servlet标准及总体设计思想

Tomcat容器

image.png

spring整合Tomcat经常看到的web.xml

<web-app>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/app-context.xml</param-value>
    </context-param>

    <servlet>
        <servlet-name>app</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value></param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>

</web-app>

Servler3.0后,提供了注解+SPI的机制来配置servlet.

ServletContainerInitializer

允许库/运行时被通知 Web 应用程序的启动阶段并执行任何必需的 servlet、过滤器和侦听器的编程注册以响应它的接口。
该接口的实现可以使用HandlesTypes进行注释,以便(在它们的onStartup方法中)接收实现、扩展或已使用注释指定的类类型进行注释的应用程序类集。
如果此接口的实现不使用HandlesTypes批注,或者没有任何应用程序类与批注指定的类匹配,则容器必须将空类集传递给onStartup 。
在检查应用程序的类以查看它们是否与ServletContainerInitializer的HandlesTypes注释指定的任何条件匹配时,如果缺少任何应用程序的可选 JAR 文件,容器可能会遇到类加载问题。 因为容器无法决定这些类型的类加载失败是否会阻止应用程序正常工作,所以它必须忽略它们,同时提供一个配置选项来记录它们。
此接口的实现必须由位于META-INF/services目录内的 JAR 文件资源声明,并以此接口的完全限定类名命名,并将使用运行时的服务提供者查找机制或容器特定机制发现在语义上等同于它。 在任一情况下,必须忽略从绝对排序中排除的 Web 片段 JAR 文件中的ServletContainerInitializer服务,并且发现这些服务的顺序必须遵循应用程序的类加载委托模型。

ServletContainerInitializer 是 Servlet 3.0 新增的一个接口,主要用于在容器启动阶段通过编程风格注册Filter, Servlet以及Listener,以取代通过web.xml配置注册.
Tomcat启动的时候会通过JAR API来发现实现这类接口的类进行配置加载

WebApplicationInitializer和SpringServletContainerInitializer
  • org.springframework.web.SpringServletContainerInitializer#onStartup
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
        throws ServletException {

    List<WebApplicationInitializer> initializers = new LinkedList<>();

    if (webAppInitializerClasses != null) {
        for (Class<?> waiClass : webAppInitializerClasses) {
            // Be defensive: Some servlet containers provide us with invalid classes,
            // no matter what @HandlesTypes says...
            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;
    }

    servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
    AnnotationAwareOrderComparator.sort(initializers);
    // 激活WebApplicationInitializer#onStartup
    for (WebApplicationInitializer initializer : initializers) {
        initializer.onStartup(servletContext);
    }
}

SpringServletContainerInitializer实现了ServletContainerInitializer,在其onStartup方法中,通过@HandlesTypes(WebApplicationInitializer.class)注入WebApplicationInitializer到Tomcat容器.激活WebApplicationInitializer#onStartup.所以实现了WebApplicationInitializer的类都会被加载.

WebApplicationInitializer家族成员

image.png

Servlet WebApplicationContext 和 Root WebApplicationContext

image.png
  • org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer#createServletApplicationContext
protected WebApplicationContext createServletApplicationContext() {
    AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
    // 获取配置类并且注入到容器中
    Class<?>[] configClasses = getServletConfigClasses();
    if (!ObjectUtils.isEmpty(configClasses)) {
        context.register(configClasses);
    }
    return context;
}
  • org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer#createRootApplicationContext
    protected WebApplicationContext createRootApplicationContext() {
        Class<?>[] configClasses = getRootConfigClasses();
        if (!ObjectUtils.isEmpty(configClasses)) {
            AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
            context.register(configClasses);
            return context;
        }
        else {
            return null;
        }
    }

Servlet WebApplicationContext:
将Controller、ViewResolver、HandleMapping相关的类扫描进Servlet的web容器上下文中.
Root WebApplicationContext:
将Service、Repositories.这类对象交由Root WebApplicationContext管理.

总体来说,是做了业务对象的分层.

Spring MVC的大致流程

  • 建立请求(RequestMapping)和Controller方法的映射集合的流程
  • 根据请求(RequestURI)查找对应的Controller方法的流程
  • 请求参数绑定到方法形参,执行方法处理请求,渲染视图

流程图

image.png
  1. 请求先经过浏览器访问tomcat容器,tomcat委派给线程池响应当前请求调用servlet进行处理。
  2. Spring MVC通过DispatcherServlet拦截了所有的请求,通过当前请求的路径与IoC提前解析好的HanlderMapping进行对比,进而定位到Controller的method进行响应.
  3. method经过响应后,会返回一个ModelAndView实例.
  4. DispatcherServlet会委派给ViewResolver进行视图解析.
  5. 返回View,放入响应流中响应给浏览器.

注解配置的容器入口-AbstractDispatcherServletInitializer

注解的配置会优于XML的配置执行.SpringServletContainerInitializer实现了ServletContainerInitializer接口,通过@HandlesTypes(WebApplicationInitializer.class)的SPI发现机制,可以将实现该接口的Class对象传递到onStartup中.

  • org.springframework.web.SpringServletContainerInitializer#onStartup
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
        throws ServletException {

    List<WebApplicationInitializer> initializers = new LinkedList<>();

    if (webAppInitializerClasses != null) {
        for (Class<?> waiClass : webAppInitializerClasses) {
            // Be defensive: Some servlet containers provide us with invalid classes,
            // no matter what @HandlesTypes says...
            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;
    }

    servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
    AnnotationAwareOrderComparator.sort(initializers);
    // 激活WebApplicationInitializer#onStartup
    for (WebApplicationInitializer initializer : initializers) {
        initializer.onStartup(servletContext);
    }
}
image.png

在对这些initializers进行排序后,最后就会依次激活所有WebApplicationInitializeronStartup方法.

  • org.springframework.web.servlet.support.AbstractDispatcherServletInitializer#onStartup
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
    super.onStartup(servletContext);
    registerDispatcherServlet(servletContext);
}
  • org.springframework.web.servlet.support.AbstractDispatcherServletInitializer#registerDispatcherServlet
protected void registerDispatcherServlet(ServletContext servletContext) {
    String servletName = getServletName();
    Assert.hasLength(servletName, "getServletName() must not return null or empty");
    // 创建spring web IoC 容器
    WebApplicationContext servletAppContext = createServletApplicationContext();
    Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");
    // 创建dispatcherServlet实例
    FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
    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());
    registration.setAsyncSupported(isAsyncSupported());
    // 请求拦截器,可以在此处控制编码
    Filter[] filters = getServletFilters();
    if (!ObjectUtils.isEmpty(filters)) {
        for (Filter filter : filters) {
            registerServletFilter(servletContext, filter);
        }
    }

    customizeRegistration(registration);
}

这是一个模板方法,其中createServletApplicationContext可以通过子类去实现.支持注解形式的实现类是AbstractAnnotationConfigDispatcherServletInitializer

XML配置的容器入口-ContextLoaderListener

ContextLoaderListener实现了Servlet的ServletContextListener,Tomcat在启动的时候,会优先加载Servlet的监听器组件,以确保在Servlet被创建之前,调用监听器的contextInitialized方法,Spring正是在ContextLoaderListener中进行了initWebApplicationContext的调用,即启动的时候,进行容器的refresh操作.

  • org.springframework.web.context.ContextLoaderListener#contextInitialized
@Override
public void contextInitialized(ServletContextEvent event) {
    initWebApplicationContext(event.getServletContext());
}
  • org.springframework.web.context.ContextLoader#initWebApplicationContext
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    // 从servletContext中查找,是否存在以WebApplicationContext的ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE为key的值
    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.
        // 在AbstractContextLoaderInitializer#onStartup  
        // ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
        // 进行了初始化
        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);
                }
                // 配置并刷新容器
                configureAndRefreshWebApplicationContext(cwac, servletContext);
            }
        }
        // 将WebApplicationContext放入servletContext中
        // 其key为ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

        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;
    }
}
  1. 先看看容器是否被创建出来了,如果没有则调用createWebApplicationContext来创建容器.
  2. 容器是否属于ConfigurableWebApplicationContext类型,对容器进行强转,然后查看是否处于激活的状态(如果经过了refresh,容器会进入active状态).
  3. 是否存在父容器,如果有进行setParent
  4. 配置并refresh容器

又是refresh

Spring会依次建立两个上下文,一个是Root WebApplicationContext.另一个是WebApplicationContext For Dispatcher-servlet.无论哪个,最终还是会回到org.springframework.context.support.AbstractApplicationContext#refresh这个方法中.

  • 第一次refresh是Root WebApplicationContext
image.png
  • 第二次refresh是Servlet WebApplicationContext
image.png

这里出现了几个关键的对象:
FrameworkerServletHttpServletBean.
再从执行的调用栈中看看调用过程:

  1. org.springframework.web.servlet.HttpServletBean#init
  2. org.springframework.web.servlet.FrameworkServlet#initServletBean
  3. org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext
  4. org.springframework.web.servlet.FrameworkServlet#configureAndRefreshWebApplicationContext

我们再用UML来梳理这些类之间的关系:

image.png

可以看到,DispatcherServlet继承自FrameworkServlet,而FrameworkServlet又继承自HttpServletBean,最后HttpServletBean继承了HttpServlet.
同时,实现了Aware接口,就可以从容器中获取到环境变量、容器上下文等资源.
这样来看,调用顺序就很好理解了.

HttpServletBean实现了HttpServlet接口,就可以重写其init方法,在init方法里面调用一个钩子方法initServletBean(HttpServletBean不负责实现,由子类实现).
FrameworkServlet重写了initServletBean方法,着手初始化容器的事项.
注意,这里提到的FrameworkSerlvetHttpSerlvetBean都是抽象类,真正的实例是-DispatcherSerlvet.

下一章,我们一起来看看DispatcherSerlvet是如何初始化其他MVC组件的.

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

推荐阅读更多精彩内容