1. springmvc簡介
同其他的mvc框架一樣,springmvc的核心還是控制流和數據流,只不過它提供了一套更爲靈活的解決方案,具體體現在兩點:一、springmvc的數據綁定特別靈活;二、springmvc的視圖解析;這主要基於springmvc提供了一套強大的組件工具,此外springmvc還能更方便的構建基於rest的web站點;這些特性只有在後續使用中我們纔會慢慢體會到,此處只是略作提及,在下一章節我們將先從springmvc的發展歷史來加深對它的理解。
2. springmvc後世前緣
關於springmvc的由來我們先從一幅圖說起:
從圖中我們可以發現,所有的MVC框架都是從基本的Servlet模型發展而來。那麼我們就先來回憶一下servlet的請求-響應模型:
配置Servlet及其映射關係(在web.xml中)
<servlet> <servlet-name>registerServlet</servlet-name> <servlet-class>com.demo2do.springmvc.web.RegisterServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>registerServlet</servlet-name> <url-pattern>/register</url-pattern> </servlet-mapping>
在Servlet實現類中完成響應邏輯
public class RegisterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 從request獲取參數 String name = req.getParameter("name"); String birthdayString = req.getParameter("birthday"); // 做必要的類型轉化 Date birthday = null; try { birthday = new SmpleDateFormat("yyyy-MM-dd").parse(birthdayString); } catch (ParseException e) { e.printStackTrace(); } // 初始化User類,並設置字段到user對象中去 User user = new User(); user.setName(name); user.setBirthday(birthday); // 調用業務邏輯代碼完成註冊 UserService userService = new UserService(); userService.register(user); // 設置返回數據 request.setAttribute("user", user); // 返回成功頁面 req.getRequestDispatcher("/success.jsp").forward(req, resp); } }
這是我們大家都熟悉的servlet的請求-響應模型,因爲這是我們做j2ee開發的基石;對整個過程加以抽象會發現servlet規範無外乎做了這樣兩件事:
- 維護請求響應的映射關係定義(web.xml)並根據這個定義找到對應的業務處理servlet
- 對請求數據和響應數據的處理
注:第一個就是軟件工程中的控制流的概念,而第二個就是數據流的概念;這兩個概念是衆多mvc框架的核心,想深入瞭解的可到網上自行查閱相關資料。
springmvc歸根結底還是要做servlet規範做的兩件事,只不過在Servlet編程模型之下會遇到了各種各樣的問題,而springmvc就是針對這些問題提供了自己的解決方案。
那麼在Servlet編程模型之下遇到的第一個問題就是:項目規模擴大之後,請求-響應的映射關係全部定義在web.xml中,將造成web.xml的不斷膨脹而變得難以維護。
針對這個問題,SpringMVC提出的方案就是:提煉一個核心的Servlet覆蓋對所有Http請求的處理。
這一被提煉出來的Servlet,通常被我們稱之爲:核心分發器。在SpringMVC中,核心分發器就是org.springframework.web.servlet.DispatcherServlet。
有了DispatcherServlet,我們至少從表面上解決了上面的問題。至少在web.xml中,我們的配置代碼就被固定了下來:
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/**</url-pattern>
</servlet-mapping>
有了DispatcherServlet,我們只相當於邁出了堅實的第一步,因爲對核心Servlet的提煉不僅僅是將所有的Servlet集中在一起那麼簡單,我們還將面臨兩大問題:
- 核心Servlet應該能夠建立起一整套完整的對所有Http請求進行規範化處理的流程。比如在哪解析數據、捕獲異常等等。
- 核心Servlet應該能夠根據一定的規則對不同的Http請求分發到不同的Servlet對象上去進行處理。
以上兩點都是根據servlet規範模型抽象出來的。對此,SpringMVC所提出的方案是:將整個處理流程規範化,並把每一個處理步驟分派到不同的組件中進行處理。
這個方案實際上涉及到兩個方面:
處理流程規範化 —— 將處理流程劃分爲若干個步驟(任務),並使用一條明確的邏輯主線將所有的步驟串聯起來
處理流程組件化 —— 將處理流程中的每一個步驟(任務)都定義爲接口,併爲每個接口賦予不同的實現模式
在SpringMVC的設計中,這兩個方面的內容總是在一個不斷交叉、互爲補充的過程中逐步完善的。
處理流程規範化是目的,對於處理過程的步驟劃分和流程定義則是手段。因而處理流程規範化的首要內容就是考慮一個通用的Servlet響應程序大致應該包含的邏輯步驟:
步驟1 —— 對Http請求進行初步處理,查找與之對應的Controller處理類(方法)
步驟2 —— 調用相應的Controller處理類(方法)完成業務邏輯
步驟3 —— 對Controller處理類(方法)調用時可能發生的異常進行處理
步驟4 —— 根據Controller處理類(方法)的調用結果,進行Http響應處理
這些邏輯步驟雖然還在我們的腦海中,不過這些過程恰恰正是我們對整個處理過程的流程化概括,稍後我們就會把它們進行程序化處理。
所謂的程序化,實際上也就是使用編程語言將這些邏輯語義表達出來。在Java語言中,最適合表達邏輯處理語義的語法結構是接口,因此上述的四個流程也就被定義爲了四個不同接口,它們分別是:
步驟1 —— HandlerMapping
步驟2 —— HandlerAdapter
步驟3 —— HandlerExceptionResolver
步驟4 —— ViewResolver
而這些接口,就是組件
除了上述組件之外,SpringMVC所定義的組件幾乎涵蓋了每一個處理過程中的重要節點。我們在這裏引用Spring官方reference中對於最基本的組件的一些說明:
我們在之後篇文章中將重點對這裏所提到的所有組件做深入的分析。大家在這裏需要理解的是SpringMVC定義這些組件的目的和初衷。
這些組件一旦被定義,自然而然也就引出了下一個問題:這些組件是如何串聯在一起的?這個過程,是在DispatcherServlet中完成的。有關這一點,我們可以從兩個不同的角度加以證明。
- 從DispatcherServlet自身數據結構的角度
如圖中所示,DispatcherServlet中包含了衆多SpringMVC的組件,這些組件是實現DispatcherServlet核心邏輯的基礎。 從DispatcherServlet的核心源碼的角度
try { // 這裏省略了部分代碼 // 獲取HandlerMapping組件返回的執行鏈 mappedHandler = getHandler(processedRequest, false); if (mappedHandler == null || mappedHandler.getHandler() == null) { noHandlerFound(processedRequest, response); return; } // 獲取HandlerAdapter組件 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 這裏省略了部分源碼 // 調用HandlerAdapter組件 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); // 這裏省略了部分源碼 }catch (ModelAndViewDefiningException ex) { logger.debug("ModelAndViewDefiningException encountered", ex); mv = ex.getModelAndView(); }catch (Exception ex) { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); // 調用HandlerExceptionResolver進行異常處理 mv = processHandlerException(processedRequest, response, handler, ex); errorView = (mv != null); }
從上面的代碼片段中,我們可以看到DispatcherServlet的核心邏輯不過是對組件的獲取和調用。
除此之外,SpringMVC對處理流程的規範化和組件化所引出的另外一個問題就是如何針對所有的組件進行管理。
先說說管理。其實管理這些組件對於SpringMVC來說完全不是問題,因爲SpringMVC作爲Spring Framework的一部分,其自身的運行環境就是Spring所定義的容器之中。我們知道,Spring Framework的核心作用之一就是對整個應用程序的組件進行管理。所以SpringMVC對於這些已定義組件的管理,只不過是借用了Spring自身已經提供的容器功能而已,這裏就不做贅述。
有了組件,也有了DispatcherServlet對所有組件的串聯,我們之前所提出的兩個問題似乎已經可以迎刃而解。所以,我們可以說:SpringMVC就是通過DispatcherServlet將一堆組件串聯起來的Web框架。
3. DispatcherServlet
DispatcherServlet —— 串聯起整個邏輯主線,是整個框架的心臟
3.1 DispatcherServlet的體系結構
通過不同的角度來觀察DispatcherServlet會得到不同的結論。我們在這裏選取了三個不同的角度:運行主線、繼承結構和數據結構。
【運行主線】
從DispatcherServlet所實現的接口來看,DispatcherServlet的核心本質:是一個Servlet。這個結論似乎很幼稚,不過這個幼稚的結論卻蘊含了一個對整個框架都至關重要的內在原則:Servlet可以根據其特性進行運行主線的劃分。
根據Servlet規範的定義,Servlet中的兩大核心方法init方法和service方法,它們的運行時間和觸發條件都截然不同:
- init方法
在整個系統啓動時運行,且只運行一次。因此,在init方法中我們往往會對整個應用程序進行初始化操作。這些初始化操作可能包括對容器(WebApplicationContext)的初始化、組件和外部資源的初始化等等。
- service方法
在整個系統運行的過程中處於偵聽模式,偵聽並處理所有的Web請求。因此,在service及其相關方法中,我們看到的則是對Http請求的處理流程。
因而在這裏,Servlet的這一特性就被SpringMVC用於對不同的邏輯職責加以劃分,從而形成兩條互不相關的邏輯運行主線:
初始化主線 —— 負責對SpringMVC的運行要素進行初始化
Http請求處理主線 —— 負責對SpringMVC中的組件進行邏輯調度完成對Http請求的處理
對於一個MVC框架而言,運行主線的劃分非常重要。因爲只有弄清楚不同的運行主線,我們才能針對不同的運行主線採取不同的研究策略。而我們在這個系列中的絕大多數分析的切入點,也是圍繞着不同的運行主線進行的。
注:SpringMVC運行主線的劃分依據是Servlet對象中不同方法的生命週期。事實上,幾乎所有的MVC都是以此爲依據來進行運行主線的劃分。這進一步可以證明所有的MVC框架的核心基礎還是Servlet規範,而設計理念的差異也導致了不同的框架走向了完全不同的發展道路。
【繼承結構】
除了運行主線的劃分以外,我們再關注一下DispatcherServlet的繼承結構:
在這個繼承結構中,我們可以看到DispatcherServlet在其繼承樹中包含了2個Spring的支持類:HttpServletBean和FrameworkServlet。我們分別來討論一下這兩個Spring的支持類在這裏所起到的作用。
HttpServletBean是Spring對於Servlet最低層次的抽象。在這一層抽象中,Spring會將這個Servlet視作是一個Spring的bean,並將init-param中的值作爲bean的屬性注入進來:
public final void init() throws ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Initializing servlet '" + getServletName() + "'");
}
// Set bean properties from init parameters.
try {
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.environment));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
throw ex;
}
// Let subclasses do whatever initialization they like.
initServletBean();
if (logger.isDebugEnabled()) {
logger.debug("Servlet '" + getServletName() + "' configured successfully");
}
}
從源碼中,我們可以看到HttpServletBean利用了Servlet的init方法的執行特性,將一個普通的Servlet與Spring的容器聯繫在了一起。在這其中起到核心作用的代碼是:BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);將當前的這個Servlet類轉化爲一個BeanWrapper,從而能夠以Spring的方式來對init-param的值進行注入。BeanWrapper的相關知識屬於Spring Framework的內容,我們在這裏不做詳細展開,讀者可以具體參考HttpServletBean的註釋獲得更多的信息。
FrameworkServlet則是在HttpServletBean的基礎之上的進一步抽象。通過FrameworkServlet真正初始化了一個Spring的容器(WebApplicationContext),並引入到Servlet對象之中:
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
if (this.logger.isInfoEnabled()) {
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
}
long startTime = System.currentTimeMillis();
try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
} catch (ServletException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
} catch (RuntimeException ex) {
this.logger.error("Context initialization failed", ex);
throw ex;
}
if (this.logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
elapsedTime + " ms");
}
}
上面的這段代碼就是FrameworkServlet初始化的核心代碼。從中我們可以看到這個FrameworkServlet將調用其內部的方法initWebApplicationContext()對Spring的容器(WebApplicationContext)進行初始化。同時,FrameworkServlet還暴露了與之通訊的結構可供子類調用:
public abstract class FrameworkServlet extends HttpServletBean {
/** WebApplicationContext for this servlet */
private WebApplicationContext webApplicationContext;
// 這裏省略了其他所有的代碼
/**
* Return this servlet's WebApplicationContext.
*/
public final WebApplicationContext getWebApplicationContext() {
return this.webApplicationContext;
}
}
我們在這裏暫且不對Spring容器(WebApplicationContext)的初始化過程詳加探查,稍後我們會討論一些WebApplicationContext初始化過程中的配置選項。不過讀者可以在這裏體會到:FrameworkServlet在其內部初始化了一個Spring的容器(WebApplicationContext)並暴露了相關的操作接口,因而繼承自FrameworkServlet的DispatcherServlet,也就直接擁有了與WebApplicationContext進行通信的能力。
通過對DispatcherServlet繼承結構的研究,我們可以明確:
結論 DispatcherServlet的繼承體系架起了DispatcherServlet與Spring容器進行溝通的橋樑。
【數據結構】
在上一篇文章中,我們曾經提到過DispatcherServlet的數據結構:
我們可以把在上面這張圖中所構成DispatcherServlet的數據結構主要分爲兩類(我們在這裏用一根分割線將其分割開來):
配置參數 —— 控制SpringMVC組件的初始化行爲方式
核心組件 —— SpringMVC的核心邏輯處理組件
可以看到,這兩類數據結構都與SpringMVC中的核心要素組件有關。因此,我們可以得出這樣一個結論:
結論 組件是整個DispatcherServlet的靈魂所在:它不僅是初始化主線中的初始化對象,同樣也是Http請求處理主線中的邏輯調度載體。
注:我們可以看到被我們劃爲配置參數的那些變量都是boolean類型的,它們將在DispatcherServlet的初始化主線中起到一定的作用,我們在之後會使用源碼進行說明。而這些boolean值可以通過web.xml中的init-param值進行設定覆蓋(這是由HttpServletBean的特性帶來的)。
SpringMVC的運行體系
DispatcherServlet繼承結構和數據結構,實際上表述的是DispatcherServlet與另外兩大要素之間的關係:
繼承結構 —— DispatcherServlet與Spring容器(WebApplicationContext)之間的關係
數據結構 —— DispatcherServlet與組件之間的關係
所以,其實我們可以這麼說:SpringMVC的整個運行體系,是由DispatcherServlet、組件和容器這三者共同構成的。
在這個運行體系中,DispatcherServlet是邏輯處理的調度中心,組件則是被調度的操作對象。而容器在這裏所起到的作用,是協助DispatcherServlet更好地對組件進行管理。這就相當於一個工廠招了一大批的工人,並把工人劃分到一個統一的工作車間而便於管理。在工廠要進行生產活動時,只需要從工作車間把工人分派到相應的生產流水線上即可。
筆者在這裏引用Spring官方reference中的一幅圖,對三者之間的關係進行簡單的描述:
注:在這幅圖中,我們除了看到在圖的左半邊DispatcherServlet、組件和容器這三者之間的調用關係以外,還可以看到SpringMVC的運行體系與其它運行體系之間存在着關係。有關這一點,我們在之後的討論中會詳細展開。
既然是三個元素之間的關係表述,我們必須以兩兩關係的形式進行歸納:
DispatcherServlet - 容器 —— DispatcherServlet對容器進行初始化
容器 - 組件 —— 容器對組件進行全局管理
DispatcherServlet - 組件 —— DispatcherServlet對組件進行邏輯調用
值得注意的是,在上面這幅圖中,三大元素之間的兩兩關係其實表現得並不明顯,尤其是“容器 - 組件”和“DispatcherServlet - 組件”之間的關係。這主要是由於Spring官方reference所給出的這幅圖是一個靜態的關係表述,如果從動態的觀點來對整個過程加以審視,我們就不得不將SpringMVC的運行體系與之前所提到的運行主線聯繫在一起,看看這些元素在不同的邏輯主線中所起到的作用。
以後,我們就分別研究DispatcherServlet的兩條運行主線。