Spring 系列(二):Spring MVC的父子容器

七分熟pizza發表於2019-04-21

1.背景

在使用Spring MVC時候大部分同學都會定義兩個配置檔案,一個是Spring的配置檔案spring.xml,另一個是Spring MVC的配置檔案spring-mvc.xml。

在這裡給大家拋個問題,如果在spring.xml和spring-mvc.xml檔案中同時定義一個相同id的單例bean會怎樣呢?大家可以先思考一下再繼續往下看。

我做了個實驗,結論是:容器中會同時存在兩個相同id 的bean,而且使用起來互不干擾。

這是為什麼呢?學過Spring的同學肯定會質疑,眾所周知id是bean的唯一標示,怎麼可能同時存在兩個相同id的bean呢?是不是我在胡扯呢?

原諒我在這和大家賣了個關子,其實大家說的都沒錯,因為這裡涉及到Spring MVC父子容器的知識點。

這個知識點是:在使用Spring MVC過程中會存在Spring MVC 、Spring兩個IOC容器,且Spring MVC是Spring的子容器。

那這個父子容器到底是什麼呢?

為了保證我所說的權威性,而不是知識的二道販子,我將從Spring 官方文件和原始碼兩方面展開介紹。

2.Spring MVC父子容器

2.1 web.xml配置

還是先找程式入口,檢視web.xml配置檔案,找到Spring MVC相關配置。

<servlet>
        <servlet-name>spring-mvc</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>
        <load-on-startup>1</load-on-startup>
</servlet>
複製程式碼

配置很簡單,只是配置了一個型別為DispatcherServlet型別的Servlet,並設定了初始化引數。那DispatcherServlet是什麼呢?

2.2 DispatcherServlet類介紹

檢視API文件

Spring 系列(二):Spring MVC的父子容器
從繼承圖看出最終繼承自HttpServlet,其實就是一個普通的Servlet。那為什麼這個Servlet就能完成Spring MVC一系列複雜的功能呢?繼續往下看。

2.3 DispatcherServlet工作流程

Spring 系列(二):Spring MVC的父子容器
DispatcherServlet工作流程如下:

  • (1) 所有請求先發到DispacherServlet
  • (2) DispacherServlet根據請求地址去查詢相應的Controller,然後返回給DispacherServlet。
  • (3) DispacherServlet得到Controller後,讓Controler處理相應的業務邏輯。
  • (4) Controler處理處理完後將結果返回給DispacherServlet。
  • (5) DispacherServlet把得到的結果用檢視解析器解析後獲得對應的頁面。
  • (6) DispacherServlet跳轉到解析後的頁面。

在整個過程中DispatcherServlet承當了一箇中心控制器的角色來處理各種請求。

2.4 DispatcherServlet上下文繼承關係

Spring 系列(二):Spring MVC的父子容器

上圖來自Spring官網:

https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html
複製程式碼

從圖中可以看到DispatcherServlet裡面有一個 Servlet WebApplicationContext,繼承自 Root WebApplicationContext。

上篇文章中我們知道WebApplicationContext其實就是一個IOC容器,root WebApplicationContext是Spring容器。

這說明DispatcherServlet中裡建立了一個IOC容器並且這個容器繼承了Spring 容器,也就是Spring的子容器。

而且官方文件中還有如下一段文字描述:

For many applications, having a single WebApplicationContext is simple and suffices. It is also possible to have a context hierarchy where one root WebApplicationContext is shared across multiple DispatcherServlet (or other Servlet) instances, each with its own child WebApplicationContext configuration. See Additional Capabilities of the ApplicationContext for more on the context hierarchy feature.

The root WebApplicationContext typically contains infrastructure beans, such as data repositories and business services that need to be shared across multiple Servlet instances.
Those beans are effectively inherited and can be overridden (that is, re-declared) in the Servlet-specific child WebApplicationContext, which typically contains beans local to the given Servlet.

複製程式碼

結合圖和上述文字我們可以得出以下資訊:

  1. 應用中可以包含多個IOC容器。
  1. DispatcherServlet的建立的子容器主要包含Controller、view resolvers等和web相關的一些bean。
  1. 父容器root WebApplicationContex主要包含包含一些基礎的bean,比如一些需要在多個servlet共享的dao、service等bean。
  1. 如果在子容器中找不到bean的時候可以去父容器查詢bean。

看到這裡也許大家心中也許就明白文章開頭中我說的Spring MVC中的父子容器了,對那個問題也有了自己的判斷和答案。

當然文章還沒有結束,畢竟這還僅限於對官方文件的理解,為了進一步驗證,我們拿出終極武器:

閱讀原始碼!

2.5 DispatcherServlet原始碼分析

本小節我們分為Spring MVC容器的建立和bean的獲取兩部分進行分析。

2.5.1 Spring MVC容器的建立

前面分析到DispatcherServlet本質上還是一個Servlet ,既然是Servlet ,瞭解Servlet生命週期的同學都知道Web 容器裝載Servlet第一步是執行init()函式,因此以DispatcherServlet 的init函式為突破口進行分析。

@Override
public final void init() throws ServletException {
   // 1.讀取init parameters 等引數,其中就包括設定contextConfigLocation 
    PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
   //2.初始化servlet中使用的bean
   initServletBean();
}
複製程式碼

在第1步讀取init parameter的函式最終會呼叫setContextConfigLocation()設定配置檔案路徑。此處重點介紹initServletBean(),繼續跟蹤。

Override
protected final void initServletBean() throws ServletException {
      //初始化webApplicationContext
      this.webApplicationContext = initWebApplicationContext();
}
複製程式碼
protected WebApplicationContext initWebApplicationContext() {
    //1.獲得rootWebApplicationContext
    WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;
    //2.如果還沒有webApplicatioinContext,建立webApplicationContext
    if (wac == null) {
	//建立webApplicationContext
        wac = createWebApplicationContext(rootContext);
    }
   return wac;
}

複製程式碼

可以看到上面初始化webApplicationContext分為2步。

  • (1)獲取父容器rootWebApplicationContext。
  • (2)建立子容器。

我們先看看rootWebApplicationContext是如何獲取的。

public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
   return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}

public static WebApplicationContext getWebApplicationContext(ServletContext sc, String attrName) {
   Object attr = sc.getAttribute(attrName);
   return (WebApplicationContext) attr;
}
複製程式碼

從上面程式碼中我沒看到是從ServletContext獲取了名為“WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE”的webApplicationContext。

認真看過上篇文章的同學應該記得這個屬性是在Spring初始化 容器initWebApplicationContext()函式中的第3步設定進去的,取得的值即Spring IOC容器。

繼續看如何建立webApplicationContext。

protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) {
   return createWebApplicationContext((ApplicationContext) parent);
}
複製程式碼
createWebApplicationContext(ApplicationContext parent) {
  //1.獲取WebApplicationContext實現類,此處其實就是XmlWebApplicationContext
  Class<?> contextClass = getContextClass();
  //生成XmlWebApplicationContext例項
  ConfigurableWebApplicationContext wac =
         (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
  //2.設定rootWebApplicationContext為父容器 
   wac.setParent(parent);
  //3.設定配置檔案
   wac.setConfigLocation(getContextConfigLocation());
  //4.配置webApplicationContext.
   configureAndRefreshWebApplicationContext(wac);
   return wac;
}
複製程式碼
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
   //開始處理bean
   wac.refresh();
}
複製程式碼

看到這裡同學們有沒有是曾相識的感覺。是的,這段邏輯和上篇文章建立Spring IOC的邏輯類似。

唯一不同的是在第2步會把Spring容器設定為自己的父容器。至於新建容器中bean的註冊、解析、例項化等流程和Spring IOC容器一樣都是交給XmlWebApplicationContext類處理,還沒有掌握的同學可以看上篇文章

2.5.2 Spring MVC Bean的獲取

Spring MVC bean的獲取其實我們在上篇文章已經介紹過,這次再單拎出來介紹一下,加深記憶。

protected <T> T doGetBean(
    // 獲取父BeanFactory
    BeanFactory parentBeanFactory = getParentBeanFactory();
    //如果父容器不為空,且本容器沒有註冊此bean就去父容器中獲取bean
    if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
         // 如果父容器有該bean,則呼叫父beanFactory的方法獲得該bean
         return (T) parentBeanFactory.getBean(nameToLookup,args);
    }
    //如果子容器註冊了bean,執行一系列例項化bean操作後返回bean.
    //此處省略例項化過程
    .....
    return (T) bean;
}
複製程式碼

上面程式碼就可以對應官方文件中“如果子容器中找不到bean,就去父容器找”的解釋了。

3.小結

看完上面的介紹,相信大家對Spring MVC父子容器的概念都有所瞭解,現在我們分析文章開頭的問題。

如果spring.xml和spring-mvc.xml定義了相同id的bean會怎樣?假設id=test。

1.首先Spring 初始化,Spring IOC 容器中生成一個id為test bean例項。

2.Spring MVC開始初始化,生成一個id為test bean例項。

此時,兩個容器分別有一個相同id的bean。那用起來會不會混淆?

答案是不會。

當你在Spring MVC業務邏輯中使用該bean時,Spring MVC會直接返回自己容器的bean。

當你在Spring業務邏輯中使用該bean時,因為子容器的bean對父親是不可見的,因此會直接返回Spring容器中的bean。

雖然上面的寫法不會造成問題。但是在實際使用過程中,建議大家都把bean定義都寫在spring.xml檔案中。

因為使用單例bean的初衷是在IOC容器中只存在一個例項,如果兩個配置檔案都定義,會產生兩個相同的例項,造成資源的浪費,也容易在某些場景下引發缺陷。

4.尾聲

現在大家基本都不使用在xml檔案中定義bean的形式,而是用註解來定義bean,然後在xml檔案中定義掃描包。如下:

<context:component-scan base-package="xx.xx.xx"/>
複製程式碼

那如果在spring.xml和spring-mvc.xml配置了重複的包會怎樣呢?

如果本文看明白的同學此時已經知道了答案。

答案是會在兩個父子IOC容器中生成大量的相同bean,這就會造成記憶體資源的浪費。

也許有同學想到,那隻在spring.xml中設定掃描包不就能避免這種問題發生了嗎,答案是這樣嗎?

大家可以試試,這樣會有什麼問題。如果不行,那是為什麼呢?

欲知分曉,敬請期待下篇分解!

想要了解更多,關注公眾號:七分熟pizza

在這裡插入圖片描述

相關文章