概述
本文主要講了一些什麼
Spring MVC的基本原理 -->
用一個簡單的示例對基本原理有個具體了解 -->
分別描述MVC的幾個重量級角色:過濾請求、綁定請求、封裝對象、處理XML和JSON、處理模型數據。
本篇文章重在知識點整合,作為讀書筆記,語言簡潔,刪繁就簡,由淺入深,引人入勝。通讀本文,你將對Spring MVC有一個深入的了解。如果讀,請深讀。如果愛,請讀完。
基本原理
要了解Spring MVC框架的工作機理,必須回答下面三個問題:
問題一:DispatcherServlet框架如何截獲特定的HTTP請求,交由Spring MVC框架處理?
答:在web.xml中配置一個Servlet,並通過<servlet-mapping>指定其處理的url。像下面這樣:
那麼,所有帶.html後綴的http請求都會被DispatcherServlet截獲並處理。
問題二:位於Web層的Spring容器(WebApplicationContext)如何與位於業務層的Spring容器(ApplicationContext)相關聯,以使web層的bean可以調用業務層的bean?
答:下面是一個web.xml:
ContextLoaderListener是一個ServletContextListener,它通過contextConfigLocation參數指定的配置文件(如本例的applicationContext.xml)啟動“業務層”的Spring容器。而由下面引入的DispatcherServlet啟動Web層的spring容器。Web層的spring容器將作為業務層容器的子容器(即:Web層容器可以引用業務層容器的bean,而業務層容器不可以引用Web層容器的bean。)
問題三:如何初始化Spring MVC的各個組件,並將它們裝配到DispatcherServlet中?
答:當DispatcherServlet加載後,就會自動掃描上下文的bean,根據名稱或者類型匹配的機制查找自定義的組件,找不到時則使用DispatcherServlet.properties中使用的默認組件。
一個簡單的實例
Spring MVC的開發一般包括以下步驟:
步驟一 :配置web.xml,指定業務層對應的配置文件,定義DispatcherServlet:
步驟二:編寫處理請求的控制器(處理器):
@Controller注解將一個POJO類轉化為處理請求的控制器,通過@RequestMapping指定控制器處理哪些請求。上例中,處理/user/register請求,返回user/register視圖(當然,該視圖字符串加上前後綴才能定位到具體JSP,後面步驟會講到)。
步驟三:編寫視圖對象(比如JSP):
步驟四:配置Spring MVC的配置文件(如applicationContext.xml):
1是默認的視圖解析器,2是視圖解析器加的前後綴,它與步驟二返回的user/register拼起來:/WEB-INF/views/user/register.jsp,表示步驟二將返回這個視圖,即:把請求交給這個jsp來處理。
過濾請求
使用@RequestMapping可以過濾請求URL、請求參數、請求方法,請求頭這4個方面的信息項。比如:
綁定請求信息到方法入參中
Spring MVC會將HTTP請求的信息綁定到相應的方法入參中,並根據方法返回值類型作出相應的後續處理。比如:
將請求信息封裝成對象
使用HttpMessageConverter<T>可以將請求信息轉換成一個對象,或者將對象輸出為響應信息。(T就是對象類型)。
兩把利劍:
1.使用RequestBody、ResponseBody。
2.使用HttpEntity<T>/ResponseEntity<T>
示例1:
客戶端:
服務端:
示例2:
客戶端:
服務端:
與RequestBody、ResponseBody不同的是,HttpEntity不僅可以訪問請求、響應報文體的數據,還可以訪問請求和響應報文頭的數據。
示例1:
客戶端:
服務端:
示例2:
客戶端:
服務端:
怎麼做到的?
HttpMessageConverter<T>提供了眾多的實現類,它組成了一個功能強大、用途廣泛的HttpMessageConverter<T>家族。
比如:
StringHttpMessageConverter負責將請求信息轉換成字符串;
FormHttpMessageConverter負責將表單數據讀取到MultiValueMap中;
還有諸如讀寫XML、Resource、JSON等等對應的實現類。
AnnotationMethodHandlerAdapter適配器默認裝配了基本的Converter(String、ByteArray、Source、XmlAwareForm),容器會根據需要自動調用不同的HttpMessageConverter實現類。
如果不能滿足需求,那麼繼續在xml中配置其他的converter:
處理XML和JSON
從上文可知,HttpMessageConverter有一個很強大的家族,能夠處理各種格式的請求及響應。當然也可以處理XML和JSON。
處理XML和JSON的Converter分別是:
MarsHallingHttpMessageConverter(處理xml)。
Jaxb2RootElementHttpMessageConverter(同上,底層使用JAXB)。
MappingJacksonHttpMessageConverter(處理JSON)。
按照前文的思路,首先要在配置文件中裝配好相應的Converter,但是還有其他必要的步驟:
步驟1:XML中配置Converter以及Marshaller:
步驟2:測試客戶端發出請求:
步驟3:響應(服務)端:
步驟4:User的注解:
步驟5:用tcptrace監聽測試
其實,如果把第92行和第93行代碼的頭部格式換成json,就變成傳送json格式的數據了。這個時候,用tcptrace監聽傳送json的結果:
可以看到,請求中,我們的報文頭格式變成了json,報文體也是json格式的。而響應中報文體也是json格式的。
為什麼我直接截圖了json格式的測試結果而沒有測試請求xml的測試結果呢?
因為在xml測試時,報出了415 Unsupported media type的錯誤,尚未發現原因。
但是思路就是這樣了。 讀者如果知道“不支持此類型”的原因,歡迎評論留言。
處理模型數據
我們在Controller控制器中接收請求參數,把處理後得到的數據封裝起來,然後返回到一個JSP,在JSP中就可以獲取封裝的數據。那麼,在這個例子中,封裝的數據就是模型數據,而JSP就是渲染模型數據的視圖。這在MVC框架中是最重要的步驟。
在控制器方法中輸出模型數據有四種方法:
方法一:ModelAndView
在控制器方法中新建ModelAndView對象,把“視圖”和“模型數據”封裝到該對象中,然後返回該對象即可。
方法二:@ModelAttribute
加到入參的前面,就自動把入參綁定為模型數據;加到方法A的前面,那麼Spring MVC就會在執行任何目標處理方法之前,先執行方法A(B\C等等),然後把這些加了標注的方法的返回值綁定為模型數據。
舉例1:
Spring MVC會先把模型數據儲存到ServletRequest的屬性列表中(使用setAttribute保存)。
然後在返回的jsp(視圖對象)中,使用${user.userName}來訪問user的屬性。
舉例2:
在執行handle62之前,spring掃描到有方法頭部標注了@ModelAttribute,那麼,spring就會先去執行getUser,並把返回值user作為模型數據,鍵為”user”。
這時執行handle62,入參前面的@ModelAttribute鍵為”user”,正好匹配了剛封裝的模型數據的鍵,那麼就把模型數據賦值給入參User user,然後285行對user做了覆蓋操作,最後交給視圖層處理。
方法三:Map and Model
如果處理方法的入參類型為Map或者Model(或者具備二者功能的對象如:ModelMap),那麼spring會將隱含的模型數據傳給該入參,在處理方法中,我們可以從該入參中取出隱含的模型數據,也可以繼續添加模型數據。
標注了@ModelAttribute的方法的返回值(getUser方法返回的user)就是隱含的模型數據。
由於handle63的入參為ModelMap類型,所以,spring會把user這個隱含模型數據授予modelMap,在300行,我們通過該入參取出模型數據,在302行,我們通過該入參繼續添加模型數據。
方法四:@SessionAttributes
如果Controller類的頭部標注了@SessionAttributes(”xxx”),那麼spring會把鍵為xxx的模型數據暫存到HttpSession中,以便多個請求之間可以共享該數據。
... ...
61行的標注表示:後面處理器在處理任何方法時,看到屬性名為user的模型屬性就會把它存到session中共享。由於328行把user作為了模型數據,那麼這個user就放到了session中。
當轉發請求到handle72時,就可以從modelMap中拿到該對象。
當使用完成時,在338行表示把該對象從session中移除。否則它會一直存在。
然而程序是跑不起來的,會報HttpSessionRequiredException。為什麼呢?
這是因為,如果有了61行標注的會話屬性user,那麼spring就會嘗試從會話中獲取該屬性,然後把它賦給入參。
也就是說,spring看到61行的標注,會從會話中找user為鍵的模型數據,找到後把它賦值給334行的modelMap。
但是現在從代碼看來,在執行61行標注之前,會話中並沒有鍵為user的模型數據。
所以,解決方法很簡單:
只需要像方法二那樣,加一個:
就好了。這樣,在執行61行之前,先執行getUser,把user作為隱含模型數據。那麼執行61行時,該會話中就已經有user了。
當你讀到這裡,你已經百毒不侵了。親,不點個贊嗎?