Spring 學(xué)習(xí)筆記(八)構(gòu)建 Spring Web 應(yīng)用程序 (下)

接受請求的輸入

有些Web應(yīng)用是只讀的。人們只能通過瀏覽器在站點上閑逛,閱讀服務(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>
image.png

處理表單

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>
image.png

編寫處理表單的控制器

當(dāng)處理注冊表單的POST請求時,控制器需要接受表單數(shù)據(jù)并將表單數(shù)據(jù)保存為Spitter對象。最后,為了防止重復(fù)提交(用戶點擊瀏覽器的刷新按鈕有可能會發(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)的支持。


image.png

請考慮要添加到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ù)。(很重要一點需要注意,Errors參數(shù)要緊跟在帶有@Valid注解的參數(shù)后面,@Valid注解所標(biāo)注的就是要檢驗的參數(shù)。)processRegistration()方法所做的第一件事就是調(diào)用Errors.hasErrors()來檢查是否有錯誤。

如果有錯誤的話,Errors.hasErrors()將會返回到registerForm,也就是注冊表單的視圖。這能夠讓用戶的瀏覽器重新回到注冊表單頁面,所以他們能夠修正錯誤,然后重新嘗試提交。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容