天天看點

SpringMVC源碼分析和啟動流程

SpringMVC是一款Web MVC架構。 它跟Struts架構類似,是目前主流的Web MVC架構之一

一、SpringMVC的由來

早期 Java Web 的開發中,把顯示層、控制層、資料層的操作全部交給 JSP 或者 JavaBean 來進行處理,我們稱之為 Model1:

SpringMVC源碼分析和啟動流程

然而出現了很多的弊端,比如:

  • JSP 和 Java Bean 之間嚴重耦合,Java 代碼和 HTML 代碼也耦合在了一起
  • 要求開發者不僅要掌握 Java ,還要有高超的前端水準
  • 前端和後端互相依賴,前端需要等待後端完成,後端也依賴前端完成,才能進行有效的測試

随後出現servlet,就有了早期的MVC模式

SpringMVC源碼分析和啟動流程

首先使用者請求到servlet,然後根據請求調用響應的JavaBean,并把所有的顯示交給Jsp去處理,這樣就稱之為mvc模式:

  • M代表模型(Model):資料、bean
  • V代表視圖(View):網頁,jsp....展示模型中的資料
  • C代表控制器(Controller):把不同的資料(Model),顯示在不同的視圖(View)上,Servlet 扮演的就是這樣的角色

為解決持久層中一直未處理好的資料庫事務的程式設計,又為了迎合 NoSQL 的強勢崛起,Spring MVC 給出了方案

SpringMVC源碼分析和啟動流程

二、初始化過程

當一個web應用部署tomcat時,在接收使用者請求之前,會進行以下初始化過程

  • 部署在web.xml檔案裡由

    <listener>

    元素标記事件的監聽器會被建立和初始化
  • 對于所有事件監聽器,如果實作了

    ServletContextListener

    接口,将會執行其實作的

    contextInitialized()

    方法
  • 部署描述檔案由

    <filter>

    元素标記的過濾器會被建立和初始化,并調用其

    init()

  • 部署在描述檔案由

    <servlet>

    元素标記的servlet會根據

    <load-on-start>

    的權值按順序建立并初始化,并調用其

    init()

    SpringMVC源碼分析和啟動流程

三、啟動流程

以下為一個常見的

web.xml

配置進行SpringMVC啟動流程分析

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>

  <display-name>spring-demo</display-name>
<!--全局變量-->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
  </context-param>

<!--監聽器-->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

<!--亂碼filter-->
  <filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>utf-8</param-value>
    </init-param>
  </filter>

  <filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/**</url-pattern>
  </filter-mapping>

<!--dispatcherServlet-->
  <servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
  </servlet>

  <servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

</web-app>           

1、Listener初始化過程

首先定義了一個

<context-param>

, 用于配置一個全局的變量,

<context-param>

标簽内容讀取後會放進application中,作為web應用的全局變量使用,接下來建立

listener

時會使用這個變量,是以,web應用在啟動時,會先讀取這個變量,之後才進行下一步,接着定義了一個

ContextLoaderListener

listener

,這個listener的源碼為

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    public ContextLoaderListener() {
    }

    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }

    public void contextInitialized(ServletContextEvent event) {
        this.initWebApplicationContext(event.getServletContext());
    }

    public void contextDestroyed(ServletContextEvent event) {
        this.closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
}           

ContextLoaderListener類繼承了ContextLoader類并實作了ServletContextListener接口,首先看一下前面講述的ServletContextListener接口源碼:

public interface ServletContextListener extends EventListener {
    void contextInitialized(ServletContextEvent var1);

    void contextDestroyed(ServletContextEvent var1);
}           

該接口隻有兩個方法

contextInitialized

contextDestroyed

,這裡采用的是觀察者模式,也稱為為訂閱-釋出模式,實作了該接口的listener會向釋出者進行訂閱,當Web應用初始化或銷毀時會分别調用上述兩個方法,

ContextLoaderListener的contextInitialized()

方法直接調用了

initWebApplicationContext()

方法,這個方法是繼承自ContextLoader類,通過函數名可以知道,該方法是用于初始化Web應用上下文,即IOC容器,這裡使用的是代理模式,繼續檢視ContextLoader類的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!");
        } else {
            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 {
                if (this.context == null) {
                    this.context = this.createWebApplicationContext(servletContext);
                }

                if (this.context instanceof ConfigurableWebApplicationContext) {
                    ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)this.context;
                    if (!cwac.isActive()) {
                        if (cwac.getParent() == null) {
                            ApplicationContext parent = this.loadParentContext(servletContext);
                            cwac.setParent(parent);
                        }

                        this.configureAndRefreshWebApplicationContext(cwac, servletContext);
                    }
                }

                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 (Error | RuntimeException var8) {
                logger.error("Context initialization failed", var8);
                servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, var8);
                throw var8;
            }
        }
    }           

initWebApplicationContext()方法如上講述,主要目的就是建立root WebApplicationContext對象即

IOC容器

,其中比較重要的就是,整個Web應用如果存在IOC容器則有且隻能有一個,

根IOC容器

作為全局變量存儲在

ServletContext

application

對象中。将

根IOC容器

放入到application對象之前進行了

IOC容器

的配置和重新整理操作,調用了

configureAndRefreshWebApplicationContext()

方法,該方法源碼如下:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
        String configLocationParam;
        if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
            configLocationParam = sc.getInitParameter("contextId");
            if (configLocationParam != null) {
                wac.setId(configLocationParam);
            } else {
                wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath()));
            }
        }

        wac.setServletContext(sc);
        configLocationParam = sc.getInitParameter("contextConfigLocation");
        if (configLocationParam != null) {
            wac.setConfigLocation(configLocationParam);
        }

        ConfigurableEnvironment env = wac.getEnvironment();
        if (env instanceof ConfigurableWebEnvironment) {
            ((ConfigurableWebEnvironment)env).initPropertySources(sc, (ServletConfig)null);
        }

        this.customizeContext(sc, wac);
        wac.refresh();
    }           

比較重要的就是擷取到了

web.xml中的<context-param>

标簽配置的全局變量

contextConfigLocation

,并最後一行調用了

refresh()

方法,

ConfigurableWebApplicationContext

是一個接口,通過對常用實作類

ClassPathXmlApplicationContext

逐層查找後可以找到一個抽象類AbstractApplicationContext實作了

refresh()

方法,其源碼如下

public void refresh() throws BeansException, IllegalStateException {
        synchronized(this.startupShutdownMonitor) {
            this.prepareRefresh();
            ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
            this.prepareBeanFactory(beanFactory);

            try {
                this.postProcessBeanFactory(beanFactory);
                this.invokeBeanFactoryPostProcessors(beanFactory);
                this.registerBeanPostProcessors(beanFactory);
                this.initMessageSource();
                this.initApplicationEventMulticaster();
                this.onRefresh();
                this.registerListeners();
                this.finishBeanFactoryInitialization(beanFactory);
                this.finishRefresh();
            } catch (BeansException var9) {
                if (this.logger.isWarnEnabled()) {
                    this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
                }

                this.destroyBeans();
                this.cancelRefresh(var9);
                throw var9;
            } finally {
                this.resetCommonCaches();
            }

        }
    }           

該方法主要用于建立并初始化

contextConfigLocation

類配置的xml檔案中的

Bean

,是以,如果我們在配置Bean時出錯,在Web應用

啟動時就會抛出異常

,而不是等到運作時才抛出異常

整個

ContextLoaderListener

類的啟動過程到此就結束了,可以發現,建立

ContextLoaderListener

是比較核心的一個步驟,

主要工作就是為了建立根IoC容器

并使用特定的key将其放入到application對象中,供整個Web應用使用,由于在ContextLoaderListener類中構造的

IOC容器

配置的Bean是全局共享的,是以,在辨別的

contextConfigLocation

的xml配置檔案一般包括: 資料庫DataSource、DAO層、Service層、事務等相關Bean

2、Filter的初始化

在監聽器

listener

初始化完成後,接下來會進行filter的初始化操作,

GenericFilterBean

是任何類型的過濾器的一個比較友善的超類,這個類主要實作的就是從

web.xml

檔案中取得

init-param中

設定的值,然後對Filter進行初始化(當然,其子類可以覆寫

init()

方法)。

Filter的生命周期如下:

  • void init(FilterConfig config): 用于完成Filter 的初始化
  • void destroy(): 用于Filter 銷毀前,完成某些資源的回收
  • void doFilter(ServletRequest request, ServletResponse response,FilterChain chain): 實作過濾功能,該方法就是對每個請求及響應增加的額外處理。 過濾器Filter也具有生命周期:init()->doFilter()->destroy(),由部署檔案中的filter元素驅動

3、Servlet的初始化

Web應用啟動的最後一個步驟就是建立和初始化相關

Servlet

,在開發中常用的Servlet就是

DispatcherServlet

類前端控制器,前端控制器作為中央控制器是整個Web應用的核心,用于擷取分發使用者請求并傳回響應

SpringMVC源碼分析和啟動流程

DispatcherServlet

類的間接父類實作了

Servlet

接口,是以其本質上依舊是一個

Servlet

DispatcherServlet

設計很巧妙,上層父類不同程度的實作了相關接口的部分方法,并留出了相關方法用于子類覆寫,将不變的部分統一實作,将變化的部分預留方法用于子類實作

具體順序圖

SpringMVC源碼分析和啟動流程

DispatcherServelt

類的本質是

Servlet

,通過文章開始的講解可知,在Web應用部署到容器後進行

Servlet

初始化時會調用相關的

init(ServletConfig)

方法,是以,

DispatchServlet

類的初始化過程也由該方法開始。上述調用邏輯中比較重要的就是

FrameworkServlet

抽象類中的

initServletBean()

方法、

initWebApplicationContext()

方法以及

DispatcherServlet

類中的

onRefresh()

a、initServletBean()
protected final void initServletBean() throws ServletException {
        this.getServletContext().log("Initializing Spring " + this.getClass().getSimpleName() + " '" + this.getServletName() + "'");
        if (this.logger.isInfoEnabled()) {
            this.logger.info("Initializing Servlet '" + this.getServletName() + "'");
        }

        long startTime = System.currentTimeMillis();

        try {
            this.webApplicationContext = this.initWebApplicationContext();
            this.initFrameworkServlet();
        } catch (RuntimeException | ServletException var4) {
            this.logger.error("Context initialization failed", var4);
            throw var4;
        }

        if (this.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";
            this.logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails + "': request parameters and headers will be " + value);
        }

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

    }           

該方法是重寫了父類

HttpServletBean

抽象類的

initServletBean()

HttpServletBean

抽象類在執行

init()

方法時會調用

initServletBean()

方法,由于多态的特性,最終會調用其子類

FrameworkServlet

initServletBean()

方法。該方法由final辨別,子類就不可再次重寫了。該方法中比較重要的就是

initWebApplicationContext()

方法的調用,該方法仍由

FrameworkServlet

抽象類實作

protected WebApplicationContext initWebApplicationContext() {
        WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
        WebApplicationContext wac = null;
        if (this.webApplicationContext != null) {
            wac = this.webApplicationContext;
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;
                if (!cwac.isActive()) {
                    if (cwac.getParent() == null) {
                        cwac.setParent(rootContext);
                    }

                    this.configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }

        if (wac == null) {
            wac = this.findWebApplicationContext();
        }

        if (wac == null) {
            wac = this.createWebApplicationContext(rootContext);
        }

        if (!this.refreshEventReceived) {
            synchronized(this.onRefreshMonitor) {
                this.onRefresh(wac);
            }
        }

        if (this.publishContext) {
            String attrName = this.getServletContextAttributeName();
            this.getServletContext().setAttribute(attrName, wac);
        }

        return wac;
    }           

該方法的主要作用同樣是建立一個

WebApplicationContext

對象,即

IOC容器

,不過前文講過每個Web應用最多隻能存在一個

IOC容器

,這裡建立的則是特定

Servlet

擁有的

子IOC容器

父子IOC容器的通路特性

父子容器類似于類的繼承關系,子類可以通路父類中的成員變量,而父類不可通路子類的成員變量,同樣的,子容器可以通路父容器中定義的Bean,但父容器無法通路子容器定義的Bean,

根IOC容器

做為全局共享的

IOC容器

放入Web應用需要共享的Bean,而

子IOC容器

根據需求的不同,放入不同的Bean,這樣能夠做到

隔離

,保證系統的

安全性

DispatcherServlet

類的子

IOC容器

建立過程,如果目前Servlet存在一個

IOC容器

則為其設定

根IOC容器

作為其父類,并配置重新整理該容器,用于構造其定義的Bean,這裡的方法與前文講述的

根IOC容器

類似,同樣會讀取使用者在

web.xml

中配置的

<servlet>

中的

<init-param>

值,用于查找相關的xml配置檔案用于構造定義的Bean。如果目前Servlet不存在一個子IoC容器就去查找一個,如果仍然沒有查找到則調用

createWebApplicationContext()方法去建立

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
        Class<?> contextClass = this.getContextClass();
        if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
            throw new ApplicationContextException("Fatal initialization error in servlet with name '" + this.getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext");
        } else {
            ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
            wac.setEnvironment(this.getEnvironment());
            wac.setParent(parent);
            String configLocation = this.getContextConfigLocation();
            if (configLocation != null) {
                wac.setConfigLocation(configLocation);
            }

            this.configureAndRefreshWebApplicationContext(wac);
            return wac;
        }
    }           

該方法用于建立一個

子IOC容器

并将

根IOC容器

做為其父容器,接着進行配置和重新整理操作用于構造相關的Bean。至此,

根IOC容器

以及相關

Servlet

子IOC容器

已經配置完成,子容器中管理的Bean一般隻被該Servlet使用,是以,其中管理的Bean一般是

局部

的,如SpringMVC中需要的各種重要元件,包括Controller、Interceptor、Converter、ExceptionResolver等

SpringMVC源碼分析和啟動流程

IOC子容器

構造完成後調用了

onRefresh()

方法,該方法的調用與

initServletBean()

方法的調用相同,由父類調用但具體實作由子類覆寫,調用

onRefresh()

方法時将前文建立的IOC子容器作為參數傳入

protected void onRefresh(ApplicationContext context) {
        this.initStrategies(context);
    }

    protected void initStrategies(ApplicationContext context) {
        this.initMultipartResolver(context);
        this.initLocaleResolver(context);
        this.initThemeResolver(context);
        this.initHandlerMappings(context);
        this.initHandlerAdapters(context);
        this.initHandlerExceptionResolvers(context);
        this.initRequestToViewNameTranslator(context);
        this.initViewResolvers(context);
        this.initFlashMapManager(context);
    }           

onRefresh()

initStrategies()

方法,源碼如上,通過函數名可以判斷,該方法用于初始化建立

multipartResovle

來支援圖檔等檔案的上傳、本地化解析器、主題解析器、HandlerMapping處理器映射器、

HandlerAdapter

處理器擴充卡、異常解析器、視圖解析器、flashMap管理器等,這些元件都是SpringMVC開發中的重要元件,相關元件的初始化建立過程均在此完成。

到此,初始化就全部結束了

四、總結

在Spring的web容器啟動時會去讀取

web.xml

檔案,相關啟動順序為:

<context-param> --> <listener> --> <filter> --> <servlet>

,具體為:

  • 1、解析

    <context-param>

    鍵值對
  • 2、建立一個application對象即ServletContext,servlet上下文,用于全局共享
  • 3、将

    <context-param>

    鍵值對放入ServletContext中,web應用全局共享
  • 4、讀取

    <listener>

    标簽,建立監聽器,一般使用

    ContextLoaderListener

    ,如果使用了

    ContextLoaderListener

    ,Spring就會建立一個WebApplicationContext對象,這個就是

    IOC容器

    ,

    ContextLoaderListener

    建立的

    IOC容器

    是全局共享的,并将其放在

    ServletContext

    中, 鍵名為

    WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE

    , 讀取

    web.xml

    檔案裡的

    contextConfigLocation

    配置中的xml檔案來建立bean
  • 5、listener建立完畢後如果有Filter會去建立Filter
  • 6、初始化Servlet,一般使用DispatchServlet類
  • 7、DispatchServlet的父類FrameworkServlet會重寫其父類的initServletBean方法,并調用

    initWebApplicationContext()

    以及

    onRefresh()

  • 8、

    initWebApplicationContext()

    方法會建立一個目前servlet的一個IOC子容器,如果存在上述的全局

    WebApplicationContext

    則将其設定為父容器,如果不存在上述全局的則父容器為null。
  • 9、讀取

    <servlet>

    标簽的

    <init-param>

    配置的xml檔案并加載相關Bean
  • 10、

    onRefresh()

    方法建立Web應用相關元件

好了,本文到此就結束了,篇幅過長,如果其中有誤,可以在下方評論留言,也希望可以關注我哦

繼續閱讀