SpringInAction笔记(五)—— 构建Spring Web应用程序(下)

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="<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>

SpringInAction笔记(五)—— 构建Spring Web应用程序 - 叶主任 - 叶主任的博客

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="<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>

结果:

SpringInAction笔记(五)—— 构建Spring Web应用程序 - 叶主任 - 叶主任的博客

点击Register按钮:

SpringInAction笔记(五)—— 构建Spring Web应用程序 - 叶主任 - 叶主任的博客

测试处理表单的控制器方法

	@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包:
SpringInAction笔记(五)—— 构建Spring Web应用程序(下) - 叶主任 - 叶主任的博客
注意, 由于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列出了这些校验注解。
SpringInAction笔记(五)—— 构建Spring Web应用程序(下) - 叶主任 - 叶主任的博客

 下面给出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,也就是注册表单的视图。这能够让用户的浏览器重新回到注册表单页面,所以他们能够修正错误,然后重新尝试提交。
SpringInAction笔记(五)—— 构建Spring Web应用程序(下) - 叶主任 - 叶主任的博客
 提交之后检验到错误,如下图:

SpringInAction笔记(五)—— 构建Spring Web应用程序(下) - 叶主任 - 叶主任的博客

猜你喜欢

转载自blog.csdn.net/ganmaotong/article/details/80377048