3、接受请求的输入
Spring MVC允许以多种方式将客户端中的数据传送到控制器的处理器方法中,包括:
- 查询参数(Query Parameter)。
- 表单参数(Form Parameter)。
- 路径变量(Path Variable)。
3.1 处理查询参数
Spittr应用的一个需求就是展现的分页spittle列表,单现在的SpittleController只能展示最近的spittle。如果要让用户可以每次得到一页的spittle记录,那么就需要提供一种方式让用户传递参数进来,进而确定要展现哪些Spittle集合。
在浏览spittle时,如果想要查看下一页的spittle,那么就需要将一个Spittle的id传入进来,这个id要恰好小于当前页最后一条Spittle的id;另外,还可以传递想要展示的spittle的数量。
为了实现分页,所编写的处理器方法要接受如下参数:
before参数,结果中所有spittle的id都要在这个参数之前;
count参数,结果中要包含的spittle数量
首先添加一个测试,这个测试反映了新spittles()方法的功能:
package spittr.test;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
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 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;
import spittr.web.SpittleController;
public class SpittleControllerTest {
@Test
public void shouldShowRecentSpittles() throws Exception {
List<Spittle> expectedSpittles = createSpittleList(20);
//创建SpittleRepository接口的mock对象
SpittleRepository mockRepository = mock(SpittleRepository.class);
//设定mock对象findSpittles方法调用时的返回值
when(mockRepository.findSpittles(Long.MAX_VALUE, 20))
.thenReturn(expectedSpittles);
SpittleController controller = new SpittleController(mockRepository);
//注册一个@Controller实例,并设置单个视图,即视图解析时总是解析到这一个
MockMvc mockMvc = standaloneSetup(controller)
.setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
.build();
mockMvc.perform(get("/spittles")) //对"/spittles"发起get请求
.andExpect(view().name("spittles"))
.andExpect(model().attributeExists("spittleList"))
.andExpect(model().attribute("spittleList",
hasItems(expectedSpittles.toArray())));
}
@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();
//预期的max和count参数
mockMvc.perform(get("/spittles?max=238900&count=50"))
.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;
}
}
shouldShowPagedSpittles()测试方法针对“/spittles”发送GET请求,同时还传入了max和count参数。SpittleController中的处理方法要同时处理有参数和没有参数的场景,改动如下:
package spittr.web;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.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 = "9223372036854775807";
private SpittleRepository spittleRepository;
@Autowired
public SpittleController(SpittleRepository spittleRepository) {
this.spittleRepository = spittleRepository;
}
//通过 @RequestParam 注解指定所绑定的 URL 参数
@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);
}
}
@RequestParam接受查询参数max的数据,并且会把其注入到max中,虽然查询参数的置为字符串类型,但是其会自动的转化。 如果参数在请求中不存在的话,就使用默认值MAX_LONG_AS_STRING和20。如果max参数没有指定的话,它将会是Long类型的最大值。因为查询参数都是String类型的,因此defaultValue属性需要String类型的值。因此,使用Long.MAX_VALUE是不行的。我们可以将Long.MAX_VALUE转换为名为MAX_LONG_AS_STRING的String类型常量。尽管defaultValue属性给定的是String类型的值,但是当绑定到方法的max参数时,它会转换为Long类型。如果请求中没有count参数的话,count参数的默认值将会设置为20.
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.findOne(spittleId));
return "spittle";
}
这个处理器方法会处理形如“/spittles/show?spittle_id=12345”这样的请求,但从面向资源的角度来看这并不理想。理想情况下,要识别资源(Spittle)应通过URL路径进行标示,而不是通过查询参数。对“/spittles/12345”发起GET请求,要优于对“/spittles/show?spittle_id=12345”发起请求。前者能够识别出要查询的资源,而后者描述的是带有参数的一个操作——本质上是通过HTTP发起的RPC。
以面向资源的控制器作为目标,进行一个测试:
@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));
}
这个测试构建了一个mock Repository 、一个控制器和MockMvc,它对"/spittles/12345"发起GET请求,然后断言试图的名称是spittle,并且预期的Spittle对象放到了模型之中。
为了实现这种路径变量,Spring MVC允许在@RequestMapping路径中添加占位符。占位符的名称要用大括号(“{”和“}”)括起来。路径中的其他部分要与所处理的请求完全匹配,但是占位符部分可以是任意的值。
下面的处理器方法使用了占位符,将Spittle ID作为路径的一部分。
@RequestMapping(value="/{spittle_Id}", method=RequestMethod.GET)
public String spittle(
@PathVariable("spittle_Id") long spittleId,
Model model) {
model.addAttribute(spittleRepository.findOne(spittleId));
return "spittle";
}
spittle()方法的spittleId参数上添加了@PathVariable("spittle_Id")注解,这表明在请求路径中,不管占位符部分的值是什么都会传递到处理器方法的spittleId参数中
当方法的参数名与占位符的名称相同,可以去掉@PathVariable中的value属性:
@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
public String spittle(
@PathVariable long spittleId,
Model model) {
model.addAttribute(spittleRepository.findOne(spittleId));
return "spittle";
}
添加spittle.jsp:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<title>Spitter</title>
<link rel="stylesheet"
type="text/css"
href="http://blog.163.com/yetong1985@126/blog/<c:url value="/resources/style.css" />" >
</head>
<body>
<div class="spittleView">
<div class="spittleMessage"><c:out value="${spittle.message}" /></div>
<div>
<span class="spittleTime"><c:out value="${spittle.time}" /></span>
</div>
</div>
</body>
</html>
4、处理表单
Spring MVC的控制器也为表单处理提供了良好的支持。使用表单分为两个方面:展现表单以及处理用户通过表单提交的数据。在Spittr应用中,需要有个表单让用户进行注册。Spring MVC的控制器也为表单处理提供了良好的支持。使用表单分为两个方面:展现表单以及处理用户通过表单提交的数据。
package spittr.web;
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 spittr.data.SpitterRepository;
@Controller
@RequestMapping("/spitter")
public class SpitterController {
private SpitterRepository spitterRepository;
@Autowired
public SpitterController(SpitterRepository spitterRepository) {
this.spitterRepository = spitterRepository;
}
//处理"/spitter/register"的GET请求
@RequestMapping(value="/register", method=RequestMethod.GET)
public String showRegistrationForm() {
return "registerForm";
}
}
showRegistrationForm()方法的@RequestMapping注解以及类级别上的@RequestMapping注解组合起来,声明了这个方法要处理的是针对“/spitter/register”的GET请求。
视图的名称为registerForm,所以JSP的名称需要是registerForm.jsp。这个JSP必须要包含一个HTML<form>标签,在这个标签中用户输入注册应用的信息。
清单 registerForm.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="http://blog.163.com/yetong1985@126/blog/<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>标签中并没有设置action属性。在这种情况下,当表单提交时,它会提交到与展现时相同的URL路径上。也就是说,它会提交到“/spitter/register”上。这就意味着需要在服务器端处理该HTTP POST请求。在SpitterController中添加一个方法处理这个表单的提交。
用户类Spitter.java
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;
import org.hibernate.validator.constraints.Email;
public class Spitter {
private Long id;
private String username;
private String password;
private String firstName;
private String lastName;
private String email;
public Spitter() {}
public Spitter(String username, String password, String firstName, String lastName, String email) {
this(null, username, password, firstName, lastName, email);
}
public Spitter(Long id, String username, String password, String firstName, String lastName, String email) {
this.id = id;
this.username = username;
this.password = password;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
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 Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
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;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public boolean equals(Object that) {
return EqualsBuilder.reflectionEquals(this, that, "firstName", "lastName", "username", "password", "email");
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this, "firstName", "lastName", "username", "password", "email");
}
}
4.1 编写处理表单的控制器
当处理注册表单的POST请求时,控制器需要接受表单数据并将表单数据保存为Spitter对象。最后,为了防止重复提交(用户点击浏览器的刷新按钮有可能会发生这种情况),应该将浏览器重定向到新创建用户的基本信息页面。
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.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import spittr.Spitter;
import spittr.data.SpitterRepository;
@Controller
@RequestMapping("/spitter")
public class SpitterController {
private SpitterRepository spitterRepository;
@Autowired
public SpitterController(SpitterRepository spitterRepository) {
this.spitterRepository = spitterRepository;
}
//处理"/spitter/register"的GET请求
@RequestMapping(value="/register", method=RequestMethod.GET)
public String showRegistrationForm() {
return "registerForm";
}
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegistration(Spitter spitter) {
spitterRepository.save(spitter);
return "redirect:/spitter/" + spitter.getUsername();
}
@RequestMapping(value="/{username}", method=RequestMethod.GET)
public String showSpitterProfile(@PathVariable String username, Model model) {
Spitter spitter = spitterRepository.findByUsername(username);
model.addAttribute(spitter);
return "profile";
}
}
当使用Spitter对象调用processRegistration()方法时,它会调用SpitterRepository的save()方法,SpitterRepository是在Spitter-Controller的构造器中 注入进来的。 processRegistration()方法做的最后一件事就是返回一个String类型,用来指定视图,返回的值还带有重定向的格式。 当InternalResourceViewResolver看到视图格式中 的“redirect:”前缀时,它就知道要将其解析为重定向的规则,而不是视图的名称。在本例中,它将会重定向到用户基本信息的页面。例 如,如果Spitter.username属性的值为“jbauer”,那么视图将会重 定向到“/spitter/jbauer”。
需要注意的是,除 了“redirect:”,InternalResourceViewResolver还能识别“forward:”前缀。当它发现视图格式中以“forward:”作为前缀时,请求将会前往(forward)指定的URL路径,而不再是重定向。
SpitterRepository通过用户名获取一个Spitter对象,showSpitterProfile()得到这个对象并将其添加到模型中,然后返回profile,也就是基本信息页面的逻辑视图名。
清单 用户基本信息页面profile.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="http://blog.163.com/yetong1985@126/blog/<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>
结果:
点击Register按钮:
测试处理表单的控制器方法
@Test
public void shouldProcessRegistration() 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);
}
在构建完 SpitterRepository的mock实现以及所要执行的控制器和 MockMvc之后,shouldProcess-Registration()对“/spitter/ register”发起了一个POST请求。作为请求的一部分,用户信息以参数的形式放到request中,从而模拟提交的表单。在处理POST类型的请求时,在请求处理完成后,最好进行一下重定向,这样浏览器的刷新就不会重复提交表单了。在这个测试中,预期请求会重定向到“/spitter/jbauer”,也就是新建用户的基本信息页面。 最后,测试会校验SpitterRepository的mock实现最终会真正用 来保存表单上传入的数据。
4.2 检验表单
如果用户在提交表单的时候,username或password文本域为空的 话,那么将会导致在新建Spitter对象中,username或password 是空的String。如果这种现象不处理的 话,这将会出现安全问题,因为不管是谁只要提交一个空的表单就能 登录应用。
同时,我们还应该阻止用户提交空的firstName和/或lastName, 使应用仅在一定程度上保持匿名性。有个好的办法就是限制这些输入域值的长度,保持它们的值在一个合理的长度范围,避免这些输入域的误用。
从Spring 3.0开 始,在Spring MVC中提供了对Java校验API的支持,但需要添加Java Validation的依赖jar包:
注意, 由于hibernate-validator-5.x.x已经不兼容validation-api-1.0.x,这是因为hibernate- validator-5.x.x已经把旧的校验框架JSR-303,改为JSR-349了,所以validation-api不能使用1.1以下的版本。
Java校验API定义了多个注解,这些注解可以放到属性上,从而限制 这些属性的值。所有的注解都位于 javax.validation.constraints包中。表5.1列出了这些校验注解。
下面给出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;
import org.hibernate.validator.constraints.Email;
public class Spitter {
private Long id;
//非空,5到16个字符
@NotNull
@Size(min=5, max=16)
private String username;
@NotNull
@Size(min=5, max=25)
private String password;
@NotNull
@Size(min=2, max=30)
private String firstName;
@NotNull
@Size(min=2, max=30)
private String lastName;
@NotNull
@Email
private String email;
public Spitter() {}
public Spitter(String username, String password, String firstName, String lastName, String email) {
this(null, username, password, firstName, lastName, email);
}
public Spitter(Long id, String username, String password, String firstName, String lastName, String email) {
this.id = id;
this.username = username;
this.password = password;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
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 Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
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;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public boolean equals(Object that) {
return EqualsBuilder.reflectionEquals(this, that, "firstName", "lastName", "username", "password", "email");
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this, "firstName", "lastName", "username", "password", "email");
}
}
Spitter的所有属性都添加了@NotNull注解,以确保它们的 值不为null。类似地,属性上也添加了@Size注解以限制它们的长度在最大值和最小值之间。对Spittr应用来说,这意味着用户必须 要填完注册表单,并且值的长度要在给定的范围内。
我们已经为Spitter添加了校验注解,接下来需要修改控制器的processRegistration()方法来应用校验功能。启用校验功能的processRegistration()如下所示:
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegistration(
@Valid Spitter spitter,
Errors errors) {
//如果校验出现错误,则重新返回表单
if (errors.hasErrors()) {
System.out.println("注册有误,返回到注册页面");
return "registerForm";
}
spitterRepository.save(spitter);
return "redirect:/spitter/" + spitter.getUsername();
}
如果有校验出现错误的话,那么这些错误可以通过Errors对象进行访问,现在这个对象已作为processRegistration()方法的参数(很重要一点需要注意,Errors参数要紧跟在带有@Valid注 解的参数后面,@Valid注解所标注的就是要检验的参数)。如果有错误的话,Errors.hasErrors()将会返回 到registerForm,也就是注册表单的视图。这能够让用户的浏览器重新回到注册表单页面,所以他们能够修正错误,然后重新尝试提交。
提交之后检验到错误,如下图: