Spring Web 编程详解

课程简介

本课程基于最畅销《Spring 实战》图书介绍了 Java Web 领域相关的技术,同时,深入剖析了 Spring 5 带来的最新 Reactive 理念,让读者既能学到当前最流行的技术,又能把握未来技术发展的方向和潮流。

作者介绍

张卫滨,资深软件工程师,十年的软件开发与设计经验,精通 Java 技术栈相关的开源框架,InfoQ 技术网站社区编辑,译有多本畅销的技术图书。

课程内容

第01课:Spring MVC 简介与环境搭建

作为企业级 Java 开发者,可能开发过一些基于 Web 的应用程序,但对于大部分 Java 开发人员来说,基于 Web 的应用程序是他们主要的关注点。如果有这方面经验的话,就能意识到这种系统所面临的挑战。具体来讲,状态管理、工作流以及验证都是需要解决的重要特性。HTTP 协议的无状态性决定了这些问题都没那么容易解决。

Spring 的 Web 框架就是为了解决这些关注点而设计的。Spring MVC 基于模型—视图—控制器(Model-View-Controller,MVC)模式实现,它能够构建像 Spring 框架那样灵活和松耦合的 Web 应用程序。

在本文中将会概要介绍 Spring MVC Web 框架以及初始化环境的搭建。

Spring MVC 起步

大家知道捕鼠器游戏吗?这是一个疯狂的游戏,它的目标是发送一个小钢球,让它经过一系列稀奇古怪的装置,最后触发捕鼠器。小钢球穿过各种复杂的配件,从一个斜坡上滚下来,被跷跷板弹起,绕过一个微型摩天轮,然后被橡胶靴从桶中踢出去。经过这些后,小钢球会对那只可怜又无辜的橡胶老鼠进行捕获。

乍看上去,Spring MVC 框架与捕鼠器有些类似。Spring 将请求在调度 Servlet、处理器映射(handler mapping)、控制器以及视图解析器(view resolver)之间移动,而捕鼠器中的钢球则会在各种斜坡、跷跷板以及摩天轮之间滚动。但是,不要将 Spring MVC 与 Rube Goldberg-esque 捕鼠器游戏做过多比较。每一个 Spring MVC 中的组件都有特定的目的,并且它也没有那么复杂。

让我们看一下请求是如何从客户端发起,经过 Spring MVC 中的组件,最终再返回到客户端。

跟踪 Spring MVC 的请求

每当用户在 Web 浏览器中单击链接或提交表单的时候,请求就开始工作了,对请求的工作描述就像是快递投送员。与邮局投递员或 FedEx 投送员一样,请求会将信息从一个地方带到另一个地方。

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

Spring Web 编程详解_第1张图片

在请求离开浏览器时,会带有用户所请求内容的信息,至少会包含请求的 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 框架内部完成的,也就是上图所示的组件中。我们首先看一下如何搭建 Spring MVC 的基础组件。

搭建 Spring MVC

基于前面的图片,看上去我们需要配置很多的组成部分。幸好,借助于最近几个 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 文件。如下的程序清单展示了所需的 Java 类。

Spring Web 编程详解_第2张图片

程序清单1 配置 DispatcherServlet

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

要理解程序是如何工作的,我们可能只需要知道扩展 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。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 了。我们稍后会学习 web.xml 和其他配置选项。但现在先看一下程序清单中所引用的 WebConfig 和 RootConfig,了解一下如何启用 Spring MVC。

启用 Spring MVC

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

我们随后会讨论<mvc:annotation-driven>。不过,现在我们会让 Spring MVC 的搭建过程尽可能简单并基于 Java 进行配置。

我们所能创建的最简单的 Spring MVC 配置就是一个带有 @EnableWebMvc 注解的类:

Spring Web 编程详解_第3张图片

这可以运行起来,它的确能够启用 Spring MVC,但还有不少问题要解决:

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

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

Spring Web 编程详解_第4张图片

在上面的程序中第一件需要注意的事情是 WebConfig 现在添加了 @ComponentScan 注解,因此将会扫描 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 相对很简单:

Spring Web 编程详解_第5张图片

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

Spring MVC 的高级配置

前面我们通过扩展 AbstractAnnotationConfigDispatcherServletInitializer 快速搭建了 Spring MVC 环境。在这个便利的基础类中,假设需要基本的 DispatcherServlet 和 ContextLoaderListener 环境,并且 Spring 配置是使用 Java 的,而不是 XML。

尽管对很多 Spring 应用来说,这是一种安全的假设,但是并不一定总能满足我们的要求。除了 DispatcherServlet 以外,可能还需要额外的 Servlet 和 Filter;可能还需要对 DispatcherServlet 本身做一些额外的配置;或者,如果需要将应用部署到 Servlet 3.0 之前的容器中,那么还需要将 DispatcherServlet 配置到传统的 web.xml 中。

自定义 DispatcherServlet 配置

虽然从程序清单1的外观上不一定能够看得出来,但是 Abstract-AnnotationConfigDispatcherServletInitializer 所完成的事情其实比看上去要多。在 SpittrWebAppInitializer 中所编写的三个方法仅仅是必须要重载的 abstract 方法。但实际上还有更多的方法可以进行重载,从而实现额外的配置。

此类的方法之一就是 customizeRegistration()。在 AbstractAnnotation-ConfigDispatcherServletInitializer 将 DispatcherServlet 注册到 Servlet 容器中之后,就会调用 customizeRegistration(),并将 Servlet 注册后得到的 Registration.Dynamic 传递进来。通过重载 customizeRegistration() 方法,可以对 DispatcherServlet 进行额外的配置。

借助 customizeRegistration() 方法中的 ServletRegistration.Dynamic,我们能够完成多项任务,包括通过调用 setLoadOnStartup() 设置 load-on-startup 优先级,通过 setInitParameter() 设置初始化参数,通过调用 setMultipartConfig() 配置 Servlet 3.0 对 multipart 的支持。

添加其他的 Servlet 和 Filter

按照 AbstractAnnotationConfigDispatcherServletInitializer 的定义,它会创建 DispatcherServlet 和 ContextLoaderListener。但是,如果你想注册其他的 Servlet、Filter 或 Listener 的话,那该怎么办呢?

基于 Java 的初始化器(initializer)的一个好处就在于我们可以定义任意数量的初始化器类。因此,如果我们想往 Web 容器中注册其他组件的话,只需创建一个新的初始化器就可以了。最简单的方式就是实现 Spring 的 WebApplicationInitializer 接口。

例如,如下的程序清单展现了如何创建 WebApplicationInitializer 实现并注册一个 Servlet。

Spring Web 编程详解_第6张图片

程序清单3 通过实现 WebApplicationInitializer 来注册 Servlet

程序清单3是相当基础的 Servlet 注册初始化器类。它注册了一个 Servlet 并将其映射到一个路径上。我们也可以通过这种方式来手动注册 DispatcherServlet。(但这并没有必要,因为 AbstractAnnotationConfigDispatcherServletInitializer 没用太多代码就将这项任务完成得很漂亮。)

类似地,我们还可以创建新的 WebApplicationInitializer 实现来注册 Listener 和 Filter。例如,如下的程序清单展现了如何注册 Filter。

Spring Web 编程详解_第7张图片

程序清单4 注册 Filter 的 WebApplicationInitializer

如果要将应用部署到支持 Servlet 3.0 的容器中,那么 WebApplicationInitializer 提供了一种通用的方式,实现在 Java 中注册 Servlet、Filter 和 Listener。不过,如果你只是注册 Filter,并且该 Filter 只会映射到 DispatcherServlet 上的话,那么在 AbstractAnnotationConfigDispatcherServletInitializer 中还有一种快捷方式。

为了注册 Filter 并将其映射到 DispatcherServlet,所需要做的仅仅是重载 AbstractAnnotationConfigDispatcherServletInitializer 的 getServletFilters() 方法。例如,在如下的代码中,重载了 AbstractAnnotationConfig-DispatcherServletInitializer 的 getServletFilters() 方法以注册 Filter:

Spring Web 编程详解_第8张图片

我们可以看到,这个方法返回的是一个 javax.servlet.Filter 的数组。在这里它只返回了一个 Filter,但它实际上可以返回任意数量的 Filter。在这里没有必要声明它的映射路径,getServletFilters() 方法返回的所有 Filter 都会映射到 DispatcherServlet 上。

如果要将应用部署到 Servlet 3.0 容器中,那么 Spring 提供了多种方式来注册 Servlet(包括DispatcherServlet)、Filter和Listener,而不必创建 web.xml 文件。但是,如果你不想采取以上所述方案的话,也是可以的。假设需要将应用部署到不支持 Servlet 3.0 的容器中(或者你只是希望使用 web.xml 文件),那么我们完全可以按照传统的方式,通过 web.xml 配置 Spring MVC。让我们看一下该怎么做。

在 web.xml 中声明 DispatcherServlet

在典型的 Spring MVC 应用中,我们会需要 DispatcherServlet 和 ContextLoaderListener。AbstractAnnotationConfigDispatcherServlet-Initializer 会自动注册它们,但是如果需要在 web.xml 中注册的话,那就需要我们自己来完成这项任务了。

如下是一个基本的 web.xml 文件,它按照传统的方式搭建了 DispatcherServlet 和 ContextLoaderListener。

Spring Web 编程详解_第9张图片

程序清单5 在 web.xml 中搭建 Spring MVC

就像曾经介绍过的,ContextLoaderListener 和 DispatcherServlet 各自都会加载一个 Spring 应用上下文。上下文参数 contextConfigLocation 指定了一个 XML 文件的地址,这个文件定义了根应用上下文,它会被 ContextLoaderListener 加载。如程序清单5所示,根上下文会从“/WEB-INF/spring/root-context.xml”中加载 bean 定义。

DispatcherServlet 会根据 Servlet 的名字找到一个文件,并基于该文件加载应用上下文。在程序清单5中,Servlet 的名字是 appServlet,因此 DispatcherServlet 会从“/WEB-INF/appServlet-context.xml”文件中加载其应用上下文。

如果你希望指定 DispatcherServlet 配置文件的位置的话,那么可以在 Servlet 上指定一个 contextConfigLocation 初始化参数。例如,如下的配置中,DispatcherServlet 会从“/WEB-INF/spring/appServlet/servlet-context.xml”加载它的 bean:

Spring Web 编程详解_第10张图片

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

Spittr 应用简介

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

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

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

舞台已经搭建完成了。我们已经配置了 DispatcherServlet,启用了基本的 Spring MVC 组件并确定了目标应用,接下来我们将会阐述如何编写并测试一个基本的控制器。

第02课:编写基本的控制器
第03课:参数输入与表单处理
第04课:文件上传功能实现
第05课:使用 Spring MVC 创建 REST API
第06课:Spring 5 中 WebFlux 的简介
第07课:Spring WebFlux 使用详解
第08课:Spring MVC 源码剖析——请求处理
第09课:Spring MVC 源码剖析——数据绑定与参数转换
第10课:WebFlux 源码剖析

阅读全文: http://gitbook.cn/gitchat/column/5a2f3a93626a7a2421b9551c

你可能感兴趣的:(Spring Web 编程详解)