java web中各種context的關係

我舉得這篇文章解決了我的很多疑惑,理清了我以前不太清楚的Context關係,讀懂這篇文章很有助於理解源碼,

原文鏈接在這裏:https://www.jianshu.com/p/2537e2fec546

我把它轉載在自己博客裏,害怕以後找不到,原文如下

 

網上博客中看到一句話,很形容的描繪了web程序和上下文的關係,這裏引用一下來說明:如果對“上下文”不太瞭解的,我這邊說下,程序裏面所謂的“上下文”就是程序的執行環境,打個比方:你有家吧?如果家都沒有就別學編程了,租的也行啊!你就相當於web程序,家就相當於web程序的上下文,你可以在家裏放東西,也可以取東西,你的衣食住行都依賴這個家,這個家就是你生活的上下文環境。

該博客地址: Spring和SpringMVC配置中父子WebApplicationContext的關係

Spring啓動過程

第一步:

 首先,對於一個web應用,其部署在web容器中,web容器提供其一個全局的上下文環境,這個上下文就是ServletContext,其爲後面的spring IoC容器提供宿主環境;

第二步:

 其次,在web.xml中會提供有contextLoaderListener。在web容器啓動時,會觸發容器初始化事件,此時contextLoaderListener會監聽到這個事件,其contextInitialized方法會被調用,在這個方法中,spring會初始化一個啓動上下文,這個上下文被稱爲根上下文,即WebApplicationContext,這是一個接口類,確切的說,其實際的實現類是XmlWebApplicationContext。這個就是spring的IoC容器,其對應的Bean定義的配置由web.xml中的context-param標籤指定。在這個IoC容器初始化完畢後,spring以【WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE】爲屬性Key,將其存儲到ServletContext中,便於獲取;

第三步:

 再次,contextLoaderListener監聽器初始化完畢後,開始初始化web.xml中配置的Servlet,這個servlet可以配置多個,以最常見的DispatcherServlet爲例,這個servlet實際上是一個標準的前端控制器,用以轉發、匹配、處理每個servlet請求。DispatcherServlet上下文在初始化的時候會建立自己的IoC上下文,用以持有spring mvc相關的bean。在建立DispatcherServlet自己的IoC上下文時,會利用【WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE】先從ServletContext中獲取之前的根上下文(即WebApplicationContext)作爲自己上下文的parent上下文(有個parent屬性作爲對Spring的ApplicationContext的引用)。有了這個parent上下文之後,再初始化自己持有的上下文。這個DispatcherServlet初始化自己上下文的工作在其initStrategies方法中可以看到,大概的工作就是初始化處理器映射、視圖解析等。這個servlet自己持有的上下文默認實現類也是XmlWebApplicationContext。初始化完畢後,spring以與servlet的名字相關(此處不是簡單的以servlet名爲Key,而是通過一些轉換,具體可自行查看源碼)的屬性爲屬性Key,也將其存到ServletContext中,以便後續使用。這樣每個servlet就持有自己的上下文,即擁有自己獨立的bean空間,同時各個servlet共享相同的bean,即根上下文(第2步中初始化的上下文)定義的那些bean。

通過上面這個Spring的啓動過程,我們可以清楚的瞭解ServletContext、WebApplicationContext、XmlWebApplicationContext、以及DispatcherServlet上下文之間的關係,並且會將WebApplicationContext放在ServletContext中。

 
                                                 XmlWebApplicationContext體系結構.png
 

1. ServletContext:

 首先說說ServletContext這個web應用級的上下文。web容器(比如tomcat、jboss、weblogic等)啓動的時候,它會爲每個web應用程序創建一個ServletContext對象 它代表當前web應用的上下文(注意:是每個web應用有且僅創建一個ServletContext,一個web應用,就是你一個web工程)。一個web中的所有servlet共享一個ServletContext對象,所以可以通過ServletContext對象來實現Servlet之間的通訊。在一個繼承自HttpServlet對象的類中,可以通過this.getServletContext來獲取。

2. WebApplicationContext:

 通過源碼詳細說明一下 第二步 的過程,web.xml(上圖)中我們配置了ContextLoaderListener,該listener實現了ServletContextListener的contextInitialized方法用來監聽Servlet初始化事件:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring/SpringApplicationContext.xml</param-value>
</context-param>
<listener>  
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>  
</listener>
 

 由下面的源碼可以發現初始化的是WebApplicationContext的IoC容器,它是一個接口類,其默認實現是XmlWebApplicationContext。

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    public ContextLoaderListener() {
    }
    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }
    @Override
    public void contextInitialized(ServletContextEvent event) {
        initWebApplicationContext(event.getServletContext());
    }
    @Override
    public void contextDestroyed(ServletContextEvent event) {
        closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
}

 

這是ContextLoaderListener中的contextInitialized()方法,這裏主要是用initWebApplicationContext()方法來初始化WebApplicationContext。這裏涉及到一個常用類WebApplicationContext:它繼承自ApplicationContext,在ApplicationContext的基礎上又追加了一些特定於Web的操作及屬性。

 initWebApplicationContext(event.getServletContext()),進行了創建根上下文,並將該上下文以key-value的方式存儲到ServletContext中。

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!");
    }

    Log logger = LogFactory.getLog(ContextLoader.class);
    servletContext.log("Initializing Spring root WebApplicationContext");
    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);
                }
                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.isDebugEnabled()) {
            logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
                    WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
        }
        if (logger.isInfoEnabled()) {
            long elapsedTime = System.currentTimeMillis() - startTime;
            logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
        }

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

 

 在initWebApplicationContext()方法中主要體現了WebApplicationContext實例的創建過程。首先,驗證WebApplicationContext的存在性,通過查看ServletContext實例中是否有對應key的屬性驗證WebApplicationContext是否已經創建過實例。如果沒有通過,createWebApplicationContext()方法來創建實例,並存放至ServletContext中。

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
    Class<?> contextClass = determineContextClass(sc);
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
                "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
    }
    return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

 

 在createWebApplicationContext()方法中,通過BeanUtils.instanceClass()方法創建實例,而WebApplicationContext的實現類名稱則通過determineContextClass()方法獲得。

protected Class<?> determineContextClass(ServletContext servletContext) {
    String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
    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);
        }
    }
}

 

 determineContextClass()方法,通過defaultStrategies.getProperty()方法獲得實現類的名稱,而defaultStrategies是在ContextLoader類的靜態代碼塊中賦值的。具體的途徑,則是讀取ContextLoader類的同目錄下的ContextLoader.properties屬性文件來確定的。

 也就是說,在初始化的過程中,程序會首先讀取ContextLoader類的同目錄下的屬性文件ContextLoader.properties,並根據其中的配置提取將要實現WebApplicationContext接口的實現類,並根據這個類通過反射進行實例的創建。

 

綜上所述:

 LoaderListener監聽器的作用就是啓動Web容器時,自動裝配ApplicationContext的配置信息。因爲它實現了ServletContextListener這個接口,在web.xml配置了這個監聽器,啓動容器時,就會默認執行它實現的contextInitialized()方法初始化WebApplicationContext實例,並放入到ServletContext中。由於在ContextLoaderListener中關聯了ContextLoader這個類,所以整個加載配置過程由ContextLoader來完成。

3. DispatcherServlet

 

 
DispatcherServlet結構圖.png

 詳解第三步,contextLoaderListener監聽器初始化完畢後,開始初始化DispatcherServlet,下面爲初始化方法的源碼:

 

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

 

需要做的八件事情如下所述:

  • initMultipartResolver:初始化MultipartResolver,用於處理文件上傳服務,如果有文件上傳,那麼就會將當前的HttpServletRequest包裝成DefaultMultipartHttpServletRequest,並且將每個上傳的內容封裝成CommonsMultipartFile對象。需要在dispatcherServlet-servlet.xml中配置文件上傳解析器。
  • initLocaleResolver:用於處理應用的國際化問題,本地化解析策略。
  • initThemeResolver:用於定義一個主題。
  • initHandlerMapping:用於定義請求映射關係。
  • initHandlerAdapters:用於根據Handler的類型定義不同的處理規則。
  • initHandlerExceptionResolvers:當Handler處理出錯後,會通過此將錯誤日誌記錄在log文件中,默認實現類是SimpleMappingExceptionResolver。
  • initRequestToViewNameTranslators:將指定的ViewName按照定義的RequestToViewNameTranslators替換成想要的格式。
  • initViewResolvers:用於將View解析成頁面。
  • initFlashMapManager:用於生成FlashMap管理器。

 通過查看DispatcherServlet(源碼內容太多就不往上放了),DispatcherServlet繼承自FrameworkServlet,而FrameworkServlet是繼承自HttpServletBean的,HttpServletBean又繼承了HttpServlet。這是因爲DispatcherServlet本身就得是一個Servlet,且含有doGet()和doPost()方法,Web容器纔可以調用它,所以它的頂級父類爲含有這倆方法的HttpServlet。具體的Web請求,會經過FrameServlet的processRequest方法簡單處理後,緊接着調用DispatcherServlet的doService方法,而在這個方法中封裝了最終調用處理器的方法doDispatch。這也意味着,DispatcherServlet的最主要的核心功能由doService和doDispatch實現的。

DispatcherServlet類的方法大致可分爲三種:

  • 初始化相關處理類的方法。
  • 響應Http請求的方法。
  • 執行處理請求邏輯的方法。
核心方法 doDispatch()
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        try {
            ModelAndView mv = null;
            Object dispatchException = null;

            try {
                processedRequest = this.checkMultipart(request);
                multipartRequestParsed = processedRequest != request;
                mappedHandler = this.getHandler(processedRequest);
                if (mappedHandler == null || mappedHandler.getHandler() == null) {
                    this.noHandlerFound(processedRequest, response);
                    return;
                }

                HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                    }

                    if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                this.applyDefaultViewName(processedRequest, mv);
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            } catch (Exception var20) {
                dispatchException = var20;
            } catch (Throwable var21) {
                dispatchException = new NestedServletException("Handler dispatch failed", var21);
            }

            this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
        } catch (Exception var22) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
        } catch (Throwable var23) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
        }

    } finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        } else if (multipartRequestParsed) {
            this.cleanupMultipart(processedRequest);
        }

    }
}

 

DispatcherServlet處理該類請求的步驟:
  1. 在接收到請求後,通過幾級Servlet類型的父類的處理,先調用doService在request中設置一些必需的參數。最終會調用DispatcherServlet的doDispatch方法。
  2. 在doDispatch方法中,首先檢測request是否包含多媒體類型(如File文件上傳),然後將檢測後的request轉換爲processedRequest對象。之後檢測processedRequest對象是否爲原始request(如果是,即原來的request不包含多媒體信息),然後將boolean結果賦給multipartRequestParsed變量(若multipartRequestParsed爲true,在最後會清除processedRequest對象中的多媒體信息)。
  3. 十分重要的一步,就是通過調用處理器映射器查找Handler。調用getHandler來獲取相關的處理器對象。在getHandler方法中,利用處理器映射器HandlerMapping通過request來獲取一個包含Handler處理器本身和其前後攔截器interceptor的處理器執行鏈HandlerExecutionChain對象。
  4. 通過HandlerExecutionChain對象獲取具體的Handler處理器對象,此時使用getHandlerAdapter方法獲取可以處理類型的處理器適配器HandlerAdapter對象。
  5. 調用HandlerAdapter對象的handle方法,將可能帶有多媒體信息的processRequest對象,原始request對象,以及Handler處理器本身作爲參數傳入,handle方法會根據這些參數去執行開發者自己開發的Handler的相關
    請求處理邏輯,並返回含有反饋信息和結果視圖信息的ModelAndView對象。
  6. 獲得ModelAndView對象後,會進行視圖渲染,將model數據填充到request域。在processDispatchResult方法中會對ModelAndView對象進行處理。而在processDispatchResult方法中包含一個render方法,其參數爲ModelAndView對象以及request和response對象。在render方法中,通過resolveViewName會獲取到實際需要使用的視圖View對象,這個對象的具體類型是由XXX決定的。然後就會執行具體的View對象的render方法來完成數據的顯示過程。這裏舉一個視圖類型的例子,他在render方法中具體執行了以下邏輯來綁定結果數據和視圖:
protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception{
  //遍歷model裏面的數據,填充到request域
  for (Map.Entry<String, Object> entry : model.entrySet()){
      String modeName = entry.getKey();
      Object modelValue = entry.getValue();
      if(modelValue != null){
          request.setAttribute(modelName, modelValue);
          if(logger.isDebugEnabled()){
              logger.debug("Added model object '"
                  + modelName + "' of type [" + modelValue.getClass.getName()
                  +"] to request in view with name '" + getBeanName() + "'");
          } else{
             request.removeAttribute(modelName);
            if(logger.isDebugEnabled()){
                logger.debug("Remove modek object '" +modelName +
                    "' from request in view with name '" +getBeanName() + "'");
            } 
         }
      }
  }
}

 

 可以看到,在這裏會把ModelAndView中model的數據遍歷出來,分爲key和value,並且將數據設置在request的attribute域中。之後加載頁面時就可以使用標籤在request域中獲取返回參數了。

 
                                                                   DispatcherServlet初始化時序圖.png
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章