天天看点

第五章 构建Spring Web应用程序5.1 Spring MVC起步5.2 编写基本的控制器5.3 接受请求的输入5.4 处理表单

Spring MVC基于模型-视图-控制器(Model-View-Controller,MVC)模式实现。

5.1 Spring MVC起步

5.1.1 跟踪Spring MVC的请求

每当用户在Web浏览器中点击链接或提交表单的时候,请求就开始工作了。请求是一个十分繁忙的家伙。从离开浏览器开始到获取响应返回,它会经历好多站,在每站都会留下一些信息同时也会带上其他信息。

第五章 构建Spring Web应用程序5.1 Spring MVC起步5.2 编写基本的控制器5.3 接受请求的输入5.4 处理表单

在请求离开浏览器时 ,会带有用户所请求内容的信息,至少会包含请求的URL。但是还可能带有其他的信息,例如用户提交的表单信息。

  1. 请求旅程的第一站是Spring的DispatcherServlet。与大多数基于Java的Web框架一样,Spring MVC所有的请求都会通过一个前端控制器(front controller)Servlet。前端控制器是常用的Web应用程序模式,在这里一个单实例的Servlet将请求委托给应用程序的其他组件来执行实际的处理。在Spring MVC中,DispatcherServlet就是前端控制器。
  2. DispatcherServlet的任务是将请求发送给Spring MVC控制器(controller)。控制器是一个用于处理请求的Spring组件。在典型的应用程序中可能会有多个控制器,DispatcherServlet需要知道应该将请求发送给哪个控制器。所以DispatcherServlet以会查询一个或多个处理器映射(handler mapping) 来确定请求的下一站在哪里。处理器映射会根据请求所携带的URL信息来进行决策。
  3. 一旦选择了合适的控制器,DispatcherServlet会将请求发送给选中的控制器 。到了控制器,请求会卸下其负载(用户提交的信息)并耐心等待控制器处理这些信息。(实际上,设计良好的控制器本身只处理很少甚至不处理工作,而是将业务逻辑委托给一个或多个服务对象进行处理。)
  4. 控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示。这些信息被称为模型(model)。不过仅仅给用户返回原始的信息是不够的——这些信息需要以用户友好的方式进行格式化,一般会是HTML。所以,信息需要发送给一个视图(view),通常会是JSP。
  5. 控制器所做的最后一件事就是将模型数据打包,并且标示出用于渲染输出的视图名。它接下来会将请求连同模型和视图名发送回DispatcherServlet 。
  6. 这样,控制器就不会与特定的视图相耦合,传递给DispatcherServlet的视图名并不直接表示某个特定的JSP。实际上,它甚至并不能确定视图是JSP。相反,它仅仅传递了一个逻辑名称,这个名字将会用来查找产生结果的真正视图。DispatcherServlet将会使用视图解析器(view resolver) 来将逻辑视图名匹配为一个特定的视图实现,它可能是也可能不是JSP。
  7. 既然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数量。

为了实现这个分页的功能,我们所编写的处理器方法要接受如下的参数:

  • before

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

    参数(表明在结果中要包含的Spittle数量)
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