[Spring MVC]Spring MVC與Servlet標(biāo)準(zhǔn)及總體設(shè)計(jì)思想

Tomcat容器

image.png

spring整合Tomcat經(jīng)??吹降膚eb.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的機(jī)制來配置servlet.

ServletContainerInitializer

允許庫(kù)/運(yùn)行時(shí)被通知 Web 應(yīng)用程序的啟動(dòng)階段并執(zhí)行任何必需的 servlet、過濾器和偵聽器的編程注冊(cè)以響應(yīng)它的接口。
該接口的實(shí)現(xiàn)可以使用HandlesTypes進(jìn)行注釋,以便(在它們的onStartup方法中)接收實(shí)現(xiàn)、擴(kuò)展或已使用注釋指定的類類型進(jìn)行注釋的應(yīng)用程序類集。
如果此接口的實(shí)現(xiàn)不使用HandlesTypes批注,或者沒有任何應(yīng)用程序類與批注指定的類匹配,則容器必須將空類集傳遞給onStartup 。
在檢查應(yīng)用程序的類以查看它們是否與ServletContainerInitializer的HandlesTypes注釋指定的任何條件匹配時(shí),如果缺少任何應(yīng)用程序的可選 JAR 文件,容器可能會(huì)遇到類加載問題。 因?yàn)槿萜鳠o法決定這些類型的類加載失敗是否會(huì)阻止應(yīng)用程序正常工作,所以它必須忽略它們,同時(shí)提供一個(gè)配置選項(xiàng)來記錄它們。
此接口的實(shí)現(xiàn)必須由位于META-INF/services目錄內(nèi)的 JAR 文件資源聲明,并以此接口的完全限定類名命名,并將使用運(yùn)行時(shí)的服務(wù)提供者查找機(jī)制或容器特定機(jī)制發(fā)現(xiàn)在語義上等同于它。 在任一情況下,必須忽略從絕對(duì)排序中排除的 Web 片段 JAR 文件中的ServletContainerInitializer服務(wù),并且發(fā)現(xiàn)這些服務(wù)的順序必須遵循應(yīng)用程序的類加載委托模型。

ServletContainerInitializer 是 Servlet 3.0 新增的一個(gè)接口,主要用于在容器啟動(dòng)階段通過編程風(fēng)格注冊(cè)Filter, Servlet以及Listener,以取代通過web.xml配置注冊(cè).
Tomcat啟動(dòng)的時(shí)候會(huì)通過JAR API來發(fā)現(xiàn)實(shí)現(xiàn)這類接口的類進(jìn)行配置加載

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實(shí)現(xiàn)了ServletContainerInitializer,在其onStartup方法中,通過@HandlesTypes(WebApplicationInitializer.class)注入WebApplicationInitializer到Tomcat容器.激活WebApplicationInitializer#onStartup.所以實(shí)現(xiàn)了WebApplicationInitializer的類都會(huì)被加載.

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相關(guān)的類掃描進(jìn)Servlet的web容器上下文中.
Root WebApplicationContext:
將Service、Repositories.這類對(duì)象交由Root WebApplicationContext管理.

總體來說,是做了業(yè)務(wù)對(duì)象的分層.

Spring MVC的大致流程

  • 建立請(qǐng)求(RequestMapping)和Controller方法的映射集合的流程
  • 根據(jù)請(qǐng)求(RequestURI)查找對(duì)應(yīng)的Controller方法的流程
  • 請(qǐng)求參數(shù)綁定到方法形參,執(zhí)行方法處理請(qǐng)求,渲染視圖

流程圖

image.png
  1. 請(qǐng)求先經(jīng)過瀏覽器訪問tomcat容器,tomcat委派給線程池響應(yīng)當(dāng)前請(qǐng)求調(diào)用servlet進(jìn)行處理。
  2. Spring MVC通過DispatcherServlet攔截了所有的請(qǐng)求,通過當(dāng)前請(qǐng)求的路徑與IoC提前解析好的HanlderMapping進(jìn)行對(duì)比,進(jìn)而定位到Controller的method進(jìn)行響應(yīng).
  3. method經(jīng)過響應(yīng)后,會(huì)返回一個(gè)ModelAndView實(shí)例.
  4. DispatcherServlet會(huì)委派給ViewResolver進(jìn)行視圖解析.
  5. 返回View,放入響應(yīng)流中響應(yīng)給瀏覽器.

注解配置的容器入口-AbstractDispatcherServletInitializer

注解的配置會(huì)優(yōu)于XML的配置執(zhí)行.SpringServletContainerInitializer實(shí)現(xiàn)了ServletContainerInitializer接口,通過@HandlesTypes(WebApplicationInitializer.class)的SPI發(fā)現(xiàn)機(jī)制,可以將實(shí)現(xiàn)該接口的Class對(duì)象傳遞到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

在對(duì)這些initializers進(jìn)行排序后,最后就會(huì)依次激活所有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");
    // 創(chuàng)建spring web IoC 容器
    WebApplicationContext servletAppContext = createServletApplicationContext();
    Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");
    // 創(chuàng)建dispatcherServlet實(shí)例
    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());
    // 請(qǐng)求攔截器,可以在此處控制編碼
    Filter[] filters = getServletFilters();
    if (!ObjectUtils.isEmpty(filters)) {
        for (Filter filter : filters) {
            registerServletFilter(servletContext, filter);
        }
    }

    customizeRegistration(registration);
}

這是一個(gè)模板方法,其中createServletApplicationContext可以通過子類去實(shí)現(xiàn).支持注解形式的實(shí)現(xiàn)類是AbstractAnnotationConfigDispatcherServletInitializer

XML配置的容器入口-ContextLoaderListener

ContextLoaderListener實(shí)現(xiàn)了Servlet的ServletContextListener,Tomcat在啟動(dòng)的時(shí)候,會(huì)優(yōu)先加載Servlet的監(jiān)聽器組件,以確保在Servlet被創(chuàng)建之前,調(diào)用監(jiān)聽器的contextInitialized方法,Spring正是在ContextLoaderListener中進(jìn)行了initWebApplicationContext的調(diào)用,即啟動(dòng)的時(shí)候,進(jìn)行容器的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);
        // 進(jìn)行了初始化
        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. 先看看容器是否被創(chuàng)建出來了,如果沒有則調(diào)用createWebApplicationContext來創(chuàng)建容器.
  2. 容器是否屬于ConfigurableWebApplicationContext類型,對(duì)容器進(jìn)行強(qiáng)轉(zhuǎn),然后查看是否處于激活的狀態(tài)(如果經(jīng)過了refresh,容器會(huì)進(jìn)入active狀態(tài)).
  3. 是否存在父容器,如果有進(jìn)行setParent
  4. 配置并refresh容器

又是refresh

Spring會(huì)依次建立兩個(gè)上下文,一個(gè)是Root WebApplicationContext.另一個(gè)是WebApplicationContext For Dispatcher-servlet.無論哪個(gè),最終還是會(huì)回到org.springframework.context.support.AbstractApplicationContext#refresh這個(gè)方法中.

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

這里出現(xiàn)了幾個(gè)關(guān)鍵的對(duì)象:
FrameworkerServlet、HttpServletBean.
再?gòu)膱?zhí)行的調(diào)用棧中看看調(diào)用過程:

  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

我們?cè)儆肬ML來梳理這些類之間的關(guān)系:

image.png

可以看到,DispatcherServlet繼承自FrameworkServlet,而FrameworkServlet又繼承自HttpServletBean,最后HttpServletBean繼承了HttpServlet.
同時(shí),實(shí)現(xiàn)了Aware接口,就可以從容器中獲取到環(huán)境變量、容器上下文等資源.
這樣來看,調(diào)用順序就很好理解了.

HttpServletBean實(shí)現(xiàn)了HttpServlet接口,就可以重寫其init方法,在init方法里面調(diào)用一個(gè)鉤子方法initServletBean(HttpServletBean不負(fù)責(zé)實(shí)現(xiàn),由子類實(shí)現(xiàn)).
FrameworkServlet重寫了initServletBean方法,著手初始化容器的事項(xiàng).
注意,這里提到的FrameworkSerlvetHttpSerlvetBean都是抽象類,真正的實(shí)例是-DispatcherSerlvet.

下一章,我們一起來看看DispatcherSerlvet是如何初始化其他MVC組件的.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容