【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)注點(diǎ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瀏覽器中點(diǎn)擊鏈接或提交表單的時候,請求就開始工作了。請求會將信息從一個地方帶到另一個地方,就像是快遞投送員。

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

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

在請求離開瀏覽器時 ①,會帶有用戶所請求內(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新版本的功能增強(qiáng),使用Spring MVC變得非常簡單了。現(xiàn)在,使用最簡單的方式來配置Spring MVC:所要實現(xiàn)的功能僅限于運(yù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的功能增強(qiáng),這種方式已經(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包來查找組件??刂破魅绻麕в?code>@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注解,效果相同,但是表意性會差一點(diǎn)。

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)了此時的首頁的樣子:

運(yùn)行效果

現(xiàn)在,對這個控制器發(fā)起一些請求,看一下它是否能夠正常工作。測試控制器最直接的辦法可能就是構(gòu)建并部署應(yīng)用,然后通過瀏覽器對其進(jìn)行訪問,但是自動化測試可提供更快的反饋和更一致的獨(dú)立結(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)行補(bǔ)充。

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)容。

如果此時運(yùn)行測試的話,它將會失敗。它不是運(yùn)行失敗,而是在編譯的時候就會失敗。這是因為我們還沒有編寫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>

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

接受請求的輸入

盡管SpittleController很簡單,但是它依然比HomeController更進(jìn)一步了。但沒有處理任何形式的輸入?,F(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)部運(yùn)行機(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,也就是基本信息頁面的邏輯視圖名?;拘畔⒁晥D實現(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有一個強(qiáng)大靈活的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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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