接受請求的輸入
有些Web應(yīng)用是只讀的碉就。人們只能通過瀏覽器在站點(diǎn)上閑逛盟广,閱讀服務(wù)器發(fā)送到瀏覽器中的內(nèi)容闷串。
不過瓮钥,這并不是一成不變的。眾多的Web應(yīng)用允許用戶參與進(jìn)去烹吵,將數(shù)據(jù)發(fā)送回服務(wù)器碉熄。如果沒有這項能力的話,那Web將完全是另一番景象肋拔。
Spring MVC允許以多種方式將客戶端中的數(shù)據(jù)傳送到控制器的處理器方法中锈津,包括:
-查詢參數(shù)(Query Parameter)。
-表單參數(shù)(Form Parameter)凉蜂。
-路徑變量(Path Variable)琼梆。
你將會看到如何編寫控制器處理這些不同機(jī)制的輸入。作為開始窿吩,我們先看一下如何處理帶有查詢參數(shù)的請求茎杂,這也是客戶端往服務(wù)器端發(fā)送數(shù)據(jù)時,最簡單和最直接的方式纫雁。
處理查詢參數(shù)
在Spittr應(yīng)用中煌往,我們可能需要處理的一件事就是展現(xiàn)分頁的Spittle列表。在現(xiàn)在的SpittleController中轧邪,它只能展現(xiàn)最新的Spittle刽脖,并沒有辦法向前翻頁查看以前編寫的Spittle歷史記錄羞海。如果你想讓用戶每次都能查看某一頁的Spittle歷史,那么就需要提供一種方式讓用戶傳遞參數(shù)進(jìn)來曲管,進(jìn)而確定要展現(xiàn)哪些Spittle集合却邓。
在確定該如何實現(xiàn)時,假設(shè)我們要查看某一頁Spittle列表院水,這個列表會按照最新的Spittle在前的方式進(jìn)行排序申尤。因此,下一頁中第一條的ID肯定會早于當(dāng)前頁最后一條的ID衙耕。所以昧穿,為了顯示下一頁的Spittle,我們需要將一個Spittle的ID傳入進(jìn)來橙喘,這個ID要恰好小于當(dāng)前頁最后一條Spittle的ID时鸵。另外,你還可以傳入一個參數(shù)來確定要展現(xiàn)的Spittle數(shù)量厅瞎。
為了實現(xiàn)這個分頁的功能饰潜,我們所編寫的處理器方法要接受如下的參數(shù):
-before參數(shù)(表明結(jié)果中所有Spittle的ID均應(yīng)該在這個值之前)。
-count參數(shù)(表明在結(jié)果中要包含的Spittle數(shù)量)和簸。
為了實現(xiàn)這個功能彭雾,我們將程序清單5.10中的spittles()方法替換為使用before和count參數(shù)的新spittles()方法。我們首先添加一個測試锁保,這個測試反映了新spittles()方法的功能薯酝。
@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())));
@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(@RequestParam("max") long max,@RequestParam("count") int count){
return spittleRepository.findSpittles(max,count);
}
這個測試方法與程序清單5.9中的測試方法關(guān)鍵區(qū)別在于它針對“/spittles”發(fā)送GET請求,同時還傳入了max和count參數(shù)爽柒。它測試了這些參數(shù)存在時的處理器方法吴菠,而另一個測試方法則測試了沒有這些參數(shù)時的情景。這兩個測試就緒后浩村,我們就能確保不管控制器發(fā)生什么樣的變化做葵,它都能夠處理這兩種類型的請求。
SpittleController中的處理器方法要同時處理有參數(shù)和沒有參數(shù)的場景心墅,那我們需要對其進(jìn)行修改酿矢,讓它能接受參數(shù),同時怎燥,如果這些參數(shù)在請求中不存在的話瘫筐,就使用默認(rèn)值Long.MAX_VALUE和20。@RequestParam注解的defaultValue屬性可以完成這項任務(wù):
@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(@RequestParam(value="max",
defaultValue=MAX_LONG_AS_STRING) long max,
@ReuquestParam(value="count",defaultValue="20") int count){
return spittleRepository.findSpittles(max,count);
}
通過路徑參數(shù)接受輸入
假設(shè)我們的應(yīng)用程序需要根據(jù)給定的ID來展現(xiàn)某一個Spittle記錄刺覆。其中一種方案就是編寫處理器方法严肪,通過使用@RequestParam注解,讓它接受ID作為查詢參數(shù):
@ReuqestMapping(value="/show", method=RequestMethod.GET)
public String showSpittle(@RequestParam("spittle_id") long spittleId, Model model) {
model.addAttribute(spittleRepository.findOne(spittleId));
return "spittle";
}
這個處理器方法將會處理形如“/spittles/show?spittle_id=12345”這樣的請求。盡管這也可以正常工作驳糯,但是從面向資源的角度來看這并不理想篇梭。在理想情況下,要識別的資源(Spittle)應(yīng)該通過URL路徑進(jìn)行標(biāo)示酝枢,而不是通過查詢參數(shù)恬偷。對“/spittles/12345”發(fā)起GET請求要優(yōu)于對“/spittles/show?spittle_id=12345”發(fā)起請求。*前者能夠識別出要查詢的資源帘睦,而后者描述的是帶有參數(shù)的一個操作——本質(zhì)上是通過HTTP發(fā)起的RPC袍患。
既然已經(jīng)以面向資源的控制器作為目標(biāo),那我們將這個需求轉(zhuǎn)換為一個測試竣付。程序清單5.12展現(xiàn)了一個新的測試方法诡延,它會斷言SpittleController中對面向資源 請求的處理。
@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("/spittle/12345"))
.andExpect(view().name("Spittles"))
.andExpect(model().attributeExists("spittle"))
.andExpect(model().attribute("spittle",expectedSpittle));
}
如果想讓這個測試通過的話古胆,我們編寫的@RequestMapping要包含變量部分肆良,這部分代表了Spittle ID。
為了實現(xiàn)這種路徑變量逸绎,Spring MVC允許我們在@RequestMapping路徑中添加占位符惹恃。占位符的名稱要用大括號(“{”和“}”)括起來。路徑中的其他部分要與所處理的請求完全匹配棺牧,但是占位符部分可以是任意的值巫糙。
@RequestMapping(value="/{spittleId}",method=RequestMethod.GET)
public String spittle(@PathVarible("spittleId") long spittleId,Model model){
model.addAttribute(spittleRepository.findOne(spittleIdd));
return "spittle";
}
我們可以看到,spittle()方法的spittleId參數(shù)上添加了@PathVariable("spittleId")注解颊乘,這表明在請求路徑中参淹,不管占位符部分的值是什么都會傳遞到處理器方法的spittleId參數(shù)中带猴。如果對“/spittles/54321”發(fā)送GET請求胸嘁,那么將會把“54321”傳遞進(jìn)來,作為spittleId的值。
如果@PathVariable中沒有value屬性的話纲爸,它會假設(shè)占位符的名稱與方法的參數(shù)名相同。這能夠讓代碼稍微簡潔一些妆够,因為不必重復(fù)寫占位符的名稱了识啦。但需要注意的是,如果你想要重命名參數(shù)時神妹,必須要同時修改占位符的名稱颓哮,使其互相匹配。
spittle()方法會將參數(shù)傳遞到SpittleRepository的findOne()方法中鸵荠,用來獲取某個Spittle對象冕茅,然后將Spittle對象添加到模型中。模型的key將會是spittle,這是根據(jù)傳遞到addAttribute()方法中的類型推斷得到的姨伤。
這樣Spittle對象中的數(shù)據(jù)就可以渲染到視圖中了哨坪,此時需要引用請求中key為spittle的屬性(與模型的key一致)。如下為渲染Spittle的JSP視圖片段:
<div class="spittleView">
<div class="spittleMessage"><c:out value="$(spittle.message)"/></div>
<div>
<span class="spittleTime"><c:out value="${spittle.time}"/></span>
</div>
</div>
處理表單
Web應(yīng)用的功能通常并不局限于為用戶推送內(nèi)容乍楚。大多數(shù)的應(yīng)用允許用戶填充表單并將數(shù)據(jù)提交回應(yīng)用中当编,通過這種方式實現(xiàn)與用戶的交互。像提供內(nèi)容一樣徒溪,Spring MVC的控制器也為表單處理提供了良好的支持忿偷。
使用表單分為兩個方面:展現(xiàn)表單以及處理用戶通過表單提交的數(shù)據(jù)。在Spittr應(yīng)用中臊泌,我們需要有個表單讓新用戶進(jìn)行注冊鲤桥。SpitterController是一個新的控制器,目前只有一個請求處理的方法來展現(xiàn)注冊表單渠概。
package spittr.web;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import org.springframework.stereotype.Controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/spitter")
public class SpitterControllerRegist {
@RequestMapping(value="register",method=GET)
public String showRegistrationForm() {
return "registerForm";
}
}
showRegistrationForm()方法的@RequestMapping注解以及類級別上的@RequestMapping注解組合起來芜壁,聲明了這個方法要處理的是針對“/spitter/register”的GET請求。這是一個簡單的方法高氮,沒有任何輸入并且只是返回名為registerForm的邏輯視圖慧妄。按照我們配置InternalResourceViewResolver的方式,這意味著將會使用“/WEB-INF/ views/registerForm.jsp”這個JSP來渲染注冊表單剪芍。
盡管showRegistrationForm()方法非常簡單塞淹,但測試依然需要覆蓋到它。因為這個方法很簡單罪裹,所以它的測試也比較簡單饱普。
@Test
public void shouleShowRegistration() throws Exception{
SpitterController controller = new SpitterController();
MockMvc mockMvc = standaloneSetup(controller).build();
mockMvc.perform(get("/spitter/register")).andExcept(view().name("registerForm"));
}
渲染注冊表單的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>Register</h1>
<form method="POST">
First Name:<input type="text" name="firstName"/><br/>
Last Name:<input type="text" name="lastName"/><br/>
Username:<input type="text" name="username"/><br/>
Password:<input type="password" name="password"/><br/>
<input type="submit" value="Register"/>
</form>
</body>
</html>
編寫處理表單的控制器
當(dāng)處理注冊表單的POST請求時,控制器需要接受表單數(shù)據(jù)并將表單數(shù)據(jù)保存為Spitter對象状共。最后套耕,為了防止重復(fù)提交(用戶點(diǎn)擊瀏覽器的刷新按鈕有可能會發(fā)生這種情況),應(yīng)該將瀏覽器重定向到新創(chuàng)建用戶的基本信息頁面峡继。這些行為通過下面的shouldProcessRegistration()進(jìn)行了測試冯袍。
@Test
public void shouldProcessRegistration() throws Exception{
SpitterRepository mockRepository = mock(SpitterRepository.class);
Spitter unsaved = new Spitter("jbauer","24hours","Jack","Bauer");
Spitter saved = new Spitter(24L,"jbauer","24hours","Jack","Bauer");
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"))
.andExpect(redirectedUrl("/spitter/jbauer"));
verify(mockRepository,atLeastOnce()).save(unsaved);
}
顯然,這個測試比展現(xiàn)注冊表單的測試復(fù)雜得多碾牌。在構(gòu)建完SpitterRepository的mock實現(xiàn)以及所要執(zhí)行的控制器和MockMvc之后康愤,shouldProcess-Registration()對“/spitter/register”發(fā)起了一個POST請求。作為請求的一部分舶吗,用戶信息以參數(shù)的形式放到request中征冷,從而模擬提交的表單。
在處理POST類型的請求時誓琼,在請求處理完成后检激,最好進(jìn)行一下重定向肴捉,這樣瀏覽器的刷新就不會重復(fù)提交表單了。在這個測試中叔收,預(yù)期請求會重定向到“/spitter/jbauer”每庆,也就是新建用戶的基本信息頁面。最后今穿,測試會校驗SpitterRepository的mock實現(xiàn)最終會真正用來保存表單上傳入的數(shù)據(jù)缤灵。
現(xiàn)在,我們來實現(xiàn)處理表單提交的控制器方法蓝晒。通過shouldProcess-Registration()方法腮出,我們可能認(rèn)為要滿足這個需求需要做很多的工作。但是芝薇,在如下的程序清單中胚嘲,我們可以看到新的SpitterController并沒有做太多的事情。
package spittr.web;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import spittr.Spitter;
import spittr.data.SpitterRepository;
@Controller
@RequestMapping("/spitter")
public class SpitterController {
private SpitterRepository spitterRepository;
@Autowired
public SpitterContriller(SpitterRepository spitterRepository) {
this.spitterRepository = spitterRepository;
}
@RequestMapping(value="/register",method=GET)
public String showRegistrationForm() {
return "registerForm";
}
@RequestMapping(value="/register",method=POST)
public String processRegistration(Spitter spitter) {
spitterRepository.save(spitter);
return "redirect:/spitter/" + spitter.getUsername();
}
}
我們之前創(chuàng)建的showRegistrationForm()方法依然還在洛二,不過請注意新創(chuàng)建的processRegistration()方法馋劈,它接受一個Spitter對象作為參數(shù)。這個對象有firstName晾嘶、lastName妓雾、username和password屬性,這些屬性將會使用請求中同名的參數(shù)進(jìn)行填充垒迂。
當(dāng)使用Spitter對象調(diào)用processRegistration()方法時械姻,它會進(jìn)而調(diào)用SpitterRepository的save()方法,SpitterRepository是在Spitter-Controller的構(gòu)造器中注入進(jìn)來的机断。
processRegistration()方法做的最后一件事就是返回一個String類型楷拳,用來指定視圖。但是這個視圖格式和以前我們所看到的視圖有所不同吏奸。這里不僅返回了視圖的名稱供視圖解析器查找目標(biāo)視圖欢揖,而且返回的值還帶有重定向的格式。
當(dāng)InternalResourceViewResolver看到視圖格式中的“redirect:”前綴時奋蔚,它就知道要將其解析為重定向的規(guī)則她混。
校驗表單
如果用戶在提交表單的時候,username或password文本域為空的話旺拉,那么將會導(dǎo)致在新建Spitter對象中产上,username或password是空的String。至少這是一種怪異的行為蛾狗。如果這種現(xiàn)象不處理的話,這將會出現(xiàn)安全問題仪媒,因為不管是誰只要提交一個空的表單就能登錄應(yīng)用沉桌。
同時谢鹊,我們還應(yīng)該阻止用戶提交空的firstName和/或lastName,使應(yīng)用僅在一定程度上保持匿名性留凭。有個好的辦法就是限制這些輸入域值的長度佃扼,保持它們的值在一個合理的長度范圍,避免這些輸入域的誤用蔼夜。
有種處理校驗的方式非常初級兼耀,那就是在processRegistration()方法中添加代碼來檢查值的合法性,如果值不合法的話求冷,就將注冊表單重新顯示給用戶瘤运。這是一個很簡短的方法,因此匠题,添加一些額外的if語句也不是什么大問題拯坟,對吧?
與其讓校驗邏輯弄亂我們的處理器方法韭山,還不如使用Spring對Java校
驗API(Java Validation API郁季,又稱JSR-303)的支持。
請考慮要添加到Spitter域上的限制條件钱磅,似乎需要使用@NotNull和@Size注解梦裂。我們所要做的事情就是將這些注解添加到Spitter的屬性上。如下的程序清單展現(xiàn)了Spitter類盖淡,它的屬性已經(jīng)添加了校驗注解塞琼。
package spittr.web;
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;
}
processRegistration():確保所提交的數(shù)據(jù)是合法的
@RequestMapping(value="/register",method=POST)
public String processRegisteration(@Valid Spitter spitter,Errors errors){
if(errors.hasErrors()){
return "registerForm";
}
spitterRepository.save(spitter);
return "redirect:/spitter/" + spitter.getUsername();
}
Spitter參數(shù)添加了@Valid注解,這會告知Spring禁舷,需要確保這個對象滿足校驗限制彪杉。
如果有校驗出現(xiàn)錯誤的話,那么這些錯誤可以通過Errors對象進(jìn)行訪問牵咙,現(xiàn)在這個對象已作為processRegistration()方法的參數(shù)派近。(很重要一點(diǎn)需要注意,Errors參數(shù)要緊跟在帶有@Valid注解的參數(shù)后面洁桌,@Valid注解所標(biāo)注的就是要檢驗的參數(shù)渴丸。)processRegistration()方法所做的第一件事就是調(diào)用Errors.hasErrors()來檢查是否有錯誤。
如果有錯誤的話另凌,Errors.hasErrors()將會返回到registerForm谱轨,也就是注冊表單的視圖。這能夠讓用戶的瀏覽器重新回到注冊表單頁面吠谢,所以他們能夠修正錯誤土童,然后重新嘗試提交。