若是你有幸能看到。css
談一些我的感覺html
系統面臨的挑戰:狀態管理、工做流、以及驗證都是須要解決的重要特性。HTTP協議的無狀態決定了這些問題都不是那麼容易解決。前端
Spring的Web框架就是爲了幫你解決這些關注點而設計的。Spring MVC基於模型-視圖-控制器(Model-View-Controller MVC)模式實現的,他可以幫你構建向Spring框架那樣靈活和鬆耦合的Web應用程序。java
在本章中,將會介紹Spring MVC Web框架,並使用新的Spring MVC註解來構建處理各類Web請求、參數、和表單輸入的控制器。git
Spring將請求在調度Servlet、處理器映射(Handler Mappering)、控制器以及視圖解析器(View resolver)之間移動,每個Spring MVC中的組件都有特定的目的,而且也沒那麼複雜。github
讓咱們看一下,請求是如何從客戶端發起,通過Spring MVC中的組件,最終返回到客戶端web
每當用戶在Web瀏覽器中點擊連接或提交表單的時候,請求就開始工做了。請求是一個十分繁忙的傢伙,從離開瀏覽器開始到獲取響應返回,它會經歷不少站,在每站都會留下一些信息,同時也會帶上一些信息。正則表達式
Spring工做流程描述原文在這裏spring
DispatcherServlet
對請求URL進行解析,獲得請求資源標識符(URI)。而後根據該URI,調用HandlerMapping得到該Handler配置的全部相關的對象(包括Handler對象以及Handler對象對應的攔截器),最後以HandlerExecutionChain
對象的形式返回;DispatcherServlet
根據得到的Handler,選擇一個合適的HandlerAdapter。(附註:若是成功得到HandlerAdapter後,此時將開始執行攔截器的preHandler(...)方法)圖片參考這裏sql
Spring工做流程描述
HandlerMapping
以及HandlerAdapter
來處理Handler?HandlerAdapter
可能會被用於處理多種Handler。一、請求旅程的第一站是Spring的DispatcherServlet
。與大多數基於Java的Web框架同樣,Spring MVC全部的請求都會經過一個前端控制器(front contrller)Servlet.前端控制器是經常使用Web應用程序模式。在這裏一個單實例的Servlet將請求委託給應用的其餘組件來執行實際的處理。在Spring MVC中,DisPatcherServlet就是前端控制器。
二、DisPactcher的任務是將請求發送Spring MVC控制器(controller).控制器是一個用於處理請求的Spring組件。在典型的應用中可能會有多個控制器,DispatcherServlet
須要知道應該將請求發送給那個哪一個控制器。因此Dispactcher以會查詢一個或 多個處理器映射(Handler mapping),來肯定請求的下一站在哪裏。處理映射器根據請求攜帶的 URL信息來進行決策。
三、一旦選擇了合適的控制器,DispatcherServlet
會將請求發送給選中的控制器。到了控制器,請求會卸下其負載(用戶提交的信息)並耐心等待控制器處理這些信息。(實際上,設計良好的控制器 自己只是處理不多,甚至不處理工做,而是將業務邏輯委託給一個或多個服務器對象進行處理)
四、控制器在完成處理邏輯後,一般會產生一些信息。這些 信息須要返回給 用戶,並在瀏覽器上顯示。這些信息被稱爲模型(Model),不過僅僅給用戶返回原始的信息是不夠的----這些信息須要以用戶友好的方式進行格式化,通常會是HTML。因此,信息須要發送一個視圖(View),一般會是JSP。
五、 控制器作的最後一件事就是將模型打包,而且表示出用於渲染輸出的視圖名。它接下來會將請求連同模型和視圖發送回DispatcherServlet。
六、這樣,控制器就不會與特定的視圖相耦合*傳遞給控制器的視圖名並不直接表示某個特定的jsp。實際上,它甚至並不能肯定視圖就是JSP。相反,它僅僅傳遞了一個邏輯名稱,這個名字將會用來查找產生結果的真正視圖。DispatcherServlet將會使用視圖解析器(View resolver),來將邏輯視圖名稱匹配爲一個特定的視圖實現,他可能也可能不是JSP
七、雖然DispatcherServlet
已經知道了哪一個駛入渲染結果、那請求的任務基本上也就完成了,它的最後一站是試圖的實現。在這裏它交付給模型數據。請求的任務就結束了。視圖將使用模型數據渲染輸出。這個輸出經過響應對象傳遞給客戶端(不會像聽上去那樣硬編碼)
能夠看到,請求要通過不少步驟,最終才能造成返回給客戶端的響應,大多數的 步驟都是在Spirng框架內部完成的。
藉助於最近幾個Spring新特性的功能加強,開始使用SpringMVC變得很是簡單了。使用最簡單的方式配置Spring MVC;所要實現的功能僅限於運行咱們所建立的控制器。
配置DisPatcherServlet
DispatcherServlet
是Spirng MVC的核心,在這裏請求會第一次接觸到框架,它要負責將請求路由到其餘組件之中。
按照傳統的方式,像DispatcherServlet這樣的Servlet會配置在web.xml中。這個文件會放到應用的war包中。固然這是配置DispatcherServlet
方法之一。藉助於Servlet 3規範和Spring 3.1 的功能加強,這種方式已經不是惟一的方案來。
咱們會使用Java將DispatcherServlet
配置在Servlet容器中。而不會在使用web.xml文件
public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected String[] getServletMappings() { //將DispatcherServlet映射到「/」 return new String[]{"/"}; } @Override protected Class<?>[] getRootConfigClasses() { return new Class<?> [] {RootConfig.class}; } @Override protected Class<?>[] getServletConfigClasses() { return new Class<?> [] { WebConfig.class}; } }
咱們只須要知道擴展AbstractAnnotationConfigDispatcherServletInitializer
的任意類都會自動的配置Dispatcherservlet和Spring應用上下文,Spirng的應用上下文會位於應用程序的Servlet上下文之中
在Servlet3.0環境中,容器會在類路徑中 查找實現javax.servlet.ServletContainerInitialzer
接口的類,若是能發現的話,就會用它來配置Servlet容器。
Spring提供了這個接口的實現名爲SpringServletContainnerInitialzer
,這個類反過來又會查找實現WebApplicationInitialzer
的類,並將配置的任務交給他們來完成。Spring 3.2引入了一個遍歷的WebApplicationInitialzer
基礎實現也就是AbstractAnnotationConfigDispatcherServletInitializer
由於咱們的Spittr-WebApplicationInitialzer
擴展了AbstractAnnotationConfigDispatcherServletInitializer
,(同時也就實現了WebApplicationInitialzer
),所以當部署Servlet3.0容器的時候,容器會自動發現它,並用它來配置Servlet上下文
第一個方法getServletMappings()
,它會將一個或多個路徑映射到DispatcherServlet
上,在本示例中,它映射的是「/」,表示它是應用默認的Servlet,它會處理應用的全部請求。
爲了理解其餘兩個方法,咱們首先須要理解DispatcherServlet
和一個Servlet監聽器(也就是ContextLoaderListener)的關係。
當DispatcherServlet
啓動的時候,它會建立應用上下文,並加載配置文件或配置類中聲明的bean。在上面那個程序中的getServletConfigClasses()
方法中,咱們要求DispatcherServlet加載應用上下文時,使用定義在WebConfig配置類(使用Java配置)中的bean
但在Spring Web應用中,一般還會有另一個應用上下文。另外這個就是由ContextLoaderListener
建立.
咱們但願DispatcherServlet
加載包含Web組件的bean,如控制器,視圖解析器,以及處理器映射,而ContextLoaderListener
要加載應用中的其餘bean。這些bean一般 是驅動應用後端的中間層和數據層組件。
實際上AbstractAnnotationConfigDispatcherServletInitializer
會同時建立DispatcherServlet
和ContextLoaderListener
。getServletConfigClasses()
方法會返回帶有@Configuration
註解的類將會用來定義DispatcherSerle應用上下文中的bean,getRootConfigClasses()
會返回帶有@Configuration
註解的類將會用來配置ContextLoaderListener
建立的應用上下文。
若是有必要兩個能夠同時存在,wex.xml和 AbstractAnnotationConfigDispatcherServletInitializer
,但其實沒有必要。
若是按照這種方式配置DispatcherServlet,而不是使用Web.xml的話,那麼惟一的問題在於它能部署到支持Servlet3.0的服務器上才能夠正常工做,如Tomcat7或更高版本,Servlet3.0規範在2009年12月份就發佈了,
若是沒有支持Servlet3.0,那別無選擇了,只能使用web.xml配置類。
啓用Spring MVC
咱們有多種方式來啓動DispatcherServlet,與之相似,啓用Spring MVC組件的方式也不止一種,之前Spring是XMl進行配置的,你能夠選擇<mvc:annotation-driver>啓用註解驅動的Spring MVC。
在第七章的時候會介紹<mvc:annotaion-driver>,如今會讓Spring MVC搭建的過程儘量簡單,並基於Java進行配置。
咱們所能建立最簡單的Spring MVC配置就是一個帶有@EnableWebMvc註解的類
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @Configuration @EnableWebMvc public class WebConfig { }
這能夠運行起來,它的確可以啓用Spring MVC,但還有很多問題要解決。
一、沒有配置視圖解析器,若是這樣的話,Spring默認會使用BeanNameView-Resolver,這個視圖解析器會查找ID與視圖名稱匹配的bean,而且查找的bean要實現View接口,它以這樣的方式來解析視圖。
二、沒有啓用組件掃描。這樣的結果就是,Spirng只能找到顯示聲明在配置類中的控制器。
三、這樣配置的話,DispatcherServlet會映射爲默認的Servlet,因此他會處理全部的請求,包括對靜態資源的請求,如圖片 和樣式表(在大多數狀況下,這可能並非你想要的結果)。
所以咱們須要在WebConfig這個最小的Spring MVC配置上再加一些內容,從而讓他變得真正實用。
@Configuration @EnableWebMvc //啓用Spring MVC @ComponentScan("com.guo.spittr.web") //啓用組件掃描 public class WebConfig extends WebMvcConfigurerAdapter { @Bean public ViewResolver viewResolver () { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); //配置JSP視圖解析器 resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".jsp"); resolver.setExposeContextBeansAsAttributes(true); return resolver; } @Override //咱們要求DispatcherServlet將靜態資源的請求轉發到Servlet容器中默認的Servlet上, //而不是使用DispatcherServlet原本來處理此類請求。 public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { //配置靜態資源的處理 configurer.enable(); } }
第一件須要注意的是WebConfig如今添加了@ComponentScan註解,此時將會掃描com.guo.spittr.web
包來查找組件。稍後你會看到,咱們編寫的控制器將會帶有@Controller註解,這會使其成爲組件掃描時的候選bean。所以,咱們不須要在配置類中顯示聲明任何的控制器。
接下來,咱們添加了一個ViewResolver bean,更具體的將是InternalResourceViewResolver
。將會在第6章更爲詳細的討論視圖解析器。咱們只須要知道他會去查找jsp文件,在查找的時候,它會在視圖名稱上加一個特定的前綴和後綴。(例如:名爲home的視圖會被解析爲/WEB-INF/views/home.jsp)
最後新的WebConfig類還擴展裏WebMvcConfigurerAdapter
並重寫了其configureDefaultServletHandling()
方法,經過調用DefaultServletHandlerConfigurer
的enable()方法,咱們要求DispatcherServlet將靜態資源的請求轉發到Servlet容器中默認的Servlet上,而不是使用DispatcherServlet原本來處理此類請求。
WebConfig已經就緒,那麼RootConfig呢?由於本章聚焦於Web開發,而Web相關的配置經過DisPatcherServlet建立的應用上下文都已經配好了,所以如今的RootConfig相對很簡單:
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.web.servlet.config.annotation.EnableWebMvc; /** * Created by guo on 23/2/2018. */ @Configuration @ComponentScan(basePackages = {"com.guo.spittr"}, excludeFilters = { @Filter(type = FilterType.ANNOTATION,value = EnableWebMvc.class)}) public class RootConfig { }
惟一須要注意的是RootConfig使用了@ComponentScan註解,這樣的話,咱們就有不少機會用非Web的組件來完善RootConfig。
爲了實如今線社交的功能,咱們將要構造一個簡單的微博(microblogging)應用,在不少方面,咱們所構建的應用於最先的微博應用Twitter很相似,在這個過程當中,咱們會添加一些小的變化。固然咱們使用Spirng技術來構建這個應用。
由於從Twitter借鑑了靈感並經過Spring來進行實現,因此它就有了一個名字:Spitter。
Spittr應用有兩個基本的領域概念:Spitter(應用的用戶)和Spittle(用戶發佈的簡短狀態更新)。當咱們在書中完善Spittr應用的功能時,將會介紹這兩個概念。在本章中,咱們會構建應用的Web層,建立展示Spittle的控制器以及處理用戶註冊爲Spitter的表單。
舞臺已經搭建完成了,咱們已經配置了DispatcherServlet,啓用了基本的Spring MVC組件,並肯定了目標應用。讓咱們進入本章的核心內容:使用Spring MVC 控制器處理Web請求。
在SpringMVC中,控制器只是在方法上添加了@RequestMapping註解的類,這個註解聲明瞭他們所要處理的請求。
開始的時候,咱們儘量簡單,假設控制器類要處理對/的請求,並對渲染應用的首頁。
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; /** * Created by guo on 24/2/2018. * 首頁控制器 */ @Controller public class HomeController { @RequestMapping(value = "/",method = RequestMethod.GET) //處理對「/」的Get請求 public String home() { return "home"; //視圖名爲home } }
寫完測試了下,好使,
你可能注意到第一件事就是HomeController帶有@Controller註解,很顯然這個註解是用來聲明控制器的,但實際上這個註解對Spirng MVC 自己影響不大。
@Controller是一個構造型(stereotype)的註解。它基於@Component註解。在這裏,它的目的就是輔助實現組件掃描。由於homeController帶有@Controller註解,所以組件掃描器會自動去找到HomeController,並將其聲明爲Spring應用上下文中的bean。
Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Controller { String value() default ""; }
其實你可讓HomeController帶有@Component註解,它所實現的效果是同樣的。可是在表意性上可能差一些,沒法肯定HomeController是什麼組件類型。
HomeController惟一的一個方法,也就是Home方法,帶有@RequestMapping註解,他的Value屬性指定了這個方法所要處理的請求路徑,method屬性細化了它所能處理的HTTP方法,在本例中,當收到對‘/’的HTTP GET請求時,就會調用home方法。
home()方法其實並無作太多的事情,它返回一個String類型的「home」,這個String將會被Spring MVC 解讀爲要渲染的視圖名稱。DispatcherServlet會要求視圖解析器將這個邏輯名稱解析爲實際的視圖。
鑑於咱們配置InternalResourceViewResolver
的方式,視圖名「home」將會被解析爲「/WEB-INF/views/home.jsp」
Spittr應用的首頁,定義爲一個簡單的JSP
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <%@ page session="false" %> <html> <head> <title>Spitter</title> <link rel="stylesheet" type="text/css" href="<c:url value="/resources/style.css" />" > </head> <body> <h1>Welcome to Spitter</h1> <a href="<c:url value="/spittles" />">Spittles</a> | <a href="<c:url value="/spitter/register" />">Register</a> </body> </html>
測試控制器最直接的辦法多是構建並部署應用,而後經過瀏覽器對其進行訪問,可是自動化測試可能會給你更快的反饋和更一致的獨立結果,因此,讓咱們編寫一個針對HomeController的測試
編寫一個簡單的類來測試HomoController。
import static org.junit.Assert.*; import org.junit.Test; public class HomeControllerTest { @Test public void testHomePage() throws Exception { HomeController controller = new HomeController(); assertEquals("home",controller.home()); } }
在測試中會直接調用home()方法,並斷言返回包含 "home"值的String類型。它徹底沒有站在Spring MVC控制器的視角進行測試。這個測試沒有斷言當接收到針對「/」的GET請求時會調用home()方法。由於它返回的值就是「home」,因此沒有真正判斷home是試圖的名稱。
不過從Spring 3.2開始,咱們能夠按照控制器的方式進行測試Spring MVC中的控制器了。而不只僅是POJO進行測試。Spring如今包含了一種mock Spirng MVC 並針對控制器執行 HTTP請求的機制。這樣的話,在測試控制器的時候,就沒有必要在啓動Web服務器和Web瀏覽器了。
爲了闡述如何測試Spirng MVC 容器,咱們重寫了HomeControllerTest並使用Spring MVC 中新的測試特性。
import org.junit.Test; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** * Created by guo on 24/2/2018. */ public class HomeControllerTest1 { @Test //你們在測試的時候注意靜態導入的方法 public void testHomePage() throws Exception { HomeController controller = new HomeController(); MockMvc mockMvc = standaloneSetup(controller).build(); //搭建MockMvc mockMvc.perform(get("/")) //對「/」執行GET請求, .andExpect(view().name("home")); //預期獲得home視圖 } }
此次咱們不是直接調用home方法並測試它的返回值,而是發起了對"/"的請求,並斷言結果視圖的名稱爲home,它首先傳遞一個HomeController實例到MockMvcBuilders.strandaloneSetup()並調用build()來構建MockMvc實例,而後它使用MockMvc實例執行鍼對「/」的GET請求,並設置 指望獲得的視圖名稱。
如今,已經爲HomeController編寫了測試,那麼咱們能夠作一些重構。並經過測試來保證不會對功能形成什麼破壞。咱們能夠作的就是拆分@RequestMapping,並將其路徑映射部分放到類級別上
@Controller @RequestMapping("/") public class HomeController { @RequestMapping(method = RequestMethod.GET) //處理對「/」的Get請求 public String home() { return "home"; //視圖名爲home } }
在這個新版本的HomeController中,路徑被轉移到類級別的@RequestMapping上,而HTTP方法依然映射在方法級別上。當控制器在類級別上添加@RequestMapping註解時,這個註解會應用到控制器的全部處理器方法上,處理器方法上的@RequestMapping註解會對類級別上的@RequestMapping的聲明進行補充。
就HomeController而言,這裏只有一個控制器方法,與類級別的@RequestMapping合併以後,這個方法的@RequestMapping代表home()將會處理對 「/」路徑的GET請求。
有了測試,因此能夠確保在這個過程當中,沒有對原有的功能形成破壞。
當咱們修改@RequestMapping時,還能夠對HomeController作另外一個變動。@RequestMapping的value接受一個String類型的數組。到目前爲止,咱們給它設置的都是一個String類型的‘/’。可是,咱們還能夠將它映射到對「/Homepage」的請求,只須要將類級別的@RequestMapping改動下
@Controller @RequestMapping({"/","/Homepage"}) public class HomeController { ... }
如今,HomeController的home()方法能夠被映射到對「/」和「/homepage」的GET請求上。
到目前爲止,就編寫超級簡單的控制器來講,HomeController已是一個不錯的樣例了,可是大多數的控制器並非那麼簡單。在Spring應用中,咱們須要有一個頁面展現最近提交的Spittle列表。所以,咱們須要有一個新的方法來處理這個頁面。
首先須要定義一個數據訪問的Repository,爲了實現解耦以及避免陷入數據庫訪問的細節中,咱們將Repository定義爲一個接口,並在稍後實現它(第十章),此時,咱們只須要一個可以獲取Spittle列表的Repository,
package com.guo.spittr.data; import com.guo.spittr.Spittle; import java.util.List; /** * Created by guo on 24/2/2018. */ public interface SpittleRepository { List<Spittle> finfSpittles(long max, int count); }
findSpittles()方法接受兩個參數,其中max參數表明所返回的Spittle中,Spittle ID屬性的最大值,而count參數代表要返回多少個Spittle對象,爲了得到最新的20個Spittle對象,咱們能夠這樣調用方法。
List<Spittle> recent = SpittleRepository.findSpittles(long.MAX_VALUE(),20)
它的屬性包括消息內容,時間戳,以及Spittle發佈時對應的經緯度。
public class Spittle { private final Long id; private final String message; private final Date time; private Double latitude; private Double longitude; public Spittle(String message, Date time) { this(null, message, time, null, null); } public Spittle(Long id, String message, Date time, Double longitude, Double latitude) { this.id = id; this.message = message; this.time = time; this.longitude = longitude; this.latitude = latitude; } //Getter和Setter略 @Override public boolean equals(Object that) { return EqualsBuilder.reflectionEquals(this, that, "id", "time"); } @Override public int hashCode() { return HashCodeBuilder.reflectionHashCode(this, "id", "time"); }
須要注意的是,咱們使用Apache Common Lang包來實現equals()和hashCode()方法,這些方法除了常規的做用之外,當咱們爲控制器的處理器方法編寫測試時,它們也是有用的。
既然咱們說到了測試,那麼咱們繼續討論這個話題,併爲新的控制器方法編寫測試,
@Test public void houldShowRecentSpittles() throws Exception { List<Spittle> expectedSpittles = createSpittleList(20); SpittleRepository mockRepository = mock(SpittleRepository.class); when(mockRepository.findSpittles(Long.MAX_VALUE, 20)) .thenReturn(expectedSpittles); SpittleController controller = new SpittleController(mockRepository); MockMvc mockMvc = standaloneSetup(controller) .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp")) .build(); mockMvc.perform(get("/spittles")) .andExpect(view().name("spittles")) .andExpect(model().attributeExists("spittleList")) .andExpect(model().attribute("spittleList", hasItems(expectedSpittles.toArray()))); } /.................佩服老外,測試代碼一大堆,省略了好多,好好研究下,..................../ private List<Spittle> createSpittleList(int count) { List<Spittle> spittles = new ArrayList<Spittle>(); for (int i=0; i < count; i++) { spittles.add(new Spittle("Spittle " + i, new Date())); } return spittles; } }
測試首先會建立SpittleRepository接口的mock實現,這個實現會從他的findSpittles()方法中返回20個Spittle對象,而後將這個Repository注入到一個新的SpittleController實例中,而後建立MockMvc並使用這個控制器。
須要注意的是這個測試在MockMvc構造器上調用了setSingleView().這樣的話,mock框架就不用解析控制器中的視圖名了。在不少場景中,其實不必這麼作,可是對於這個控制器方法,視圖和請求路徑很是類似,這樣按照默認的駛入解析規則,MockMvc就會發生失敗,由於沒法區分視圖路徑和控制器的路徑,在這個測試中,構建InternalResourceViewResolver時所設置的路徑是可有可無的,但咱們將其設置爲InternalResourceViewResolver
一致。
這個測試對「/spittles」發起Get請求,而後斷言視圖的名稱爲spittles而且模型中包含名爲spittleList的屬性,在spittleList中包含預期的內容。
固然若是此時運行測試的話,它將會失敗。他不是運行失敗,而是編譯的時候就失敗,這是由於咱們還沒編寫SpittleController。
@Controller @RequestMapping("/spittles") public class SpittleController { private SpittleRepository spittleRepository; @Autowired public SpittleController(SpittleRepository spittleRepository) { //注入SpittleRepository this.spittleRepository = spittleRepository; } @RequestMapping(method = RequestMethod.GET) public String spittles(Model model) { model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE,20)); // 將spittle添加到視圖 return "spittles"; // 返回視圖名 } }
咱們能夠看到SpittleController有一個構造器,這個構造器使用@Autowired註解,用來注入SpittleRepository。這個SpittleRepository隨後又在spittls()方法中,用來獲取最新的spittle列表。
須要注意的是咱們在spittles()方法中給定了一個Model做爲參數。這樣,spittles()方法就能夠將Repository中獲取到的Spittle列表填充到模型中,Model實際上就是一個Map(也就是key-value的集合)它會傳遞給視圖,這樣數據就能渲染到客戶端了。當調用addAttribute()方法而且指定key的時候,那麼key會根據值的對象類型來推斷肯定。
sittles()方法最後一件事是返回spittles做爲視圖的名字,這個視圖會渲染模型。
若是你但願顯示模型的key的話,也能夠指定,
@RequestMapping(method = RequestMethod.GET) public String spittles(Model model) { model.addAttribute("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE,20)); // 將spittle添加到視圖 return "spittles"; // 返回視圖名 }
若是你但願使用非Spring類型的話,那麼可使用java.util.Map來代替Model
@RequestMapping(method = RequestMethod.GET) public String spittles(Map model) { model.put("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE,20)); // 將spittle添加到視圖 return "spittles"; // 返回視圖名 }
既然咱們如今提到了各類可替代方案,那下面還有另一種方式來編寫spittles()方法
@RequestMapping(method = RequestMethod.GET) public List<String> spittles() { return spittleRepository.findSpittles(Long.MAX_VALUE,20)); }
這個並無返回值,也沒有顯示的設定模型,這個方法返回的是Spittle列表。。當處理器方法像這樣返回對象或集合時,這個值會放到模型中,模型的key會根據其類型推斷得出。在本示例中也就是(spittleList)
邏輯視圖的名稱也會根據請求的路徑推斷得出。由於這個方法處理針對「/spittles」的GET請求,所以視圖的名稱將會是spittles,(去掉開頭的線。)
無論使用哪一種方式來編寫spittles()方法,所達成的結果都是相同的。模型會存儲一個Spittle列表,ket爲spittleList,而後這個列表會發送到名爲spittles的視圖中。視圖的jsp會是「/WEB-INF/views/spittles.jsp」
如今數據已經放到了模型中,在JSP中該如何訪問它呢?實際上,當視圖是JSP的時候,模型數據會做爲請求屬性放入到請求之中(Request) ,所以在spittles.jsp文件中可使用JSTL(JavaServer Pages Standard Tag Library) 的<c:forEach>標籤渲染spittle列表。
<c:forEach items="${spittleList}" var="spittle" > <li id="spittle_<c:out value="spittle.id"/>"> <div class="spittleMessage"><c:out value="${spittle.message}" /></div> <div> <span class="spittleTime"><c:out value="${spittle.time}" /></span> <span class="spittleLocation">(<c:out value="${spittle.latitude}" />, <c:out value="${spittle.longitude}" />)</span> </div> </li> </c:forEach>
儘管SpittleController很簡單,可是它依然比homeController更進一步,不過,SpittleController和HomeController都沒有處理任何形式的輸入。如今,讓咱們擴展SpittleContorller,讓它從客戶端接受一些輸入。
Spring MVC 容許以多種方法將客戶端中的數據傳送到控制器的處理器方法中
做爲開始,先來看下如何處理帶有查詢參數的請求,這也是客戶端往服務器發送數據時,最簡單和最直接的方法。
在Spittr應用中,可能須要處理的一件事就是展示分頁的Spittle列表,若是你想讓用戶每次查看某一頁的Spittle歷史,那麼就須要提供一種方式讓用戶傳遞參數進來,進而肯定展示那些Spittle列表。
爲了實現這個分頁功能,咱們編寫的處理方法要接受兩個參數
爲了實現這個功能,咱們將程序修改成spittles()方法替換爲使用before參數和count參數的新spittles()方法。
首先添加一個測試,這個測試反映了xinspittles()方法的功能
@Test public void shouldShowPagedSpittles() throws Exception { List<Spittle> expectedSpittles = createSpittleList(50); SpittleRepository mockRepository = mock(SpittleRepository.class); when(mockRepository.findSpittles(238900, 50)) .thenReturn(expectedSpittles); SpittleController controller = new SpittleController(mockRepository); MockMvc mockMvc = standaloneSetup(controller) .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp")) .build(); mockMvc.perform(get("/spittles?max=238900&count=50")) .andExpect(view().name("spittles")) .andExpect(model().attributeExists("spittleList")) .andExpect(model().attribute("spittleList", hasItems(expectedSpittles.toArray()))); }
這個測試方法關鍵點在於同時傳入了max和count參數,它測試了這些參數存在時的處理方法,而另外一個則測試了沒有這些參數的情景。
在這個測試以後,咱們就能確保無論控制器發生了什麼樣的變化,它都可以處理這兩種類型的請求。
@RequestMapping(method = RequestMethod.GET) public List<Spittle> spittles( @RequestParam(value = "max") long max, @RequestParam(value = "count") int count) { return spittleRepository.findSpittles(max, count); }
SittleController中的處理器方法同時要處理有參數和沒參數的場景,那咱們須要對其進行修改,讓它能接受參數。同時若是這些參數在請求中不存在的話,就是用默認值Long.MAX_VALUE和20.@RequestParam註解的defaultValue屬性能夠完成這個任務。
@RequestMapping(method=RequestMethod.GET) public List<Spittle> spittles( @RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max, @RequestParam(value="count", defaultValue="20") int count) { return spittleRepository.findSpittles(max, count); }
如今若是max若是沒有參數指定的話,它將會是Long的最大值。
由於查詢參數都是String 類型 ,所以defaultValue屬性須要String類型,
private static final String MAX_LONG_AS_STRING = long.toString(Long.MAX.VALUE)
請求中的查詢參數是往控制器中傳遞信息的經常使用手段。另一種方式就是將傳遞的參數做爲請求路徑的一部分。
假設咱們的應用程序須要根據給定的ID來展示某一個Spittle記錄。其中一種方案就是編寫處理器方法,經過使用@RequestParam註解,讓它接受ID做爲查詢參數。
@RequestMapping(value="/show",method = RequestMethod.GET) public String showSpittle( @RequestParam("spittle_id") long spittleId, Model model) { model.addAttribute(spittleRepository.findOne(spittleId)); return "spittle"; }
在理想狀況下,要識別資源應用應該經過URL路徑來標識,而不是經過查詢參數。對「/spittles/12345」發起請求要優於對「/spittles/show?spittle_id=12345」發起的請求。前者能識別出要查詢的資源,然後者描述的是帶有參數的一個操做——本質上是經過HTTP發起的RPC。
既然已經以面向資源的控制器做爲目標,那咱們將這個需求轉化爲一個測試。
@Test public void testSpittle() throws Exception { Spittle expectedSpittle = new Spittle("Hello", new Date()); SpittleRepository mockRepository = mock(SpittleRepository.class); when(mockRepository.findOne(12345)).thenReturn(expectedSpittle); SpittleController controller = new SpittleController(mockRepository); MockMvc mockMvc = standaloneSetup(controller).build(); mockMvc.perform(get("/spittles/12345")) .andExpect(view().name("spittle")) //斷言圖片的名稱爲spittle .andExpect(model().attributeExists("spittle")) //預期Spittle放到了模型之中 .andExpect(model().attribute("spittle", expectedSpittle)); }
這個測試構建了一個mockRepository,一個控制器和MockMvc
到目前爲止,咱們所編寫的控制器,全部的方法都映射到了靜態定義好的路徑上,還須要包含變量部分
爲了實現這種路徑變量,Spring MVC容許咱們在@RequestMapping路徑中添加佔位符,佔位符的名稱須要({..}),路徑中的其餘部分要與所處理的請求徹底匹配,可是佔位符但是是任意的值。
@RequestMapping(value="/{spittleId}",method = RequestMethod.GET) public String showSpittle(@PathVariable("spittleId") long spittleId, Model model) { model.addAttribute(spittleRepository.findOne(spittleId)); return "spittle"; }
@PathVariable("spittleId") 代表在請求路徑中,無論佔位符部分的值是什麼都會傳遞給處理器方法的showSpittle參數中。
也能夠去掉這個value的值,由於方法的參數碰巧與佔位符的名稱相同。
@RequestMapping(value="/{spittleId}",method = RequestMethod.GET) public String showSpittle(@PathVariable long spittleId, Model model) { model.addAttribute(spittleRepository.findOne(spittleId)); return "spittle"; }
若是傳遞請求中少許的數據,那查詢參數和路徑變量是合適的,但一般咱們還須要傳遞不少的數據,(表單數據),那麼查詢顯得有些笨拙和受限制了。
Web應用的功能不侷限於爲用戶推送內容,大多數的應用容許用戶填充表單,並將數據提交回應用中,經過這種方式實現與用戶的交互。
使用表單分爲兩個方面:展示表單以及處理用戶經過表單提交的數據。在Spittr應用中,咱們須要有個表單讓用戶進行註冊,SitterController是一個新的控制器,目前只有一個請求處理的方法來展示註冊表單。
@Controller @RequestMapping("/spitter") public class SpitterController { //處理對「/spitter/register」 @RequestMapping(value = "/register",method = RequestMethod.GET) public String showRegistrationForm() { return "registerForm"; } }
測試展示表單的控制器方法(老外每次都測試)
@Test public void shouldShowRegistration() throws Exception { SpitterController controller = new SpitterController(); MockMvc mockMvc = standaloneSetup(controller).build(); mockMvc.perform(get("/spitter/register")) .andExpect(view().name("registerForm")); } }
這個JSP必須包含一個HTML<form>標籤,
<form method="POST" name="spittleForm"> <input type="hidden" name="latitude"> <input type="hidden" name="longitude"> <textarea name="message" cols="80" rows="5"></textarea><br/> <input type="submit" value="Add" /> </form>
須要注意的是這裏的<form>標籤中並無設置action屬性。在這種狀況下,當表單體提交的時,它會提交到與展示時相同的URL路徑上,它會提交到「/spitter/reqister」上。
這意味着須要在服務器端編寫該HTTP POST請求。
當處理註冊表單的POST請求時,控制器須要接受表單數據,並將表單數據保存爲Spitter對象。最後爲了防止重複提交(用戶刷新頁面),應該將瀏覽器重定向到新建立用戶的基本信息頁面。
@Test public void shouldProcessRegistration() throws Exception { SpitterRepository mockRepository = mock(SpitterRepository.class); Spitter unsaved = new Spitter("jbauer", "24hours", "Jack", "Bauer", "jbauer@ctu.gov"); Spitter saved = new Spitter(24L, "jbauer", "24hours", "Jack", "Bauer", "jbauer@ctu.gov"); when(mockRepository.save(unsaved)).thenReturn(saved); SpitterController controller = new SpitterController(mockRepository); MockMvc mockMvc = standaloneSetup(controller).build(); mockMvc.perform(post("/spitter/register") .param("firstName", "Jack") .param("lastName", "Bauer") .param("username", "jbauer") .param("password", "24hours") .param("email", "jbauer@ctu.gov")) .andExpect(redirectedUrl("/spitter/jbauer")); verify(mockRepository, atLeastOnce()).save(unsaved); }
但願你們也能夠學會這樣方式
在構建完SpitterRepository的mock實現以及所要執行的控制器和MockNvc以後,shouldProcessRegistration()對「/spitter/register」發起了一個POST請求,做爲請求的一部分,用戶信息以參數的形式放到request中,從而模擬提交的表單。
/** * Created by guo on 24/2/2018. */ @Controller @RequestMapping("/spitter") public class SpitterController { private SpitterRepository spitterRepository; @Autowired public SpitterController(SpitterRepository spitterRepository) { //注入SpiterRepository this.spitterRepository = spitterRepository; } @RequestMapping(value = "/register", method = RequestMethod.GET) public String showRegistrationForm() { return "registerForm"; } @RequestMapping(value = "/register",method = RequestMethod.POST) public String procesRegistration(Spitter spitter) { spitterRepository.save(spitter); //保存Spitter return "redirect:/spitter/" + spitter.getUsername(); //重定向到基本信息頁面 } }
返回一個String類型,用來指定視圖。可是這個視圖格式和之前有所不一樣。這裏不只返回了視圖的名稱供視圖解析器查找目標視圖,並且返回的值還帶有重定向的格式return "redirect:/spitter/"
當看到視圖格式中有「redirect:」前綴時,它就知道要將其解析爲重定向的規則,而不是試圖的名稱。在本例中,它將會重定向到基本信息的頁面。
須要注意的是除了能夠「redirect」還能夠識別「forward:」前綴,請求將會前(forward)往指定的URL路徑,而再也不是重定向。
在SpitterController中添加一個處理器方法,用來處理對基本信息頁面的請求。
@RequestMapping(value = "/{username}",method = RequestMethod.GET) public String showSpitterProfile(@PathVariable String username, Model model) { Spitter spitter = spitterRepository.findByUsername(username); model.addAttribute(spitter); return "profile"; }
spitterRepository經過用戶獲取一個Spitter對象,showSpitterProfile()方法獲得這個對象並將其添加到模型中,而後返回profile。也就是基本信息頁面的邏輯視圖。
<body> <h1>Your Profile</h1> <c:out value="${spitter.username}" /><br/> <c:out value="${spitter.firstName}" /> <c:out value="${spitter.lastName}" /><br/> <c:out value="${spitter.email}" /> </body>
注意:這裏使用H2數據庫,太有用了。
@Configuration public class DataConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("schema.sql") .build(); } @Bean public JdbcOperations jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } }
若是表單中沒有發送username或password,會發生什麼狀況呢?或者名字太長,由會怎麼樣?,接下來,讓咱們看一下爲表單添加校驗,而從避免數據呈現不一致性。
若是用戶在提交表單的時候,username和password爲空的話,那麼將會致使在新建Spitter對象中,username和password是空的String。若是不處理,將會出項安全問題。
同時咱們應該阻止用戶提交空的名字。限制這些輸入的長度。
從Spring 3.0 開始,在Spring MVC中提供了java校驗的API的支持。只須要在類路徑下包含這個JavaAPI的實現便可。好比Hibernate validator.
Java校驗API定義了多個註解,這些註解能夠用在屬性上,從而限制這些屬性的值。
public class Spitter { private Long id; @NotNull @Size(min=5, max=16) private String username; @NotNull @Size(min=5, max=25) private String password; @NotNull @Size(min=2, max=30) private String firstName; @NotNull @Size(min=2, max=30) private String lastName; @NotNull @Email private String email; 忽略其餘方法。 }
@RequestMapping(value="/register", method=POST) //老外喜歡靜態導入特性 public String processRegistration( @Valid Spitter spitter, //校驗Spitter輸入 Errors errors) { if (errors.hasErrors()) { return "registerForm"; //若是校驗出現錯誤,則從新返回表單 } spitterRepository.save(spitter); return "redirect:/spitter/" + spitter.getUsername(); }
Spitter參數添加了@Valid註解,這會告訴Spring,須要確保這個對象知足校驗限制。
若是表單出錯的話,那麼這些錯誤能夠經過Errors進行反問。
很重要一點須要注意的是:Errors參數要緊跟在帶有Valid註解參數的後面。@Valid註解所標註的就是要校驗的參數。
若是沒有錯誤的話,Spitter對象將會經過Repository進行保存,控制器會像以前那樣重定向到基本信息頁面。
在本章中,咱們爲編寫應用程序的Web部分開來一個好頭,能夠看到Spring有一個強大而靈活的Web框架。藉助於註解,Spring MVC 提供了近似於POJO的開發模式,這使得開發處理請求的控制器變得簡單,同時也易於測試。
當編寫控制器的處理方法時,Spring MVC及其靈活。歸納來說,若是你的處理器方法須要內容的話,只需將對應的對象做爲參數,而他不須要的內容,則沒有必要出如今參數列表中。這樣,就爲請求帶來了無限的可能性,同時還能保持一種簡單的編程模型。
儘管本章中不少內容都是關於控制器的請求處理的,可是渲染響應也一樣重要,咱們經過使用JSP的方式,簡單瞭解瞭如何爲控制器編寫視圖,可是,就Spring MVC視圖來講,它並非本章所看到的簡單JSP。
在接下來的第6章,咱們將會更深刻的學習Spring視圖,包括如何在JSP中使用Spring標籤庫,還會學習如何藉助於Apache Tiles爲視圖添加一致的結構。同時,還會了解Thymeleaf,這是一個頗有意思的JSP替代方法,Spring爲其提供了內置的支持。
真的很是期待下一章,,,,,加油