SpringMVC原始碼剖析(二)- DispatcherServlet的前世今生

五柳-先生發表於2016-04-21

上一篇文章《SpringMVC原始碼剖析(一)- 從抽象和介面說起》中,我介紹了一次典型的SpringMVC請求處理過程中,相繼粉墨登場的各種核心類和介面。我刻意忽略了原始碼中的處理細節,只列出最簡單的類甚至是介面類,目的就是讓大家先從最高層次的抽象意義上來審視SpringMVC這個框架;我也刻意將SpringMVC和Struts2做對比,目的是讓大家看到,SpringMVC究竟吸取了Sturts2設計思想中的哪些精華,又彌補了它的哪些遺憾。

DispatcherServlet作為SpringMVC的核心之中的核心類,再怎麼強調它的重要性也不為過。SpringMVC所有的核心類和介面,都密集地出現在DispatcherServlet的原始碼中,SpringMVC原始碼剖析,很大程度上可以說也是在剖析DispatcherServlet這一個類。這一篇文章裡,我先說幾點關於DispatcherServlet的前世今生,希望能幫助你更好的理解它。

1.對擴充套件開放,對修改封閉

SpringMVC是一個基於著名的Open-Closed,即開閉原則進行設計的框架。在Spring官方文件裡面關於SpringMVC的介紹開宗明義地進行了說明:

 

  1. A key design principle in Spring Web MVC and in Spring in general is the “Open for extension,closed for modification” principle.  

開閉原則是一個很寬泛的原則,具體體現到DispatcherServlet的原始碼中,我們可以大致摸得到一些線索:

  • 類中所有的變數宣告,幾乎都以介面的形式給出,並沒有繫結在具體的實現類上。
  • 使用模版方法模式,在父類中對基礎行為進行定義,讓子類實現模版方法擴充套件行為。

其中第一點,在一個框架的設計中尤為重要,也是貫徹開閉原則最重要的一點。因為當你通過一些高層次的介面或者抽象類,將一個類完成的邏輯或流程編寫完成後(具體點說,是通過一個介面的引用呼叫介面方法),整個邏輯或流程的功能就被確實的在原始碼中固定下來了。可是這時,這些介面或抽象類的具體實現者是誰,還沒有固定!這就給了你的系統或框架近乎無限的擴充套件性,因為你可以任意安排和實現這些類。

我認為,物件導向設計的精髓,是對現實世界中“行為和契約”的描述。這個“行為和契約”,體現在介面和抽象類的方法宣告中。軟體設計師要用物件導向的眼光去觀察和抽象這個世界中的事物,這裡的事物可以是一些商業邏輯、可以是一些處理流程,然後用高層次的介面去描述這些行為和契約。當你在越抽象的層次上將這些行為和契約描述清楚後,你所設計的系統就是越符合開閉原則的。

SpringMVC框架在物件導向設計上,做出了絕佳的示範。它通過高度抽象的介面,描述出了一次請求處理的流程,從而讓整個框架從一開始就是符合開閉原則的。同時它也提供了這些介面的一系列預設實現類,讓你不需要很複雜的配置,就能很好的使用SpringMVC進行開發。抽象的確是個利器,但是框架絕不能執行在空中樓閣中,SpringMVC提供的的這一系列預設實現類必須要有容身之所。聰明的你可能早已想到:Spring IOC容器。這就引出了我要說的第二點。

2.配置元素的物件化

所有的框架,都需要有這樣一個功能,叫做:配置元素的物件化。因為幾乎所有的框架,都將配置元素集中到外部的xml配置檔案中,然後在框架的初始化流程中,對這些配置檔案進行解析,再變成java世界中的一個個物件供框架使用,這整個過程,可以被稱為配置元素的物件化。為什麼要有配置檔案呢?這個問題的回答也是很簡單,因為沒有人會想要使用一個配置散佈在框架中各個java類原始碼裡面的框架。框架也不允許使用者這樣子做,因為框架在釋出的時候,提供的是一個個jar包檔案,jar包內是已經編譯好的class檔案。配置檔案由使用者外部提供,框架對它進行解析,使用者能得到集中配置的好處,框架也樂於這樣子,可以說是合情合理。

那麼作為Spring產品族的新成員,SpringMVC在設計的時候,相信設計者們不做它想,這一個“配置元素的物件化”功能既然不可避免,那麼使用Spring IOC容器,通過bean配置檔案來配置SpringMVC,絕對是不二之選。不可能像Struts2一樣,內部再搞一個別的容器,因為Spring容器本身已經是被高度設計,而且已經在java世界獲得巨大成功。從推廣的角度上來說,如果對spring容器的所有知識,都可以完整的應用到SpringMVC,那麼對於開發者無疑是一個極大的吸引力。

剩下的問題就只有:到底該如何將Spring容器和SpringMVC的初始化過程整合起來呢?

答案就是WebApplicationContext介面,更具體點說,是XmlWebApplicationContext這個Spring上下文實現類。SpringMVC也使用了這一個為了將Spring容器和Web環境整合而特意設計的Spring上下文類。我們開啟WebApplicationContext的原始碼:

 

  1. package org.springframework.web.context;  
  2.   
  3. import javax.servlet.ServletContext;  
  4.   
  5. import org.springframework.context.ApplicationContext;  
  6.   
  7. public interface WebApplicationContext extends ApplicationContext {  
  8.   
  9.     String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";  
  10.   
  11.     String SCOPE_REQUEST = "request";  
  12.   
  13.     String SCOPE_SESSION = "session";  
  14.   
  15.     String SCOPE_GLOBAL_SESSION = "globalSession";  
  16.   
  17.     String SCOPE_APPLICATION = "application";  
  18.   
  19.     String SERVLET_CONTEXT_BEAN_NAME = "servletContext";  
  20.   
  21.     String CONTEXT_PARAMETERS_BEAN_NAME = "contextParameters";  
  22.   
  23.     String CONTEXT_ATTRIBUTES_BEAN_NAME = "contextAttributes";  
  24.   
  25.     ServletContext getServletContext();  
  26.       
  27. }  

發現它是繼承於ApplicationContext這個普通Spring容器所使用的上下文介面類,除了一些常量的宣告,只多了一個可以獲取到ServletContext的getServletContext()方法。回到上面提到的“行為和契約的描述”上,我們可以大膽的斷言,Spring容器和Web環境的整合,是在ServletContext上做文章。

開啟所有使用了Spring的Web專案的web.xml檔案,必定有這樣一段配置:

 

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

ContextLoaderListener實現了ServletContextListener介面,在Servlet容器啟動的時候,會初始化一個WebApplicationContext的實現類,並將其作為ServletContext的一個屬性設定到Servlet環境中,摘抄原始碼如下: 

 

  1. servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);  

WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE的值,在上面WebApplicationContext的原始碼中的第一個常量中就被宣告,是WebApplicationContext.class.getName() + ".ROOT",更直接一點,它是“org.springframework.web.context.WebApplicationContext.ROOT”。ContextLoaderListener所初始化的這個Spring容器上下文,被稱為根上下文。

SpringMVC在DispatcherServlet的初始化過程中,同樣會初始化一個WebApplicationContext的實現類,作為自己獨有的上下文,這個獨有的上下文,會將上面的根上下文作為自己的父上下文,來存放SpringMVC的配置元素,然後同樣作為ServletContext的一個屬性,被設定到ServletContext中,只不過它的key就稍微有點不同,key和具體的DispatcherServlet註冊在web.xml檔案中的名字有關,從這一點也決定了,我們可以在web.xml檔案中註冊多個DispatcherServlet,因為Servlet容器中註冊的Servlet名字肯定不一樣,設定到Servlet環境中的key也肯定不同。

由於在Spring容器中,子上下文可以訪問到所有父上下文中的資訊,而父上下文訪問不到子上下文的資訊,這個根上下文,就很適合作為多個子上下文配置的集中點。以官方文件中的圖來說明:

3.前端控制器

前端控制器,即所謂的Front Controller,體現的是設計模式中的前端控制器模式。前端控制器處理所有從使用者過來的請求。所有使用者的請求都要通過前端控制器。SpringMVC框架和其他請求驅動的表示層框架一樣,也是圍繞一個將請求分發到相應控制器的核心Servlet來設計的。DispatcherServlet和其他框架中的Servlet不一樣的地方在於,它和Spring容器無縫整合在了一起,因此你可以在SpringMVC中使用Spring容器所有的特性。

DispatcherServlet這個前端控制器,在SpringMVC中的作用,以官方文件中的配圖來說明:

整個流程可以被大致描述為:一個http請求到達伺服器,被DispatcherServlet接收。DispatcherServlet將請求委派給合適的處理器Controller,此時處理控制權到達Controller物件。Controller內部完成請求的資料模型的建立和業務邏輯的處理,然後再將填充了資料後的模型即model和控制權一併交還給DispatcherServlet,委派DispatcherServlet來渲染響應。DispatcherServlet再將這些資料和適當的資料模版檢視結合,向Response輸出響應。

可以看到Model-View-Controller這三樣東西協同合作,共同體現出MVC的設計理念,三個層次可以分別獨立演化,整個系統架構又清晰又簡潔。這是SpringMVC為我們描述的美好願景,後面我們也將看到,SpringMVC為了實現這一承諾,究竟做出了什麼樣的努力。

相關文章