Spring实战(五)-构建Spring Web应用程序

本文基于《Spring实战(第4版)》所写。

Spring MVC起步

Spring将请求调度Servlet、处理器映射(handler mapping)、控制器以及视图解析器(view resolver)之间移动。

跟踪Spring MVC的请求

Web请求从离开浏览器开始到获取响应返回,它会经历好多站,每站都会留下一些信息同时带上其他信息。

一路上请求会将信息带到很多站点,并产生期望的结果

请求带着URL以及其他信息离开浏览器后,第一站是Spring的DispatcherServlet(前端控制器)。它的任务将请求发送给Spring MVC控制器。由于可能有多个控制器,DispatcherServlet会查询一个或多个处理器映射来确定请求的下一站,处理器映射会根据URL信息来决策。选择合适的控制器后,DispatcherServlet将请求发送给选中的控制器。到了控制器,它会处理请求带来的信息。

控制器处理完成后,会将模型数据打包,并且标示出用于渲染输出的视图名。它接下来将请求连同模型和视图名发送回DispatcherServlet,目的是解耦。传递给DispatcherServlet的视图名并不直接标示某个特定的JSP。它仅仅传递了一个逻辑名称,用来查找产生结果的真正视图。DispatcherServlet将会使用视图解析器来将逻辑视图名匹配为一个特定的视图实现(可能是JSP)。

DispatcherServlet的最后一站是视图的实现(可能是JSP),在这里它交付模型数据。视图将使用模型数据渲染输出,通过响应对象传递给客户端。

搭建Spring MVC

首先配置DispatcherServlet,它是Spring MVC的核心。如果按传统方式,DispatcherServlet会配置在web.xml中。另一种方式可以将DispatcherServlet配置在Servlet容器中。

package spittr.config;

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

public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

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

    @Override   // 指定配置类
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { WebConfig.class };
    }

    @Override   // 将DispatcherServlet映射到“/”
    protected String[] getServletMappings() {
        return new String[]{ "/"};
    }
}

我们需要知道的是AbstractAnnotationConfigDispatcherServletInitializer的任意类都会自动地配置DispatcherServlet和Spring应用上下文,Spring的应用上下文会位于应用程序的Servlet上下文中。

AbstractAnnotationConfigDispatcherServletInitializer剖析

在Servlet3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果能发现的话,就会用它来配置Servlet容器。

Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给它们来完成。Spring3.2引入了一个便利的WebApplicationInitializer基础实现,也就是AbstractAnnotationConfigDispatcherServletInitializer。因为我们的SpittrWebAppInitializer扩展了AbstractAnnotationConfigDispatcherServletInitializer(同时也就实现了WebApplicationInitializer),因此当部署到Servlet3.0容器中的时候,容器会自动发现它,并用它来配置Servlet上下文。

在本例中,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。getServletConfigClasses()方法返回的带有@Configuration注解的类将会用来定义DispatcherServlet应用上下文中的bean。getRootConfigClasses()方法返回的带有@Configuration注解的类将会用来配置ContextLoaderListener创建的应用上下文中bean。

在本例中,根配置定义在RootConfig中,DispatcherServlet的配置声明在WebConfig中。

如果按照这种方式配置DispatcherServlet,它只能部署到支持Servlet 3.0的服务器中才能正常工作,如Tomcat 7或更高版本。

启动Spring MVC

启动Spring MVC组件的方法也不仅一种。除了传统的XML配置,还可选择基于Java进行配置。

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
@ComponentScan("spittr.web") // 启动组件扫描
@EnableWebMvc   // 启动spring mvc
public class WebConfig extends WebMvcConfigurerAdapter{

    @Bean
    public ViewResolver viewResolver(){   // 配置jsp视图解析器
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/views/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

    // 配置静态资源的处理
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

程序中第一件需要注意的事情就是WebConfig添加了@ComponentScan注解,因此将会扫描spitter.web包来查找组件。

接下来,添加了viewResolver bean,表示配置视图解析器。如果没有配置,则Spring默认会使用BeanNameViewResolver,这个视图解析器会查找ID与视图名称匹配的bean,并且查找的bean要实现View接口。而InternalResourceViewResolver会查找JSP文件,在查找的时候,它会在视图名称上加一个特定的前缀和后缀。

最后,新的WebConfig类还扩展了WebMvcConfigurerAdapter并重写了其configureDefaultServletHandling()方法。通过调用DefaultServletHandlerConfigurer的enable()方法,我们要求DispatcherServlet将对静态资源的请求转发到Servlet容器中默认的Servlet上,而不是使用DispatcherServlet本身来处理此类请求。

WebConfig已经就绪,下面来看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 {
}

我们基本已经可以使用Spring MVC构建Web应用了。

编写基本的控制器

在Spring MVC中,控制器只是方法上添加了@RequestMapping注解的类,这个注解声明了它们所要处理的请求。

下面是简单的HomeController控制器

package spittr.web;

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

@Controller   // 声明为一个控制器
public class HomeController {
    @RequestMapping(value = "/",method = GET)  // 处理对“/”的GET请求
    public String home(){
        return "home";   // 视图名为 home
    }
}

@Controller是一个构造型(stereotype)的注解,它基于@Component注解。在这里,它的目的就是辅助实现组件扫描。因为HomeController带有
@Controller注解,因此组件扫描器会自动找到HomeController,并将其声明为Spring应用上下文中的一个bean。其实也可用@Component,但在表意性上可能会差一些,无法确定HomeController是什么组件类型。

HomeController的home()方法带有@RequestMapping注解。它的value属性指定了这个方法所要处理的请求路径,method属性细化了它所要处理的HTTP方法。在本例中,当收到对“/”的HTTP GET请求时,就会调用home()方法。

home()方法其实只是返回了一个String类型的“home”。这个String将会被Spring MVC解读为要渲染的视图名称。DispatcherServlet会要求视图解析器将这个逻辑名称解析为实际的视图。

鉴于我们配置InternalResourceViewResolver的方式,视图名“home”将会解析为“/views/home.jsp”路径的JSP。下面来定义home.jsp

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" session="false"%>
<html>
<head>
    <title>Spittr</title>
    <link rel="stylesheet" type="text/css" href="<c:url value="/resources/style.css" />" />
</head>
<body>

    <h1>Welcome to Spittr</h1>
    <a href="<c:url value="/spittles" />">Spittles</a> |
    <a href="<c:url value="/spitter/register" />">Register</a>
</body>
</html>

结果显示一个简单的jsp

当前的Spittr首页

测试控制器

我们可以编写一个简单的类来测试HomeController,

package spittr.web;

import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class HomeControllerTest {
    @Test
    public void testHomePage() throws Exception {
        HomeController controller = new HomeController();
        assertEquals("home", controller.home());
    }
}

这个测试没有断言当接收到针对“/”的GET请求时会调用home()方法。
从Spring 3.2开始,我们可以按照控制器的方式来测试Spring MVC中的控制器了,就是使用mock Spring MVC测试。

package spittr.web;

import org.junit.Test;
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 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
        mockMvc.perform(get("/")).   // 对"/"执行GET请求
                andExpect(view().name("home"));  // 预期得到home视图
    }
}

定义类级别的请求处理

我们可以拆分@RequestMapping,并将其路径映射部分放到类级别上。

package spittr.web;

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

@Controller  
@RequestMapping("/")  // 将控制器映射到"/"
public class HomeController {
    @RequestMapping(method = GET)  // 处理GET请求
    public String home(){
        return "home";   // 视图名为 home
    }
}

当控制器在类级别上添加@RequestMapping注解时,这个注解会应用到控制器的所有处理器方法上。处理器方法上的@RequestMapping注解会对类级别上的@RequestMapping的声明进行补充。

我们还可以将它映射到对“/homepage”的请求

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

传递模型数据到视图中

我们需要有一个页面展现最近提交的Spittle列表。因此,我们需要一个新的方法来处理这个页面。

首先,需要定义一个数据访问的Repository。为了实现解耦以及避免陷入数据库访问的细节之中,我们将Repository定义为一个接口,并在稍后实现它。

package spittr.data;
import spittr.model.Spittle;
import java.util.List;

public interface SpittleRepository {
    List<Spittle> findSpittles(long max, int count);
}

为了获得最新的20个Spittle对象,我们可以这样调用findSpittles():

List<Spittle> recent = spittleRepository.findSpittles(Long.MAX_VALUE, 20);

下面实现Spittle类

package spittr.model;

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){
        this(message, time, null, null);
    }

    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 that){
        return EqualsBuilder.reflectionEquals(this,that,"id", "time");
    }

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

然后我们可以编写测试方法。

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

    SpittleController controller = new SpittleController(mockRepository);
    MockMvc mockMvc = standaloneSetup(controller).setSingleView(new InternalResourceView("/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())));
}

....

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

需要注意的是,这个测试在MockMvc构造器上调用了setSingleView()。这样的话,mock框架就不用解析控制器中的视图名了。这个场景下,如果不用这种方式,按照默认的视图解析规则时,MockMvc就会发生失败,因为无法区分视图路径和控制器的路径。在这个测试中,构建InternalResourceView时所设置的实际路径是无关紧要的,但我们将其设置为与InternalResourceViewResolver配置一致。

下一步我们创建SpittleController

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.*;
import spittr.data.SpittleRepository;
import spittr.model.Spittle;
import java.util.List;

@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) {
        // 将spittle添加到模型中
        model.addAttribute(spittleRepository   
                     .findSpittles(Long.MAX_VALUE,20));
        return "spittles";  // 返回视图名
    }
}

需要注意的是,我们在spittles()方法中定了一个Model作为参数。这样,spittles()方法就能将Repository中获取到的Spittle列表填充到模型中。Model实际上就是一个Map(可直接将Model替换为Map类),它会传递给视图,这样数据就能渲染到客户端了,当调用addAttribule()方法并且不指定key的时候,那么key会根据值的对象类型推断确定。在本例中,因为它是一个List<Spittle>,因此,键将会推断为spittleList。

spittles()方法所做的最后一件事是返回spittles作为视图的名字,这个视图会渲染模型。

也可以显示声明模型的key

@RequestMapping(method=RequestMethod.GET)
    public String spittles(Model model) {
        model.addAttribute("spittleList", spittleRepository   
                     .findSpittles(Long.MAX_VALUE,20));
        return "spittles"; 
    }

下面还有另一种方式来编写spittles()方法:

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

当处理器方法像这样返回对象或集合时,这个值会放到模型中,模型的key会根据其类型推断得出(在本例中,也就是spittleList)。

而逻辑视图的名称将会根据请求路径推断得出。因为这个方法处理针对“/spittles”的GET请求,因此视图的名称将会是spittles(去掉开头的斜线)。

现在,数据已经放到了模型中,当视图是JSP的时候,模型数据会作为请求属性放到请求之中。因此,在spittles.jsp文件中可以使用JSTL的<c:forEach>标签渲染spittle列表:

<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.toString()}"/>
             </span>
             <span class="spittleLocation">
                 (<c:out value="${spittle.latitude}"/>, 
                  <c:out value="${spittle.longitude}"/>)
             </span>
         </div>
     </li>
</c:forEach>
控制器中的Spittle模型数据将会作为请求参数,并在Web页面上渲染为列表形式

接受请求的输入

Spring MVC允许以多种方式将客户端中的数据传送到控制器的处理方法中,包括:

  • 查询参数(Query Parameter)
  • 表单参数(Form Parameter)
  • 路径变量(Path Variable)

处理查询参数

我们来实现一个查询Spittle列表的分页功能。为了实现这个功能,我们所编写的处理器方法要接受如下的参数:

  • before参数(表明结果中所有Spittle的ID均应该在这个值之前)
  • count参数(表明在结果中要包含的Spittle数量)

我们首先添加一个测试,这个测试反映了新spittles()方法的功能

    @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("/views/spittles.jsp")).build();
        mockMvc.perform(get("/spittles?max=238900&count=50"))
                .andExpect(view().name("spittles"))
                .andExpect(model().attributeExists("spittleList"))
                .andExpect(model().attribute("spittleList",hasItems(expectedSpittles.toArray())));
    }

这个测试方法针对“/spittles”发送GET请求,同时还传入了max和count参数。它测试了这些参数存在时的处理器方法。spittles()方法需要改进为以下代码:

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

需要注意的是max和count都指定了默认值。

通过路径参数接受输入

假设我们的应用程序需要根据给定的ID来展现某一个Spittle记录。其中一种方法就是编写处理器方法,通过使用@RequestParam注解,让它接受ID作为查询参数。另一种方式,要识别的资源应该通过URL路径进行标示。对“/spittles/12345”发起GET请求要优于对“/spittles/show?spittle_id=12345”发起请求。

我们先将这个需求转换为一个测试

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

在Controller中,添加一个针对这种方式请求的方法

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

Spring MVC允许我们在@RequestMapping路径中添加占位符。占位符的名称要用大括号(“{”和“}”)括起来。路径中的其他部分要与所处理的请求完全匹配,但是占位符可以是任意的值。

我们可以看到,spittle()方法的spittleId参数上添加了@PathVariable("spittleId")注解,这表明在请求路径中,不管占位符部分的值是什么都会传递到处理器方法的spittleId参数中。

需要注意的是:在样例中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视图片段:

    <div class="spittleView">
        <div class="spittleMessage"><c:out value="${spittle.message}" /></div>
        <div>
            <span class="spittleTime"><c:out value="${spittle.time}" /></span>
        </div>
    </div>
在浏览器中展现一个spittle

处理表单

Spring MVC的控制器也为表单处理提供了良好的支持。
使用表单分为两个方面:展现表单以及处理用户通过表单提交的数据。在Spittr应用中,我们需要有个表单让新用户注册。

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.RequestPart;
import spittr.data.SpitterRepository;
import spittr.model.Spitter;
import static org.springframework.web.bind.annotation.RequestMethod.*;

@Controller
@RequestMapping("/spitter")
public class SpitterController {
    @RequestMapping(value="/register", method=GET)
    public String showRegistrationForm(Model model) {
        return "registerForm";
    }
}

因为视图的名称为registerForm,所以JSP的名称需要是registerForm.jsp。如下就是我们现在所要使用的JSP

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" language="java" pageEncoding="UTF-8" contentType="text/html; charset=utf-8"%>
<!DOCTYPE HTML>
<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 path="firstName" /><br/>
      Last Name: <input path="lastName" /><br/>
      Email: <input type="email" path="email" /><br/>
      Username: <input path="username" /><br/>
      Password: <password path="password" /><br/>
      <input type="submit" value="Register" />
    </form>
  </body>
</html>

需要注意的是:这里的<form>标签中并没有设置action属性。在这种情况下,当表单提交时,它会提交到与展现时相同的URL路径上。也就是,它会提交到“/spitter/register”上。

编写处理表单的控制器

当处理注册表单的POST请求时,控制器需要接受表单数据并将表单数据保存为Spitter对象。最后,为了防止重复提交(用户点击浏览器的刷新按钮又可能会发生这种情况),应该将浏览器重定向到新创建用户的基本信息页面。这些行为通过下面的shouldProcessRegistration()进行测试。

    @Test
    public void shouldProcessRegistration() throws Exception {
        SpitterRepository mockRepository = mock(SpitterRepository.class);
        Spitter unsaved = new Spitter("jbauer", "24hours","Jack", "Bauer");
        Spitter saved = new Spitter(24L,"jbauer", "24hours","Jack", "Bauer");

        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")
                .andExpect(redirectedUrl("/spitter/jbauer"));
        verify(mockRepository,atLeastOnce()).save(unsaved);
    }

在处理POST类型的请求时,在请求处理完成后,最好进行一下重定向,这样浏览器的刷新就不会重复提交表单了。在这个测试中,预期请求会重定向到“/spitter/jbauer”,也就是新建用户的基本信息页面。

最后,测试会校验SpitterRepository的mock实现最终会真正用来保存表单上传入的数据。

现在我们来实现处理表单提交的控制器方法。

package spittr.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
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 spittr.data.SpitterRepository;
import spittr.model.Spitter;
import static org.springframework.web.bind.annotation.RequestMethod.*;

@Controller
@RequestMapping("/spitter")
public class SpitterController {

    private SpitterRepository spitterRepository;

    @Autowired
    public SpitterController(SpitterRepository spitterRepository){
        this.spitterRepository = spitterRepository;
    }

    @RequestMapping(value="/register", method=GET)
    public String showRegistrationForm(Model model) {
        return "registerForm";
    }

    @RequestMapping(value="/register", method=POST)
    public String processRegistration(Spitter spitter) {
        spitterRepository.save(spitter);
        return "redirect:/spitter/"+spitter.getUsername();
    }
}

当使用Spitter对象调用processRegistration()方法时,它会进而调用SpitterRepository的save()方法。

processRegistration()方法做的最后一件事就是返回一个String类型,用来指定视图。

当InternalResourceViewResolver看到视图格式中的“redirect:”前缀时,它就知道要将其解析为重定向的规则,而不是视图的名称。

需要注意的是,除了“redirect:”,InternalResourceViewResolver还能识别“forward:”前缀。当它发现视图格式中以“forward:”最为前缀时,请求将会前往(forward)指定的URL路径,而不再是重定向。

下面在SpitterController中添加一个处理器方法

    @RequestMapping(value="/{username}", method=GET)
    public String showSpitterProfile(@PathVariable String username, Model model) {
        Spitter spitter = spitterRepository.findByUsername(username);
        model.addAttribute(spitter);
        return "profile";
    }

像其他视图一样,现在的基本信息视图非常简单:

    <h1>Your Profile</h1>
    <c:out value="${spitter.username}" /><br/>
    <c:out value="${spitter.firstName}" /> <c:out value="${spitter.lastName}" />
Spittr的基本信息页展示了用户的情况,这些信息是由SpitterController填充到模型中的

校验表单

如果用户在提交表单的时候,文本域为空,会导致新建对象中某些关键字段为空。如果不处理这种现象,将会出现安全问题。

从Spring 3.0开始,在Spring MVC中提供了对Java校验API的支持。在Spring MVC中要使用它的话,只要保证在类路径下包含这个Java API的实现即可,比如Hibernate Validator。

Java校验API定义了多个注解,这些注解可以放到属性上,从而限制这些属性的值。所有注解都位于javax.validation.constraints包中。下表列出了这些校验注解

注解 描述
@AssertFalse 所注解的元素必须是Boolean类型,并且值为false
@AssertTrue 所注解的元素必须是Boolean类型,并且值为true
@DecimalMax 所注解的元素必须是数字,并且它的值要小于或等于给定的BigDecimalString值
@DecimalMin 所注解的元素必须是数字,并且它的值要大于或等于给定的BigDecimalString值
@Digits 所注解的元素必须是数字,并且它的值必须有指定的位数
@Future 所注解的元素的值必须是一个将来的日期
@Max 所注解的元素必须是数字,并且它的值要小于或等于给定的值
@Min 所注解的元素必须是数字,并且它的值要大于或等于给定的值
@NotNull 所注解的元素的值必须不能为Null
@Null 所注解的元素的值必须为Null
@Past 所注解的元素的值必须是一个已过期的日期
@Pattern 所直接的元素的值必须匹配给定的正则表达式
@Size 所注解的元素的值必须是String、集合或数组,并且它的长度要符合给定的范围

除了以上表提供的注解,Java校验API的实现可能还会提供额外的校验注解。同时,也可以定义自己的限制条件。

我们现在把Spitter类的属性添加校验注解

package spittr.model;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import  org.apache.commons.lang3.builder.EqualsBuilder;
import  org.apache.commons.lang3.builder.HashCodeBuilder;

public class Spitter {
    private Long id;

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

    public Spitter() {}

    public Spitter(String username, String password, String firstName, String lastName) {
        this(null, username, password, firstName, lastName);
    }

    public Spitter(Long id, String username, String password, String firstName, String lastName) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.firstName = firstName;
        this.lastName = lastName;
    }

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

    @Override
    public boolean equals(Object that) {
        return EqualsBuilder.reflectionEquals(this, that, "firstName", "lastName", "username", "password");
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this, "firstName", "lastName", "username", "password");
    }
}

现在,Spitter的所有属性都添加了@NotNull和@Size,接下来需要修改processRegistration()方法来应用校验功能。

    @RequestMapping(value="/register", method=POST)
    public String processRegistration(
            @Valid Spitter spitter,   // 校验 Spitter输入
            Errors errors) {
        if (errors.hasErrors()) {
            return "registerForm";   // 如果校验出现错误,则重新返回表单
        }
        spitterRepository.save(spitter);
        return "redirect:/spitter/" + spitter.getUsername();
    }

Spitter参数添加了@Valid注解,这会告知Spring,需要确保这个对象满足校验限制。

如果校验出现错误的话,那么这些错误可以通过Errors对象进行访问,现在这个对象已作为processRegistration()方法的参数。(很重要一点需要注意,Errors参数要紧跟在带有@Valid注解的参数后面,@Valid注解所标注的就是要检验的参数。)processRegistration()方法所做的第一件事就是调用Errors.hasErrors()来检查是否有错误。

如果有错误的话,Errors.hasErrors()将会返回到registerForm,也就是注册表单的视图。如果没有错误的话,Spitter对象将会通过保存,控制器会像之前那样重定向到基本信息页面。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容