天天看點

第五章 建構Spring Web應用程式5.1 Spring MVC起步5.2 編寫基本的控制器5.3 接受請求的輸入5.4 處理表單

Spring MVC基于模型-視圖-控制器(Model-View-Controller,MVC)模式實作。

5.1 Spring MVC起步

5.1.1 跟蹤Spring MVC的請求

每當使用者在Web浏覽器中點選連結或送出表單的時候,請求就開始工作了。請求是一個十分繁忙的家夥。從離開浏覽器開始到擷取響應傳回,它會經曆好多站,在每站都會留下一些資訊同時也會帶上其他資訊。

第五章 建構Spring Web應用程式5.1 Spring MVC起步5.2 編寫基本的控制器5.3 接受請求的輸入5.4 處理表單

在請求離開浏覽器時 ,會帶有使用者所請求内容的資訊,至少會包含請求的URL。但是還可能帶有其他的資訊,例如使用者送出的表單資訊。

  1. 請求旅程的第一站是Spring的DispatcherServlet。與大多數基于Java的Web架構一樣,Spring MVC所有的請求都會通過一個前端控制器(front controller)Servlet。前端控制器是常用的Web應用程式模式,在這裡一個單執行個體的Servlet将請求委托給應用程式的其他元件來執行實際的處理。在Spring MVC中,DispatcherServlet就是前端控制器。
  2. DispatcherServlet的任務是将請求發送給Spring MVC控制器(controller)。控制器是一個用于處理請求的Spring元件。在典型的應用程式中可能會有多個控制器,DispatcherServlet需要知道應該将請求發送給哪個控制器。是以DispatcherServlet以會查詢一個或多個處理器映射(handler mapping) 來确定請求的下一站在哪裡。處理器映射會根據請求所攜帶的URL資訊來進行決策。
  3. 一旦選擇了合适的控制器,DispatcherServlet會将請求發送給選中的控制器 。到了控制器,請求會卸下其負載(使用者送出的資訊)并耐心等待控制器處理這些資訊。(實際上,設計良好的控制器本身隻處理很少甚至不處理工作,而是将業務邏輯委托給一個或多個服務對象進行處理。)
  4. 控制器在完成邏輯處理後,通常會産生一些資訊,這些資訊需要傳回給使用者并在浏覽器上顯示。這些資訊被稱為模型(model)。不過僅僅給使用者傳回原始的資訊是不夠的——這些資訊需要以使用者友好的方式進行格式化,一般會是HTML。是以,資訊需要發送給一個視圖(view),通常會是JSP。
  5. 控制器所做的最後一件事就是将模型資料打包,并且标示出用于渲染輸出的視圖名。它接下來會将請求連同模型和視圖名發送回DispatcherServlet 。
  6. 這樣,控制器就不會與特定的視圖相耦合,傳遞給DispatcherServlet的視圖名并不直接表示某個特定的JSP。實際上,它甚至并不能确定視圖是JSP。相反,它僅僅傳遞了一個邏輯名稱,這個名字将會用來查找産生結果的真正視圖。DispatcherServlet将會使用視圖解析器(view resolver) 來将邏輯視圖名比對為一個特定的視圖實作,它可能是也可能不是JSP。
  7. 既然DispatcherServlet已經知道由哪個視圖渲染結果,那請求的任務基本上也就完成了。它的最後一站是視圖的實作(可能是JSP) ,在這裡它傳遞模型資料。請求的任務就完成了。視圖将使用模型資料渲染輸出,這個輸出會通過響應對象傳遞給用戶端(不會像聽上去那樣寫死) 。

5.1.2 搭建Spring MVC

配置DispatcherServlet

DispatcherServlet是Spring MVC的核心。在這裡請求會第一次接觸到架構,它要負責将請求路由到其他的元件之中。

按照傳統的方式,像DispatcherServlet這樣的Servlet會配置在web.xml檔案中,這個檔案會放到應用的WAR包裡面。當然,這是配置DispatcherServlet的方法之一。但是,借助于Servlet 3規範和Spring 3.1的功能增強,這種方式已經不是唯一的方案了。

我們會使用Java将DispatcherServlet配置在Servlet容器中,而不會再使用web.xml檔案。

package spittr.config;

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

//配置DispatcherServlet
public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

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

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

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

}
           

擴充AbstractAnnotationConfigDispatcherServletInitializer的任意類都會自動地配置DispatcherServlet和Spring應用上下文,Spring的應用上下文會位于應用程式的Servlet上下文之中。

AbstractAnnotationConfigDispatcherServletInitializer剖析

在Servlet 3.0環境中,容器會在類路徑中查找實作

javax.servlet.ServletContainerInitializer

接口的類,如果能發現的話,就會用它來配置Servlet容器。Spring提供了這個接口的實作,名為SpringServletContainerInitializer,這個類反過來又會查找實作WebApplicationInitializer的類并将配置的任務交給它們來完成。Spring 3.2引入了一個便利的WebApplicationInitializer基礎實作,也就是AbstractAnnotationConfigDispatcherServletInitializer。因為我們

的SpittrWebAppInitializer擴充了AbstractAnnotationConfigDispatcherServletInitializer(同時也就實作了WebApplicationInitializer),是以當部署到Servlet 3.0容器中的時候,容器會自動發現它,并用它來配置Servlet上下文。

盡管它的名字很長,但是AbstractAnnotationConfigDispatcherServletInitializer使用起來很簡便。SpittrWebAppInitializer重寫了三個方法。

第一個方法是getServletMappings(),它會将一個或多個路徑映射到DispatcherServlet上。在本例中,它映射的是“/”,這表示它會是應用的預設Servlet。它會處理進入應用的所有請求。

為了了解其他的兩個方法,我們首先要了解DispatcherServlet和一個Servlet監聽器(也就是ContextLoaderListener)的關系。

兩個應用上下文之間的故事

當DispatcherServlet啟動的時候,它會建立Spring應用上下文,并加載配置檔案或配置類中所聲明的bean。

getServletConfigClasses()

方法中,我們要求DispatcherServlet加載應用上下文時,使用定義在WebConfig配置類(使用Java配置)中的bean。

但是在Spring Web應用中,通常還會有另外一個應用上下文。另外的這個應用上下文是由ContextLoaderListener建立的。

我們希望DispatcherServlet加載包含Web元件的bean,如控制器、視圖解析器以及處理器映射,而ContextLoaderListener要加載應用中的其他bean。這些bean通常是驅動應用後端的中間層和資料層元件。

實際上,AbstractAnnotationConfigDispatcherServletInitializer會同時建立DispatcherServlet和ContextLoaderListener。

getServlet-ConfigClasses()

方法傳回的帶有

@Configuration

注解的類将會用來定義DispatcherServlet應用上下文中的bean。

getRootConfigClasses()

方法傳回的帶有

@Configuration

注解的類将會用來配置ContextLoaderListener建立的應用上下文中的bean。

在本例中,根配置定義在RootConfig中,DispatcherServlet的配置聲明在WebConfig中。稍後我們将會看到這兩個類的内容。

需要注意的是,通過AbstractAnnotationConfigDispatcherServletInitializer來配置DispatcherServlet是傳統web.xml方式的替代方案。如果你願意的話,可以同時包含web.xml和AbstractAnnotationConfigDispatcherServletInitializer,但這其實并沒有必要。

如果按照這種方式配置DispatcherServlet,而不是使用web.xml的話,那唯一問題在于它隻能部署到支援Servlet 3.0的伺服器中才能正常工作,如Tomcat 7或更高版本。Servlet 3.0規範在2009年12月份就釋出了,是以很有可能你會将應用部署到支援Servlet 3.0的Servlet容器之中。

如果你還沒有使用支援Servlet 3.0的伺服器,那麼在AbstractAnnotationConfigDispatcherServletInitializer子類中配置DispatcherServlet的方法就不适合你了。你别無選擇,隻能使用web.xml了。但現在,我們先看一下程式所引用的WebConfig和RootConfig,了解一下如何啟用Spring MVC。

啟用Spring MVC

我們有多種方式來配置DispatcherServlet,與之類似,啟用Spring MVC元件的方法也不僅一種。以前,Spring是使用XML進行配置的,你可以使用

<mvc:annotation-driven>

啟用注解驅動的Spring MVC。不過,現在我們會讓Spring MVC的搭建過程盡可能簡單并基于Java進行配置。

我們所能建立的最簡單的Spring MVC配置就是一個帶有

@EnableWebMvc

注解的類:

package spittr.config;

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

@Configuration
@EnableWebMvc
public class WebConfig {

}
           

雖然這樣可以允許起來,不過還有不少問題:

  • 沒有配置視圖解析器。如果這樣的話,Spring預設會使用BeanNameViewResolver,這個視圖解析器會查找ID與視圖名稱比對的bean,并且查找的bean要實作View接口,它以這樣的方式來解析視圖
  • 沒有啟用元件掃描。這樣的結果就是,Spring隻能找到顯式聲明在配置類中的控制器
  • 這樣配置的話,DispatcherServlet會映射為應用的預設Servlet,是以它會處理所有的請求,包括對靜态資源的請求,如圖檔和樣式表(在大多數情況下,這可能并不是你想要的效果)

我們繼續寫WebConfig:

package 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("spittr.web")
public class WebConfig extends WebMvcConfigurerAdapter {

    /**
     * 配置JSP視圖解析器
     * @return
     */
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

    /**
     * 配置靜态資源的處理
     */
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

}
           

在程式中第一件需要注意的事情是WebConfig現在添加了

@Component-Scan

注解,是以将會掃描

spitter.web

包來查找元件。稍後你就會看到,我們所編寫的控制器将會帶有

@Controller

注解,這會使其成為元件掃描時的候選bean。是以,我們不需要在配置類中顯式聲明任何的控制器。

接下來,我們添加了一個ViewResolver bean。更具體來講,是InternalResourceViewResolver。我們隻需要知道它會查找JSP檔案,在查找的時候,它會在視圖名稱上加一個特定的字首和字尾(例如,名為

home

的視圖将會解析為

/WEB-INF/views/home.jsp

)。

最後,新的WebConfig類還擴充了WebMvcConfigurerAdapter并重寫了其

configureDefaultServletHandling()

方法。通過調用DefaultServletHandlerConfigurer的

enable()

方法,我們要求DispatcherServlet将對靜态資源的請求轉發到Servlet容器中預設的Servlet上,而不是使用DispatcherServlet本身來處理此類請求。

WebConfig已經就緒,那RootConfig呢?因為本章聚焦于Web開發,而Web相關的配置通過DispatcherServlet建立的應用上下文都已經配置好了,是以現在的RootConfig相對很簡單:

package spittr.config;

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 = { "spittr" }, excludeFilters = {
        @Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class) })
public class RootConfig {

}
           

唯一需要注意的是RootConfig使用了

@ComponentScan

注解。這樣的話,在本書中,我們就有很多機會用非Web的元件來充實完善RootConfig。

5.1.3 Spitter應用簡介

為了實作線上社交的功能,我們将要建構一個簡單的微網誌(microblogging)應用。在很多方面,我們所建構的應用與最早的微網誌應用Twitter很類似。在這個過程中,我們會添加一些小的變化。當然,我們要使用Spring技術來建構這個應用。

Spittr應用有兩個基本的領域概念:Spitter(應用的使用者)和Spittle(使用者釋出的簡短狀态更新)。當我們在書中完善Spittr應用的功能時,将會介紹這兩個領域概念。在本章中,我們會建構應用的Web層,建立展現Spittle的控制器以及處理使用者注冊成為Spitter的表單。

5.2 編寫基本的控制器

在Spring MVC中,控制器隻是方法上添加了

@RequestMapping

注解的類,這個注解聲明了它們所要處理的請求。

開始的時候,我們盡可能簡單,假設控制器類要處理對

/

的請求,并渲染應用的首頁。程式所示的HomeController可能是最簡單的Spring MVC控制器類了。

package 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";
    }

}
           

然後測試:

package spittr.web;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;

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

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執行個體到

MockMvcBuilders.standaloneSetup()

并調用

build()

來建構MockMvc執行個體。然後它使用MockMvc執行個體來執行針對

/

GET

請求并設定期望得到的視圖名稱。

5.2.2 定義類級别的請求處理

我們可以做的一件事就是拆分

@RequestMapping

,并将其路徑映射部分放到類級别上:

package spittr.web;

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

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

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

}
           

在這個新版本的HomeController中,路徑現在被轉移到類級别的

@RequestMapping

上,而HTTP方法依然映射在方法級别上。當控制器在類級别上添加

@RequestMapping

注解時,這個注解會應用到控制器的所有處理器方法上。處理器方法上的

@RequestMapping

注解會對類級别上的

@RequestMapping

的聲明進行補充。

就HomeController而言,這裡隻有一個控制器方法。與類級别的

@RequestMapping

合并之後,這個方法的

@RequestMapping

表明

home()

将會處理對

/

路徑的

GET

請求。

換言之,我們其實沒有改變任何功能,隻是将一些代碼換了個地方,但是HomeController所做的事情和以前是一樣的。因為我們現在有了測試,是以可以確定在這個過程中,沒有對原有的功能造成破壞。

當我們在修改

@RequestMapping

時,還可以對HomeController做另外一個變更。

@RequestMapping

的value屬性能夠接受一個String類型的數組:

package spittr.web;

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

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

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

}
           

5.2.3 傳遞模型資料到視圖中

在Spittr應用中,我們需要有一個頁面展現最近送出的Spittle清單。是以,我們需要一個新的方法來處理這個頁面。

首先,需要定義一個資料通路的Repository。為了實作解耦以及避免陷入資料庫通路的細節之中,我們将Repository定義為一個接口,并在稍後實作它(第10章中)。此時,我們隻需要一個能夠擷取Spittle清單的Repository,如下所示的SpittleRepository功能已經足夠了:

package spittr.data;

import java.util.List;

import spittr.Spittle;

public interface SpittleRepository {

    List<Spittle> findSpittles(long max, int count);

}
           

findSpittles()

方法接受兩個參數。其中

max

參數代表所傳回的Spittle中,Spittle ID屬性的最大值,而

count

參數表明要傳回多少個Spittle對象。為了獲得最新的20個Spittle對象,我們可以這樣調用

findSpittles()

findSpittles(Long.MAX_VALUE, );
           

對應的Spittle類為:

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) {
        id = null;
        this.message = message;
        this.time = time;
    }

    public Spittle(String message, Date time, Double longitude, Double latitude) {
        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 obj) {
        return EqualsBuilder.reflectionEquals(this, obj, "id", "time");
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this, "id", "time");
    }

}
           

然後編寫Controller:

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, ));
        return "spittles";
    }

}
           

進行測試:

package spittr.web;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

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

import spittr.Spittle;
import spittr.data.SpittleRepository;

public class SpittleControllerTest {

    @Test
    public void shouldShowRecentSpittles() throws Exception {
        List<Spittle> expectedSpittles = createSpittleList();
        SpittleRepository mockRepository = mock(SpittleRepository.class);
        when(mockRepository.findSpittles(Long.MAX_VALUE, )).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", expectedSpittles));

    }

    private List<Spittle> createSpittleList(int count) {
        List<Spittle> spittles = new ArrayList<Spittle>();
        for (int i = ; i < count; i++) {
            spittles.add(new Spittle("Spittle " + i, new Date()));
        }
        return spittles;
    }

}
           

我們在

spittles()

方法中給定了一個Model作為參數。這樣,

spittles()

方法就能将Repository中擷取到的Spittle清單填充到模型中。Model實際上就是一個Map(也就是key-value對的集合),它會傳遞給視圖,這樣資料就能渲染到用戶端了。當調用

addAttribute()

方法并且不指定key的時候,那麼key會根據值的對象類型推斷确定。在本例中,因為它是一個

List<Spittle>

,是以,鍵将會推斷為

spittleList

對于替代方案,還有一種:

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

它并沒有傳回視圖名稱,也沒有顯式地設定模型,這個方法傳回的是Spittle清單。當處理器方法像這樣傳回對象或集合時,這個值會放到模型中,模型的key會根據其類型推斷得出(在本例中,也就是

spittleList

)。而邏輯視圖的名稱将會根據請求路徑推斷得出。因為這個方法處理針對

/spittles

的GET請求,是以視圖的名稱将會是

spittles

(去掉開頭的斜線)。

然後我們編寫

/WEB-INF/views/spittles.jsp

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>

<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
            + path + "/";
%>
<%@page session="false"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>">

<title>Spittr</title>

<link rel="stylesheet" type="text/css"
    href="<c:url value="resources/style.css" />">

</head>

<body>
    <h1>Recent Spittles</h1>

    <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 }"></c:out>
                </span> <span class="spittleLocation"> (<c:out
                        value="${ spittle.latitude }"></c:out>, <c:out
                        value="${ spittle.longitude }"></c:out>)
                </span>
            </div>
        </li>
    </c:forEach>

</body>
</html>
           

5.3 接受請求的輸入

Spring MVC允許以多種方式将用戶端中的資料傳送到控制器的處理器方法中:

  • 查詢參數(Query Parameter)
  • 表單參數(Form Parameter)
  • 路徑參數(Path Variable)

5.3.1 處理查詢參數

假設我們要檢視某一頁Spittle清單,這個清單會按照最新的Spittle在前的方式進行排序。是以,下一頁中第一條的ID肯定會早于目前頁最後一條的ID。是以,為了顯示下一頁的Spittle,我們需要将一個Spittle的ID傳入進來,這個ID要恰好小于目前頁最後一條Spittle的ID。另外,你還可以傳入一個參數來确定要展現的Spittle數量。

為了實作這個分頁的功能,我們所編寫的處理器方法要接受如下的參數:

  • before

    參數(表明結果中所有Spittle的ID均應該在這個值之前)
  • count

    參數(表明在結果中要包含的Spittle數量)
package spittr.web;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import spittr.Spittle;
import spittr.data.SpittleRepository;

@Controller
@RequestMapping("/spittles")
public class SpittleController {

    private static final String MAX_LONG_AS_STRING = "" + Long.MAX_VALUE;

    private SpittleRepository spittleRepository;

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

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

}
           

對應的測試代碼為:

@Test
public void shouldShowRecentSpittles() throws Exception {
    List<Spittle> expectedSpittles = createSpittleList();
    SpittleRepository mockRepository = mock(SpittleRepository.class);
    when(mockRepository.findSpittles(, )).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", expectedSpittles));

}
           

5.3.2 通過路徑參數接受輸入

假設我們的應用程式需要根據給定的ID來展現某一個Spittle記錄。其中一種方案就是編寫處理器方法,通過使用

@RequestParam

注解,讓它接受ID作為查詢參數:

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

這個處理器将會處理形如:

/spittles/show?spittle_id=12345

的請求,不過從面向資源的角度看這并不理想。對

spittles/12345

請求要優于對

spittles/show?spittle_id=12345

。前者能夠識别出要查詢的資源,而後者描述的是帶有參數的一個操作—本質上是通過HTTP發起的RPC。

控制器代碼如下:

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

測試代碼為:

@Test
public void shouldShowRecentSpittles() throws Exception {
    List<Spittle> expectedSpittles = createSpittleList();
    SpittleRepository mockRepository = mock(SpittleRepository.class);
    when(mockRepository.findSpittles(, )).thenReturn(expectedSpittles);
    SpittleController controller = new SpittleController(mockRepository);

    MockMvc mockMvc = standaloneSetup(controller)
            .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp")).build();

    mockMvc.perform(get("/spittles/12345")).andExpect(view().name("spittle"))
            .andExpect(model().attributeExists("spittleList"))
            .andExpect(model().attribute("spittleList", expectedSpittles));

}
           

Spring MVC允許我們在

@RequestMapping

路徑中添加占位符。占位符的名稱要用大括

{

}

括起來。路徑中的其他部分要與所處理的請求完全比對,但是占位符部分可以是任意的值。

在樣例中

spittleId

這個詞出現了好幾次:先是在

@RequestMapping

的路徑中,然後作為

@PathVariable

屬性的值,最後又作為方法的參數名稱。因為方法的參數名碰巧與占位符的名稱相同,是以我們可以去掉

@PathVariable

中的

value

屬性。

如果

@PathVariable

中沒有

value

屬性的話,它會假設占位符的名稱與方法的參數名相同。但需要注意的是,如果你想要重命名參數時,必須要同時修改占位符的名稱,使其互相比對。

/WEB-INF/views/spittle.jsp

如下:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>

<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
            + path + "/";
%>
<%@page session="false"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>">

<title>Spittr</title>

<link rel="stylesheet" type="text/css"
    href="<c:url value="resources/style.css" />">

</head>

<body>
    <h1>Recent Spittles</h1>

    <div class="spittleView">
        <div class="spittleMessage">
            <c:out value="${ spittle.message }" />
        </div>
        <div>
            <span class="spittleTime"> <c:out value="${ spittle.time }"></c:out>
            </span> <span class="spittleLocation"> (<c:out
                    value="${ spittle.latitude }"></c:out>, <c:out
                    value="${ spittle.longitude }"></c:out>)
            </span>
        </div>
    </div>
</body>
</html>
           

5.4 處理表單

使用表單分為兩個方面:展現表單以及處理使用者通過表單送出的資料。在Spittr應用中,我們需要有個表單讓新使用者進行注冊。SpitterController是一個新的控制器,目前隻有一個請求處理的方法來展現系統資料庫單:

package 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";
    }

}
           

對應的系統資料庫單

registerFrom.jsp

為:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>

<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
            + path + "/";
%>
<%@page session="false"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>">

<title>Spittr</title>

<link rel="stylesheet" type="text/css"
    href="<c:url value="resources/style.css" />">

</head>

<body>
    <h1>Register</h1>

    <form method="POST">
        First Name: <input type="text" name="firstName" /> <br />
        Last Name: <input type="text" name="lastName" /> <br />
        Username: <input type="text" name="username" /> <br />
        Password: <input type="text" name="password" /> <br />

        <input type="submit" value="Register">
    </form>

</body>
</html>
           

需要注意的是:這裡的

<form>

标簽中并沒有設定

action

屬性。在這種情況下,當表單送出時,它會送出到與展現時相同的URL路徑上。也就是說,它會送出到

/spitter/register

上。

5.4.1 編寫處理表單的控制器

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(Spitter spitter) {
    spitterRepository.save(spitter);

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

請注意新建立的

processRegistration()

方法,它接受一個Spitter對象作為參數。這個對象有

firstName

lastName

username

password

屬性,這些屬性将會使用請求中同名的參數進行填充。

processRegistration()

方法做的最後一件事就是傳回一個String類型,用來指定視圖。但是這個視圖格式和以前我們所看到的視圖有所不同。這裡不僅傳回了視圖的名稱供視圖解析器查找目标視圖,而且傳回的值還帶有重定向的格式。當InternalResourceViewResolver看到視圖格式中的

redirect:

字首時,它就知道要将其解析為重定向的規則,而不是視圖的名稱。在本例中,它将會重定向到使用者基本資訊的頁面。例如,如果

spitter.username

屬性的值為

jbauer

,那麼視圖将會重定向到

/spitter/jbauer

需要注意的是,除了

redirect:

,InternalResourceViewResolver還能識别

forward:

字首。當它發現視圖格式中以

forward:

作為字首時,請求将會前往

forward

指定的URL路徑,而不再是重定向。

5.4.2 校驗表單

如果使用者在送出表單的時候,

username

password

文本域為空的話,那麼将會導緻在建立Spitter對象中,

username

password

是空的String。至少這是一種怪異的行為。如果這種現象不處理的話,這将會出現安全問題,因為不管是誰隻要送出一個空的表單就能登入應用。

從Spring 3.0開始,在Spring MVC中提供了對Java校驗API的支援。在Spring MVC中要使用Java校驗API的話,并不需要什麼額外的配置。隻要保證在類路徑下包含這個Java API的實作即可,比如Hibernate Validator。

Java校驗API定義了多個注解,這些注解可以放到屬性上,進而限制這些屬性的值。所有的注解都位于

javax.validation.constraints

包中。

表5.1 Java校驗API所提供的校驗注解

注解 描述
@AssertFalse 所注解的元素必須是Boolean類型,并且值為false
@AssertTrue 所注解的元素必須是Boolean類型,并且值為true
@DecimalMax 所注解的元素必須是數字,并且它的值要小于或等于給定的BigDecimalString值
@DecimalMin 所注解的元素必須是數字,并且它的值要大于或等于給定的BigDecimalString值
@Digits 所注解的元素必須是數字,并且它的值必須有指定的位數
@Future 所注解的元素的值必須是一個将來的日期
@Past 所注解的元素的值必須是一個已過去的日期
@Max 所注解的元素必須是數字,并且它的值要小于或等于給定的值
@Min 所注解的元素必須是數字,并且它的值要大于或等于給定的值
@NotNull 所注解元素的值必須不能為null
@Null 所注解元素的值必須為null
@Pattern 所注解的元素的值必須比對給定的正規表達式
@Size 所注解的元素的值必須是String、集合或數組,并且它的長度要符合給定的範圍

然後完善Spitter:

package spittr;

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

public class Spitter {

    private Long id;

    @NotNull
    @Size(min = , max = )
    private String username;

    @NotNull
    @Size(min = , max = )
    private String password;

    @NotNull
    @Size(min = , max = )
    private String firstName;

    @NotNull
    @Size(min = , max = )
    private String lastName;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

}
           

現在我們已經做好校驗準備,此時隻需要修改

processRegistration()

來啟用校驗功能即可:

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

確定

Java Validation API(JSR-303)

實作存在類路徑下,由于使用的Spring版本為4.xx,是以我們導入的是

hibernate-validator-5.1.3.Final