Spring實(shí)戰(zhàn)5-基于Spring構(gòu)建Web應(yīng)用

主要內(nèi)容

  • 將web請(qǐng)求映射到Spring控制器
  • 綁定form參數(shù)
  • 驗(yàn)證表單提交的參數(shù)

寫(xiě)在前面:關(guān)于Java Web,首先推薦一篇文章——寫(xiě)給java web一年左右工作經(jīng)驗(yàn)的人,這篇文章的作者用精練的話語(yǔ)勾勒除了各種Java框架的緣由和最基本的原理生棍。我們?cè)趯W(xué)習(xí)Spring的過(guò)程中也要切記溜腐,不僅要知道怎么做?還要深究背后的思考和權(quán)衡。

對(duì)于很多Java程序員來(lái)說(shuō)流译,他們的主要工作就是開(kāi)發(fā)Web應(yīng)用封字,如果你也在做這樣的工作黔州,那么你一定會(huì)了解到構(gòu)建這類(lèi)系統(tǒng)所面臨的挑戰(zhàn),例如狀態(tài)管理阔籽、工作流和參數(shù)驗(yàn)證等流妻。HTTP協(xié)議的無(wú)狀態(tài)性使得這些任務(wù)極具挑戰(zhàn)性。

Spring的web框架用于解決上述提到的問(wèn)題笆制,基于Model-View-Controller(MVC)模型绅这,Spring MVC可以幫助開(kāi)發(fā)人員構(gòu)建靈活易擴(kuò)展的Web
應(yīng)用。

這一章將涉及Spring MVC框架的主要知識(shí)在辆,由于基于注解開(kāi)發(fā)是目前Spring社區(qū)的潮流证薇,因此我們將側(cè)重介紹如何使用注解創(chuàng)建控制器,進(jìn)而處理各類(lèi)web請(qǐng)求和表單提交开缎。在深入介紹各個(gè)專題之前棕叫,首先從一個(gè)比較高的層面觀察和理解下Spring MVC的工作原理。

5.1 Spring MVC入門(mén)

5.1.1 request的處理過(guò)程

用戶每次點(diǎn)擊瀏覽器界面的一個(gè)按鈕奕删,都發(fā)出一個(gè)web請(qǐng)求(request)俺泣。一個(gè)web請(qǐng)求的工作就像一個(gè)快遞員,負(fù)責(zé)將信息從一個(gè)地方運(yùn)送到另一個(gè)地方完残。

從web請(qǐng)求離開(kāi)瀏覽器(1)到返回響應(yīng)伏钠,中間經(jīng)歷了幾個(gè)節(jié)點(diǎn),在每個(gè)節(jié)點(diǎn)都進(jìn)行一些操作用于交換信息谨设。下圖展示了Spring MVC應(yīng)用中web請(qǐng)求會(huì)遇到的幾個(gè)節(jié)點(diǎn)熟掂。

web請(qǐng)求經(jīng)過(guò)幾個(gè)節(jié)點(diǎn)處理然后產(chǎn)生響應(yīng)信息

請(qǐng)求旅行的第一站是Spring的DispatcherServlet,和大多數(shù)Javaweb應(yīng)用相同扎拣,Spring MVC通過(guò)一個(gè)單獨(dú)的前端控制器過(guò)濾分發(fā)請(qǐng)求赴肚。當(dāng)Web應(yīng)用委托一個(gè)servlet將請(qǐng)求分發(fā)給應(yīng)用的其他組件時(shí)素跺,這個(gè)servlert稱為前端控制器(front controller)。在Spring MVC中誉券,DispatcherServlet就是前端控制器指厌。

DispatcherServlet的任務(wù)是將請(qǐng)求發(fā)送給某個(gè)Spring控制器。控制器(controller)是Spring應(yīng)用中處理請(qǐng)求的組件踊跟。一般在一個(gè)應(yīng)用中會(huì)有多個(gè)控制器踩验,DispatcherServlet來(lái)決定把請(qǐng)求發(fā)給哪個(gè)控制器處理。DispatcherServlet會(huì)維護(hù)一個(gè)或者多個(gè)處理器映射(2)商玫,用于指出request的下一站——根據(jù)請(qǐng)求攜帶的URL做決定箕憾。

一旦選好了控制器,DispatcherServlet會(huì)把請(qǐng)求發(fā)送給指定的控制器(3)拳昌,控制器中的處理方法負(fù)責(zé)從請(qǐng)求中取得用戶提交的信息袭异,然后委托給對(duì)應(yīng)的業(yè)務(wù)邏輯組件(service objects)處理。

控制器的處理結(jié)果包含一些需要傳回給用戶或者顯示在瀏覽器中的信息地回。這些信息存放在模型(model)中扁远,但是直接把原始信息返回給用戶非常低效——最好格式化成用戶友好的格式,例如HTML或者JSON格式刻像。為了生成HTML格式的文件畅买,需要把這些信息傳給指定的視圖(view),一般而言是JSP细睡。

控制器的最后一個(gè)任務(wù)就是將數(shù)據(jù)打包在模型中谷羞,然后指定一個(gè)視圖的邏輯名稱(由該視圖名稱解析HTML格式的輸出),然后將請(qǐng)求和模型溜徙、視圖名稱一起發(fā)送回DispatcherServlet4)湃缎。

注意,控制器并不負(fù)責(zé)指定具體的視圖蠢壹,返回給DispatcherServlet的視圖名稱也不會(huì)指定具體的JSP頁(yè)面(或者其他類(lèi)型的頁(yè)面)嗓违;控制器返回的僅僅是視圖的邏輯名稱,DispatcherServlet用這個(gè)名稱查找對(duì)應(yīng)的視圖解析器(5)图贸,負(fù)責(zé)將邏輯名稱轉(zhuǎn)換成對(duì)應(yīng)的頁(yè)面實(shí)現(xiàn)蹂季,可能是JSP也可能不是。

現(xiàn)在DispatcherServlet就已經(jīng)知道將由哪個(gè)視圖渲染結(jié)果疏日,至此一個(gè)請(qǐng)求的處理就基本完成了偿洁。最后一步就是視圖的實(shí)現(xiàn)(6),最經(jīng)典的是JSP沟优。視圖會(huì)使用模型數(shù)據(jù)填充到視圖實(shí)現(xiàn)中涕滋,然后將結(jié)果放在HTTP響應(yīng)對(duì)象中(7)。

5.1.2 設(shè)置Spring MVC

如上一小節(jié)的圖展示的挠阁,看起來(lái)需要填寫(xiě)很多配置信息宾肺。幸運(yùn)地是溯饵,Spring的最新版本提供了很多容易配置的選項(xiàng),降低了Spring MVC的學(xué)習(xí)門(mén)檻爱榕。這里我們先簡(jiǎn)單配置一個(gè)Spring MVC應(yīng)用瓣喊,作為這一章將會(huì)不斷完善的例子。

CONFIGURING DISPATCHERSERVLET

DispatcherServlet是Spring MVC的核心黔酥,每當(dāng)應(yīng)用接受一個(gè)HTTP請(qǐng)求,由DispatcherServlet負(fù)責(zé)將請(qǐng)求分發(fā)給應(yīng)用的其他組件洪橘。

在舊版本中跪者,DispatcherServlet之類(lèi)的servlet一般在web.xml文件中配置,該文件一般會(huì)打包進(jìn)最后的war包種熄求;但是Spring 3引入了注解渣玲,我們?cè)谶@一章將展示如何基于注解配置Spring MVC。

既然不適用web.xml文件弟晚,你需要在servlet容器中使用Java配置DispatcherServlet忘衍,具體的代碼列舉如下:

package org.test.spittr.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class SpittrWebAppInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() { //根容器
        return new Class<?>[] { RootConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() { //Spring mvc容器
        return new Class<?>[] { WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() { //DispatcherServlet映射,從"/"開(kāi)始
        return new String[] { "/" };
    }
}

spitter這個(gè)單詞是我們應(yīng)用的名稱,SpittrWebAppInitializer類(lèi)是整個(gè)應(yīng)用的總配置類(lèi)卿城。

AbstractAnnotationConfigDispatcherServletInitializer這個(gè)類(lèi)負(fù)責(zé)配置DispatcherServlet枚钓、初始化Spring MVC容器和Spring容器。getRootConfigClasses()方法用于獲取Spring應(yīng)用容器的配置文件瑟押,這里我們給定預(yù)先定義的RootConfig.class搀捷;getServletConfigClasses負(fù)責(zé)獲取Spring MVC應(yīng)用容器,這里傳入預(yù)先定義好的WebConfig.class多望;getServletMappings()方法負(fù)責(zé)指定需要由DispatcherServlet映射的路徑嫩舟,這里給定的是"/",意思是由DispatcherServlet處理所有向該應(yīng)用發(fā)起的請(qǐng)求怀偷。

A TALE OF TWO APPLICATION CONTEXT

當(dāng)DispatcherServlet啟動(dòng)時(shí)家厌,會(huì)創(chuàng)建一個(gè)Spring MVC應(yīng)用容器并開(kāi)始加載配置文件中定義好的beans。通過(guò)getServletConfigClasses()方法椎工,可以獲取由DispatcherServlet加載的定義在WebConfig.class中的beans饭于。

在Spring Web應(yīng)用中,還有另一個(gè)Spring應(yīng)用容器晋渺,這個(gè)容器由ContextLoaderListener創(chuàng)建镰绎。

我們希望DispatcherServlet僅加載web組件之類(lèi)的beans,例如controllers(控制器)木西、view resolvers(視圖解析器)和處理器映射(handler mappings)畴栖;而希望ContextLoaderListener加載應(yīng)用中的其他類(lèi)型的beans——例如業(yè)務(wù)邏輯組件、數(shù)據(jù)庫(kù)操作組件等等八千。

實(shí)際上吗讶,AbstractAnnotationConfigDispatcherServletInitializer創(chuàng)建了DispatcherServletContextLoaderListenergetServletConfigClasses()返回的配置類(lèi)定義了Spring MVC應(yīng)用容器中的beans燎猛;getRootConfigClasses()返回的配置類(lèi)定義了Spring應(yīng)用根容器中的beans。【書(shū)中沒(méi)有說(shuō)的】:Spring MVC容器是根容器的子容器照皆,子容器可以看到根容器中定義的beans重绷,反之不行。

注意:通過(guò)AbstractAnnotationConfigDispatcherServletInitializer配置DispatcherServlet僅僅是傳統(tǒng)的web.xml文件方式的另一個(gè)可選項(xiàng)膜毁。盡管你也可以使用AbstractAnnotationConfigDispatcherServletInitializer的一個(gè)子類(lèi)引入web.xml文件來(lái)配置昭卓,但這沒(méi)有必要。

這種方式配置DispatcherServlet需要支持Servlert 3.0的容器瘟滨,例如Apache Tomcat 7或者更高版本的候醒。

ENABLING SPRING MVC

正如可以通過(guò)多種方式配置DispatcherServlet一樣,也可以通過(guò)多種方式啟動(dòng)Spring MVC特性杂瘸。原來(lái)我們一般在xml文件中使用<mvc:annotation-driven>元素啟動(dòng)注解驅(qū)動(dòng)的Spring MVC特性倒淫。

這里我們?nèi)匀皇褂肑avaConfig配置,最簡(jiǎn)單的Spring MVC配置類(lèi)代碼如下:

package org.test.spittr.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@EnableWebMvc
public class WebConfig {
}

@Configuration表示這是Java配置類(lèi)败玉;@EnableWebMvc注解用于啟動(dòng)Spring MVC特性敌土。

僅僅這些代碼就可以啟動(dòng)Spring MVC了,雖然它換缺了一些必要的組件:

  • 沒(méi)有配置視圖解析器运翼。這種情況下返干,Spring會(huì)使用BeanNameViewResolver,這個(gè)視圖解析器通過(guò)查找ID與邏輯視圖名稱匹配且實(shí)現(xiàn)了View接口的beans南蹂。
  • 沒(méi)有啟動(dòng)Component-scanning犬金。
  • DispatcherServlet作為默認(rèn)的servlet,將負(fù)責(zé)處理所有的請(qǐng)求六剥,包括對(duì)靜態(tài)資源的請(qǐng)求晚顷,例如圖片和CSS文件等。

因此疗疟,我們還需要在配置文件中增加一些配置该默,使得這個(gè)應(yīng)用可以完成最簡(jiǎn)單的功能,代碼如下:

package org.test.spittr.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan("org.test.spittr.web")
public class WebConfig extends WebMvcConfigurerAdapter{
    @Bean
    public ViewResolver viewResolver() { //配置JSP視圖解析器
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        //可以在JSP頁(yè)面中通過(guò)${}訪問(wèn)beans
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable(); //配置靜態(tài)文件處理
    }
}

首先策彤,通過(guò)@ComponentScan("org.test.spittr.web")注解指定bean的自動(dòng)發(fā)現(xiàn)機(jī)制作用的范圍栓袖,待會(huì)會(huì)看到,被@Controller等注解修飾的web的bean將被發(fā)現(xiàn)并加載到spring mvc應(yīng)用容器店诗。這樣就不需要在配置類(lèi)中顯式定義任何控制器bean了裹刮。

然后,你通過(guò)@Bean注解添加一個(gè)ViewResolverbean庞瘸,具體來(lái)說(shuō)是InternalResourceViewResolver捧弃。后面我們會(huì)專門(mén)探討視圖解析器,這里的三個(gè)函數(shù)的含義依次是:setPrefix()方法用于設(shè)置視圖路徑的前綴;setSuffix()用于設(shè)置視圖路徑的后綴违霞,即如果給定一個(gè)邏輯視圖名稱——"home"嘴办,則會(huì)被解析成"/WEB-INF/views/home.jsp";* setExposeContextBeansAsAttributes(true)使得可以在JSP頁(yè)面中通過(guò)${ }*訪問(wèn)容器中的bean买鸽。

這里需要注意靜態(tài)路徑的設(shè)置涧郊,目前我的項(xiàng)目目錄如下:

項(xiàng)目目錄

最后,WebConfig繼承了WebMvcConfigurerAdapter類(lèi)眼五,然后覆蓋了其提供的configureDefaultServletHandling()方法妆艘,通過(guò)調(diào)用configer.enable()DispatcherServlet將會(huì)把針對(duì)靜態(tài)資源的請(qǐng)求轉(zhuǎn)交給servlert容器的default servlet處理弹砚。

RootConfig的配置就非常簡(jiǎn)單了双仍,唯一需要注意的是,它在設(shè)置掃描機(jī)制的時(shí)候桌吃,將之前WebConfig設(shè)置過(guò)的那個(gè)包排除了;也就是說(shuō)苞轿,這兩個(gè)掃描機(jī)制作用的范圍正交茅诱。RootConfig的代碼如下:

package org.test.spittr.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@ComponentScan(basePackages = {"org.test.spittr"},
        excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)})
public class RootConfig {
}

5.1.3 Spittr應(yīng)用簡(jiǎn)介

這一章要用的例子應(yīng)用,從Twitter獲取了一些靈感搬卒,因此最開(kāi)始叫Spitter瑟俭;然后又借鑒了最近比較流行的網(wǎng)站Flickr,因此我們也把e去掉契邀,最終形成Spittr這個(gè)名字摆寄。這也有利于區(qū)分領(lǐng)域名稱(類(lèi)似于twitter,這里用spring實(shí)現(xiàn)坯门,因此叫spitter)和應(yīng)用名稱微饥。

Spittr應(yīng)用有兩個(gè)關(guān)鍵的領(lǐng)域概念:spitters(應(yīng)用的用戶)和spittles(用戶發(fā)布的狀態(tài)更新)。在這一章中古戴,將專注于構(gòu)建該應(yīng)用的web層欠橘,創(chuàng)建控制器和顯示spittles,以及處理用戶注冊(cè)的表單现恼。

基礎(chǔ)已經(jīng)打好了肃续,你已經(jīng)配置好了DispatcherServlet,啟動(dòng)了Spring MVC特性等叉袍,接下來(lái)看看如何編寫(xiě)Spring MVC控制器始锚。

5.2 編寫(xiě)簡(jiǎn)單的控制器

在Spring MVC應(yīng)用中,控制器類(lèi)就是含有被@RequestMapping注解修飾的方法的類(lèi)喳逛,其中該注解用于指出這些方法要處理的請(qǐng)求類(lèi)型瞧捌。

我們從最簡(jiǎn)單的請(qǐng)求"/"開(kāi)始,用于渲染該應(yīng)用的主頁(yè)艺配,HomeController的代碼列舉如下:

package org.test.spittr.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class HomeController {
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home() {
        return "home";
    }
}

@Controller是一個(gè)模式化的注解察郁,它的作用跟@Component一樣衍慎;Component-scanning機(jī)制會(huì)自動(dòng)發(fā)現(xiàn)該控制器,并在Spring容器中創(chuàng)建對(duì)應(yīng)的bean皮钠。

HomeController中的home()方法用于處理http://localhost:8080/這個(gè)URL對(duì)應(yīng)的"/"請(qǐng)求稳捆,且僅處理GET方法,方法的內(nèi)容是返回一個(gè)邏輯名稱為"home"的視圖麦轰。DispatcherServlet將會(huì)讓視圖解析器通過(guò)這個(gè)邏輯名稱解析出真正的視圖乔夯。

根據(jù)之前配置的InternalResourceViewResolver,最后解析成/WEB-INF/views/home.jsp款侵,home.jsp的內(nèi)容列舉如下:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" session="false" %>
<html>
<head>
    <title>Spittr</title></head>
<body>
    <h1>Welcome to Spittr</h1>
    <a href="<c:url value="/spittles" /> ">Spittles</a>
    <a href="<c:url value="/spitter/register"/> ">Register</a>
</body>
</html>

啟動(dòng)應(yīng)用末荐,然后訪問(wèn)http://localhost:8080/,Spittr應(yīng)用的主頁(yè)如下圖所示:

welcom to spittr

5.2.1 控制器測(cè)試

控制器的測(cè)試通過(guò)Mockito框架進(jìn)行新锈,首先在pom文件中引入需要的依賴庫(kù):

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
</dependency>
<!-- test support -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>${mockito.version}</version>
</dependency><dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>${junit.version}</version>
</dependency>

然后甲脏,對(duì)應(yīng)的單元測(cè)試用例HomeControllerTest的代碼如下所示:

package org.test.spittr.web;

import org.junit.Before;import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;

public class HomeControllerTest {
    MockMvc mockMvc;

    @Before
    public void setupMock() {
        HomeController controller = new HomeController();
        mockMvc = standaloneSetup(controller).build();
    }

    @Test
    public void testHomePage() throws Exception {
        mockMvc.perform(get("/"))
                .andExpect(view().name("home"));
    }
}

首先stanaloneSetup()方法通過(guò)HomeController的實(shí)例模擬出一個(gè)web服務(wù),然后使用perform執(zhí)行對(duì)應(yīng)的GET請(qǐng)求妹笆,并檢查返回的視圖的名稱块请。MockMvcBuilders類(lèi)有兩個(gè)靜態(tài)接口,代表兩種模擬web服務(wù)的方式:獨(dú)立測(cè)試和集成測(cè)試拳缠。上面這段代碼是獨(dú)立測(cè)試墩新,我們也嘗試了集成測(cè)試的方式,最終代碼如下:

package org.test.spittr.web;

import org.junit.Before;import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.ContextHierarchy;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;
import org.test.spittr.config.RootConfig;
import org.test.spittr.config.WebConfig;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration(value = "src/main/webapp")
@ContextHierarchy({
        @ContextConfiguration(name = "parent", classes = RootConfig.class),
        @ContextConfiguration(name = "child", classes = WebConfig.class)})
public class HomeControllerTest {
    @Autowired
    private WebApplicationContext context;

    MockMvc mockMvc;

    @Before
    public void setupMock() {
        //HomeController controller = new HomeController();
        //mockMvc = standaloneSetup(controller).build();
        mockMvc = webAppContextSetup(context).build();
    }

    @Test
    public void testHomePage() throws Exception {
        mockMvc.perform(get("/"))
                .andExpect(view().name("home"));
    }
}

5.2.2 定義類(lèi)級(jí)別的請(qǐng)求處理

上面一節(jié)對(duì)之前的HomeController進(jìn)行了簡(jiǎn)單的測(cè)試窟坐,現(xiàn)在可以對(duì)它進(jìn)行進(jìn)一步的完善:將@RequestMapping從修飾函數(shù)改成修飾類(lèi)海渊,代碼如下:

package org.test.spittr.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping(value = "/")
public class HomeController {
    @RequestMapping(method = RequestMethod.GET)
    public String home() {
        return "home";
    }
}

在新的HomeController中,"/"被移動(dòng)到類(lèi)級(jí)別的@RequestMapping中哲鸳,而定義HTTP方法的@RequestMapping仍然用于修飾home()方法臣疑。RequestMapping注解可以接受字符串?dāng)?shù)組,即可以同時(shí)映射多個(gè)路徑帕胆,因此我們還可以按照下面這種方式修改:

@Controller
@RequestMapping({"/", "/homepage"})
public class HomeController {
    }
}

5.2.3 給視圖傳入模型數(shù)據(jù)

對(duì)于DispatcherServlet傳來(lái)的請(qǐng)求朝捆,控制器通常不會(huì)實(shí)現(xiàn)具體的業(yè)務(wù)邏輯,而是調(diào)用業(yè)務(wù)層的接口懒豹,并且將業(yè)務(wù)層服務(wù)返回的數(shù)據(jù)放在模型對(duì)象中返回給DispatcherServlet芙盘。

在Spittr應(yīng)用中,需要一個(gè)頁(yè)面顯示最近的spittles列表脸秽。首先需要定義數(shù)據(jù)庫(kù)存取接口儒老,這里不需要提供具體實(shí)現(xiàn),只需要用Mokito框架填充模擬測(cè)試數(shù)據(jù)即可记餐。SpittleRepository接口的代碼列舉如下:

package org.test.spittr.data;

import java.util.List;

public interface SpittleRepository {
    List<Spittle> findSpittles(long max, int count);
}

SpittleRepository接口中的findSpittles()方法有兩個(gè)參數(shù):max表示要返回的Spittle對(duì)象的最大ID驮樊;count表示指定需要返回的Spittle對(duì)象數(shù)量。為了返回20個(gè)最近發(fā)表的Spittle對(duì)象,則使用List<Spittle> recent = spittleRepository.findSpittle(Long.MAX_VALUE, 20)這行代碼即可囚衔。該接口要處理的實(shí)體對(duì)象是Spittle挖腰,因此還需要定義對(duì)應(yīng)的實(shí)體類(lèi),代碼如下:

package org.test.spittr.data;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import java.util.Date;

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(message, time, null, null);
    }

    public Spittle(String message,Date time, Double latitude, Double longitude) {
        this.id = null;
        this.time = time;
        this.latitude = latitude;
        this.longitude = longitude;
        this.message = message;
    }

    public Long getId() {
        return id;
    }

    public String getMessage() {
        return message;
    }

    public Date getTime() {
        return time;
    }

    public Double getLongitude() {
        return longitude;
    }

    public Double getLatitude() {
        return latitude;
    }

    @Override
    public boolean equals(Object obj) {
        return EqualsBuilder.reflectionEquals(this, obj,
                new String[]{"message","latitude", "longitude"});
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this,
                new String[]{"message", "latitude", "longitude"});
    }
}

Spittle對(duì)象還是POJO练湿,并沒(méi)什么復(fù)雜的猴仑。唯一需要注意的就是,利用Apache Commons Lang庫(kù)的接口肥哎,用于簡(jiǎn)化equals和hashCode方法的實(shí)現(xiàn)辽俗。參考Apache Commons EqualsBuilder and HashCodeBuilder

首先為新的控制器接口寫(xiě)一個(gè)測(cè)試用例,利用Mockito框架模擬repository對(duì)象篡诽,并模擬出request請(qǐng)求崖飘,代碼如下:

package org.test.spittr.web;

import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.servlet.view.InternalResourceView;
import org.test.spittr.data.Spittle;import org.test.spittr.data.SpittleRepository;import java.util.ArrayList;
import java.util.Date;import java.util.List;

import static org.hamcrest.Matchers.hasItems;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;

public class SpittleControllerTest {
    @Test
    public void shouldShowRecentSpittles() throws Exception {
        //step1 準(zhǔn)備測(cè)試數(shù)據(jù)
        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();

        //step2 and step3
        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;
    }
}

單元測(cè)試的基本組成是:準(zhǔn)備測(cè)試數(shù)據(jù)、調(diào)用待測(cè)試接口杈女、校驗(yàn)接口的執(zhí)行結(jié)果朱浴。對(duì)于shouldShowRecentSpittles()這個(gè)用例我們也可以這么分割:首先規(guī)定在調(diào)用SpittleRepository接口的findSpittles()方法時(shí)將返回20個(gè)Spittle對(duì)象。

這里選擇獨(dú)立測(cè)試达椰,跟HomeControllerTest不同的地方在于赊琳,這里構(gòu)建MockMvc對(duì)象時(shí)還調(diào)用了setSingleView()函數(shù),這是為了防止mock框架從控制器解析view名字砰碴。在很多情況下并沒(méi)有這個(gè)必要,但是對(duì)于SpittleController控制器來(lái)說(shuō)板丽,視圖名稱和路徑名稱相同呈枉,如果使用默認(rèn)的視圖解析器,則MockMvc會(huì)混淆這兩者而失敗埃碱,報(bào)出如下圖所示的錯(cuò)誤:

default view resovler will confuse the view name and path

在這里其實(shí)可以隨意設(shè)置InternalResourceView的路徑猖辫,但是為了和WebConfig中的配置相同。

通過(guò)get方法構(gòu)造GET請(qǐng)求砚殿,訪問(wèn)"/spittles"啃憎,并確保返回的視圖名稱是"spittles",返回的model數(shù)據(jù)中包含spittleList屬性似炎,且對(duì)應(yīng)的值為我們之前創(chuàng)建的測(cè)試數(shù)據(jù)辛萍。

最后,為了使用hasItems羡藐,需要在pom文件中引入hamcrest庫(kù)贩毕,代碼如下

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-library</artifactId>
    <version>1.3</version>
</dependency>

現(xiàn)在跑單元測(cè)試的話,必然會(huì)失敗仆嗦,因?yàn)槲覀冞€沒(méi)有提供SpittleController的對(duì)應(yīng)方法辉阶,代碼如下:

package org.test.spittr.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.test.spittr.data.SpittleRepository;

@Controller
@RequestMapping("/spittles")
public class SpittleController {
    private SpittleRepository spittleRepository;

    @Autowired
    SpittleController(SpittleRepository spittleRepository) {
        this.spittleRepository = spittleRepository;
    }

    @RequestMapping(method = RequestMethod.GET)
    public String spittles(Model model) {
        model.addAttribute(
                spittleRepository.findSpittles(Long.MAX_VALUE, 20));
        return "spittles";
    }
}

Model對(duì)象本質(zhì)上是一個(gè)Map,spittles方法負(fù)責(zé)填充數(shù)據(jù),然后跟視圖的邏輯名稱一起回傳給DispatcherServlet谆甜。在調(diào)用addAttribute方法的時(shí)候垃僚,如果不指定key字段,則key字段會(huì)從value的類(lèi)型推導(dǎo)出规辱,在這個(gè)例子中默認(rèn)的key字段是spittleList谆棺。

如果你希望顯式指定key字段,則可以按照如下方式指定:

@RequestMapping(method = RequestMethod.GET)
public String spittles(Model model) {
    model.addAttribute("spittleList",
            spittleRepository.findSpittles(Long.MAX_VALUE, 20));
    return "spittles";
}

另外按摘,如果你希望盡量少使用Spring規(guī)定的數(shù)據(jù)類(lèi)型包券,則可以使用Map代替Model。

還有另一種spittles方法的實(shí)現(xiàn)炫贤,如下所示:

@RequestMapping(method = RequestMethod.GET)
public List<Spittle> spittles() {
    return spittleRepository.findSpittles(Long.MAX_VALUE, 20));
}

這個(gè)版本和之前的不同溅固,并沒(méi)有返回一個(gè)邏輯名稱以及顯式設(shè)置Model對(duì)象,這個(gè)方法直接返回Spittle列表兰珍。在這種情況下侍郭,Spring會(huì)將返回值直接放入Model對(duì)象,并從值類(lèi)型推導(dǎo)出對(duì)應(yīng)的關(guān)鍵字key掠河;然后從路徑推導(dǎo)出視圖邏輯名稱亮元,在這里是spittles

無(wú)論你選擇那種實(shí)現(xiàn)唠摹,最終都需要一個(gè)頁(yè)面——spittles.jsp爆捞。JSP頁(yè)面使用JSTL庫(kù)的<c:forEach>標(biāo)簽獲取model對(duì)象中的數(shù)據(jù),如下所示:

<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還是很簡(jiǎn)單勾拉,但是它比HomeController復(fù)雜了一點(diǎn)煮甥,不過(guò),這兩個(gè)控制器都沒(méi)有實(shí)現(xiàn)的一個(gè)功能是處理表單輸入藕赞。接下來(lái)將擴(kuò)展SpittleController成肘,使其能夠處理表單上輸入。

5.3 訪問(wèn)request輸入

Spring MVC提供了三種方式斧蜕,可以讓客戶端給控制器的handler傳入?yún)?shù)双霍,包括:

  • 查詢參數(shù)(Query parameters)
  • 表單參數(shù)(Form parameters)
  • 路徑參數(shù)(Path parameters)

5.3.1 獲取查詢參數(shù)

Spittr應(yīng)用需要一個(gè)頁(yè)面顯示spittles列表,目前的SpittleController僅能返回最近的所有spittles批销,還不能提供根據(jù)spittles的生成歷史進(jìn)行查詢洒闸。如果你想提供這個(gè)功能,首先要提供用戶一個(gè)傳入?yún)?shù)的方法风钻,從而可以決定返回歷史spittles的那一個(gè)子集顷蟀。

spittles列表是按照ID的生成先后倒序排序的:下一頁(yè)spittles的第一條spittle的ID應(yīng)正好在當(dāng)前頁(yè)的最后一條spittle的ID后面。因此骡技,為了顯示下一頁(yè)spttles鸣个,應(yīng)該能夠傳入僅僅小于當(dāng)前頁(yè)最后一條spittleID的參數(shù)羞反;并且提供設(shè)置每頁(yè)返回幾個(gè)spittles的參數(shù)count。

  • before參數(shù)囤萤,代表某個(gè)Spittle的ID昼窗,包含該ID的spittles集合中所有的spittles都在當(dāng)前頁(yè)的spittles之前發(fā)布;
  • count參數(shù)涛舍,代表希望返回結(jié)果中包含多少條spittles澄惊。

我們將改造5.2.3小節(jié)實(shí)現(xiàn)的spittles()方法,來(lái)處理上述兩個(gè)參數(shù)富雅。首先編寫(xiě)測(cè)試用例:

@Test
public void shouldShowRecentSpittles_NORMAL() 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())));
}

這個(gè)測(cè)試用例的關(guān)鍵在于:為請(qǐng)求"/spittles"傳入兩個(gè)參數(shù)掸驱,max和count。這個(gè)測(cè)試用例可以測(cè)試提供參數(shù)的情況没佑,兩個(gè)測(cè)試用例都應(yīng)該提供毕贼,這樣可以覆蓋到所有測(cè)試條件。改造后的spittles方法列舉如下:

@RequestMapping(method = RequestMethod.GET)
public List<Spittle> spittles(
        @RequestParam("max") long max,
        @RequestParam("count") int count) {
    return spittleRepository.findSpittles(max, count);
}

如果SpittleController的handle方法需要默認(rèn)處理同時(shí)處理兩種情況:提供了max和count參數(shù)蛤奢,或者沒(méi)有提供的情況鬼癣,代碼如下:

@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_AS_STRING是Long的最大值的字符串形式,定義為:private static final String MAX_LONG_AS_STRING = Long.MAX_VALUE + "";啤贩,默認(rèn)值都給定字符串形式待秃,不過(guò)一旦需要綁定到參數(shù)上時(shí),就會(huì)自動(dòng)轉(zhuǎn)為合適的格式痹屹。

5.3.2 通過(guò)路徑參數(shù)獲取輸入

假設(shè)Spittr應(yīng)用應(yīng)該支持通過(guò)指定ID顯示對(duì)應(yīng)的Spittle章郁,可以使用@RequestParam給控制器的處理方法傳入?yún)?shù)ID,如下所示:

@RequestMapping(value = "/show", method = RequestMethod.GET)
public String showSpittle(
        @RequestParam("spittle_id") long spittleId,
        Model model) {
    model.addAttribute(spittleRepository.findOne(spittleId));
    return "spittle";
}

這個(gè)方法將處理類(lèi)似/spittles/show?spittle_id=12345的請(qǐng)求志衍,盡管這可以工作驱犹,但是從基于資源管理的角度并不理想。理想情況下足画,某個(gè)指定的資源應(yīng)該可以通過(guò)路徑指定,而不是通過(guò)查詢參數(shù)指定佃牛,因此GET請(qǐng)求最好是這種形式:/spittles/12345淹辞。

首先編寫(xiě)一個(gè)測(cè)試用例,代碼如下:

@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"))
            .andExpect(model().attributeExists("spittle"))
            .andExpect(model().attribute("spittle", expectedSpittle));
}

該測(cè)試用例首先模擬一個(gè)repository俘侠、控制器和MockMvc對(duì)象象缀,跟之前的幾個(gè)測(cè)試用例相同。不同之處在于這里構(gòu)造的GET請(qǐng)求——/spittles/12345爷速,并希望返回的視圖邏輯名稱是spittle央星,返回的模型對(duì)象中包含關(guān)鍵字spittle,且與該key對(duì)應(yīng)的值為我們創(chuàng)建的測(cè)試數(shù)據(jù)惫东。

為了實(shí)現(xiàn)路徑參數(shù)莉给,Spring MVC在@RequestMapping注解中提供占位符機(jī)制毙石,并在參數(shù)列表中通過(guò)@PathVariable("spittleId")獲取路徑參數(shù),完整的處理方法列舉如下:

@RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
public String showSpittle(
        @PathVariable("spittleId") long spittleId,
        Model model) {
    model.addAttribute(spittleRepository.findOne(spittleId));
    return "spittle";
}

@PathVariable注解的參數(shù)應(yīng)該和@RequestMapping注解中的占位符名稱完全相同颓遏;如果函數(shù)參數(shù)也和占位符名稱相同徐矩,則可以省略@PathVariable注解的參數(shù)集嵌,代碼如下所示:

@RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
public String showSpittle(
        @PathVariable long spittleId,
        Model model) {
    model.addAttribute(spittleRepository.findOne(spittleId));
    return "spittle";
}

這么寫(xiě)確實(shí)可以使得代碼更加簡(jiǎn)單附较,不過(guò)需要注意:如果要修改函數(shù)參數(shù)名稱,則要同時(shí)修改路徑參數(shù)的占位符名稱忧饭。

5.4 處理表單

Web應(yīng)用通常不僅僅是給用戶顯示數(shù)據(jù)曼玩,也接受用戶的表單輸入鳞骤,最典型的例子就是賬號(hào)注冊(cè)頁(yè)面——需要用戶填入相關(guān)信息,應(yīng)用程序按照這些信息為用戶創(chuàng)建一個(gè)賬戶黍判。

關(guān)于表單的處理有兩個(gè)方面需要考慮:顯示表單內(nèi)容和處理用戶提交的表單數(shù)據(jù)豫尽。在Spittr應(yīng)用中,需要提供一個(gè)表單供新用戶注冊(cè)使用样悟;需要一個(gè)SpitterController控制器顯示注冊(cè)信息拂募。

package org.test.spittr.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/spitter")
public class SpitterController {
    @RequestMapping(value = "/register", method = RequestMethod.GET)
    public String showRegistrationForm() {
        return "registerForm";
    }
}

修飾showRegistrationForm()方法的@RequestMapping(value = "/register", method = RequestMethod.GET)注解,和類(lèi)級(jí)別的注解一起窟她,表明該方法需要處理類(lèi)似"/spitter/register"的GET請(qǐng)求陈症。這個(gè)方法非常簡(jiǎn)單,沒(méi)有輸入震糖,且僅僅返回一個(gè)邏輯名稱——"registerForm"录肯。

即使showRegistrationForm()方法非常簡(jiǎn)單,也應(yīng)該寫(xiě)個(gè)單元測(cè)試吊说,代碼如下所示:

@Test
public void shouldShowRegistrationForm() throws Exception {
    SpitterController controller = new SpitterController();
    MockMvc mockMvc = standaloneSetup(controller).build();

    mockMvc.perform(get("/spitter/register"))
            .andExpect(view().name("registerForm"));
}

為了接受用戶的輸入论咏,需要提供一個(gè)JSP頁(yè)面——registerForm.jsp,該頁(yè)面的代碼如下所示:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Spittr</title>
</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>

上述JSP頁(yè)面在瀏覽器中渲染圖如下所示:

注冊(cè)頁(yè)面

因?yàn)?em><form>標(biāo)簽并沒(méi)有設(shè)置action參數(shù)颁井,因此厅贪,當(dāng)用戶單擊submit按鈕的時(shí)候,將向后臺(tái)發(fā)出/spitter/register的POST請(qǐng)求雅宾。這就需要我們?yōu)?em>SpitterController編寫(xiě)對(duì)應(yīng)的處理方法养涮。

5.4.1 編寫(xiě)表單控制器

在處理來(lái)自注冊(cè)表單的POST請(qǐng)求時(shí),控制器需要接收表單數(shù)據(jù)眉抬,然后構(gòu)造Spitter對(duì)象贯吓,并保存在數(shù)據(jù)庫(kù)中。為了避免重復(fù)提交蜀变,應(yīng)該重定向到另一個(gè)頁(yè)面——用戶信息頁(yè)悄谐。

按照慣例,首先編寫(xiě)測(cè)試用例库北,如下所示:

@Test
public void shouldProcessRegistration() throws Exception {
    SpitterRepository mockRepository = mock(SpitterRepository.class);
    Spitter unsaved = new Spitter("Jack", "Bauer", "jbauer", "24hours");
    Spitter saved = new Spitter(24L, "Jack", "Bauer", "jbauer", "24hours");
    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"));

    //Verified save(unsaved) is called atleast once
    verify(mockRepository, atLeastOnce()).save(unsaved);
}

顯然爬舰,這個(gè)測(cè)試比之前驗(yàn)證顯示注冊(cè)頁(yè)面的測(cè)試更加豐富们陆。首先設(shè)置好SpitterRepository對(duì)象、控制器和MockMvc對(duì)象洼专,然后構(gòu)建一個(gè)POST請(qǐng)求——/spitter/register棒掠,且該請(qǐng)求會(huì)攜帶四個(gè)參數(shù),用于模擬submit的提交動(dòng)作屁商。

在處理POST請(qǐng)求的最后一般需要利用重定向到一個(gè)新的頁(yè)面烟很,以防瀏覽器刷新引來(lái)的重復(fù)提交。在這個(gè)例子中我們重定向到/spitter/jbaure蜡镶,即新添加的用戶的個(gè)人信息頁(yè)面雾袱。

最后,該測(cè)試用例還需要驗(yàn)證模擬對(duì)象mockRepository確實(shí)用于保存表單提交的數(shù)據(jù)了官还,即save()方法之上調(diào)用了一次芹橡。

SpitterController中添加處理表單的方法,代碼如下:

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(Spitter spitter) {
    spitterRepository.save(spitter);
    return "redirect:/spitter/" + spitter.getUsername();
}

shouldShowRegistrationForm()這個(gè)方法還在望伦,新加的處理方法processRegistration()以Spitter對(duì)象為參數(shù)林说,Spring利用POST請(qǐng)求所攜帶的參數(shù)初始化Spitter對(duì)象。

現(xiàn)在執(zhí)行之前的測(cè)試用例屯伞,發(fā)現(xiàn)一個(gè)錯(cuò)誤如下所示:

argument are different

我分析了這個(gè)錯(cuò)誤腿箩,原因是測(cè)試用例的寫(xiě)法有問(wèn)題:verify(mockRepository, atLeastOnce()).save(unsaved);這行代碼表示,希望調(diào)用至少保存unsave這個(gè)對(duì)象一次劣摇,而實(shí)際上在控制器中執(zhí)行save的時(shí)候珠移,參數(shù)對(duì)象的ID是另一個(gè)——根據(jù)參數(shù)新創(chuàng)建的∧┤冢回顧我們寫(xiě)這行代碼的初衷:確保save方法至少被調(diào)用一次钧惧,而保存哪個(gè)對(duì)象則無(wú)所謂,因此勾习,這行語(yǔ)句改成verify(mockRepository, atLeastOnce());后浓瞪,再次執(zhí)行測(cè)試用例就可以通過(guò)了。

注意:無(wú)論使用哪個(gè)框架巧婶,請(qǐng)盡量不要使用verify追逮,也就是傳說(shuō)中的Mock模式,那是把代碼拉入泥潭的開(kāi)始粹舵。參見(jiàn)你應(yīng)該更新的Java知識(shí)之常用程序庫(kù)

當(dāng)InternalResourceViewResolver看到這個(gè)函數(shù)返回的重定向URL是以view標(biāo)志開(kāi)頭,就知道需要把該URL當(dāng)做重定向URL處理骂倘,而不是按照視圖邏輯名稱處理眼滤。在這個(gè)例子中,頁(yè)面將被重定向至用戶的個(gè)人信息頁(yè)面历涝。因此诅需,我們還需要給SpitterController添加一個(gè)處理方法漾唉,用于顯示個(gè)人信息,showSpitterProfile()方法代碼如下:

@RequestMapping(value = "/{username}", method = RequestMethod.GET)
public String showSpitterProfile(
    @PathVariable String username, Model model) {
    Spitter spitter = spitterRepository.findByUsername(username);
    model.addAttribute(spitter);
    return "profile";
}

showSpitterProfile()方法根據(jù)username從SpitterRepository中查詢Spitter對(duì)象堰塌,然后將該對(duì)象存放在model對(duì)象中赵刑,并返回視圖的邏輯名稱profile

profile.jsp的頁(yè)面代碼如下所示:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Your Profile</title>
</head>
<body>
    <h1>Your Profile</h1>
    <c:out value="${spitter.username}"/><br/>
    <c:out value="${spitter.firstName}"/><br/>
    <c:out value="${spitter.lastName}" /><br/>
</body>
</html>

上述代碼的渲染圖如下圖所示:

Your Profile

5.4.2 表單驗(yàn)證

如果用戶忘記輸入username或者password就點(diǎn)了提交场刑,則可能創(chuàng)建一個(gè)這兩個(gè)字段為空字符串的Spitter對(duì)象般此。往小了說(shuō),這是丑陋的開(kāi)發(fā)習(xí)慣牵现,往大了說(shuō)這是會(huì)應(yīng)發(fā)安全問(wèn)題铐懊,因?yàn)橛脩艨梢酝ㄟ^(guò)提交一個(gè)空的表單來(lái)登錄系統(tǒng)。

綜上所述瞎疼,需要對(duì)用戶的輸入進(jìn)行有效性驗(yàn)證科乎,一種驗(yàn)證方法是為processRegistration()方法添加校驗(yàn)輸入?yún)?shù)的代碼,因?yàn)檫@個(gè)函數(shù)本身非常簡(jiǎn)單贼急,參數(shù)也不多茅茂,因此在開(kāi)頭加入一些If判斷語(yǔ)句還可以接受。

除了使用這種方法太抓,換可以利用Spring提供的Java驗(yàn)證支持(a.k.a JSR-303)空闲。從Spring 3.0開(kāi)始,Spring支持在Spring MVC項(xiàng)目中使用Java Validation API腻异。

首先需要在pom文件中添加依賴:

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
</dependency>

然后就可以使用各類(lèi)具體的注解进副,進(jìn)行參數(shù)驗(yàn)證了,以Spitter類(lèi)的實(shí)現(xiàn)為例:

package org.test.spittr.data;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

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注解表示被它修飾的字段不能為空悔常;@Size字段用于限制指定字段的長(zhǎng)度范圍影斑。在Spittr應(yīng)用的含義是:用戶必須填寫(xiě)表單中的所有字段,并且滿足一定的長(zhǎng)度限制机打,才可以注冊(cè)成功矫户。

除了上述兩個(gè)注解,Java Validation API提供了很多不同功能的注解残邀,都定義在javax.validation.constraints包種皆辽,下表列舉出這些注解:

Java Validation API列表
Java Validation API列表(續(xù))

Spittr類(lèi)的定義中規(guī)定驗(yàn)證條件后,需要在控制器的處理方法中應(yīng)用驗(yàn)證條件芥挣。

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(
        @Valid Spitter spitter,
        Errors errors) {
    if (errors.hasErrors()) {
        return "registerForm";
    }
    spitterRepository.save(spitter);
    return "redirect:/spitter/" + spitter.getUsername();
}

如果用戶輸入的參數(shù)有誤驱闷,則返回registerForm這個(gè)邏輯名稱,瀏覽器將返回到表單填寫(xiě)頁(yè)面空免,以便用戶重新輸入空另。當(dāng)然,為了更好的用戶體驗(yàn)蹋砚,還需要提示用戶具體哪個(gè)字段寫(xiě)錯(cuò)了扼菠,應(yīng)該怎么改摄杂;最好是在用戶填寫(xiě)之前就做出提示,這就需要前端工程師做很多工作了循榆。

5.5 總結(jié)

這一章比較適合Spring MVC的入門(mén)學(xué)習(xí)資料析恢。涵蓋了Spring MVC處理web請(qǐng)求的處理過(guò)程、如何寫(xiě)簡(jiǎn)單的控制器和控制器方法來(lái)處理Http請(qǐng)求秧饮、如何使用mockito框架測(cè)試控制器方法映挂。

基于Spring MVC的應(yīng)用有三種方式讀取數(shù)據(jù):查詢參數(shù)、路徑參數(shù)和表單輸入浦楣。本章用兩節(jié)介紹了這些內(nèi)容袖肥,并給出了類(lèi)似錯(cuò)誤處理和參數(shù)驗(yàn)證等關(guān)鍵知識(shí)點(diǎn)。


本號(hào)專注于后端技術(shù)振劳、JVM問(wèn)題排查和優(yōu)化椎组、Java面試題、個(gè)人成長(zhǎng)和自我管理等主題历恐,為讀者提供一線開(kāi)發(fā)者的工作和成長(zhǎng)經(jīng)驗(yàn)寸癌,期待你能在這里有所收獲。


javaadu
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末弱贼,一起剝皮案震驚了整個(gè)濱河市蒸苇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吮旅,老刑警劉巖溪烤,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異庇勃,居然都是意外死亡檬嘀,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)责嚷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鸳兽,“玉大人,你說(shuō)我怎么就攤上這事罕拂∽嵋欤” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵爆班,是天一觀的道長(zhǎng)衷掷。 經(jīng)常有香客問(wèn)我,道長(zhǎng)柿菩,這世上最難降的妖魔是什么戚嗅? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上渡处,老公的妹妹穿的比我還像新娘。我一直安慰自己祟辟,他們只是感情好医瘫,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著旧困,像睡著了一般醇份。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吼具,一...
    開(kāi)封第一講書(shū)人閱讀 52,713評(píng)論 1 312
  • 那天僚纷,我揣著相機(jī)與錄音,去河邊找鬼拗盒。 笑死怖竭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的陡蝇。 我是一名探鬼主播痊臭,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼登夫!你這毒婦竟也來(lái)了广匙?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤恼策,失蹤者是張志新(化名)和其女友劉穎鸦致,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體涣楷,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡分唾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了总棵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鳍寂。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖情龄,靈堂內(nèi)的尸體忽然破棺而出迄汛,到底是詐尸還是另有隱情,我是刑警寧澤骤视,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布鞍爱,位于F島的核電站,受9級(jí)特大地震影響专酗,放射性物質(zhì)發(fā)生泄漏睹逃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望沉填。 院中可真熱鬧疗隶,春花似錦、人聲如沸翼闹。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)猎荠。三九已至坚弱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間关摇,已是汗流浹背荒叶。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留输虱,地道東北人些楣。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像悼瓮,于是被迫代替她去往敵國(guó)和親戈毒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容