Spring MVC起步
前面几篇文章学习了 Spring Core,接下来学习下 Java Web 中的大头 Spring MVC。
对于很多 Java 开发人员来说,基于 Web 的应用程序是他们主要的关注点。如果你有这方面经验的话,你会意识到这种系统所面临的挑战。具体来讲,状态管理、工作流以及验证都是需要解决的重要特性。HTTP 协议的无状态性决定了这些问题都不那么容易解决。
Spring 的 Web 框架就是为了帮你解决这些关注点而设计的。Spring MVC 基于模型-视图-控制器(Model-View-Controller,MVC)模式实现,它能够帮你构建像 Spring 框架那样灵活和松耦合的 Web 应用程序。
从现在开始我们将会介绍 Spring MVC Web 框架,并使用新的 Spring MVC 注解来构建处理各种 Web 请求、参数和表单输入的控制器。在深入介绍 Spring MVC 之前,让我们先总体上介绍一下 Spring MVC,并建立起 Spring MVC 运行的基本配置。
跟踪 Spring MVC 的请求
每当用户在 Web 浏览器中点击链接或提交表单的时候,请求就开始工作了。对请求的工作描述就像是快递投送员。与邮局投递员或 FedEx 投送员一样,请求会将信息从一个地方带到另一个地方。
请求是一个十分繁忙的家伙。从离开浏览器开始到获取响应返回,它会经历好多站,在每站都会留下一些信息同时也会带上其他信息。图 5.1 展示了请求使用 Spring MVC 所经历的所有站点。

在请求离开浏览器时①,会带有用户所请求内容的信息,至少会包含请求的 URL。但是还可能带有其他的信息,例如用户提交的表单信息。
请求旅程的第一站是 Spring 的 DispatcherServlet。与大多数基于 Java 的 Web 框架一样,Spring MVC 所有的请求都会通过一个前端控制器(front controller)Servlet。前端控制器是常用的 Web 应用程序模式,在这里一个单实例的 Servlet 将请求委托给应用程序的其他组件来执行实际的处理。在 Spring MVC 中,DispatcherServlet 就是前端控制器。
DispatcherServlet 的任务是将请求发送给 Spring MVC 控制器 (controller)。控制器是一个用于处理请求的 Spring 组件。在典型的应用程序中可能会有多个控制器,DispatcherServlet 需要知道应该将请求发送给哪个控制器。所以 DispatcherServlet 以会查询一个或多个处理器映射(handler mapping)② 来确定请求的下一站在哪里。处理器映射会根据请求所携带的 URL 信息来进行决策。
一旦选择了合适的控制器,DispatcherServlet 会将请求发送给选中的控制器③。到了控制器,请求会卸下其负载(用户提交的信息)并耐心等待控制器处理这些信息。(实际上,设计良好的控制器本身只处理很少甚至不处理工作,而是将业务逻辑委托给一个或多个服务对象进行处理。)
控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示。这些信息被称为模型(model)。不过仅仅给用户返回原始的信息是不够的 —— 这些信息需要以用户友好的方式进行格式化,一般会是 HTML。所以,信息需要发送给一个视图 (view),通常会是 JSP。
控制器所做的最后一件事就是将模型数据打包,并且标示出用于渲染输出的视图名。它接下来会将请求连同模型和视图名发送回 DispatcherServlet ④。
这样,控制器就不会与特定的视图相耦合,传递给 DispatcherServlet 的视图名并不直接表示某个特定的 JSP。实际上,它甚至并不能确定视图就是 JSP。相反,它仅仅传递了一个逻辑名称,这个名字将会用来查找产生结果的真正视图。DispatcherServlet 将会使用视图解析器(view resolver)⑤ 来将逻辑视图名匹配为一个特定的视图实现,它可能是也可能不是 JSP。
既然 DispatcherServlet 已经知道由哪个视图渲染结果,那请求的任务基本上也就完成了。它的最后一站是视图的实现(可能是 JSP)⑥,在这里它交付模型数据。请求的任务就完成了。视图将使用模型数据渲染输出,这个输出会通过响应对象传递给客户端(不会像听上去那样硬编码)⑦ 。
可以看到,请求要经过很多的步骤,最终才能形成返回给客户端的响应。大多数的步骤都是在 Spring 框架内部完成的,也就是图 5.1 所示的组件中。尽管这里的主要内容都关注于如何编写控制器,但在此之前我们首先看一下如何搭建 Spring MVC 的基础组件。
搭建 Spring MVC
从 iDEA 新建 Maven 项目:

在 pom.xml中添加依赖:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<spring.version>5.3.19</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
在 src\main目录下添加 java 目录和 resources 目录,在src\webapp目录下添加 resources 目录。
这样大致的环境已经搭建好了。
基于图 5.1,看上去我们需要配置很多的组成部分。幸好,借助于最近几个 Spring 新版本的功能增强,开始使用 Spring MVC 变得非常简单了。现在,我们要使用最简单的方式来配置 Spring MVC:所要实现的功能仅限于运行我们所创建的控制器。
配置 DispatcherServlet
DispatcherServlet 是 Spring MVC 的核心。在这里请求会第一次接触到框架,它要负责将请求路由到其他的组件之中。
按照传统的方式,像 DispatcherServlet 这样的 Servlet 会配置在 web.xml 文件中,这个文件会放到应用的 WAR 包里面。当然,这是配置 DispatcherServlet 的方法之一。但是,借助于 Servlet 3 规范和 Spring 3.1 的功能增强,这种方式已经不是唯一的方案了,这也不是我们这次所使用的配置方法。
我们会使用 Java 将 DispatcherServlet 配置在 Servlet 容器中,而不会再使用 web.xml 文件。
package syuez.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* getServletMappings() 会将一个或者多个路径映射到 DispatcherServlet 上
* 本例中它映射的是 "/" ,这表示它会是应用默认的 Servlet,它会处理进入应用的所有请求
*/
@Override
protected String[] getServletMappings() {
return new String[] { "/" }; // 将 DispatcherServlet 映射到 "/"
}
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class }; // 指定配置类
}
}
这里我们在 java 目录新建了软件包 syuez 并进一步创建了用于存放配置的 config 目录。上面这个类的名字是 SpittrWebAppInitializer,它位于名为 syuez.config 的包中。稍后会对其进行介绍,但现在,你只需要知道我们所要创建的应用名为 Spittr。
要理解SpittrWebAppInitializer类是如何工作的,我们可能只需要知道扩展 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(),它会将一个或多个路径映射到 Dispatcher-Servlet上。在本例中,它映射的是 "/",这表示它会是应用的默认 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 中。稍后我们将会看到这两个类的内容。
需要注意的是,通过 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 进行配置的,不过,现在我们会让 Spring MVC 的搭建过程尽可能简单并基于 Java 进行配置。
我们所能创建的最简单的 Spring MVC 配置就是一个带有 @EnableWebMvc 注解的类:
package syuez.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@Configuration
@EnableWebMvc
public class WebConfig {
}
这可以运行起来,它的确能够启用 Spring MVC,但还有不少问题要解决:
- 没有配置视图解析器。如果这样的话,Spring 默认会使用 BeanNameViewResolver,这个视图解析器会查找 ID 与视图名称匹配的 bean,并且查找的 bean 要实现 View 接口,它以这样的方式来解析视图。
- 没有启用组件扫描。这样的结果就是,Spring 只能找到显式声明在配置类中的控制器。
- 这样配置的话,DispatcherServlet 会映射为应用的默认 Servlet,所以它会处理所有的请求,包括对静态资源的请求,如图片和样式表(在大多数情况下,这可能并不是你想要的效果)。
因此,我们需要在 WebConfig 这个最小的 Spring MVC 配置上再加一些内容,从而让它变得真正有用。如下程序清单中的 WebConfig 解决了上面所述的问题。
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.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@EnableWebMvc // 启用 Spring MVC
@ComponentScan("syuez.web") // 启用组件扫描
public class WebConfig implements WebMvcConfigurer {
// 配置 JSP 视图解析器
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
// 配置静态资源的处理
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
在上面的程序清单中第一件需要注意的事情是 WebConfig 现在添加了 @ComponentScan 注解,因此将会扫描 syuez.web 包来查找组件。稍后你就会看到,我们所编写的控制器将会带有 @Controller 注解,这会使其成为组件扫描时的候选 bean。因此,我们不需要在配置类中显式声明任何的控制器。
接下来,我们添加了一个 ViewResolver bean。更具体来讲,是 InternalResourceViewResolver。我们将会在后续更为详细地讨论视图解析器。我们只需要知道它会查找 JSP 文件,在查找的时候,它会在视图名称上加一个特定的前缀和后缀(例如,名为 home 的视图将会解析为 /WEB-INF/views/home.jsp)。
最后,新的 WebConfig 类还扩展了 WebMvcConfigurer 并重写了其 configureDefaultServletHandling() 方法。通过调用 DefaultServletHandlerConfigurer 的 enable() 方法,我们要求 DispatcherServlet 将对静态资源的请求转发到 Servlet 容器中默认的 Servlet 上,而不是使用 DispatcherServlet 本身来处理此类请求。
WebConfig 已经就绪,那 RootConfig 呢?因为本章聚焦于 Web 开发,而 Web 相关的配置通过 DispatcherServlet 创建的应用上下文都已经配置好了,因此现在的 RootConfig 相对很简单:
package syuez.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={"syuez"},
excludeFilters={
@Filter(type=FilterType.ANNOTATION, value= EnableWebMvc.class)
})
public class RootConfig {
}
唯一需要注意的是 RootConfig 使用了 @ComponentScan 注解。这样的话,在本书中,我们就有很多机会用非 Web 的组件来充实完善 RootConfig。
现在,我们基本上已经可以开始使用 Spring MVC 构建 Web 应用了。此时,最大的问题在于,我们要构建的应用到底是什么。
Spittr 应用简介
为了实现在线社交的功能,我们将要构建一个简单的微博(microblogging)应用。在很多方面,我们所构建的应用与最早的微博应用 Twitter 很类似。在这个过程中,我们会添加一些小的变化。当然,我们要使用 Spring 技术来构建这个应用。
因为从 Twitter 借鉴了灵感并且通过 Spring 来进行实现,所以它就有了一个名字:Spitter。再进一步,应用网站命名中流行的模式,如 Flickr,我们去掉字母 e,这样的话,我们就将这个应用称为 Spittr。这个名称也有助于区分应用名称和领域类型,因为我们将会创建一个名为 Spitter 的领域类。
Spittr 应用有两个基本的领域概念:Spitter(应用的用户)和 Spittle(用户发布的简短状态更新)。当我们在书中完善 Spittr 应用的功能时,将会介绍这两个领域概念。在本章中,我们会构建应用的 Web 层,创建展现 Spittle 的控制器以及处理用户注册成为 Spitter 的表单。
舞台已经搭建完成了。我们已经配置了 DispatcherServlet,启用了基本的 Spring MVC 组件并确定了目标应用。让我们进入本章的核心内容:使用 Spring MVC 控制器处理 Web 请求。
编写基本的控制器
在 Spring MVC 中,控制器只是方法上添加了 @RequestMapping 注解的类,这个注解声明了它们所要处理的请求。
开始的时候,我们尽可能简单,假设控制器类要处理对 / 的请求, 并渲染应用的首页。创建一个HomeController 控制器,它可能是最简单的 Spring MVC 控制器类了:
package syuez.web;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller // 声明为一个控制器
public class HomeController {
@RequestMapping(value="/", method = GET) // 处理对 "/" 的 GET 请求
public String home(Model model) {
return "home"; // 视图名为 home
}
}
你可能注意到的第一件事情就是 HomeController 带有 @Controller 注解。很显然这个注解是用来声明控制器的,但实际上这个注解对 Spring MVC 本身的影响并不大。
HomeController 是一个构造型(stereotype)的注解,它基于 @Component 注解。在这里,它的目的就是辅助实现组件扫描。因为 HomeController 带有 @Controller 注解,因此组件扫描器会自动找到 HomeController,并将其声明为 Spring 应用上下文中的一个 bean。
其实,你也可以让 HomeController 带有 @Component 注解,它所实现的效果是一样的,但是在表意性上可能会差一些,无法确定 HomeController 是什么组件类型。
HomeController 唯一的一个方法,也就是 home() 方法,带有 @Request-Mapping 注解。它的 value 属性指定了这个方法所要处理的请求路径,method 属性细化了它所处理的 HTTP 方法。在本例中,当收到对 / 的 HTTP GET 请求时,就会调用 home() 方法。
你可以看到,home() 方法其实并没有做太多的事情:它返回了一个 String 类型的 home 。这个 String 将会被 Spring MVC 解读为要渲染的视图名称。DispatcherServlet 会要求视图解析器将这个逻辑名称解析为实际的视图。
鉴于我们配置 InternalResourceViewResolver 的方式,视图名 home 将会解析为 /WEB-INF/views/home.jsp 路径的 JSP。现在,我们会让 Spittr 应用的首页相当简单,如下所示。
<%@ 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>Welcome to Spitter</h1>
<a href="<c:url value="/spittles" />">Spittles</a> |
<a href="<c:url value="/spitter/register" />">Register</a>
</body>
</html>
其中 value="/resources/style.css" 就是最开始创建项目时的 webapp/resources,这里放上基本的 CSS 样式:
body {
font-family: sans-serif;
}
.spittleList h1 {
font-size: 16pt;
}
.spittleList ul {
list-style: none;
margin-left: 0px;
padding-left: 0px;
}
.spittleList ul li:first-child {
border-top: 0px;
}
.spittleList ul li {
border-top: 1px solid #cccccc;
}
.spittleTime {
font-size: 8pt;
}
.spittleLocation {
font-size: 8pt;
}
.spittleForm h1 {
font-size: 16pt;
}
至于
<a href="<c:url value="/spittles" />">Spittles</a> |
<a href="<c:url value="/spitter/register" />">Register</a>
这两个 a 标签的连接所指向的控制器现在还没创建好,暂时不用管。
简单介绍下 home 页面。它只是欢迎应用的用户,并提供了两个链接:一个是查看 Spittle 列表,另一个是在应用中进行注册。
在本章完成之前,我们将会实现处理这些请求的控制器方法。但现在,让我们对这个控制器( home 控制器)发起一些请求,看一下它是否能够正常工作。测试控制器最直接的办法可能就是构建并部署应用,然后通过浏览器对其进行访问,但是自动化测试可能会给你更快的反馈和更一致的独立结果。所以,让我们编写一个针对 HomeController 的测试。
测试控制器
让我们再审视一下 HomeController。如果你眼神不太好的话,你甚至可能注意不到这些注解,所看到的仅仅是一个简单的 POJO。我们都知道测试 POJO 是很容易的。因此,我们可以编写一个简单的类来测试 HomeController,如下所示:
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import syuez.web.HomeController;
public class HomeControllerTest {
@Test
public void testHomePage() throws Exception {
HomeController controller = new HomeController();
assertEuqals("home", controller.home());
}
}
这个测试很简单,但它只测试了 home() 方法中会发生什 么。在测试中会直接调用 home() 方法,并断言返回包含 home 值的 String。它完全没有站在 Spring MVC 控制器的视角进行测试。这个测试没有断言当接收到针对 / 的 GET 请求时会调用 home() 方法。因为它返回的值就是 home,所以也没有真正判断 home 是视图的名称。
不过从 Spring 3.2 开始,我们可以按照控制器的方式来测试 Spring MVC 中的控制器了,而不仅仅是作为 POJO 进行测试。Spring 现在包含了一种 mock Spring MVC 并针对控制器执行 HTTP 请求的机制。这样的话,在测试控制器的时候,就没有必要再启动 Web 服务器和 Web 浏览器 了。
为了阐述如何测试 Spring MVC 的控制器,我们重写 HomeControllerTest 并使用 Spring MVC 中新的测试特性。
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.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import syuez.web.HomeController;
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 视图
}
}
尽管新版本的测试只比之前版本多了几行代码,但是它更加完整地测试了 HomeController。这次我们不是直接调用 home() 方法并测试它的返回值,而是发起了对 / 的 GET 请求,并断言结果视图的名称为 home。它首先传递一个 HomeController 实例到 MockMvcBuilders.standaloneSetup() 并调用 build() 来构建 MockMvc 实例。然后它使用 MockMvc 实例来执行针对 / 的 GET 请求并设置期望得到的视图名称。
启动 Tomcat
尽管针对 HomeController 控制器的测试通过了,但是我们仍然没有在浏览器中真的测试过。

点击编辑配置

点击添加,找到Tomcat

根据实际环境按照上图配置,然后添加部署标签

最后点击确定。运行 Tomcat 即可,在浏览器中输入 http://localhost:8080/

文本来源: