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="<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>
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="<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="<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", "[email protected]"); Spitter saved = new Spitter(24L, "jbauer", "24hours", "Jack", "Bauer", "[email protected]"); 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", "[email protected]")) .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,也就是注册表单的视图。这能够让用户的浏览器重新回到注册表单页面,所以他们能够修正错误,然后重新尝试提交。