Spring实战(第4版)第5章 构建Spring Web应用程序

第5章 构建Spring Web应用程序

Spring MVC基于模型-视图-控制器(Model-View-Controller,MVC)模式实现,它能够帮你构建像Spring框架那样灵活和松耦合的Web应用程序。

在本章中,我们将会介绍Spring MVC Web框架,并使用新的Spring MVC注解来构建处理各种Web请求、参数和表单输入的控制器。在深入介绍Spring MVC之前,让我们先总体上介绍一下Spring MVC,并建立起Spring MVC运行的基本 配置。

5.1 Spring MVC起步

5.1.1 跟踪Spring MVC的请求

请求是一个十分繁忙的家伙。从离开浏览器开始到获取响应返回,它会经历好多站,在每站都会留下一些信息同时也会带上其他信息。图5.1展示了请求使用Spring MVC所经历的所有站点。

image-20210827143111077

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

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

请求旅程的第一站是Spring的DispatcherServlet。与大多数基于Java的Web框架一样,Spring MVC所有的请求都会通过一个前端控制器(front controller)Servlet。前端控制器是常用的Web应用程序模式,在这里一个单实例的Servlet将请求委托给应用程序的其他组件来执行实际的处理。在Spring MVC中,DispatcherServlet就是前端控制器。

DispatcherServlet的任务是将请求发送给Spring MVC控制器(controller)。控制器是一个用于处理请求的Spring组件。在典型的应用程序中可能会有多个控制器,DispatcherServlet需要知道应该将请求发送给哪个控制器。所以DispatcherServlet以会查询一个或多个处理器映射(handler mapping)image-20210827143824969来确定请求的下一站在哪里。处理器映射会根据请求所携带的URL信息来进行决策。

一旦选择了合适的控制器,DispatcherServlet会将请求发送给选中的控制器image-20210827143840574。到了控制器,请求会卸下其负载(用户提交的信息)并耐心等待控制器处理这些信息。(实际上,设计良好的控制器本身只处理很少甚至不处理工作,而是将业务逻辑委托给一个或多个服务对象进行处理。)

控制器在完成逻辑处理后,通常会产生一些信息,这些信息需要返回给用户并在浏览器上显示。这些信息被称为模型(model)。不过仅仅给用户返回原始的信息是不够的——这些信息需要以用户友好的方式进行格式化,一般会是HTML。所以,信息需要发送给一个视图(view),通常会是JSP。

控制器所做的最后一件事就是将模型数据打包,并且标示出用于渲染输出的视图名。它接下来会将请求连同模型和视图名发送回DispatcherServletimage-20210827143859680

这样,控制器就不会与特定的视图相耦合,传递给DispatcherServlet的视图名并不直接表示某个特定的JSP。实际上,它甚至并不能确定视图就是JSP。相反,它仅仅传递了一个逻辑名称,这个名字将会用来查找产生结果的真正视图。DispatcherServlet将会使用视图解析器(view resolver)image-20210827143920116来将逻辑视图名匹配为一个特定的视图实现,它可能是也可能不是JSP。

既然DispatcherServlet已经知道由哪个视图渲染结果,那请求的任务基本上也就完成了。它的最后一站是视图的实现(可能是JSP)image-20210827143933563,在这里它交付模型数据。请求的任务就完成了。视图将使用模型数据渲染输出,这个输出会通过响应对象传递给客户端(不会像听上去那样硬编码)image-20210827143943093

可以看到,请求要经过很多的步骤,最终才能形成返回给客户端的响应。大多数的步骤都是在Spring框架内部完成的,也就是图5.1所示的组件中。尽管本章的主要内容都关注于如何编写控制器,但在此之前我们首先看一下如何搭建Spring MVC的基础组件。

5.1.2 搭建Spring MVC

基于图5.1,看上去我们需要配置很多的组成部分。幸好,借助于最近几个Spring新版本的功能增强,开始使用Spring MVC变得非常简单了。现在,我们要使用最简单的方式来配置Spring MVC:所要实现的功能仅限于运行我们所创建的控制器。在第7章中,我们会看一些其他的配置选项。

配置DispatcherServlet

DispatcherServlet是Spring MVC的核心。在这里请求会第一次接触到框架,它要负责将请求路由到其他的组件之中。

按照传统的方式,像DispatcherServlet这样的Servlet会配置在web.xml文件中,这个文件会放到应用的WAR包里面。当然,这是配置DispatcherServlet的方法之一。但是,借助于Servlet 3规范和Spring 3.1的功能增强,这种方式已经不是唯一的方案了,这也不是我们本章所使用的配置方法。

我们会使用Java将DispatcherServlet配置在Servlet容器中,而不会再使用web.xml文件。如下的程序清单展示了所需的Java类。

程序清单5.1 配置DispatcherServlet

image-20210830110529596

在我们深入介绍程序清单5.1之前,你可能想知道spittr到底是什么意思。这个类的名字是SpittrWebAppInitializer,它位于名为spittr.config的包中。我稍后会对其进行介绍(在5.1.3小节中),但现在,你只需要知道我们所要创建的应用名为Spittr。

要理解程序清单5.1是如何工作的,我们可能只需要知道扩展AbstractAnnotation-ConfigDispatcherServletInitializer的任意类都会自动地配置Dispatcher-Servlet和Spring应用上下文,Spring的应用上下文会位于应用程序的Servlet上下文之中。

AbstractAnnotationConfigDispatcherServletInitializer剖析

如果你坚持要了解更多细节的话,那就看这里吧。在Servlet 3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果能发现的话,就会用它来配置Servlet容器。

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

尽管它的名字很长,但是AbstractAnnotationConfigDispatcherServlet-Initializer使用起来很简便。在程序清单5.1中,SpittrWebAppInitializer重写了三个方法。

第一个方法是getServletMappings(),它会将一个或多个路径映射到DispatcherServlet上。在本例中,它映射的是“/”,这表示它会是应用的默认Servlet。它会处理进入应用的所有请求。

为了理解其他的两个方法,我们首先要理解DispatcherServlet和一个Servlet监听器(也就是ContextLoaderListener)的关系。

两个应用上下文之间的故事

DispatcherServlet启动的时候,它会创建Spring应用上下文,并加载配置文件或配置类中所声明的bean。在程序清单5.1的getServletConfigClasses()方法中,我们要求DispatcherServlet加载应用上下文时,使用定义在WebConfig配置类(使用Java配置)中的bean。

但是在Spring Web应用中,通常还会有另外一个应用上下文。另外的这个应用上下文是由ContextLoaderListener创建的。

我们希望DispatcherServlet加载包含Web组件的bean,如控制器、视图解析器以及处理器映射,而ContextLoaderListener要加载应用中的其他bean。这些bean通常是驱动应用后端的中间层和数据层组件。

实际上,AbstractAnnotationConfigDispatcherServletInitializer会同时创建DispatcherServletContextLoaderListenerGetServlet-ConfigClasses()方法返回的带有@Configuration注解的类将会用来定义DispatcherServlet应用上下文中的bean。getRootConfigClasses()方法返回的带有@Configuration注解的类将会用来配置ContextLoaderListener创建的应用上下文中的bean。

在本例中,根配置定义在RootConfig中,DispatcherServlet的配置声明在WebConfig中。稍后我们将会看到这两个类的内容。

需要注意的是,通过AbstractAnnotationConfigDispatcherServlet-Initializer来配置DispatcherServlet是传统web.xml方式的替代方案。如果你愿意的话,可以同时包含web.xml和AbstractAnnotationConfigDispatcher-ServletInitializer,但这其实并没有必要。

如果按照这种方式配置DispatcherServlet,而不是使用web.xml的话,那唯一问题在于它只能部署到支持Servlet 3.0的服务器中才能正常工作,如Tomcat 7或更高版本。Servlet 3.0规范在2009年12月份就发布了,因此很有可能你会将应用部署到支持Servlet 3.0的Servlet容器之中。

如果你还没有使用支持Servlet 3.0的服务器,那么在AbstractAnnotation-ConfigDispatcherServletInitializer子类中配置DispatcherServlet的方法就不适合你了。你别无选择,只能使用web.xml了。我们将会在第7章学习web.xml和其他配置选项。但现在,我们先看一下程序清单5.1中所引用的WebConfigRootConfig,了解一下如何启用Spring MVC。

启用Spring MVC

我们有多种方式来配置DispatcherServlet,与之类似,启用Spring MVC组件的方法也不仅一种。以前,Spring是使用XML进行配置的,你可以使用<mvc:annotation-driven>启用注解驱动的Spring MVC。

我们会在第7章讨论Spring MVC配置可选项的时候,再讨论<mvc:annotation-driven>。不过,现在我们会让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 MVC,但还有不少问题要解决:

  • 没有配置视图解析器。如果这样的话,Spring默认会使用BeanNameView-Resolver,这个视图解析器会查找ID与视图名称匹配的bean,并且查找的bean要实现View接口,它以这样的方式来解析视图。
  • 没有启用组件扫描。这样的结果就是,Spring只能找到显式声明在配置类中的控制器。
  • 这样配置的话,DispatcherServlet会映射为应用的默认Servlet,所以它会处理所有的请求,包括对静态资源的请求,如图片和样式表(在大多数情况下,这可能并不是你想要的效果)。

因此,我们需要在WebConfig这个最小的Spring MVC配置上再加一些内容,从而让它变得真正有用。如下程序清单中的WebConfig解决了上面所述的问题。

程序清单5.2 最小但可用的Spring MVC配置

image-20210830150116910

在程序清单5.2中第一件需要注意的事情是WebConfig现在添加了@Component-Scan注解,因此将会扫描spitter.web包来查找组件。稍后你就会看到,我们所编写的控制器将会带有@Controller注解,这会使其成为组件扫描时的候选bean。因此,我们不需要在配置类中显式声明任何的控制器。

接下来,我们添加了一个ViewResolver bean。更具体来讲,是Internal-ResourceViewResolver。我们将会在第6章更为详细地讨论视图解析器。我们只需要知道它会查找JSP文件,在查找的时候,它会在视图名称上加一个特定的前缀和后缀(例如,名为home的视图将会解析为/WEB-INF/views/home.jsp)。

最后,新的WebConfig类还扩展了WebMvcConfigurerAdapter并重写了其configureDefaultServletHandling()方法。通过调用DefaultServlet-HandlerConfigurerenable()方法,我们要求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={"spitter"},
    excludeFilters={
        @Filter(type=FilterType.ANNOTATION, value=EnableWebMvc.class)
    })
public class RootConfig {
}

唯一需要注意的是RootConfig使用了@ComponentScan注解。这样的话,在本书中,我们就有很多机会用非Web的组件来充实完善RootConfig

现在,我们基本上已经可以开始使用Spring MVC构建Web应用了。此时,最大的问题在于,我们要构建的应用到底是什么。

5.1.3 Spittr应用简介

为了实现在线社交的功能,我们将要构建一个简单的微博(microblogging)应用。在很多方面,我们所构建的应用与最早的微博应用Twitter很类似。在这个过程中,我们会添加一些小的变化。当然,我们要使用Spring技术来构建这个应用。

因为从Twitter借鉴了灵感并且通过Spring来进行实现,所以它就有了一个名字:Spitter。再进一步,应用网站命名中流行的模式,如Flickr,我们去掉字母e,这样的话,我们就将这个应用称为Spittr。这个名称也有助于区分应用名称和领域类型,因为我们将会创建一个名为Spitter的领域类。

Spittr应用有两个基本的领域概念:Spitter(应用的用户)和Spittle(用户发布的简短状态更新)。当我们在书中完善Spittr应用的功能时,将会介绍这两个领域概念。在本章中,我们会构建应用的Web层,创建展现Spittle的控制器以及处理用户注册成为Spitter的表单。

舞台已经搭建完成了。我们已经配置了DispatcherServlet,启用了基本的Spring MVC组件并确定了目标应用。让我们进入本章的核心内容:使用Spring MVC控制器处理Web请求。

5.2 编写基本的控制器

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

开始的时候,我们尽可能简单,假设控制器类要处理对“/”的请求,并渲染应用的首页。程序清单5.3所示的HomeController可能是最简单的Spring MVC控制器类了。

程序清单5.3 HomeController:超级简单的控制器

image-20210830150459837

你可能注意到的第一件事情就是HomeController带有@Controller注解。很显然这个注解是用来声明控制器的,但实际上这个注解对Spring MVC本身的影响并不大。

HomeController是一个构造型(stereotype)的注解,它基于@Component注解。在这里,它的目的就是辅助实现组件扫描。因为HomeController带有@Controller注解,因此组件扫描器会自动找到HomeController,并将其声明为Spring应用上下文中的一个bean。

其实,你也可以让HomeController带有@Component注解,它所实现的效果是一样的,但是在表意性上可能会差一些,无法确定HomeController是什么组件类型。

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

你可以看到,home()方法其实并没有做太多的事情:它返回了一个String类型的“home”。这个String将会被Spring MVC解读为要渲染的视图名称。DispatcherServlet会要求视图解析器将这个逻辑名称解析为实际的视图。

鉴于我们配置InternalResourceViewResolver的方式,视图名“home”将会解析为“/WEB-INF/views/home.jsp”路径的JSP。现在,我们会让Spittr应用的首页相当简单,如下所示。

程序清单5.4 Spittr应用的首页,定义为一个简单的JSP

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page 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并没有太多需要注意的地方。它只是欢迎应用的用户,并提供了两个链接:一个是查看Spittle列表,另一个是在应用中进行注册。图5.2展现了此时的首页是什么样子的。

在本章完成之前,我们将会实现处理这些请求的控制器方法。但现在,让我们对这个控制器发起一些请求,看一下它是否能够正常工作。测试控制器最直接的办法可能就是构建并部署应用,然后通过浏览器对其进行访问,但是自动化测试可能会给你更快的反馈和更一致的独立结果。所以,让我们编写一个针对HomeController的测试。

image-20210830150614544

5.2.1 测试控制器
让我们再审视一下HomeController。如果你眼神不太好的话,你甚至可能注意不到这些注解,所看到的仅仅是一个简单的POJO。我们都知道测试POJO是很容易的。因此,我们可以编写一个简单的类来测试HomeController,如下所示:

程序清单5.5 HomeControllerTest:测试HomeController

package spittr.web;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import spittr.web.HomeController;

public class HomeControllerTest {
@Test
public void testHomePage() throws Exception {
HomeController controller = new HomeController();
assertEquals(“home”, controller.home());
}
}
程序清单5.5中的测试很简单,但它只测试了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中新的测试特性。程序清单5.6展现了新的HomeControllerTest。

程序清单5.6 改进HomeControllerTest

image-20210830150812209

尽管新版本的测试只比之前版本多了几行代码,但是它更加完整地测试了HomeController。这次我们不是直接调用home()方法并测试它的返回值,而是发起了对“/”的GET请求,并断言结果视图的名称为home。它首先传递一个HomeController实例到MockMvcBuilders.standaloneSetup()并调用build()来构建MockMvc实例。然后它使用MockMvc实例来执行针对“/”的GET请求并设置期望得到的视图名称。

5.2.2 定义类级别的请求处理
现在,已经为HomeController编写了测试,那么我们可以做一些重构,并通过测试来保证不会对功能造成什么破坏。我们可以做的一件事就是拆分@RequestMapping,并将其路径映射部分放到类级别上。程序清单5.7展示了这个过程。

程序清单5.7 拆分HomeController中的@RequestMapping

image-20210830151008806

在这个新版本的HomeController中,路径现在被转移到类级别的@RequestMapping上,而HTTP方法依然映射在方法级别上。当控制器在类级别上添加@RequestMapping注解时,这个注解会应用到控制器的所有处理器方法上。处理器方法上的@RequestMapping注解会对类级别上的@RequestMapping的声明进行补充。

就HomeController而言,这里只有一个控制器方法。与类级别的@Request-Mapping合并之后,这个方法的@RequestMapping表明home()将会处理对“/”路径的GET请求。

换言之,我们其实没有改变任何功能,只是将一些代码换了个地方,但是HomeController所做的事情和以前是一样的。因为我们现在有了测试,所以可以确保在这个过程中,没有对原有的功能造成破坏。

当我们在修改@RequestMapping时,还可以对HomeController做另外一个变更。@RequestMapping的value属性能够接受一个String类型的数组。到目前为止,我们给它设置的都是一个String类型的“/”。但是,我们还可以将它映射到对“/homepage”的请求,只需将类级别的@RequestMapping改为如下所示:

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

}
现在,HomeController的home()方法能够映射到对“/”和“/homepage”的GET请求。

5.2.3 传递模型数据到视图中

到现在为止,就编写超级简单的控制器来说,HomeController已经是一个不错的样例了。但是大多数的控制器并不是这么简单。在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中,SpittleID属性的最大值,而count参数表明要返回多少个Spittle对象。为了获得最新的20个Spittle对象,我们可以这样调用findSpittles()

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

现在,我们让Spittle类尽可能的简单,如下面的程序清单5.8所示。它的属性包括消息内容、时间戳以及Spittle发布时对应的经纬度。

程序清单5.8 Spittle类:包含消息内容、时间戳和位置信息

package spittr;
import java.util.Date;

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 getLongitude() {
    return longitude;
  }

  public Double getLatitude() {
    return latitude;
  }

  @Override
  public boolean equals(Object that) {
    return EqualsBuilder.reflectionEquals(this, that, "id", "time");
  }

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

就大部分内容来看,Spittle就是一个基本的POJO数据对象——没有什么复杂的。唯一要注意的是,我们使用Apache Common Lang包来实现equals()hashCode()方法。这些方法除了常规的作用以外,当我们为控制器的处理器方法编写测试时,它们也是有用的。

既然我们说到了测试,那么我们继续讨论这个话题并为新的控制器方法编写测试。如下的程序清单使用Spring的MockMvc来断言新的处理器方法中你所期望的行为。

程序清单5.9 测试SpittleController处理针对“/spittles”的GET请求

reflectionHashCode(this, “id”, “time”);
}
}


就大部分内容来看,`Spittle`就是一个基本的POJO数据对象——没有什么复杂的。唯一要注意的是,我们使用Apache Common Lang包来实现`equals()`和`hashCode()`方法。这些方法除了常规的作用以外,当我们为控制器的处理器方法编写测试时,它们也是有用的。

既然我们说到了测试,那么我们继续讨论这个话题并为新的控制器方法编写测试。如下的程序清单使用Spring的`MockMvc`来断言新的处理器方法中你所期望的行为。

**程序清单5.9 测试SpittleController处理针对“/spittles”的GET请求**

![image-20210830151403433](https://img-blog.csdnimg.cn/img_convert/c4359f80b68e26aebae62eb26f8eb6e2.png)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值