Spring MVC基于模型-視圖-控制器(Model-View-Controller,MVC)模式實作。
5.1 Spring MVC起步
5.1.1 跟蹤Spring MVC的請求
每當使用者在Web浏覽器中點選連結或送出表單的時候,請求就開始工作了。請求是一個十分繁忙的家夥。從離開浏覽器開始到擷取響應傳回,它會經曆好多站,在每站都會留下一些資訊同時也會帶上其他資訊。

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