【Spring實戰(zhàn)】構(gòu)建Spring Web應(yīng)用程序

本章內(nèi)容:

  • 映射請求到Spring控制器
  • 透明地綁定表單參數(shù)
  • 校驗表單提交

狀態(tài)管理颜曾、工作流以及驗證都是Web 開發(fā)需要解決的重要特性岭粤。HTTP協(xié)議的無狀態(tài)性決定了這些問題都不那么容易解決佑颇。

Spring的Web框架就是為了幫助解決這些關(guān)注點而設(shè)計的馋劈。Spring MVC基于模型-視圖-控制器(Model-View-Controller维蒙,MVC)模式實現(xiàn)简烤,它能構(gòu)建像Spring框架那樣靈活和松耦合的Web應(yīng)用程序错蝴。


Spring MVC起步

Spring將請求在調(diào)度Servlet洲愤、處理器映射(handler mapping)、控制器以及視圖解析器(view resolver)之間移動顷锰。

跟蹤Spring MVC的請求

每當(dāng)用戶在Web瀏覽器中點擊鏈接或提交表單的時候柬赐,請求就開始工作了。請求會將信息從一個地方帶到另一個地方官紫,就像是快遞投送員肛宋。

從離開瀏覽器開始到獲取響應(yīng)返回州藕,請求會經(jīng)歷好多站,在每站都會留下一些信息同時也會帶上其他信息酝陈。

請求使用Spring MVC所經(jīng)歷的所有站點

在請求離開瀏覽器時 ①床玻,會帶有用戶所請求內(nèi)容的信息,比如請求的URL沉帮,用戶提交的表單信息锈死。

請求旅程的第一站是Spring的DispatcherServlet。與大多數(shù)基于Java的Web框架一樣穆壕,Spring MVC所有的請求都會通過一個前端控制器(front controller)Servlet待牵。

前端控制器是常用的Web應(yīng)用程序模式,在這里一個單實例的Servlet將請求委托給應(yīng)用程序的其他組件來執(zhí)行實際的處理喇勋。在Spring MVC中缨该,DispatcherServlet就是前端控制器。

DispatcherServlet的任務(wù)是將請求發(fā)送給Spring MVC控制器(controller)川背》∧茫控制器是一個用于處理請求的Spring組件。在典型的應(yīng)用程序會有多個控制器熄云,DispatcherServlet需要知道應(yīng)該將請求發(fā)送給哪個控制器膨更。所以DispatcherServlet以會查詢一個或多個處理器映射(handler mapping)②來確定請求的下一站在哪里。處理器映射會根據(jù)請求所攜帶的URL信息來進(jìn)行決策皱碘。

一旦選擇了合適的控制器询一,DispatcherServlet會將請求發(fā)送給選中的控制器③ 。到了控制器癌椿,請求會卸下其負(fù)載(用戶提交的信息)并等待控制器處理這些信息健蕊。(設(shè)計良好的控制器本身只處理很少甚至不處理工作,而是將業(yè)務(wù)邏輯委托給一個或多個服務(wù)對象進(jìn)行處理踢俄。)

控制器在完成邏輯處理后缩功,通常會產(chǎn)生一些信息,這些信息需要返回給用戶并在瀏覽器上顯示都办。這些信息被稱為模型(model)嫡锌。這些信息會以用戶友好的方式進(jìn)行格式化,一般會是HTML琳钉。所以势木,信息需要發(fā)送給一個視圖(view),通常會是JSP歌懒。

控制器所做的最后一件事就是將模型數(shù)據(jù)打包啦桌,并且標(biāo)示出用于渲染輸出的視圖名。接下來會將請求連同模型和視圖名發(fā)送回DispatcherServlet ④及皂。

這樣控制器就不會與特定的視圖相耦合甫男,傳遞給DispatcherServlet的視圖名并不直接表示某個特定的JSP且改。甚至并不能確定視圖就是JSP。它僅僅傳遞了一個邏輯名稱板驳,這個名字將會用來查找產(chǎn)生結(jié)果的真正視圖又跛。DispatcherServlet將會使用視圖解析器(view resolver)⑤來將邏輯視圖名匹配為一個特定的視圖實現(xiàn)。

DispatcherServlet知道由哪個視圖渲染結(jié)果以后若治,請求的最后一站是視圖的實現(xiàn)⑥慨蓝,請求在這里交付模型數(shù)據(jù)。請求的任務(wù)完成直砂。視圖將使用模型數(shù)據(jù)渲染輸出菌仁,這個輸出會通過響應(yīng)對象傳遞給客戶端⑦浩习。

請求要經(jīng)過很多的步驟静暂,最終才能形成返回給客戶端的響應(yīng)。大多數(shù)的步驟都是在Spring框架內(nèi)部完成的谱秽。

若不使用Spring MVC洽蛀,則需要用戶負(fù)責(zé)編寫一個Dispatcher servlet,這個Dispatcher servlet要能做如下事情:

  1. 根據(jù)URI調(diào)用相應(yīng)的action疟赊。
  2. 實例化正確的控制器類郊供。
  3. 根據(jù)請求參數(shù)值來構(gòu)造表單bean
  4. 調(diào)用控制器對象的相應(yīng)方法。
  5. 轉(zhuǎn)向到一個視圖(JSP頁面)近哟。
搭建Spring MVC

借助于最近幾個Spring新版本的功能增強驮审,使用Spring MVC變得非常簡單了。現(xiàn)在吉执,使用最簡單的方式來配置Spring MVC:所要實現(xiàn)的功能僅限于運行所創(chuàng)建的控制器疯淫。

配置DispatcherServlet

Spring MVC中提供了一個Dispatcher Servlet,它會調(diào)用控制器方法并轉(zhuǎn)發(fā)到視圖戳玫。DispatcherServlet是Spring MVC的核心熙掺。在這里請求會第一次接觸到框架,它要負(fù)責(zé)將請求路由到其他的組件之中咕宿。

傳統(tǒng)的方式币绩,像DispatcherServlet這樣的Servlet會配置在web.xml文件中,這個文件會放到應(yīng)用的WAR包里面府阀。

借助于Servlet 3規(guī)范和Spring 3.1的功能增強缆镣,這種方式已經(jīng)不是唯一的方案了,可以使用Java將DispatcherServlet配置在Servlet容器中:

package spittr.config;

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

import spittr.web.WebConfig;

public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
  
  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" };
  }

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class<?>[] { RootConfig.class };
  }

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

}

擴(kuò)展AbstractAnnotationConfigDispatcherServletInitializer的任意類都會自動地配置Dispatcher-ServletSpring應(yīng)用上下文试浙,Spring的應(yīng)用上下文會位于應(yīng)用程序的Servlet上下文之中董瞻。

AbstractAnnotationConfigDispatcherServletInitializer剖析

在Servlet 3.0環(huán)境中,容器會在類路徑中查找實現(xiàn)javax.servlet.ServletContainerInitializer接口的類川队,如果能發(fā)現(xiàn)的話力细,就會用它來配置Servlet容器睬澡。Spring提供了這個接口的實現(xiàn),名為SpringServletContainerInitializer眠蚂,這個類反過來又會查找實現(xiàn)WebApplicationInitializer的類并將配置的任務(wù)交給它們來完成煞聪。Spring 3.2引入了一個便利的WebApplicationInitializer基礎(chǔ)實現(xiàn),也就是AbstractAnnotationConfigDispatcherServletInitializer因為我們的Spittr-WebAppInitializer擴(kuò)展了AbstractAnnotationConfigDispatcherServletInitializer(同時也就實現(xiàn)了WebApplicationInitializer)逝慧,因此當(dāng)部署到Servlet 3.0容器中的時候昔脯,容器會自動發(fā)現(xiàn)它,并用它來配置Servlet上下文笛臣。

盡管AbstractAnnotationConfigDispatcherServletInitializer的名字很長云稚,但使用起來很簡便。在上面的程序中沈堡,SpittrWebAppInitializer重寫了三個方法静陈。

第一個方法是getServletMappings(),它會將一個或多個路徑映射到DispatcherServlet上诞丽。在本例中鲸拥,它映射的是“/”,這表示它會是應(yīng)用的默認(rèn)Servlet僧免。它會處理進(jìn)入應(yīng)用的所有請求刑赶。

要理解其他的兩個方法,首先要理解DispatcherServlet和一個Servlet監(jiān)聽器(ContextLoaderListener)的關(guān)系懂衩。

兩個應(yīng)用上下文之間的故事

當(dāng)DispatcherServlet啟動的時候撞叨,它會創(chuàng)建Spring應(yīng)用上下文,并加載配置文件或配置類中所聲明的bean浊洞。getServletConfigClasses()方法中牵敷,指明DispatcherServlet加載應(yīng)用上下文時,使用定義在WebConfig配置類(使用Java配置)中的bean沛申。

但是在Spring Web應(yīng)用中劣领,通常還會有另外一個應(yīng)用上下文。另外的這個應(yīng)用上下文是由ContextLoaderListener創(chuàng)建的铁材。

我們希望DispatcherServlet加載包含Web組件的bean尖淘,如控制器、視圖解析器以及處理器映射著觉,而ContextLoaderListener要加載應(yīng)用中的其他bean村生。這些bean通常是驅(qū)動應(yīng)用后端的中間層和數(shù)據(jù)層組件。

實際上饼丘,AbstractAnnotationConfigDispatcherServletInitializer會同時創(chuàng)建DispatcherServletContextLoaderListener趁桃。GetServletConfigClasses()方法返回的帶有@Configuration注解的類將會用來定義DispatcherServlet應(yīng)用上下文中的bean。getRootConfigClasses()方法返回的帶有@Configuration注解的類將會用來配置ContextLoaderListener創(chuàng)建的應(yīng)用上下文中的bean。

如果按照這種方式配置DispatcherServlet卫病,而不是使用web.xml的話油啤,只能部署到支持Servlet 3.0的服務(wù)器中才能正常工作,如Tomcat 7或更高版本蟀苛。

如果沒有使用支持Servlet 3.0的服務(wù)器益咬,就無法在AbstractAnnotationConfigDispatcherServletInitializer子類中配置DispatcherServlet,只能使用web.xml配置帜平。

啟用Spring MVC

啟用Spring MVC組件的方法不僅一種幽告。使用XML進(jìn)行配置的,可以使用<mvc:annotation-driven>啟用注解驅(qū)動的Spring MVC裆甩。

基于Java的配置冗锁,使用帶有@EnableWebMvc注解的類:

package spittr.web;

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

@Configuration
@EnableWebMvc
public class WebConfig {
}

但還有其他問題要解決:

  • 沒有配置視圖解析器。Spring默認(rèn)會使用BeanNameView-Resolver嗤栓,這個視圖解析器會查找ID與視圖名稱匹配的bean冻河,并且查找的bean要實現(xiàn)View接口,以這樣的方式來解析視圖抛腕。
  • 沒有啟用組件掃描芋绸。Spring只能找到顯式聲明在配置類中的控制器媒殉。
  • DispatcherServlet會映射為應(yīng)用的默認(rèn)Servlet担敌,所以它會處理所有的請求,包括對靜態(tài)資源的請求廷蓉,如圖片和樣式表(可能并不是想要的效果)全封。

需要為這個最小的Spring MVC配置再加上一些內(nèi)容,從而讓它變得真正有用桃犬。

package spittr.web;

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.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc
@ComponentScan("spittr.web")
public class WebConfig extends WebMvcConfigurerAdapter {

  @Bean
  public ViewResolver viewResolver() {
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp");
    return resolver;
  }
  
  @Override
  public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable();
  }
  
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // TODO Auto-generated method stub
    super.addResourceHandlers(registry);
  }

}

WebConfig現(xiàn)在添加了@Component-Scan注解刹悴,因此將會掃描spitter.web包來查找組件≡芟荆控制器如果帶有@Controller注解土匀,會使其成為組件掃描時的候選bean。不需要在配置類中顯式聲明任何的控制器形用。

接著就轧,添加了一個ViewResolver bean。具體來講田度,是InternalResourceViewResolver妒御。它會查找JSP文件,在查找的時候镇饺,會在視圖名稱上加一個特定的前綴和后綴(例如乎莉,名為home的視圖將會解析為/WEB-INF/views/home.jsp)。

新的WebConfig類還擴(kuò)展了WebMvcConfigurerAdapter并重寫了其configureDefaultServletHandling()方法。通過調(diào)用DefaultServletHandlerConfigurerenable()方法惋啃,要求DispatcherServlet將對靜態(tài)資源的請求轉(zhuǎn)發(fā)到Servlet容器中默認(rèn)的Servlet上哼鬓,而不是使用DispatcherServlet本身來處理此類請求。

Web相關(guān)的配置通過DispatcherServlet創(chuàng)建的應(yīng)用上下文都已經(jīng)配置好了边灭,因此現(xiàn)在的RootConfig相對很簡單:

package spittr.config;

import java.util.regex.Pattern;

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;

@Configuration
@ComponentScan(basePackages={"spitter"}, 
    excludeFilters={
        @Filter(type=FilterType.ANNOTATION, value=EnableWebMVC.class)
    })
public class RootConfig {
}

RootConfig使用了@ComponentScan注解魄宏。這樣的話,就有很多機(jī)會用非Web的組件來充實完善RootConfig存筏。

現(xiàn)在宠互,已經(jīng)可以開始使用Spring MVC構(gòu)建Web應(yīng)用了。

Spittr應(yīng)用簡介

因為從Twitter借鑒了靈感并且通過Spring來進(jìn)行實現(xiàn)椭坚,所以它就有了一個名字:Spitter予跌。再進(jìn)一步,應(yīng)用網(wǎng)站命名中流行的模式善茎,如Flickr券册,我們?nèi)サ糇帜竐,這樣的話垂涯,我們就將這個應(yīng)用稱為Spittr烁焙。這個名稱也 有助于區(qū)分應(yīng)用名稱和領(lǐng)域類型,因為我們將會創(chuàng)建一個名為Spitter的領(lǐng)域類耕赘。

Spittr應(yīng)用有兩個基本的領(lǐng)域概念:Spitter(應(yīng)用的用戶)和Spittle(用戶發(fā)布的簡短狀態(tài)更新)骄蝇。當(dāng)我們在書中完善Spittr應(yīng)用的功能時,將會介紹這兩個領(lǐng)域概念操骡。在本章中九火,會構(gòu)建應(yīng)用的Web層,創(chuàng)建展現(xiàn)Spittle的控制器以及處理用戶注冊成為Spitter的表册招。

編寫基本的控制器

在Spring MVC中岔激,控制器只是方法上添加了@RequestMapping注解的類,這個注解聲明了它們所要處理的請求是掰。

假設(shè)控制器類要處理對“/”的請求虑鼎,并渲染應(yīng)用的首頁:

package spittr.web;

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

@Controller
public class HomeController {

    @RequestMapping(value = "/", method = GET)
    public String home(){
        return "home";
    }
}

HomeController是一個構(gòu)造型(stereotype)的注解,它基于@Component注解键痛,輔助實現(xiàn)組件掃描炫彩。因為HomeController帶有@Controller注解,因此組件掃描器會自動找到HomeController散休,并將其聲明為Spring應(yīng)用上下文中的一個bean媒楼。

可以讓HomeController帶有@Component注解,效果相同戚丸,但是表意性會差一點划址。

HomeController唯一的一個方法扔嵌,也就是home()方法,帶有@RequestMapping注解夺颤。它的value屬性指定了這個方法所要處理的請求路徑痢缎,method屬性細(xì)化了它所處理的HTTP方法。當(dāng)收到對“/”的HTTP GET請求時世澜,就會調(diào)用home()方法独旷。

home()方法返回了一個String類型的“home”。這個String將會被Spring MVC解讀為要渲染的視圖名稱寥裂。DispatcherServlet會要求視圖解析器將這個邏輯名稱解析為實際的視圖嵌洼。

根據(jù)視圖解析器InternalResourceViewResolver的配置,視圖名“home”將會解析為“/WEB-INF/views/home.jsp”路徑的JSP封恰。

定義一個簡單的首頁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>

這個JSP提供了兩個鏈接:一個是查看Spittle列表麻养,另一個是在應(yīng)用中進(jìn)行注冊。下圖展現(xiàn)了此時的首頁的樣子:

運行效果

現(xiàn)在诺舔,對這個控制器發(fā)起一些請求鳖昌,看一下它是否能夠正常工作。測試控制器最直接的辦法可能就是構(gòu)建并部署應(yīng)用低飒,然后通過瀏覽器對其進(jìn)行訪問许昨,但是自動化測試可提供更快的反饋和更一致的獨立結(jié)果革答。

測試控制器

編寫一個簡單的類來測試HomeController:

package java.spittr.web;

import static org.junit.Assert.assertEquals;
import org.junit.Test;
import spittr.web.HomeController;

public class HomeControllerTest {
    @Test
    public void testHomePage() throws Exception {
        HomeController controller = new HomeController();
        assertEquals("home",controller.home());
    }

}

代碼只測試了home()方法中會發(fā)生什么扒磁。在測試中會直接調(diào)用home()方法,并斷言返回包含“home”值的String萍聊。它完全沒有站在Spring MVC控制器的視角進(jìn)行測試崭倘。這個測試沒有斷言當(dāng)接收到針對“/”的GET請求時會調(diào)用home()方法翼岁。因為它返回的值就是“home”,所以也沒有真正判斷home是視圖的名稱司光。

從Spring 3.2開始,我們可以按照控制器的方式來測試Spring MVC中的控制器了悉患,而不僅僅是作為POJO進(jìn)行測試残家。Spring現(xiàn)在包含了一種mock Spring MVC并針對控制器執(zhí)行HTTP請求的機(jī)制。這樣的話售躁,在測試控制器的時候坞淮,就沒有必要再啟動Web服務(wù)器和Web瀏覽器了。

重寫HomeControllerTest并使用Spring MVC中新的測試特性:

package spittr.web;

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.*;
import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import spittr.web.HomeController;

public class HomeControllerTest {
    @Test
    public void testHomePage() throws Exception {
        HomeController controller = new HomeController();
        MockMvc mockMvc = standaloneSetup(controller).build();
        mockMvc.perform(get("/")).andExpect(view().name("home"));
    }

新版本的測試比之前更加完整地測試了HomeController陪捷。這次我們不是直接調(diào)用home()方法并測試它的返回值回窘,而是發(fā)起了對“/”的GET請求,并斷言結(jié)果視圖的名稱為home市袖。它首先傳遞一個HomeController實例到MockMvcBuilders.standaloneSetup()并調(diào)用build()來構(gòu)建MockMvc實例啡直。然后它使用MockMvc實例來執(zhí)行針對“/”的GET請求并設(shè)置期望得到的視圖名稱烁涌。

定義類級別的請求處理

拆分@RequestMapping,并將其路徑映射部分放到類級別上:

package spittr.web;

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

@Controller
@RequestMapping(value = "/")
public class HomeController {

    @RequestMapping(method = GET)
    public String home(){
        return "home";
    }
}

新版本的HomeController中酒觅,路徑現(xiàn)在被轉(zhuǎn)移到類級別的@RequestMapping上,而HTTP方法依然映射在方法級別上舷丹。當(dāng)控制器在類級別上添加@RequestMapping注解時抒钱,這個注解會應(yīng)用到控制器的所有處理器方法上。處理器方法上的@RequestMapping注解會對類級別上的@RequestMapping的聲明進(jìn)行補充颜凯。

HomeController只有一個控制器方法谋币。與類級別的@Request-Mapping合并之后,這個方法的@RequestMapping表明home()將會處理對“/”路徑的GET請求症概。

測試代碼可以確保在這個修改過程中瑞信,沒有對原有的功能造成破壞。

當(dāng)我們在修改@RequestMapping時穴豫,還可以對HomeController做另外一個變更@RequestMapping的value屬性能夠接受一個String類型的數(shù)組凡简。到目前為止,我們給它設(shè)置的都是一個String類型的“/”精肃。但是秤涩,我們還可以將它映射到對“/homepage”的請求,只需將類級別的@RequestMapping改為如下所示:

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

現(xiàn)在司抱,HomeControllerhome()方法能夠映射到對“/”和“/homepage”的GET請求筐眷。

傳遞模型數(shù)據(jù)到視圖中

大多數(shù)的控制器并不像HomeController那么簡單。在Spittr應(yīng)用中习柠,我們需要有一個頁面展現(xiàn)最近提交的Spittle列表匀谣。因此,我們需要一個新的方法來處理這個頁面资溃。

首先武翎,需要定義一個數(shù)據(jù)訪問的Repository。為了實現(xiàn)解耦以及避免陷入數(shù)據(jù)庫訪問的細(xì)節(jié)之中溶锭,我們將Repository定義為一個接口宝恶,并在稍后實現(xiàn)它(第10章中)。此時趴捅,我們只需要一個能夠獲取Spittle列表的Repository垫毙,如下所示的SpittleRepository功能已經(jīng)足夠了:

package spittr.data;
import java.util.List;
import spittr.Spittle;

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

findSpittles()方法接受兩個參數(shù)。其中max參數(shù)代表所返回的Spittle中拱绑,Spittle ID屬性的最大值综芥,而count參數(shù)表明要返回多少個Spittle對象。為了獲得最新的20個Spittle對象猎拨,我們可以這樣調(diào)用findSpittles():

List<Spittle> recent = spittleRepository.findSpittles(Long.MAX_VALUE,20);

現(xiàn)在膀藐,定義一個Spittle類屠阻,讓Spittle類盡可能的簡單。它的屬性包括消息內(nèi)容消请、時間戳以及Spittle發(fā)布時對應(yīng)的經(jīng)緯度:

package spittr;

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

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.message = message;
        this.time = time;
        this.longitude = longitude;
        this.latitude = latitude;
    }

    public Long getId() {
        return id;
    }

    public String getMessage() {
        return message;
    }

    public Date getTime() {
        return time;
    }

    public Double getLatitude() {
        return latitude;
    }

    public Double getLongitude() {
        return longitude;
    }

    @Override
    public boolean equals(Object that){
        return EqualsBuilder.reflectionEquals(this, that, "id", "time")
    }
    @Override
    public int hashCode(){
        return HashCodeBuilder.reflectionHashCode(this,  "id", "time")
    }

}

就大部分內(nèi)容來看栏笆,Spittle就是一個基本的POJO數(shù)據(jù)對象沒有什么復(fù)雜的。唯一要注意的是臊泰,我們使用Apache Common Lang包來實現(xiàn)equals()hashCode()方法蛉加。這些方法除了常規(guī)的作用以外,當(dāng)我們?yōu)榭刂破鞯奶幚砥鞣椒ň帉憸y試時缸逃,它們也是有用的针饥。

使用Spring的MockMvc來斷言新的處理器方法中所期望的行為:

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

這個測試首先會創(chuàng)建SpittleRepository接口的mock實現(xiàn),這個實現(xiàn)會從它的findSpittles()方法中返回20個Spittle對象需频。然后丁眼,它將這個Repository注入到一個新的SpittleController實例中,然后創(chuàng)建MockMvc并使用這個控制器昭殉。

這個測試在MockMvc構(gòu)造器上調(diào)用了setSingleView()苞七。這樣的話,mock框架就不用解析控制器中的視圖名了挪丢。在很多場景中蹂风,其實沒有必要這樣做。但是對于這個控制器方法乾蓬,視圖名與請求路徑是非常相似的惠啄,這樣按照默認(rèn)的視圖解析規(guī)則時,MockMvc就會發(fā)生失敗任内,因為無法區(qū)分視圖路徑和控制器的路徑撵渡。在這個測試中,構(gòu)建InternalResourceView時所設(shè)置的實際路徑是無關(guān)緊要的死嗦,但我們將其設(shè)置為與InternalResourceViewResolver配置一致趋距。

這個測試對“/spittles”發(fā)起GET請求,然后斷言視圖的名稱為spittles并且模型中包含名為spittleList的屬性越走,在spittleList中包含預(yù)期的內(nèi)容棚品。

如果此時運行測試的話,它將會失敗廊敌。它不是運行失敗,而是在編譯的時候就會失敗门怪。這是因為我們還沒有編寫SpittleController÷獬海現(xiàn)在創(chuàng)建一個SpittleController:

package 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 spittr.data.SpittleRepository;

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

    @Autowired
    public 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";
    }
}

可以看到SpittleController有一個構(gòu)造器,這個構(gòu)造器使用了@Autowired注解掷空,用來注入SpittleRepository肋殴。這個SpittleRepository又用在spittles()方法中囤锉,用來獲取最新的spittle列表。

在spittles()方法中給定了一個Model作為參數(shù)护锤。這樣官地,spittles()方法就能將Repository中獲取到的Spittle列表填充到模型中。Model實際上就是一個Map(也就是key-value對的集合)烙懦,它會傳遞給視圖驱入,這樣數(shù)據(jù)就能渲染到客戶端了。當(dāng)調(diào)用addAttribute()方法并且不指定key的時候氯析,那么key會根據(jù)值的對象類型推斷確定亏较。在本例中,因為它是一個List<Spittle>掩缓,因此雪情,鍵將會推斷為spittleList。

spittles()方法所做的最后一件事是返回spittles作為視圖的名字你辣,這個視圖會渲染模型巡通。

如果希望顯式聲明模型的key的話,也可以這樣進(jìn)行指定:

    @RequestMapping(method = RequestMethod.GET)
    public String spittles(Model model) {
        model.addAttribute("spittleList",
                spittleRepository.findSpittles(Long.MAX_VALUE, 20));
        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));
        return "spittles";
    }

還有一種編寫spittles()的方式:

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

這個版本與其他的版本有些差別宴凉。它并沒有返回視圖名稱,也沒有顯式地設(shè)定模型蠢熄,這個方法返回的是Spittle列表跪解。當(dāng)處理器方法像這樣返回對象或集合時,這個值會放到模型中签孔,模型的key會根據(jù)其類型推斷得出(在本例中叉讥,也就是spittleList)。

而邏輯視圖的名稱將會根據(jù)請求路徑推斷得出饥追。因為這個方法處理針對“/spittles”的GET請求图仓,因此視圖的名稱將會是spittles。

不管選擇哪種方式來編寫spittles()方法但绕,所得到的結(jié)果都是相同的救崔。模型中會存儲一個Spittle列表,key為spittleList捏顺,然后這個列表會發(fā)送到名為spittles的視圖中六孵。按照我們配置InternalResourceViewResolver的方式,視圖的JSP將會是“/WEB-INF/views/spittles.jsp”幅骄。

現(xiàn)在劫窒,數(shù)據(jù)已經(jīng)放到了模型中,在JSP中該如何訪問它呢拆座?實際上主巍,
當(dāng)視圖是JSP的時候冠息,模型數(shù)據(jù)會作為請求屬性放到請求(request)
之中。因此孕索,在spittles.jsp文件中可以使用JSTL(JavaServer Pages
Standard Tag Library)的<c:forEach>標(biāo)簽渲染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>

運行結(jié)果如圖逛艰,由于沒有初始化數(shù)據(jù),因此結(jié)果列表為空:

接受請求的輸入

盡管SpittleController很簡單搞旭,但是它依然比HomeController更進(jìn)一步了散怖。但沒有處理任何形式的輸入。現(xiàn)在选脊,我們要擴(kuò)展SpittleController杭抠,讓它從客戶端接受一些輸入。

Spring MVC允許以多種方式將客戶端中的數(shù)據(jù)傳送到控制器的處理器方法中恳啥,包括:

  • 查詢參數(shù)(Query Parameter)偏灿。
  • 表單參數(shù)(Form Parameter)。
  • 路徑變量(Path Variable)钝的。
處理查詢參數(shù)

帶有查詢參數(shù)的請求翁垂,這也是客戶端往服務(wù)器端發(fā)送數(shù)據(jù)時,最簡單和最直接的方式硝桩。

在Spittr應(yīng)用中沿猜,我們可能需要處理的一件事就是展現(xiàn)分頁的Spittle列表。在現(xiàn)在的SpittleController中碗脊,它只能展現(xiàn)最新的Spittle啼肩,沒有辦法向前翻頁查看以前編寫的Spittle歷史記錄。如果想讓用戶每次都能查看某一頁的Spittle歷史衙伶,那么就需要提供一種方式讓用戶傳遞參數(shù)進(jìn)來祈坠,進(jìn)而確定要展現(xiàn)哪些Spittle集合。

假設(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ù)量)隘击。

首先添加一個測試侍芝,這個測試反映了新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())));
  }

測試方法關(guān)鍵針對“/spittles”發(fā)送GET請求埋同,同時還傳入了max和count參數(shù)州叠。它測試了這些參數(shù)存在時的處理器方法,而另一個測試方法則測試了沒有這些參數(shù)時的情景凶赁。這兩個測試就緒后咧栗,我們就能確保不管控制器發(fā)生什么樣的變化,它都能夠處理這兩種類型的請求:

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

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,
            @RequestParam(value="count", defaultValue="20") int count) {
        return spittleRepository.findSpittles(max, count);
    }

現(xiàn)在集嵌,如果max參數(shù)沒有指定的話萝挤,它將會是Long類型的最大值。因為查詢參數(shù)都是String類型的根欧,因此defaultValue屬性需要String類型的值怜珍。因此,使用Long.MAX_VALUE是不行的凤粗。我們可以將Long.MAX_VALUE轉(zhuǎn)換為名為MAX_LONG_-AS_STRING的String類型常量:

private static final String MAX_LONG_AS_STRING = 
        Long.toString(Long.MAX_VALUE);

盡管defaultValue屬性給定的是String類型的值酥泛,但是當(dāng)綁定到
方法的max參數(shù)時,它會轉(zhuǎn)換為Long類型嫌拣。

如果請求中沒有count參數(shù)柔袁,count參數(shù)會被設(shè)為默認(rèn)值20。

通過路徑參數(shù)接受輸入

在構(gòu)建面向資源的控制器時亭罪,這種方式就是將傳遞參數(shù)作為請求路徑的一部分瘦馍,實現(xiàn)信息的輸入。

假設(shè)應(yīng)用程序需要根據(jù)給定的ID來展現(xiàn)某一個Spittle記錄应役。其中一種方案就是編寫處理器方法情组,通過使用@RequestParam注解,讓它接受ID作為查詢參數(shù):

  @RequestMapping(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拨扶。

現(xiàn)在將這個需求轉(zhuǎn)換為一個測試:

@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));
  }

這個測試構(gòu)建了一個mock Repository、一個控制器和MockMvc茁肠,這與本章中我們所編寫的其他測試很類似患民。這個測試的最后幾行,它對“/spittles/12345”發(fā)起GET請求垦梆,然后斷言視圖的名稱是spittle匹颤,并且預(yù)期的Spittle對象放到了模型之中。由于我們還沒有為這種請求實現(xiàn)處理器方法托猩,因此這個請求將會失敗印蓖。但是,可以通過為SpittleController添加新的方法來修正這個失敗的測試京腥。

到目前為止赦肃,在我們編寫的控制器中,所有的方法都映射到了(通過@RequestMapping)靜態(tài)定義好的路徑上绞旅。但是摆尝,如果想讓這個測試通過的話,我們編寫的@RequestMapping要包含變量部分因悲,這部分代表了Spittle ID堕汞。

為了實現(xiàn)這種路徑變量,Spring MVC允許我們在@RequestMapping路徑中添加占位符晃琳。占位符的名稱要用大括號(“{”和“}”)括起來讯检。路徑中的其他部分要與所處理的請求完全匹配,但是占位符部分可以是任意的值卫旱。

下面的處理器方法使用了占位符人灼,將Spittle ID作為路徑的一部分:

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

spittle()方法的spittleId參數(shù)上添加了@PathVariable("spittleId")注解,這表明在請求路徑中顾翼,不管占位符部分的值是什么都會傳遞到處理器方法的spittleId參數(shù)中投放。如果對“/spittles/54321”發(fā)送GET請求,那么將會把“54321”傳遞進(jìn)來适贸,作為spittleId的值灸芳。

因為方法的參數(shù)名碰巧與占位符的名稱相同,因此可以去掉@PathVariable中的value屬性:

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

如果@PathVariable中沒有value屬性拜姿,它會假設(shè)占位符的名稱與方法的參數(shù)名相同烙样。這能夠讓代碼稍微簡潔一些。但如果你想要重命名參數(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>

如果傳遞請求中少量的數(shù)據(jù)卦停,查詢參數(shù)和路徑變量是很合適的。但通常還需要傳遞很多的數(shù)據(jù)(也許是表單提交的數(shù)據(jù))恼蓬,那查詢參數(shù)顯得有些笨拙和受限了惊完。

處理表單

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.web.bind.annotation.RequestMapping;

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

showRegistrationForm()方法的@RequestMapping注解以及類級別上的@RequestMapping注解組合起來骡显,聲明了這個方法要處理的是針對“/spitter/register”的GET請求疆栏。按照配置InternalResourceViewResolver,這意味著會使用“/WEB-INF/ views/registerForm.jsp”這個JSP來渲染注冊表單惫谤。

盡管showRegistrationForm()方法非常簡單壁顶,但測試依然需要覆蓋到它:

package spittr.web;

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.*;

import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;

public class SpitterControllerTest {
    @Test
    public void shouldShowRegistration() throws Exception {
        SpitterRepository mockRepository = mock(SpitterRepository.class);
        SpitterController controller = new SpitterController(mockRepository);
        MockMvc mockMvc = standaloneSetup(controller).build();
        mockMvc.perform(get("/spitter/register"))
                .andExpect(view().name("registerForm"));
    }

對“/spitter/register”發(fā)送GET請求,然后斷言結(jié)果的視圖名為registerForm溜歪。

因為視圖的名稱為registerForm若专,所以JSP的名稱需要為registerForm.jsp。這個JSP必須要包含一個HTML<form>標(biāo)簽蝴猪,在這個標(biāo)簽中用戶輸入注冊應(yīng)用的信息:

<%@ 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/>
            Email: <input type="email" name="email" /><br/>
            Username: <input type="text" name="username" /><br/>
            Password: <input type="password" name="password" /><br/>
            <input type="submit" value="Register" />
        </form>
    </body>
</html>

表單域中記錄用戶的名字调衰、姓氏、用戶名以及密碼拯腮,還包含一個提交表單的按鈕窖式。在瀏覽器渲染之后,它的樣子如下所示动壤。

這里的<form>標(biāo)簽中并沒有設(shè)置action屬性萝喘。在這種情況下,當(dāng)表單提交時,會提交到與展現(xiàn)時相同的URL路徑上阁簸。即提交到“/spitter/register”上爬早。

意味著要在服務(wù)器端處理該HTTP POST請求。需要在Spitter-Controller中再添加一個方法來處理這個表單提交启妹。

編寫處理表單的控制器

當(dāng)處理注冊表單的POST請求時筛严,控制器需要接受表單數(shù)據(jù)并將表單數(shù)據(jù)保存為Spitter對象。為了防止重復(fù)提交饶米,應(yīng)該將瀏覽器重定向到新創(chuàng)建用戶的基本信息頁面桨啃。這些行為通過下面的shouldProcessRegistration()進(jìn)行了測試:

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

在構(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ù)筒主。

通過shouldProcessRegistration()方法實現(xiàn)處理表單提交的控制器方法:

package spittr.web;

import static org.springframework.web.bind.annotation.RequestMethod.*;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
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 SpitterController(SpitterRepository spitterRepository) {
    this.spitterRepository = spitterRepository;
  }
  
  @RequestMapping(value="/register", method=GET)
  public String showRegistrationForm() {
    return "registerForm";
  }
  
  @RequestMapping(value="/register", method=POST)
  public String processRegistration(
      @Valid Spitter spitter, 
      Errors errors) {
    if (errors.hasErrors()) {
      return "registerForm";
    }
    
    spitterRepository.save(spitter);
    return "redirect:/spitter/" + spitter.getUsername();
  }
  
  @RequestMapping(value="/{username}", method=GET)
  public String showSpitterProfile(@PathVariable String username, Model model) {
    Spitter spitter = spitterRepository.findByUsername(username);
    model.addAttribute(spitter);
    return "profile";
  }
  
}

新創(chuàng)建的processRegistration()方法关噪,它接受一個Spitter對象作為參數(shù)。這個對象有firstName乌妙、lastName使兔、username和password屬性,這些屬性將會使用請求中同名的參數(shù)進(jìn)行填充藤韵。

當(dāng)使用Spitter對象調(diào)用processRegistration()方法時虐沥,它會調(diào)用SpitterRepository的save()方法,SpitterRepository是在SpitterController的構(gòu)造器中注入進(jìn)來的泽艘。

processRegistration()方法最后返回一個String類型欲险,用來指定視圖。但是這個視圖格式和以前我們所看到的視圖有所不同匹涮。不僅返回了視圖的名稱供視圖解析器查找目標(biāo)視圖天试,返回的值還帶有重定向的格式。

當(dāng)InternalResourceViewResolver看到視圖格式中的“redirect:”前綴時然低,它就知道要將其解析為重定向的規(guī)則喜每,而不是視圖的名稱务唐。

本例中,它將會重定向到用戶基本信息的頁面带兜。

除了“redirect:”枫笛,InternalResourceViewResolver還能識別“forward:”前綴。當(dāng)它發(fā)現(xiàn)視圖格式中以“forward:”作為前綴時刚照,請求將會前往(forward)指定的URL路徑刑巧,而不是重定向。

盡管HttpServletResponse.sendRedirect方法和RequestDispatcher.forward方法都可以讓瀏覽器獲 得另外一個URL所指向的資源无畔,但兩者的內(nèi)部運行機(jī)制有著很大的區(qū)別啊楚。
Forward和Redirect代表了兩種請求轉(zhuǎn)發(fā)方式:直接轉(zhuǎn)發(fā)和間接轉(zhuǎn)發(fā)。

  • 直接轉(zhuǎn)發(fā)方式(Forward)檩互,客戶端和瀏覽器只發(fā)出一次請求特幔,Servlet、HTML闸昨、JSP或其它信息資源,由第二個信息資源響應(yīng)該請求薄风,在請求對象request中饵较,保存的對象對于每個信息資源是共享的。轉(zhuǎn)發(fā)頁面和轉(zhuǎn)發(fā)到的頁面可以共享request的數(shù)據(jù)遭赂。
  • 間接轉(zhuǎn)發(fā)方式(Redirect)實際是兩次HTTP請求循诉,服務(wù)器端在響應(yīng)第一次請求的時候,讓瀏覽器再向另外一個URL發(fā)出請求撇他,從而達(dá)到轉(zhuǎn)發(fā)的目的茄猫。轉(zhuǎn)發(fā)頁面和轉(zhuǎn)發(fā)到的頁面不能共享request的數(shù)據(jù)

因為我們重定向到了用戶基本信息頁面困肩,那么我們應(yīng)該往SpitterController中添加一個處理器方法showSpitterProfile()划纽,用來處理對基本信息頁面的請求:

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

SpitterRepository通過用戶名獲取一個Spitter對象,showSpitter-Profile()得到這個對象并將其添加到模型中锌畸,然后返回profile勇劣,也就是基本信息頁面的邏輯視圖名√对妫基本信息視圖實現(xiàn)如下:

<%@ 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>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>
</html>

基本信息視圖渲染效果如下圖所示:

如果表單中沒有發(fā)送username或password的話比默,如果firstName或lastName的值為空或太長的話,程序會出現(xiàn)問題盆犁,為此需要為表單提交添加校驗命咐,避免數(shù)據(jù)呈現(xiàn)的不一致性。

校驗表單

如果用戶在提交表單的時候谐岁,username或password文本域為空的話醋奠,會導(dǎo)致在新建Spitter對象中榛臼,username或password是空的String。如果這種現(xiàn)象不處理的話钝域,這將會出現(xiàn)安全問題讽坏,不管是誰只要提交一個空的表單就能登錄應(yīng)用。

同時應(yīng)該阻止用戶提交空的firstName和/或lastName例证,使應(yīng)用僅在一定程度上保持匿名性路呜。可以限制這些輸入域值的長度织咧,保持它們的值在一個合理的長度范圍胀葱,避免這些輸入域的誤用。

從Spring 3.0開始,在Spring MVC中提供了對Java校驗API的支持艇搀。在Spring MVC中使用Java校驗API辟宗,不需要什么額外的配置晨抡。只要保證在類路徑下包含這個Java API的實現(xiàn)即可,比如Hibernate Validator。

Java校驗API定義了多個注解赤拒,這些注解可以放到屬性上,從而限制這些屬性的值汛闸。所有的注解都位于javax.validation.constraints包中畸肆。下表列出了這些校驗注解:

除了表中的注解宙址,Java校驗API的實現(xiàn)可能還會提供額外的校驗注解轴脐。同時,也可以定義自己的限制條件抡砂。

修改Spitter類大咱,為屬性添加校驗注解:

package spittr;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

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;

    . . .
}

現(xiàn)在,Spitter的所有屬性都添加了@NotNull注解注益,以確保它們的值不為null碴巾。屬性上也添加了@Size注解以限制它們的長度在最大值和最小值之間。對Spittr應(yīng)用來說丑搔,這意味著用戶必須要填完注冊表單厦瓢,并且值的長度要在給定的范圍內(nèi)提揍。

接下來要修改processRegistration()方法來應(yīng)用校驗功能。啟用校驗功能的processRegistration()如下所示:

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

        spitterRepository.save(spitter);
        return "redirect:/spitter/" + spitter.getUsername();
    }

Spitter參數(shù)添加了@Valid注解煮仇,這會告知Spring劳跃,需要確保這個對象滿足校驗限制。

在Spitter屬性上添加校驗限制并不能阻止表單提交浙垫。即便用戶輸入不符合規(guī)范
刨仑,processRegistration()方法依然會被調(diào)用。

如果有校驗出現(xiàn)錯誤的話绞呈,那么這些錯誤可以通過Errors對象進(jìn)行訪問贸人,現(xiàn)在這個對象作為processRegistration()方法的參數(shù)。(需要注意佃声,Errors參數(shù)要緊跟在帶有@Valid注解的參數(shù)后面艺智。)

processRegistration()方法所做的第一件事就是調(diào)用Errors.hasErrors()來檢查是否有錯誤。如果有錯誤的話圾亏,Errors.hasErrors()將會返回到registerForm十拣,也就是注冊表單的視圖。這能夠讓用戶的瀏覽器重新回到注冊表單頁面志鹃,所以他們能夠修正錯誤夭问,然后重新嘗試提交。

如果沒有錯誤的話曹铃,Spitter對象將會通過Repository進(jìn)行保存缰趋,重定向到基本信息頁面。

小結(jié)

Spring有一個強大靈活的Web框架陕见。借助于注解秘血,Spring MVC提供了近似于POJO的開發(fā)模式,這使得開發(fā)處理請求的控制器變得非常簡單评甜,同時也易于測試灰粮。

當(dāng)編寫控制器的處理器方法時,Spring MVC極其靈活忍坷。如果你的處理器方法需要內(nèi)容的話粘舟,只需將對應(yīng)的對象作為參數(shù),而它不需要的內(nèi)容佩研,則沒有必要出現(xiàn)在參數(shù)列表中柑肴。這樣,就為請求處理帶來了無限的可能性韧骗,同時還能保持一種簡單的編程模型嘉抒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市袍暴,隨后出現(xiàn)的幾起案子些侍,更是在濱河造成了極大的恐慌隶症,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件岗宣,死亡現(xiàn)場離奇詭異蚂会,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)耗式,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門胁住,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人刊咳,你說我怎么就攤上這事彪见。” “怎么了娱挨?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵余指,是天一觀的道長。 經(jīng)常有香客問我跷坝,道長酵镜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任柴钻,我火速辦了婚禮淮韭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贴届。我一直安慰自己躬窜,他們只是感情好羹幸,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布高诺。 她就那樣靜靜地躺著蝗拿,像睡著了一般翻擒。 火紅的嫁衣襯著肌膚如雪宠进。 梳的紋絲不亂的頭發(fā)上可训,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天淤堵,我揣著相機(jī)與錄音耀鸦,去河邊找鬼柬批。 笑死,一個胖子當(dāng)著我的面吹牛袖订,可吹牛的內(nèi)容都是我干的氮帐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼洛姑,長吁一口氣:“原來是場噩夢啊……” “哼上沐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起楞艾,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤参咙,失蹤者是張志新(化名)和其女友劉穎龄广,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蕴侧,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡择同,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了净宵。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片敲才。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖择葡,靈堂內(nèi)的尸體忽然破棺而出紧武,到底是詐尸還是另有隱情,我是刑警寧澤敏储,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布阻星,位于F島的核電站,受9級特大地震影響虹曙,放射性物質(zhì)發(fā)生泄漏迫横。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一酝碳、第九天 我趴在偏房一處隱蔽的房頂上張望矾踱。 院中可真熱鬧,春花似錦疏哗、人聲如沸呛讲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贝搁。三九已至,卻和暖如春芽偏,著一層夾襖步出監(jiān)牢的瞬間雷逆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工污尉, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留膀哲,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓被碗,卻偏偏與公主長得像某宪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子锐朴,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

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