原文:Pro Spring MVC with WebFlux
协议:CC BY-NC-SA 4.0
一、建立本地开发环境
Spring 于 2002 年 10 月发布,是一个使用 Java 开发的开源框架和控制反转(IoC)容器,它是为 Java 平台而构建的。它已经从一个小型的库集合转变为一个大型的成熟项目集合,旨在简化开发,即使解决方案很复杂。
这本书从打包成 jar 并部署到应用服务器的经典 web 应用,到由一组易于部署在云环境中的微服务组成的应用,每个微服务都在自己的 VM 或容器上。
这一切都始于开发人员在编写和运行代码之前需要安装的一组工具。
如果你知道如何使用 SDKMAN, 1 你可以跳过下两节解释如何安装 Java SDK 和 Gradle。如果你不知道如何使用 SDKMAN 或者从来不知道它的存在,试试看;它是一个管理多个 SDK 并行版本的工具。如果您有其他项目在本地使用不同版本的 Java 和 Gradle,这个工具可以帮助您在它们之间轻松切换。
安装 Java SDK
由于 Spring 是一个用来编写和运行 Spring 应用的 Java 框架,所以您需要安装 Java SDK。这个项目是用 JDK 14 编写建造的。要安装 JDK 14,请从 www.oracle.com/java/
下载与您的操作系统匹配的 JDK 并安装。因此,如果您正在构建一个应用,并打算使用它来获取经济利益,您可能需要考虑 Oracle 许可或使用开源 JDK。 2
图 1-1
Java 标志 3
我们建议您将JAVA_HOME
环境变量设置为指向 Java 14 的安装目录(JDK 解压缩的目录),并将$JAVA_HOME/bin (%JAVA_HOME%in
添加到系统的常规路径中(Windows 用户使用)
)。这背后的原因是为了确保用 Java 编写的任何其他开发应用都使用这个版本的 Java,并防止开发过程中出现奇怪的不兼容错误。如果您想从终端运行构建,您肯定使用了预期的 Java 版本。
重新启动终端,并通过打开终端(Windows 中的命令提示符或 macOS 和 Linux 上安装的任何类型的终端)并键入以下命令,验证操作系统看到的 Java 版本是否是您安装的版本。
> java -version # to check the runtime
12
然后是下面。
> javac -version # to check the compiler
12
您应该会看到类似下面的输出。
> java -version
java version "14.0.2" 2020-07-14
Java(TM) SE Runtime Environment (build 14.0.2+12-46)
Java HotSpot(TM) 64-Bit Server VM (build 14.0.2+12-46, mixed mode, sharing)
> javac -version
javac 14.0.2
12345678
安装 Gradle
Gradle 是一个开放源码的构建自动化工具,它足够灵活,可以构建几乎任何类型的软件。它在配置文件中使用 Groovy,这使得它是可定制的。本书附带的项目是用 Gradle 6.x 成功搭建的。
图 1-2
Gradle logo 4
本书附带的源代码可以使用 Gradle Wrapper 编译和执行,Gradle Wrapper 是 Windows 上的批处理脚本和其他操作系统上的 shell 脚本。
当您通过包装器启动 Gradle 构建时,Gradle 会自动下载到您的项目中来运行构建;因此,您不需要在您的系统上显式安装它。接下来介绍的推荐开发编辑器知道如何使用 Gradle Wrapper 构建代码。在 www.gradle.org/docs/current/userguide/gradle_wrapper.html
的公开文档中有关于如何使用 Gradle 包装器的说明。
推荐的做法是将代码和构建工具分开保存。如果你决定在你的系统上安装 Gradle,你可以从 www.gradle.org
下载二进制文件,解压并将内容复制到硬盘上。(或者,如果您有兴趣,可以下载包含二进制文件、源代码和文档的完整包。)创建一个GRADLE_HOME
环境变量,并将其指向解包 Gradle 的位置。此外,将$GRADLE_HOME/bin
(对于 Windows 用户为%GRADLE_HOME%in
)添加到系统的常规路径中,以便您可以在终端中构建项目。
Gradle 被选为本书源代码的构建工具,因为它设置简单,配置文件小,定义执行任务灵活,Spring 团队目前使用它来构建所有的 Spring 项目。
要验证操作系统是否看到您刚刚安装的 Gradle 版本,请打开一个终端(Windows 中的命令提示符,以及安装在 macOS 和 Linux 上的任何类型的终端)并键入
gradle -version
12
您应该会看到类似下面的内容。
gradle -version
------------------------------------------------------------
Gradle 6.7
------------------------------------------------------------
Build time: 2020-08-04 22:01:06 UTC
Revision: 00a2948da9ea69c523b6b094331593e6be6d92bc
Kotlin: 1.3.72
Groovy: 2.5.12
Ant: Apache Ant(TM) version 1.10.8 compiled on May 10 2020
JVM: 14.0.2 (Oracle Corporation 14.0.2+12-46)
OS: Mac OS X 10.15.6 x86_64
123456789101112131415
运行这个命令还可以验证 Gradle 使用的是预期的 JDK 版本。
安装 Apache Tomcat
Web 应用应该由应用服务器托管,除非它们是使用 Spring Boot 构建的,在这种情况下,依赖嵌入式服务器更实际。Apache Tomcat 5 是 Java Servlet、JavaServer Pages、Java Expression Language 和 WebSocket 技术的开源实现。
图 1-3
阿帕奇雄猫标志 6
本书的 Spring MVC 项目是在 Apache Tomcat 9.x 中测试的,要安装 Apache Tomcat,去官方网站获取与你的操作系统匹配的版本。在熟悉的地方打开包装。在基于 Unix 的系统上,您可以使用软件包管理器来安装它。如果你手动安装,记得转到bin
目录,并使所有文件可执行。
推荐的 IDE
我们建议您将 IntelliJ IDEA 用于本书中的代码。它是最智能的 Java IDE。
图 1-4
IntelliJ IDEA logo7
IntelliJ IDEA 为 Java EE 提供了优秀的特定于框架的编码帮助和生产力提升特性,Spring 也包含了对 Gradle 的良好支持。它是帮助您专注于学习 Spring(而不是如何使用 IDE)的完美选择。可以从 JetBrains 官方网站( www.jetbrains.com/idea/
)下载。它在你的操作系统上也很轻,并且易于使用。
IntelliJ IDEA 还可以与 Apache Tomcat 很好地集成,这允许您部署项目以从编辑器启动和停止服务器。
既然已经讨论了工具,我们来谈谈项目。
书店项目
包含本书源代码的项目被组织成一个多模块的梯度项目。每一章都有一个或多个相应的项目,您可以很容易地识别它们,因为它们以章节号为前缀。表 1-1 列出了这些项目,并对每个项目进行了简短描述。
表 1-1
书店项目模块
|
回
|
项目名
|
描述
|
| — | — | — |
| – | 书店-MVC-共享 | Spring MVC 项目使用的实体和实用程序类 |
| – | 书店-共享 | Spring Boot 项目使用的实体和公用事业分类 |
| one | 第一章-书店 | 一个简单的 Spring Boot Web MVC 项目,具有典型的 Web 结构(静态资源在webapp
目录中) |
| one | 第一章-MVC-书店 | 一个简单的 Spring MVC 项目。 |
| Two | 第二章-书店 | 一个简单的 Spring Boot Web MVC 项目,具有典型的引导结构(resources/static
目录中的静态资源) |
| Two | 第二章-样本 | 一个简单的项目与第二章的非网页样本 |
| five | 第五章-书店 | Spring Boot 书店 MVC 项目,使用百里香叶视图 |
| six | 第六章-书店 | 书店 Spring Boot MVC 项目,使用 Apache Tiles 视图 |
| seven | 第七章-书店 | 支持上传文件的 Spring Boot 书店 MVC 项目 |
| eight | 第八章-书店 | 支持各种视图类型的 Spring Boot 书店 MVC 项目 |
| nine | 第 9-1 章-书店-禁止开机 | 部署在 Apache Tomcat 上的书店 Spring WebFlux 项目(使用反应式控制器) |
| nine | 第 9-2 章-书店 | 书店 Spring Boot WebFlux 项目(使用反应式控制器) |
| nine | 第 9-3 章-书店 | 书店 Spring Boot WebFlux 项目(使用功能端点) |
| Ten | 第 10-1 章-书店 | 书店 Spring Boot WebFlux 项目通过 web 过滤器支持不区分大小写的 URIs 和国际化(最优雅的解决方案) |
| Ten | 第 10-2 章-书店 | 书店 Spring Boot WebFlux 项目支持验证 |
| Ten | 第 10-3 章-书店 | 书店 Spring Boot WebFlux 项目通过LocaleContextResolver
支持不区分大小写的 URIs 和国际化 |
| Eleven | 第 11-1 章-书店 | 使用 WebSocket 聊天的 Spring Boot 书店 MVC 项目 |
| Eleven | 第 11-2 章-书店 | 书店 Spring Boot WebFlux 项目,通过 WebSocket 上的反应流发布科技新闻 |
| Eleven | 第 11-3 章-客户-书店 | 火箭客户项目 |
| Eleven | 第 11-3 章服务器-书店 | RSocket 服务器项目 |
| Eleven | 第 11-4 章-服务器-书店 | 书店 Spring Boot WebFlux 项目使用反应式安全 |
| Twelve | 第十二章-书店 | 使用 Spring Security 的 Spring Boot 书店 MVC 项目 |
| Twelve | 第十二章-MVC-书店 | 使用 Spring Security 的书店 Spring MVC 项目 |
| Thirteen | 第十三章-账户服务 | 微服务提供反应式账户 API |
| Thirteen | 第十三章-图书服务 | 提供反应式图书 API 的微服务 |
| Thirteen | 第十三章-发现-服务 | 微服务发现并注册其他微服务 |
| Thirteen | 第十三章-新发布-服务 | 微服务提供单个反应端点,随机发出Book
个实例 |
| Thirteen | 第十三章-展示-服务 | 具有与其他界面交互的百里香网络界面的微服务 |
| Thirteen | 第十三章-技术新闻-服务 | 微服务提供单一反应端点,随机发出代表技术新闻的String
实例 |
名称中包含-mvc-
和chapter9-1-bookstore-no-boot
的项目被编译并打包成一个*.war
,可以在 Apache Tomcat 中运行。除了chapter2-sample,
之外,所有其他项目都是使用 Spring Boot 构建的,并且可以通过执行它们的主类来运行。chapter2-sample project
有多个主类,您可以运行它们来测试特定的场景。
构建项目
一旦安装了推荐的工具,下一步就是从 GitHub 获取项目源代码。
GitHub 项目页面位于 https://github.com/Apress/spring-mvc-and-webflux
。
您可以下载 repo 页面源代码,使用 IntelliJ IDEA 克隆项目,或者在终端中使用Git
克隆项目。您可以使用 HTTPS 或 Git 协议——任何感觉熟悉和简单的协议。
您可以使用 IntelliJ IDEA 构建该项目,但是如果您是第一次打开它,则需要一段时间来弄清楚项目结构并对文件进行索引。我们建议您打开一个终端,通过执行清单 1-1 中的命令来构建项目。输出应该类似于这个,而且它肯定包含 BUILD SUCCESSFUL。
> gradle clean build
...
BUILD SUCCESSFUL in 3m 1s
150 actionable tasks: 148 executed, 2 up-to-date
Listing 1-1Building the Project for This Book
1234567
一旦在终端中构建了项目,您就可以验证您拥有正确的项目和正确的工具。现在是时候在 IntelliJ IDEA 中打开它了。
你注意到的第一件事是 IntelliJ IDEA 正试图决定 Gradle 和 JDK 版本。而且它并不总是有效的,特别是当您的系统上有每一个的多个版本时。在右上角,您可能会看到如图 1-5 所示的通知。
图 1-5
IntelliJ 想法试图推断格雷尔和 JDK 版本
要解决这个问题,您必须执行以下操作。
First, if you want to use Gradle Wrapper, skip this step. Otherwise, go to the Gradle view, click the little wrench button (the one labeled Build Tool Settings), and a window appears to allow you to choose the Gradle version. If you have Gradle installed on your system, and the GRADLE_HOME
environment variable is set up, IntelliJ IDEA finds it. Still, it does not use it if the project contains a Gradle Wrapper configuration. To use Gradle on your system, choose Specified location in the section of the window marked in Figure 1-6.
图 1-6
IntelliJ IDEA Gradle 和 Gradle JVM 设置
同时,确保 Gradle JVM 也设置为 JDK 14。
In the IntelliJ IDEA main menu, select File > Project structure…. The Project Structure window allows you to configure the project SDK and the project language level. Make sure it is JDK 14 for both, as depicted in Figure 1-7.
图 1-7
IntelliJ IDEA 项目 JDK 设置
如果一切顺利,IntelliJ IDEA 使用格雷和 JDK 来构建你的项目并执行测试。如果您想在 IntelliJ IDEA 中构建项目,请使用 Gradle 视图。当项目被正确加载时,所有的模块应该和一组按目的分组的分级任务一起列出。在构建组下,一个名为构建的任务相当于清单 1-1 中的 Gradle 命令。图 1-8 显示了 IntelliJ IDEA 中一次成功的 Gradle 构建运行。
图 1-8
IntelliJ IDEA 成功的分级构建
运行项目
不使用 Spring Boot 构建的项目需要部署到 Apache Tomcat 服务器上。在成功的 Gradle 构建之后,应该已经为所有项目生成了工件。要在本地 Apache 服务器上部署项目,您必须执行以下操作。
单击右上角的项目启动程序列表。
选择编辑配置… 。
在“编辑配置”窗口中,选择要创建的启动器类型。
In the upper-left corner, click the +
button
. In the list of launcher types, select Tomcat Server > Local (see Figure 1-9).
图 1-9
IntelliJ IDEA 启动器选项
在运行/调试配置窗口中,需要用 Apache 服务器的位置和要部署的项目填充一个表单。首先,命名配置。选择与您的项目相关的名称。
点击配置按钮。
选择您的 Apache Tomcat 服务器目录。
点击确定按钮。
Click the Fix button. You are warned that you must select something to deploy (see Figure 1-10).
图 1-10
用于配置要部署的 Apache Tomcat 服务器和工件的 IntelliJ IDEA 启动器选项
在列表中,选择要部署的项目。
接下来,在 Deployment 选项卡中,您可以编辑上下文路径,因为自动生成的路径很奇怪。
Click the OK button, and you are done (see Figure 1-11).
图 1-11
用于配置要部署的工件的 IntelliJ IDEA 启动器选项
现在,您的启动器的名称出现在第一步中提到的列表中。您可以通过单击 Run 按钮(启动器列表旁边的绿色三角形)来启动 Apache Tomcat。如果一切顺利,IntelliJ 会打开一个浏览器选项卡,进入项目的主页。
IntelliJ IDEA 中 Apache Tomcat 的日志控制台可以在部署失败时提供更多信息。图 1-12 显示了 chapter1-mvc-bookstore 项目(在成功部署之后)和 Apache Tomcat 日志控制台的页面。
图 1-12
Apache Tomcat 日志控制台
运行 Spring Boot 项目甚至更容易。找到主类,右击它,选择运行。如果项目构建成功,应用应该启动并出现在服务视图中,如图 1-13 所示。
图 1-13
IntelliJ IDEA Spring Boot 主应用类和服务视图
IntelliJ IDEA 似乎在 Gradle 多模块项目上遇到了一些困难,因为对于 Spring Boot Web 应用,它无法检测工作目录,这意味着它无法正确构建应用上下文。要解决这个问题,请打开为 Spring Boot 应用生成的项目启动器,并选择您想要运行的项目的目录作为工作目录选项的值,如图 1-14 所示。
图 1-14
带有显式填充的工作目录的 IntelliJ IDEA Spring Boot 启动器
摘要
希望本章的说明足以帮助你开始。如果有任何遗漏或不清楚的地方,请随时首先询问 Google。如果这不起作用,在 GitHub 上创建一个问题。
编码快乐!
Footnotes 1
https://sdkman.io/
2
https://adoptopenjdk.net/
3
图片来源: https://www.programmableweb.com
4
图片来源: https://www.gradle.org
5
https://tomcat.apache.org/
6
图片来源: https://tomcat.apache.org
7
图片来源: https://www.jetbrains.com/idea/
二、Spring 框架基础
Spring 框架是由 Rod Johnson (Wrox,2002)为专家一对一 J2EE 设计和开发编写的代码发展而来的。 1 该框架结合了业界 Java 企业版(JEE)开发的最佳实践,并与同类最佳的第三方框架相集成。如果您需要一个尚不存在的集成,它还提供了简单的扩展点来编写您自己的集成。该框架的设计考虑到了开发人员的生产力,它使得使用现有的、有时很麻烦的 Java 和 JEE API 变得更容易。
Spring Boot 于 2014 年 4 月发布,旨在简化云时代的应用开发。Spring Boot 使得创建独立的、生产级的基于 Spring 的应用变得容易。这些应用可以独立运行,也可以部署到传统的 Servlet 容器或 JEE 服务器上。
Spring Boot 坚持认为 Spring 平台是一个整体,并支持第三方库。它让您不费吹灰之力就能开始,但如果您想要更复杂的配置或让配置对您来说更简单,它就不会碍事。
在开始我们的 Spring MVC 和 Spring WebFlux 之旅之前,我们先快速回顾一下 Spring(也称为 Spring Framework )。Spring 是 Java 企业软件开发的事实上的标准。它介绍了依赖注入、面向方面编程 (AOP),以及用plain-old-Java-objects(POJO)编程。
在这一章中,我们将讨论依赖注入和 AOP。具体来说,我们将介绍 Spring 如何帮助我们实现依赖注入,以及如何利用编程为我们带来优势。为了做这里提到的事情,我们探索控制反转(IoC)容器;应用上下文。
这里我们只涉及 Spring 框架的必要基础。如果想要更深入的了解它,我们建议优秀的 Spring 框架文档 2 或书籍如 Pro Spring 5 (Apress,2017) 3 或 Spring 5 Recipes,4thEdition(Apress,2017) 4 。
除了 Spring 框架复习之外,我们还将触及 Spring Boot 的基础知识。关于 Spring Boot 更深入的信息,我们建议优秀的 Spring Boot 参考指南 5 或 Spring Boot 2 食谱 (Apress,2018) 6 。
让我们从快速浏览一下 Spring 框架和组成它的模块开始。
你可以在 chapter2-samples 项目中找到本章的示例代码。示例的不同部分包含一个带有main
方法的类,您可以运行该方法来执行代码。
Spring 框架
在介绍中,我们提到了 Spring 框架是由 Rod Johnson 为《一对一 J2EE 设计和开发专家》一书编写的代码演变而来的。这本书旨在解释 JEE 的一些复杂情况以及如何克服它们。虽然 JEE 的许多复杂性和问题已经在新的 JEE 规范中解决了(特别是从 JEE 6 开始),但是 Spring 已经变得非常流行,因为它简单(不是简单化!)构建应用的方法。它还为不同的技术提供了一致的编程模型,无论是数据访问还是消息传递基础设施。该框架允许开发人员针对离散的问题,专门为它们构建解决方案。
该框架由几个模块组成(见图 2-1 ),这些模块协同工作并相互构建。我们几乎可以精挑细选我们想要使用的模块。
图 2-1
Spring 框架概述
图 2-1 中的所有模块代表 jar 文件,如果我们需要特定的技术,我们可以将它们包含在类路径中。表 2-1 列出了 Spring 5.2 附带的所有模块,包括每个模块内容的简要描述和用于依赖管理的任何工件名称。实际 jar 文件的名称可能不同,这取决于您获取模块的方式。
表 2-1
Spring 框架模块概述
|
组件
|
假象
|
描述
|
| — | — | — |
| 面向切面编程 | Spring-aop | Spring 的基于代理的 AOP 框架 |
| 方面 | Spring 方面 | Spring 基于 AspectJ 的方面 |
| 豆子 | 春豆 | Spring 的核心 bean 工厂支持 |
| 语境 | Spring 的背景 | 应用上下文运行时实现;还包含调度和远程处理支持类 |
| 语境 | spring 上下文索引器 | 支持提供应用中使用的 beans 的静态索引;提高启动性能 |
| 语境 | spring 上下文支持 | 将第三方库与 Spring Integration 的支持类 |
| 核心 | Spring 芯 | 核心实用程序 |
| 表达语言 | Spring 的表情 | Spring 表达式语言(SpEL)的类 |
| 使用仪器 | Spring 乐器 | 与 Java 代理一起使用的检测类 |
| 作业控制语言 | Spring-jcl | Spring 专用于 commons-logging 的替代品 |
| 数据库编程 | spring-jdbc | JDBC 支持包,包括数据源设置类和 JDBC 访问支持 |
| (同 JavaMessageService)Java 消息服务 | spring-jms | JMS 支持包,包括同步 JMS 访问和消息监听器容器 |
| 对象关系映射(Object Relation Mapping) | Spring 形状 | ORM 支持包,包括对 Hibernate 5+和 JPA 的支持 |
| 信息发送 | Spring 短信 | Spring 消息传递抽象;由 JMS 和 WebSocket 使用 |
| 泌酸调节素 | Spring-oxm | XML 支持包,包括对对象到 XML 映射的支持;还包括对 JAXB、JiBX、XStream 和 Castor 的支持 |
| 试验 | Spring 试验 | 测试支持类 |
| 处理 | Spring-tx | 交易基础设施类别;包括 JCA 集成和 DAO 支持类 |
| 网 | Spring 网 | 适用于任何 web 环境的核心 web 包 |
| webflux | spring web lux | Spring WebFlux 支持包包括对 Netty 和 Undertow 等几种反应式运行时的支持 |
| 小型应用 | spring web VC(Spring web 控制台) | 在 Servlet 环境中使用的 Spring MVC 支持包包括对通用视图技术的支持 |
| WebSocket | Spring 腹板插座 | Spring WebSocket 支持包包括对 WebSocket 协议通信的支持 |
大多数模块都依赖于 Spring 框架中的其他模块。核心模块是这一规则的例外。图 2-2 给出了常用模块及其对其他模块的依赖性的概述。请注意,图中缺少了仪器、方面和测试模块;这是因为它们的依赖依赖于项目和使用的其他模块。其他依赖项根据项目的需要而有所不同。
图 2-2
Spring 框架模块依赖关系
依赖注入
在依赖注入(DI)中,对象在构造时被赋予依赖关系。它是一个 Spring 框架基础。你可能听说过控制反转 (IoC)。 7 国际奥委会是一个更宽泛、更通用的概念,可以用不同的方式来称呼。IoC 允许开发人员分离并专注于对企业应用的给定部分重要的事情,而不用考虑系统的其他部分做什么。接口编程是考虑解耦的一种方式。
几乎每个企业应用都由需要协同工作的多个组件组成。在 Java 企业开发的早期,我们简单地将构造那些对象(以及它们需要的对象)的所有逻辑放在构造器中(参见清单 2-1 )。乍一看,这种方法没有错;然而,随着时间的推移,对象构造变得很慢,对象拥有了很多本不应该拥有的知识(参见单责任原则)。 8 这些类变得难以维护,并且它们也难以进行单元和/或集成测试。
package com.apress.prospringmvc.moneytransfer.simple;
import java.math.BigDecimal;
import com.apress.prospringmvc.moneytransfer.domain.Account;
import com.apress.prospringmvc.moneytransfer.domain.MoneyTransferTransaction;
import com.apress.prospringmvc.moneytransfer.domain.Transaction;
import com.apress.prospringmvc.moneytransfer.repository.AccountRepository;
import com.apress.prospringmvc.moneytransfer.repository.MapBasedAccountRepository;
import com.apress.prospringmvc.moneytransfer.repository.MapBasedTransactionRepository;
import com.apress.prospringmvc.moneytransfer.repository.TransactionRepository;
import com.apress.prospringmvc.moneytransfer.service.MoneyTransferService;
public class SimpleMoneyTransferServiceImpl implements MoneyTransferService {
private AccountRepository accountRepository = new MapBasedAccountRepository();
private TransactionRepository transactionRepository = new MapBasedTransactionRepository();
@Override
public Transaction transfer(String source, String target, BigDecimal amount) {
Account src = this.accountRepository.find(source);
Account dst = this.accountRepository.find(target);
src.credit(amount);
dst.debit(amount);
MoneyTransferTransaction transaction = new MoneyTransferTransaction(src, dst, amount);
this.transactionRepository.store(transaction);
return transaction;
}
}
Listing 2-1A MoneyTransferService Implementation with Hardcoded Dependencies
123456789101112131415161718192021222324252627282930313233
从清单 2-1 程序到接口的类,但是它仍然需要知道接口的具体实现,只是为了进行对象构造。通过解耦构造逻辑(协作对象)来应用 IoC 使得应用更易于维护,并增加了可测试性。有七种方法可以分离这种依赖关系构造逻辑。
工厂模式
服务定位器模式
依赖注入
基于构造函数
基于 Setter
基于现场
情境化查找
当使用工厂模式、服务定位器模式或上下文化查找时,需要依赖关系的类仍然具有一些关于获取依赖关系的知识。这可以使事情更容易维护,但仍然很难测试。清单 2-2 显示了来自 JNDI (Java 命名和目录接口)的上下文化查找。构造函数代码需要知道如何查找和处理异常。
package com.apress.prospringmvc.moneytransfer.jndi;
import javax.naming.InitialContext;
import javax.naming.NamingException;
//other import statements omitted.
public class JndiMoneyTransferServiceImpl implements MoneyTransferService {
private AccountRepository accountRepository;
private TransactionRepository transactionRepository;
public JndiMoneyTransferServiceImpl() {
try {
InitialContext context = new InitialContext();
this.accountRepository = (AccountRepository) context.lookup("accountRepository");
this.transactionRepository = (TransactionRepository) context.lookup("transactionRepository");
} catch (NamingException e) {
throw new IllegalStateException(e);
}
}
//transfer method omitted, same as listing 2-1
}
Listing 2-2MoneyTransferService Implementation with Contextualized Lookup
1234567891011121314151617181920212223242526272829
前面的代码不是特别干净;例如,想象来自不同上下文的多个依赖项。代码会很快变得混乱,越来越难以进行单元测试。
为了解决对象构造器中的构造/查找逻辑,我们可以使用依赖注入。我们只是传递给对象完成工作所需的依赖关系。这使得我们的代码干净、解耦,并且易于测试(参见清单 2-3 )。依赖注入是一个过程,其中对象指定它们所使用的依赖。IoC 容器使用该规范;当它构造一个对象时,它也注入它的依赖项。这样,我们的代码更干净,我们不再用构造逻辑来增加类的负担。维护更容易,进行单元和/或集成测试也更容易。测试更容易,因为我们可以注入一个存根或模拟对象来验证我们的对象的行为。
package com.apress.prospringmvc.moneytransfer.constructor;
// import statements ommitted
public class MoneyTransferServiceImpl implements MoneyTransferService {
private final AccountRepository accountRepository;
private final TransactionRepository transactionRepository;
public MoneyTransferServiceImpl(AccountRepository accountRepo,
TransactionRepository transactionRepo) {
this.accountRepository = accountRepo;
this.transactionRepository = transactionRepo;
}
//transfer method omitted, same as listing 2-1
}
Listing 2-3A MoneyTransferService Implementation with Constructor-Based Dependency Injection
123456789101112131415161718192021
顾名思义,基于构造函数的依赖注入使用构造函数来注入对象中的依赖。清单 2-3 使用基于构造函数的依赖注入。它有一个接受两个对象作为参数的构造函数:com.apress.prospringmvc.moneytransfer.repository.AccountRepository
和com.apress.prospringmvc.moneytransfer.repository.TransactionRepository
。当我们构造一个com.apress.prospringmvc.moneytransfer.constructor.MoneyTransferServiceImpl
的实例时,我们需要给它所需的依赖项。
基于 Setter 的依赖注入使用一个 setter 方法来注入依赖。JavaBeans 规范定义了 setter 和 getter 方法。如果我们有一个名为setAccountService
的方法,我们设置一个名为accountService
的属性。属性名是使用方法名创建的,减去“set”,第一个字母小写(完整规范在 JavaBeans 规范中)?? 9。清单 2-4 展示了一个基于 setter 的依赖注入的例子。一个属性不一定要同时有 getter 和 setter。属性可以是只读的(只定义了一个 getter 方法)或只写的(只定义了一个 setter 方法)。清单 2-4 只显示了 setter 方法,因为我们只需要写属性;在内部,我们可以直接引用字段。
package com.apress.prospringmvc.moneytransfer.setter;
// imports ommitted
public class MoneyTransferServiceImpl implements MoneyTransferService {
private AccountRepository accountRepository;
private TransactionRepository transactionRepository;
public void setAccountRepository(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
public void setTransactionRepository(TransactionRepository transactionRepo) {
this.transactionRepository = transactionRepository;
}
//transfer method omitted, same as listing 2-1
}
Listing 2-4A MoneyTransferService Implementation with Setter-Based Dependency
Injection
12345678910111213141516171819202122232425
最后,还有使用注释的基于字段的依赖注入(参见清单 2-5 )。我们不需要指定构造函数参数或 setter 方法来设置依赖关系。我们首先定义一个可以保存依赖关系的类级字段。接下来,我们在该字段上添加了一个注释,以表达我们将该依赖项注入到对象中的意图。Spring 接受几种不同的注释:@Autowired
、@Resource
和@Inject
。所有这些注释或多或少都以相同的方式工作。深入解释这些注释之间的差异不在本书的范围内,所以如果你想了解更多,我们建议使用 Spring Boot 参考指南或 Pro Spring 5 (Apress,2017)。主要区别在于@Autowired
注释来自 Spring 框架,而@Resource
和@Inject
是 Java 标准注释。
package com.apress.prospringmvc.moneytransfer.annotation;
import org.springframework.beans.factory.annotation.Autowired;
//other imports omitted
public class MoneyTransferServiceImpl implements MoneyTransferService {
@Autowired
private AccountRepository accountRepository;
@Autowired
private TransactionRepository transactionRepository;
//transfer method omitted, same as listing 2.1
}
Listing 2-5A MoneyTransferService Implementation with Field-Based Dependency Injection
1234567891011121314151617181920
@Autowired
*@Inject
可以放在方法和构造函数上表示依赖注入配置,即使有多个实参!当对象只有一个构造函数时,可以省略注释。*
*综上所述,我们出于以下原因想要使用依赖注入。
清除器代码
去耦代码
更简单的代码测试
前两个原因使我们的代码更容易维护。代码更容易测试的事实应该允许我们编写单元测试来验证我们的对象的行为——以及我们的应用。
应用上下文
为了在 Spring 中进行依赖注入,你需要一个应用上下文。在 Spring 中,这是一个org.springframework.context.ApplicationContext
接口的实例。应用上下文负责管理其中定义的 beans。它还支持更复杂的事情,比如将 AOP 应用于其中定义的 beans。
Spring 提供了几种不同的ApplicationContext
实现(参见图 2-3 )。这些实现中的每一个都提供了相同的特性,但是在加载应用上下文配置的方式上有所不同。图 2-3 也向我们展示了org.springframework.web.context.WebApplicationContext
界面,它是在网络环境中使用的ApplicationContext
界面的特殊版本。
图 2-3
各种ApplicationContext
实现(简化)
如前所述,不同的实现具有不同的配置机制(即 XML 或 Java)。表 2-2 显示了默认配置选项,并指出资源加载位置。
表 2-2
应用上下文概述
|
履行
|
位置
|
文件类型
|
| — | — | — |
| ClassPathXmlApplicationContext
| 类路径 | 可扩展置标语言 |
| FileSystemXmlApplicationContext
| 文件系统 | 可扩展置标语言 |
| AnnotationConfigApplicationContext
| 类路径 | 爪哇 |
| XmlWebApplicationContext
| Web 应用根 | 可扩展置标语言 |
| AnnotationConfigWebApplicationContext
| Web 应用类路径 | 爪哇 |
让我们来看一个基于 Java 的配置文件——com.apress.prospringmvc.moneytransfer.annotation.ApplicationContextConfiguration
类(参见清单 2-6 )。该类中使用了两个注释:org.springframework.context.annotation.Configuration
和org.springframework.context.annotation.Bean
。第一个将我们的类构造为一个配置文件,而第二个表示该方法的结果被用作创建 bean 的工厂。默认情况下,bean 的名称是方法名称。
在清单 2-6 中,有三个 beans。它们被命名为accountRepository
、transactionRepository
和moneyTransferService
。我们还可以通过在@Bean
注释上设置name
属性来显式指定一个 bean 名称。
package com.apress.prospringmvc.moneytransfer.annotation;
import com.apress.prospringmvc.moneytransfer.repository.AccountRepository;
import com.apress.prospringmvc.moneytransfer.repository.MapBasedAccountRepository;
import com.apress.prospringmvc.moneytransfer.repository.MapBasedTransactionRepository;
import com.apress.prospringmvc.moneytransfer.repository.TransactionRepository;
import com.apress.prospringmvc.moneytransfer.service.MoneyTransferService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ApplicationContextConfiguration {
@Bean
public AccountRepository accountRepository() {
return new MapBasedAccountRepository();
}
@Bean
public TransactionRepository transactionRepository() {
return new MapBasedTransactionRepository();
}
@Bean
public MoneyTransferService moneyTransferService() {
return new MoneyTransferServiceImpl();
}
}
Listing 2-6The ApplicationContextConfiguration Configuration File
12345678910111213141516171819202122232425262728293031
配置类可以是abstract
;但是,他们不可能是final
。为了解析这个类,Spring 可能会创建一个 configuration 类的动态子类。
拥有一个只有@Configuration
注释的类是不够的。我们还需要一些东西来引导我们的应用上下文。我们用它来启动我们的应用。在示例项目中,这是MoneyTransferSpring
类的责任(参见清单 2-7 )。这个类通过创建一个org.springframework.context.annotation.AnnotationConfigApplicationContext
的实例来引导我们的配置,并将包含我们配置的类传递给它(参见清单 2-6 )。
package com.apress.prospringmvc.moneytransfer.annotation;
import com.apress.prospringmvc.ApplicationContextLogger;
import com.apress.prospringmvc.moneytransfer.domain.Transaction;
import com.apress.prospringmvc.moneytransfer.service.MoneyTransferService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.math.BigDecimal;
public class MoneyTransferSpring {
private static final Logger logger =
LoggerFactory.getLogger(MoneyTransferSpring.class);
/**
* @param args
*/
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(ApplicationContextConfiguration.class);
transfer(ctx);
ApplicationContextLogger.log(ctx);
}
private static void transfer(ApplicationContext ctx) {
MoneyTransferService service =
ctx.getBean("moneyTransferService", MoneyTransferService.class);
Transaction transaction = service.transfer("123456", "654321", new BigDecimal("250.00"));
logger.info("Money Transfered: {}", transaction);
}
}
Listing 2-7The MoneyTransferSpring Class
1234567891011121314151617181920212223242526272829303132333435363738
最后,请注意,应用上下文可以在一个层次结构中。我们可以有一个应用上下文作为另一个上下文的父上下文(见图 2-4 )。一个应用上下文只能有一个父级,但可以有多个子级。子上下文可以访问父上下文中定义的 beans 但是,父 bean 不能访问子上下文中的 bean。例如,如果我们在父上下文中启用事务,这将不适用于子上下文(请参阅本章后面的“启用功能”一节)。
图 2-4
应用上下文层次结构
这个特性允许我们将应用 bean(例如,服务、存储库和基础设施)与 web beans(例如,请求处理程序和视图)分开。这种分离是有用的。例如,假设多个 servlets 需要重用相同的应用 beans。我们可以简单地重用已经存在的实例,而不是为每个 servlet 重新创建它们。当一个 servlet 处理 web UI,另一个 servlet 处理 web 服务时,就会出现这种情况。
资源加载
表 2-2 提供了不同ApplicationContext
实现和默认资源加载机制的概述。然而,这并不意味着您只能从默认位置加载资源。您还可以通过包含适当的前缀从特定位置加载资源(参见表 2-3 )。
表 2-3
前缀概述
|
前缀
|
位置
|
| — | — |
| classpath:
| 类路径的根 |
| file:
| 文件系统 |
| http:
| Web 应用根 |
除了能够指定从哪里加载文件之外,还可以使用 ant 样式的正则表达式来指定加载哪些文件。ant 样式的正则表达式是包含和/或*字符的资源位置。**一个字符表示“在当前级别”或“单个级别”,而多个字符表示“这个和所有子级别”
表 2-4 显示了一些例子。这种技术只在处理类路径或文件系统上的文件资源时有效;它不适用于 web 资源或包名。
表 2-4
蚂蚁风格的正则表达式
|
表示
|
描述
|
| — | — |
| classpath:/META-INF/spring/*.xml
| 从 META-INF/spring 目录中的类路径加载所有带有 XML 文件扩展名的文件 |
| file:/var/conf/*/.properties
| 从/var/conf 目录和所有子目录中加载具有属性文件扩展名的所有文件 |
组件扫描
Spring 还有一个叫的东西,组件扫描。简而言之,这个特性使 Spring 能够扫描您的类路径,寻找用org.springframework.stereotype.Component
(或者像@Service, @Repository, @Controller
或org.springframework.context.annotation.Configuration
这样的专用注释)注释的类。如果我们想要启用组件扫描,我们需要指示应用上下文这样做。org.springframework.context.annotation.ComponentScan
注释使我们能够做到这一点。这个注释需要放在我们的配置类中,以启用组件扫描。清单 2-8 显示了修改后的配置类。
package com.apress.prospringmvc.moneytransfer.scanning;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* @author Marten Deinum
*/
@Configuration
@ComponentScan(basePackages = {
"com.apress.prospringmvc.moneytransfer.scanning",
"com.apress.prospringmvc.moneytransfer.repository" })
public class ApplicationContextConfiguration {}
Listing 2-8Implementing Component Scanning with ApplicationContextConfiguration
12345678910111213141516
看一下清单 2-8 就会发现这个类没有更多内容。只有两个注解。一个注释表示该类用于配置,而另一个注释启用组件扫描。组件扫描注释配置有要扫描的包。
不指定一个包来扫描整个类路径或者使用太宽的包(像com.apress
)被认为是不好的做法。这可能导致扫描大多数或所有类,从而严重影响应用的启动时间。
领域
默认情况下,Spring 应用上下文中的所有 beans 都是单态的。顾名思义,bean 只有一个实例,它用于整个应用。这通常不会造成问题,因为我们的服务和存储库不保存状态;它们只是执行某个操作并(可选地)返回值。
然而,如果我们想将状态保存在 bean 中,那么单例就有问题了。我们正在开发一个 web 应用,希望能吸引成千上万的用户。如果一个 bean 只有一个实例,并且所有用户都在同一个实例中操作,那么用户可以看到和修改彼此的数据或者来自几个用户组合的数据。这不是我们想要的。幸运的是,Spring 为 beans 提供了几个我们可以利用的范围(见表 2-5 )。
表 2-5
范围概述
|
前缀
|
描述
|
| — | — |
| singleton
| 默认范围。创建一个 bean 实例,并在整个应用中共享。 |
| prototype
| 每次需要某个 bean 时,都会返回该 bean 的一个新实例。 |
| thread
| bean 在需要时创建,并绑定到当前执行的线程。如果线程死了,bean 就被破坏了。 |
| request
| bean 在需要时创建,并绑定到传入的javax.servlet.ServletRequest
的生命周期。如果请求结束,bean 实例被销毁。 |
| session
| bean 在需要时创建并存储在javax.servlet.HttpSession
中。当会话被销毁时,bean 实例也被销毁。 |
| globalSession
| bean 在需要时创建,并存储在全局可用的会话中(在 Portlet 环境中可用)。如果没有这样的会话可用,则作用域恢复到会话作用域功能。 |
| application
| 这个作用域非常类似于单例作用域;但是,这个作用域的 beans 也在javax.servlet.ServletContext
中注册。 |
轮廓
Spring 在 3.1 版本中引入了概要文件。概要文件使得为不同的环境创建不同的应用配置变得容易。例如,我们可以为本地环境、测试和部署到 CloudFoundry 创建单独的概要文件。这些环境中的每一个都需要一些特定于环境的配置或 beans。您可以考虑数据库配置、消息传递解决方案和测试环境,以及某些 beans 的存根。
为了启用概要文件,我们需要告诉应用上下文哪些概要文件是活动的。为了激活某些概要文件,我们需要设置一个名为spring.profiles.active
的系统属性(在 web 环境中,这可以是 servlet 初始化参数或 web 上下文参数)。这是一个逗号分隔的字符串,包含活动配置文件的名称。如果我们现在添加一些带有org.springframework.context.annotation.Configuration
和org.springframework.context.annotation.Profile
注释的(在本例中是静态内部)类(参见清单 2-9 ,那么只有匹配其中一个活动概要的类才会被处理。所有其他类都被忽略。
package com.apress.prospringmvc.moneytransfer.annotation.profiles;
import com.apress.prospringmvc.moneytransfer.annotation.MoneyTransferServiceImpl;
import com.apress.prospringmvc.moneytransfer.repository.AccountRepository;
import com.apress.prospringmvc.moneytransfer.repository.MapBasedAccountRepository;
import com.apress.prospringmvc.moneytransfer.repository.MapBasedTransactionRepository;
import com.apress.prospringmvc.moneytransfer.repository.TransactionRepository;
import com.apress.prospringmvc.moneytransfer.service.MoneyTransferService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
public class ApplicationContextConfiguration {
@Bean
public AccountRepository accountRepository() {
return new MapBasedAccountRepository();
}
@Bean
public MoneyTransferService moneyTransferService() {
return new MoneyTransferServiceImpl();
}
@Configuration
@Profile(value = "test")
public static class TestContextConfiguration {
@Bean
public TransactionRepository transactionRepository() {
return new StubTransactionRepository();
}
}
@Configuration
@Profile(value = "local")
public static class LocalContextConfiguration {
@Bean
public TransactionRepository transactionRepository() {
return new MapBasedTransactionRepository();
}
}
}
Listing 2-9ApplicationContextConfiguration with Profiles
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
清单 2-10 显示了一些示例引导代码。一般来说,我们不会从我们的引导代码中设置活动概要文件。相反,我们使用系统变量的组合来设置我们的环境。这使我们能够保持我们的应用不变,但仍然有改变我们的运行时配置的灵活性。
package com.apress.prospringmvc.moneytransfer.annotation.profiles;
import com.apress.prospringmvc.ApplicationContextLogger;
import com.apress.prospringmvc.moneytransfer.domain.Transaction;
import com.apress.prospringmvc.moneytransfer.service.MoneyTransferService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.math.BigDecimal;
/**
* @author Marten Deinum
*/
public class MoneyTransferSpring {
private static final Logger logger = LoggerFactory.getLogger(MoneyTransferSpring.class);
/**
* @param args
*/
public static void main(String[] args) {
System.setProperty("spring.profiles.active", "test");
AnnotationConfigApplicationContext ctx1 =
new AnnotationConfigApplicationContext(ApplicationContextConfiguration.class);
transfer(ctx1);
ApplicationContextLogger.log(ctx1);
System.setProperty("spring.profiles.active", "local");
AnnotationConfigApplicationContext ctx2 =
new AnnotationConfigApplicationContext(ApplicationContextConfiguration.class);
transfer(ctx2);
ApplicationContextLogger.log(ctx2);
}
private static void transfer(ApplicationContext ctx) {
MoneyTransferService service = ctx.getBean("moneyTransferService", MoneyTransferService.class);
Transaction transaction = service.transfer("123456", "654321", new BigDecimal("250.00"));
logger.info("Money Transfered: {}", transaction);
}
}
Listing 2-10MoneyTransferSpring with Profiles
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
你可能想知道为什么我们应该使用概要文件。一个原因是它允许灵活的配置。这意味着我们的整个配置都在版本控制之下,并且在相同的源代码中,而不是分散在不同的服务器、工作站等等上。当然,我们仍然可以加载包含一些属性(如用户名和密码)的附加文件。如果公司的安全策略不允许我们将这些属性置于版本控制之下,那么这可能是有用的。当我们讨论测试和部署到云时,我们广泛地使用概要文件,因为这两个任务需要不同的数据源配置。
启用功能
Spring 框架比依赖注入提供了更多的灵活性;它还提供了许多我们可以启用的不同功能。我们可以使用注释来启用这些功能(参见表 2-6 )。注意,我们不会使用表 2-6 中的所有注释;然而,我们的示例应用使用了事务,并且我们使用了一些 AOP。这本书最大的部分是关于由org.springframework.web.servlet.config.annotation.EnableWebMvc
和org.springframework.web.reactive.config.EnableWebFlux
注释提供的特性。
Spring Boot 自动启用其中一些功能;这取决于在类路径上检测到的类。
表 2-6
注释支持的功能概述
|
注释
|
描述
|
被 Spring Boot 探测到
|
| — | — | — |
| org.springframework.context.annotation.EnableAspectJAutoProxy
| 支持处理构造为 org . AspectJ . lang . annotation . aspect 的 beans。 | 是 |
| org.springframework.scheduling.annotation.EnableAsync
| 启用对使用org.springframework.scheduling.annotation.Async
或javax.ejb.Asynchronous
注释处理 bean 方法的支持。 | 不 |
| org.springframework.cache.annotation.EnableCaching
| 启用对带有 org . spring framework . cache . annotation . cache able 批注的 bean 方法的支持。 | 是 |
| org.springframework.context.annotation.EnableLoadTimeWeaving
| 启用对加载时编织的支持。默认情况下,Spring 使用基于代理的 AOP 方法;然而,这个注释使我们能够切换到加载时编织。一些 JPA 提供商需要它。 | 不 |
| org.springframework.scheduling.annotation.EnableScheduling
| 启用对注释驱动的调度的支持,使我们能够解析用 org . spring framework . scheduling . annotation . scheduled 注释注释的 bean 方法。 | 不 |
| org.springframework.beans.factory.aspectj.EnableSpringConfigured
| 支持对非 Spring 管理的 beans 应用依赖注入。一般来说,这样的 beans 用org.springframework.beans.factory.annotation.Configurable
注释进行注释。这个特性需要加载时或编译时编织,因为它需要修改类文件。 | 不 |
| org.springframework.transaction.annotation.EnableTransactionManagement
| 启用注释驱动的事务支持,使用org.springframework.transaction.annotation.Transactional
或javax.ejb.TransactionAttribute
来驱动事务。 | 是 |
| org.springframework.web.servlet.config.annotation.EnableWebMvc
| 通过请求处理方法支持强大而灵活的注释驱动控制器。该特性检测带有org.springframework.stereotype.Controller
注释的 beans,并将带有org.springframework.web.bind.annotation.RequestMapping
注释的方法绑定到 URL。 | 是 |
| org.springframework.web.reactive.config.EnableWebFlux
| 使用 Spring web MVC 中众所周知的概念来支持强大而灵活的反应式 Web 实现,并在可能的情况下对其进行扩展。 | 是 |
关于这些特性的更多信息,我们建议您查看 Java 文档中不同的注释和专门的参考指南章节。
面向方面编程
为了启用表 2-4 中列出的特性,Spring 使用了面向方面编程(AOP)。AOP 是思考软件结构的另一种方式。它使您能够将诸如事务管理或性能日志之类的事情模块化,这些特性跨越多种类型和对象(横切关注点)。在 AOP 中,有几个重要的概念需要记住(见表 2-7 )。
表 2-7
核心 AOP 概念
|
概念
|
描述
|
| — | — |
| 方面 | 横切关注点的模块化。一般来说,这是一个带有org.aspectj.lang.annotation.Aspect
注释的 Java 类。 |
| 连接点 | 程序执行过程中的一个点。这可以是方法的执行、字段的赋值或异常的处理。在 Spring 中,连接点总是一个方法的执行! |
| 建议 | 某个方面在特定连接点采取的特定动作。建议有几种类型:前的*、后的、后的、后的、前后的。在 Spring 中,一个通知被称为拦截器,因为我们正在拦截方法调用。* |
| 切入点 | 匹配连接点的谓词。通知与一个切入点表达式相关联,并在任何匹配切入点的连接点上运行。Spring 默认使用 AspectJ 表达式语言。可以使用org.aspectj.lang.annotation.Pointcut
注释编写连接点。 |
现在让我们看看事务管理,以及 Spring 如何使用 AOP 来围绕方法应用事务。交易通知或拦截器是org.springframework.transaction.interceptor.TransactionInterceptor
。这个建议放在带有org.springframework.transaction.annotation.Transactional
注释的方法周围。为此,Spring 在实际对象周围创建了一个包装器,称为代理(见图 2-5 )。代理的行为类似于封闭对象,但它允许添加(动态)行为(在本例中,是方法的事务性)。
图 2-5
代理方法调用
org.springframework.transaction.annotation.EnableTransactionManagement
注释注册包含切入点的 beans(作用于org.springframework.transaction.annotation.Transactional
注释)。此时,拦截器就可以使用了。用于启用特性的其他注释的工作方式类似;他们注册 beans 来启用期望的特性,包括大多数特性的 AOP(以及代理创建)。
网络应用
那么如何将所有这些技术应用到 web 应用中呢?例如,应用上下文如何发挥作用?那么提到的其他事情呢?
在开发 web 应用时,存在实际的业务逻辑(例如,服务、存储库和基础设施信息),并且存在基于 web 的 beans。这些东西应该是分开的,所以我们需要有多个应用上下文和关系。
我们还需要引导应用的代码,否则什么都不会发生。在本章的例子中,我们使用了一个带有 main 方法的MoneyTransferSpring
类来启动应用上下文。这不是我们在网络环境中能做到的。Spring 附带了两个可以引导应用的组件:org.springframework.web.servlet.DispatcherServlet
和org.springframework.web.context.ContextLoaderListener
。这两个组件引导并配置应用上下文。
我们来看看配置DispatcherServlet
的类。这是com.apress.prospringmvc.bookstore.web.BookstoreWebApplicationInitializer
级(见清单 2-11 )。我们的 Servlet 3.0+容器检测到这个类,它初始化我们的应用(关于这个主题的更多信息,参见第三章)。我们创建DispatcherServlet
并传递它org.springframework.web.context.support.AnnotationConfigWebApplicationContext
。接下来,我们将 servlet 映射到所有内容(“/”),并告诉它在启动时加载。
package com.apress.prospringmvc.bookstore.web;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import com.apress.prospringmvc.bookstore.web.config.WebMvcContextConfiguration;
public class BookstoreWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(final ServletContext servletContext) throws ServletException {
registerDispatcherServlet(servletContext);
}
private void registerDispatcherServlet(final ServletContext servletContext) {
WebApplicationContext dispatcherContext =
createContext(WebMvcContextConfiguration.class);
DispatcherServlet dispatcherServlet =
new DispatcherServlet(dispatcherContext);
ServletRegistration.Dynamic dispatcher =
servletContext.addServlet("dispatcher", dispatcherServlet);
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("*.htm");
}
private WebApplicationContext createContext(final Class<?>... annotatedClasses) {
AnnotationConfigWebApplicationContext context =
new AnnotationConfigWebApplicationContext();
context.register(annotatedClasses);
return context;
}
}
Listing 2-11The BookstoreWebApplicationInitializer Class
123456789101112131415161718192021222324252627282930313233343536373839404142
让我们通过添加一个ContextLoaderListener
类来让事情变得有趣一点,这样我们可以有一个父上下文和一个子上下文(参见清单 2-12 )。新注册的监听器使用com.apress.prospringmvc.bookstore.config.InfrastructureContextConfiguration
(参见清单 2-13 )来决定加载哪些 beans。已经配置好的DispatcherServlet
自动检测ContextLoaderListener
加载的应用上下文。
package com.apress.prospringmvc.bookstore.web;
import org.springframework.web.context.ContextLoaderListener;
import com.apress.prospringmvc.bookstore.config.InfrastructureContextConfiguration;
// other imports omitted, see listing 2-11
public class BookstoreWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(final ServletContext servletContext) throws ServletException {
registerListener(servletContext);
registerDispatcherServlet(servletContext);
}
// registerDispatcherServlet method ommitted see Listing 2-11
// createContext method omitted see Listing 2-11
private void registerListener(final ServletContext servletContext) {
AnnotationConfigWebApplicationContext rootContext =
createContext(InfrastructureContextConfiguration.class);
servletContext.addListener(new ContextLoaderListener(rootContext));
}
}
Listing 2-12The Modifcation
for the BookstoreWebApplicationInitializer Class
12345678910111213141516171819202122232425262728293031323334
清单 2-13 是我们的主要应用上下文。它包含我们的服务和存储库的配置。这个清单还展示了我们的 JPA 实体管理器,包括其基于注释的事务支持。
package com.apress.prospringmvc.bookstore.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {
"com.apress.prospringmvc.bookstore.service",
"com.apress.prospringmvc.bookstore.repository"})
public class InfrastructureContextConfiguration {
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean();
emfb.setDataSource(dataSource);
emfb.setJpaVendorAdapter(jpaVendorAdapter());
return emfb;
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
}
}
Listing 2-13The InfrastructureContextConfiguration Source File
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
Spring Boot
本章前面提到的所有内容也适用于 Spring Boot。Spring Boot 构建并扩展了 Spring 框架的特性。然而,这确实让事情变得简单多了。默认情况下,Spring Boot 会自动配置它在类路径中找到的特性。当 Spring Boot 检测到 Spring MVC 类时,它启动 Spring MVC。当它找到一个DataSource
实现时,它就引导它。
可以通过向application.properties
或application.yml
文件添加属性来进行定制。您可以通过它来配置数据源、视图处理和服务器端口等。另一种选择是手动配置,就像在常规的 Spring 应用中一样。当 Spring Boot 检测到某个功能的预配置部分时,它通常不会自动配置该功能。
前面章节中的应用可以通过 Spring Boot 进行简化(参见清单 2-14 和清单 2-15 )。
package com.apress.prospringmvc.bookstore;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
@SpringBootApplication
public class BookstoreApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(BookstoreApplication.class);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(BookstoreApplication.class);
}
}
Listing 2-14The BookstoreApplication
123456789101112131415161718192021222324
BookstoreApplication
类有@SpringBootApplication,
,可以自动配置检测到的特性和第三方库。在这种情况下,它扩展了SpringBootServletInitializer
,因为应用被打包成一个 WAR 并部署到一个容器中。Spring Boot 没有编写我们自己的WebApplicationInitializer
,而是提供了一个现成的。它在一个经典容器中实现了大多数 Spring Boot 功能。
配置属性可以在一个application.properties
或application.yml
文件中给出(见清单 2-15 ),以配置缺省值不适用时所需的特性。有关最常见功能的列表,请查看 Spring Boot 参考指南的附录 A10。
server.port=8080 # 8080 is also the default servlet port
spring.application.name=bookstore
Listing 2-15application.properties
12345
Spring Boot 的一个很好的特性是,当在不同的环境下运行时,我们可以使用概要文件来加载不同的/额外的配置文件。例如,当启用local
概要文件时,Spring Boot 也会加载一个application-local.properties
或application-local.yml
。当在基于云的环境中运行时,属性也可以从 Git 存储库或 Docker 环境中获得。
摘要
本章介绍了 Spring Core 的基本知识。我们回顾了依赖注入,并简要介绍了依赖注入的三个不同版本。我们还讨论了基于构造函数、基于设置器和基于注释的依赖注入。
接下来,我们进入了 Spring 世界,检查了org.springframework.context.ApplicationContext
s,包括它们在我们的应用中扮演的角色。我们还解释了不同的应用上下文(例如,基于 XML 或 Java 的)以及每个上下文中的资源加载。在我们的 web 环境中,我们在org.springframework.web.context.WebApplicationContext
接口的实现中使用应用上下文的专门版本。我们还介绍了应用上下文中的 beans 在默认情况下是如何限定单例范围的。幸运的是,Spring 为我们提供了额外的范围,比如request
、session
、globalSession
、prototype
、application
、thread
。
为了在不同的环境中使用不同的配置,Spring 还包含了概要文件。我们简要地解释了如何启用概要文件以及如何使用它们。当我们测试样例应用并将其部署到 Cloud Foundry 时,我们在样例应用中使用概要文件。
我们还深入研究了 Spring 需要几个启用注释来启用某些特性的方式。这些注释在应用上下文中注册了支持所需特性的附加 beans。这些特性中的大部分依赖于 AOP 来启用(例如,声明式事务管理)。Spring 创建代理来将 AOP 应用于在我们的应用上下文中注册的 beans。
最后,我们快速浏览了一下 Spring Boot,以及它是如何让我们作为软件开发人员的生活变得轻松的。Spring Boot 使用自动配置来配置在类路径上检测到的功能。它构建并扩展了 Spring 框架。
下一章着眼于 MVC web 应用的架构,不同的层,以及它们在我们的应用中的角色。
Footnotes 1
https://www.amazon.com/Expert-One-One-Design-Development/dp/0764543857
2
https://docs.spring.io/spring/docs/current/spring-framework-reference/index.html
3
https://www.apress.com/gp/book/9781484228074
4
https://www.apress.com/gp/book/9781484227893
5
https://docs.spring.io/spring-boot/docs/current/reference/html/index.html
6
https://www.apress.com/gp/book/9781484239629
7
http://www.martinfowler.com/articles/injection.html
8
https://www.oodesign.com/single-responsibility-principle.html
9
3 http://download.oracle.com/otn-pub/jcp/7224-javabeans-1.01-fr-spec-oth-JSpec/beans.101.pdf
10
https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
*
三、Web 应用架构
在我们开始 Spring MVC 内部的旅程之前,我们首先需要理解 web 应用的不同层。我们将从简单介绍 MVC 模式开始讨论,包括它是什么以及为什么我们应该使用它。我们还将介绍 Spring 框架提供的一些接口和类,以表达 MVC 模式的不同部分。
在回顾了 MVC 模式之后,我们将浏览 web 应用中的不同层,看看每一层在应用中扮演什么角色。我们还探索了 Spring 框架如何在不同的层中帮助我们,并利用它们为我们服务。
MVC 模式
模型视图控制器模式(MVC 模式)最初是由 Trygve Reenskaug 在 Xerox 从事 Smalltalk 工作时描述的。当时,该模式针对的是桌面应用。这种模式将表示层分成不同种类的组件。每个组件都有自己的职责。视图使用模型来呈现自己。基于用户操作,视图触发控制器,控制器反过来更新模型。然后模型通知视图(重新)呈现自己(见图 3-1 )。
图 3-1
MVC 模式
MVC 模式完全是关于关注点的分离。每个组件都有自己的角色(见表 3-1 )。关注点的分离在表示层中很重要,因为它有助于我们保持不同组件的整洁。这样,我们就不会给实际视图增加业务逻辑、导航逻辑和模型数据的负担。遵循这种方法可以很好地将一切分开,这使得维护和测试我们的应用更加容易。
表 3-1
简言之,MVC
|
组件
|
描述
|
| — | — |
| 模型 | 模型是视图需要的数据,这样它就可以被渲染。它可能是用户下的订单或请求的图书列表。 |
| 视角 | 视图是实际的实现,它使用模型在 web 应用中呈现自己。这可能是 JSP 或 JSF 页面,但也可能是资源的 PDF、XML 或 JSON 表示。 |
| 控制器 | 控制器是负责响应用户动作的组件,比如表单提交或单击链接。控制器更新模型并采取其他所需的行动,比如调用服务方法来下订单。 |
MVC 模式的经典实现(如图 3-1 所示)包括用户触发一个动作。这将提示控制器更新模型,从而将更改推回到视图中。然后视图用来自模型的更新数据更新自己。这是 MVC 模式的理想实现,例如,它在基于 Swing 的桌面应用中工作得非常好。然而,由于 HTTP 协议的性质,这种方法在 web 环境中是不可行的。对于 web 应用,用户通常通过发出请求来启动操作。这将提示应用更新和呈现视图,并将其发送回用户。这意味着在 web 环境中我们需要一个稍微不同的方法。我们需要从服务器中提取更改,而不是将更改推送到视图中。
这种方法似乎可行,但是在 web 应用中应用起来并不像人们想象的那样简单。Web(或 HTTP)在设计上是无状态的,所以保持一个模型是很困难的。对于 Web,MVC 模式被实现为模型 2 架构(见图 3-2 )。 1 原始模式(模型 1 如图 3-1 所示)与修改后的模式的区别在于,它加入了一个前端控制器,将传入的请求分派给其他控制器。这些控制器处理传入的请求,返回模型,并选择视图。
图 3-2
模型 2 MVC 模式
前端控制器是处理传入请求的组件。首先,它将请求委托给合适的控制器。当该控制器完成处理和更新模型时,前端控制器根据结果确定渲染哪个视图。在大多数情况下,这个前端控制器被实现为一个javax.servlet.Servlet
servlet(例如,JSF 的FacesServlet
)。在 Spring MVC 中,这个前端控制器是org.springframework.web.servlet.DispatcherServlet
。
应用分层
在简介中,我们提到了一个应用由几层组成(见图 3-3 )。我们喜欢把层看作是应用关注的领域。因此,我们也使用分层来实现关注点的分离。例如,视图不应该负担业务或数据访问逻辑,因为这些都是不同的关注点,通常位于不同的层。
图 3-3
典型应用分层
层应该被认为是概念上的边界,但是它们不必彼此物理隔离(在另一个虚拟机中)。对于 web 应用,这些层通常运行在同一个虚拟机中。Rod Johnson 的书,专家一对一的 J2EE 设计和开发 (Wrox,2002),对应用的分布和扩展进行了很好的讨论。
图 3-3 是应用各层的高度概括视图。数据访问在应用的底部,表示在顶部,服务(实际的业务逻辑)在中间。这一章着眼于这个架构,以及一切是如何组织的。表 3-2 提供了不同层的简要描述。
表 3-2
层的简要概述
|
层
|
描述
|
| — | — |
| 陈述 | 这很可能是一个基于网络的解决方案。表示层应该尽可能薄。还应该有可能提供替代的表示层,如 web 前端或 web 服务外观。这些都应该在设计良好的服务层上运行。 |
| 服务 | 包含业务逻辑的实际系统的入口点。它提供了一个粗粒度的接口,支持系统的使用。这一层也应该是系统的事务边界(可能也是安全边界)。这一层不应该知道任何关于持久性或所使用的视图技术的事情(或者知道得越少越好)。 |
| 数据存取 | 基于接口的层提供了对底层数据访问技术的访问,而无需将其暴露给上层。这一层抽象了实际的持久性框架(例如,JDBC、JPA 或类似 MongoDB 的东西)。注意,这一层不应该包含业务逻辑。 |
各层之间的交流是自上而下的。服务层可以访问数据访问层,但数据访问层不能访问服务层。如果您看到这种循环依赖悄悄进入您的应用,请后退几步,重新考虑您的设计。循环依赖(或自下而上的依赖)几乎总是糟糕设计的标志,并导致复杂性增加和更难维护的应用。
Note
有时候,你会遇到术语,层。许多人交替使用 tier 和 layer 然而,将它们分开有助于讨论应用架构或其部署。我们喜欢使用层来表示应用中的概念层,而层表示部署时不同机器上的层的物理分离。在层中思考有助于软件开发人员,而在层中思考有助于系统管理员。
尽管图 3-3 给出了一个 web 应用各层的概述,我们可以进一步细分。在一个典型的 web 应用中,我们可以识别五个概念层(见图 3-4 )。我们可以将表示层分为 web 和用户界面层,但是应用还包括一个域层(参见本章后面的“Spring MVC 应用层”一节)。通常,领域层跨越所有层,因为从数据访问层到用户界面,它无处不在。
图 3-4
Web MVC 应用层
Note
分层架构并不是唯一的应用架构;然而,它是 web 应用最常遇到的架构。
如果你看一下样例应用,图 3-4 中显示的架构在包结构中变得清晰。这些包可以在书店共享项目中找到(见图 3-5 )。主要包包括以下内容。
com.apress.prospringmvc.bookstore.domain
:畴层
com.apress.prospringmvc.bookstore.service
:服务层
com.apress.prospringmvc.bookstore.repository
:数据访问层
其他包是 web 层的支持包,com.apress.prospringmvc.bookstore.config
包包含根应用上下文的配置类。我们在本书的过程中构建的用户界面和 web 层,这些层在用户界面所需的com.apress.prospringmvc.bookstore.web
包和百里香 2 模板中。
图 3-5
书店包装概述
关注点分离
正如在第二章中提到的,清楚地分离关注点是很重要的。如果你看图 3-4 中的架构,关注点的分离出现在层中。将关注点分成不同的层有助于我们实现清晰的设计和灵活且可测试的应用。
创建或检测图层可能很困难。一个经验法则是,如果一个层对其他层有太多的依赖,您可能希望引入另一个层来合并所有的依赖。另一方面,如果您在不同的层中看到一个单独的层,您可能想要重新考虑这个层,并使它成为应用的一个方面。在这种情况下,我们可以使用 Spring 框架的 AOP 功能在运行时应用这些方面(参见第二章)。
耦合层——例如,服务层需要与数据访问层对话——是通过定义清晰的接口来实现的。定义接口和接口编程减少了与具体实现的实际耦合。耦合性和复杂性的降低使得应用更易于测试和维护。使用接口的另一个好处是,Spring 可以使用 JDK 动态代理 3 来创建代理并应用 AOP。Spring 还可以使用字节码生成库(cglib)在基于类的代理上应用 AOP,该库以重新打包的形式随 Spring 框架一起提供。
要点是:应用中的分层导致更易维护和测试的应用。关注点的清晰分离也导致良好的应用架构。
Spring MVC 应用层
您可能想知道所有的层如何适应 Spring MVC 应用,以及所有不同的层如何帮助我们构建 Spring MVC 应用。本节着眼于图 3-4 中描绘的五层。我们特别关注不同层所扮演的角色以及每一层中应该包含的内容。
领域层
领域是应用中最重要的一层。它是我们正在解决的业务问题的代码表示,并且包含我们领域的业务规则。这些规则可能会检查我们是否有足够的资金从我们的帐户转账,或者确保字段是唯一的(例如,我们系统中的用户名)。
确定领域模型的一个流行技术是使用用例描述中的名词作为领域对象(例如,Account
或Transaction
)。这些对象包含状态(例如,Account
的用户名)和行为(例如,Account
上的credit
方法)。这些方法通常比服务层中的方法更细粒度。例如,在第二章的货币转移示例中,com.apress.prospringmvc.moneytransfer.domain.Account
对象有一个debit
和credit
方法。credit 方法包含一些业务逻辑,用于检查我们的帐户中是否有足够的资金来转账。
在第二章中,com.apress.prospringmvc.moneytransfer.service.MoneyTransferService
的实现使用这些支持方法来实现一个用例(在这个例子中,它将钱从一个账户转移到另一个账户)。这不要与贫血的域模型 4 相混淆,在这种模型中,我们的域对象只有状态,没有行为。
一般来说,你的领域模型不需要依赖注入;但是,这样做还是有可能的。例如,可以使用 Spring 框架和 AspectJ 在我们的域对象中实现依赖注入。在这种情况下,我们会给我们的域类加上org.springframework.beans.factory.annotation.Configurable
注释。接下来,我们需要设置加载时编织或编译时编织,并注入我们的依赖关系。关于这个主题的更多信息,请参阅 Spring 框架文档。 5
用户界面层
用户界面层将应用呈现给用户。该层将服务器生成的响应呈现为用户客户端请求的类型。例如,web 浏览器可能会请求 HTML 文档,web 服务可能需要 XML 文档,而另一个客户端可能会请求 PDF 或 Excel 文档。
我们将表示层分为用户界面层和 web 层,因为尽管有各种不同的视图技术,我们还是希望尽可能多地重用代码。我们的目标是只重新实现用户界面。有许多不同的视图技术,包括 JSF、JSP(X)、FreeMarker 和百里香叶等等。在理想的情况下,我们可以在不改变应用后端的情况下切换用户界面。
Spring MVC 帮助我们将用户界面与系统的其他部分隔离开来。在 Spring 中,视图由一个界面表示:org.springframework.web.servlet.View
。这个接口负责将来自用户的动作结果(模型)转换成用户请求的响应类型。View
接口是通用的,它不依赖于特定的视图技术。Spring 框架或视图技术为每种支持的视图技术提供了一个实现。开箱即用,Spring 支持以下视图技术。
JSP
便携文档格式
超过
FreeMarker
胸腺泡
瓷砖 3
XML(封送处理、XSLT 或普通)
JSON(使用 Jackson 或 GSON)
Groovy 标记
脚本视图(车把、ERB、科特林脚本模板)
通常,用户界面依赖于领域层。有时候,直接暴露和呈现领域模型是很方便的。当我们开始在应用中使用表单时,这尤其有用。例如,这将让我们直接处理域对象,而不是额外的间接层。一些人认为这在层之间产生了不必要的或不想要的耦合。然而,仅仅为了从视图中分离域而创建另一层会导致不必要的复杂性和重复。在任何情况下,重要的是要记住 Spring MVC 不要求我们直接向视图公开域模型——我们是否这样做完全取决于我们自己。
Web 层
web 层有两个职责。第一个责任是引导用户通过 web 应用。二是做服务层和 HTTP 之间的集成层。
在网站中导航用户可以像将 URL 映射到视图或像 Spring Web Flow 这样的成熟页面流解决方案一样简单。导航通常只绑定到 web 层,在域或服务层中没有任何导航逻辑。
作为集成层,web 层应该尽可能的薄。应该是这个层将传入的 HTTP 请求转换为服务层可以处理的内容,然后将来自服务器的结果(如果有)转换为用户界面的响应。web 层不应该包含任何业务逻辑,这是服务层的唯一目的。
web 层也由 cookies、HTTP 头和可能的 HTTP 会话组成。一致和透明地管理所有这些事情是 web 层的责任。不同的 HTTP 元素不应该渗入我们的服务层。如果他们这样做,整个服务层(以及我们的应用)就会与 web 环境联系在一起。这样做会增加维护和测试应用的难度。保持服务层的整洁还允许我们为不同的通道重用相同的服务。例如,它使我们能够添加 web 服务或 JMS 驱动的解决方案。web 层应该被视为连接到服务层并向最终用户公开的客户端或代理。
在 Java web 开发的早期,servlets 或 JavaServer Pages 主要实现这一层。servlets 负责处理请求并将其转换成服务层可以理解的内容。通常情况下,servlets 会将所需的 HTML 直接写回客户机。这种实现很快变得难以维护和测试。几年后,Model 2 MVC 模式出现了,我们最终拥有了高级的 Web MVC 功能。
像 Spring MVC、Struts、JSF 和 Tapestry 这样的框架为这种模式提供了不同的实现,它们都以不同的方式工作。然而,我们可以确定两种主要类型的 web 层实现:请求/响应框架(例如,struts 和 Spring MVC)和基于组件的框架(例如,JSF 和 Tapestry)。请求/响应框架对javax.servlet.ServletRequest
和javax.servlet.ServletResponse
对象进行操作。因此,他们在 Servlet API 上操作的事实并没有真正对用户隐藏。基于组件的框架提供了一个完全不同的编程模型。他们试图对程序员隐藏 Servlet API,并提供基于组件的编程模型。使用基于组件的框架感觉很像使用 Swing 桌面应用。
这两种方法各有利弊。Spring MVC 功能强大,在两者之间取得了很好的平衡。它可以隐藏使用 Servlet API 的事实;但是,访问该 API 很容易(尤其是)。
web 层依赖于领域层和服务层。在大多数情况下,您希望将传入的请求转换成一个域对象,并调用服务层上的方法来处理该域对象(例如,更新客户或创建订单)。Spring MVC 使得将传入的请求映射到对象变得很容易,我们可以使用依赖注入来访问服务层。
在 Spring MVC 中,web 层由带有org.springframework.stereotype.Controller
注释的org.springframework.web.servlet.mvc.Controller
接口或类表示。基于接口的方法是有历史的,从一开始它就是 Spring 框架的一部分;然而,它现在被认为是过时的。不管怎样,它对于简单的用例仍然有用,Spring 提供了一些现成的方便的实现。新的基于注释的方法比原来的基于接口的方法更加强大和灵活。本书的重点是基于注释的方法。
在执行一个控制器后,基础设施(参见第四章了解更多关于这个主题的信息)期待一个org.springframework.web.servlet.ModelAndView
类的实例。这个类包含了模型(以org.springframework.ui.ModelMap
的形式)和要呈现的视图。这个视图可以是一个实际的org.springframework.web.servlet.View
实现或者一个视图的名称。
Caution
不要在带有Controller
接口的类上使用Controller
注释。这些是以不同的方式处理的,混合使用这两种策略会导致令人惊讶和不希望的结果!
服务层
服务层在应用的架构中非常重要。它被认为是我们应用的核心,因为它向用户公开了系统的功能(用例)。它通过提供一个粗粒度的 API 来做到这一点(如表 3-2 中所述)。清单 3-1 描述了一个粗粒度的服务接口。
package com.apress.prospringmvc.bookstore.service;
import com.apress.prospringmvc.bookstore.domain.Account;
public interface AccountService {
Account save(Account account);
Account login(String username, String password) throws AuthenticationException;
Account getAccount(String username);
}
Listing 3-1A Coarse-Grained Service Interface
12345678910111213141516
这个清单被认为是粗粒度的,因为它需要从客户端调用一个简单的方法来完成一个用例。这与清单 3-2 (细粒度服务方法)中的代码形成对比,后者需要几次调用来执行一个用例。
package com.apress.prospringmvc.bookstore.service;
import com.apress.prospringmvc.bookstore.domain.Account;
public interface AccountService {
Account save(Account account);
Account getAccount(String username);
void checkPassword(Account account, String password);
void updateLastLogin(Account account);
}
Listing 3-2A Fine-Grained Service Interface
123456789101112131415161718
如果可能的话,我们不应该调用一系列方法来执行一个系统函数。我们应该尽可能地屏蔽用户的数据访问和 POJO 交互。在理想情况下,粗粒度函数应该代表一个成功或失败的工作单元。用户可以使用不同的客户端(例如,网络应用、网络服务或桌面应用);然而,这些客户端应该执行相同的业务逻辑。因此,服务层应该是我们实际系统(即业务逻辑)的单一入口点。
在服务层使用单一入口点和粗粒度方法的额外好处是,我们可以在这一层简单地应用事务和安全性。我们不必让应用的不同客户端承担安全和事务性需求。它现在是系统核心的一部分,一般通过 AOP 来应用。
在基于 web 的环境中,我们可能有多个用户同时操作服务。服务必须是无状态的,因此将服务设为单例是一个好的做法。在领域模型中,应该尽可能地保留状态。保持服务层的无状态提供了一个额外的好处:它还使得服务层是线程安全的。
将服务层保持在单个入口点,保持层的无状态,并在该层上应用事务和安全性,这使得 Spring 框架的其他特性能够将服务层公开给不同的客户端。例如,我们可以使用配置轻松地通过 RMI 或 JMS 公开我们的服务层。有关 Spring Framework 远程支持的更多信息,我们建议使用 Pro Spring 5 (Apress,2017)或在线 Spring Framework 文档。 6
在我们的书店示例应用中,com.apress.prospringmvc.bookstore.service.BookstoreService
接口(参见清单 3-3 )充当我们的服务层的接口(还有几个其他接口,但这是最重要的一个)。这个接口包含几个粗粒度的方法。在大多数情况下,执行一个用例需要一个方法调用(例如,createOrder)。
package com.apress.prospringmvc.bookstore.service;
import java.util.List;
import com.apress.prospringmvc.bookstore.domain.Account;
import com.apress.prospringmvc.bookstore.domain.Book;
import com.apress.prospringmvc.bookstore.domain.BookSearchCriteria;
import com.apress.prospringmvc.bookstore.domain.Cart;
import com.apress.prospringmvc.bookstore.domain.Category;
import com.apress.prospringmvc.bookstore.domain.Order;
public interface BookstoreService {
List<Book> findBooksByCategory(Category category);
Book findBook(long id);
Order findOrder(long id);
List<Book> findRandomBooks();
List<Order> findOrdersForAccount(Account account);
Order store(Order order);
List<Book> findBooks(BookSearchCriteria bookSearchCriteria);
Order createOrder(Cart cart, Account account);
List<Category> findAllCategories();
}
Listing 3-3The BookstoreService Interface
1234567891011121314151617181920212223242526272829303132333435
如清单 3-3 所示,服务层依赖于领域层来执行业务逻辑。然而,它也依赖于数据访问层来存储和检索底层数据存储中的数据。服务层可以作为一个或多个域对象之间的绑定器来执行业务功能。服务层应该协调它需要哪些域对象,以及它们如何相互作用。
Spring 框架没有帮助我们实现服务层的接口;然而,这并不奇怪。服务层是我们的应用的基础;事实上,它是专门为我们的应用。然而,Spring 框架可以帮助我们构建架构和编程模型。我们可以使用依赖注入和应用方面来驱动我们的事务。所有这些对我们的编程模型都有积极的影响。
数据访问层
数据访问层负责与底层的持久性机制进行交互。这一层知道如何在数据存储中存储和检索对象。它这样做是因为服务层不知道使用了哪个底层数据存储。(数据存储可以是数据库,但也可以由文件系统上的平面文件组成。)
创建单独的数据访问层有几个原因。首先,我们不想让服务层知道我们使用的数据存储类型;我们希望透明地处理持久性。在我们的示例应用中,我们使用内存数据库和 JPA (Java 持久性 API)来存储数据。现在想象一下,我们的 com . a press . prospring MVC . book store . domain . account 不是来自数据库,而是来自活动目录服务。我们可以简单地创建一个新的接口实现,它知道如何处理 Active Directory——而不需要改变我们的服务层。理论上,我们可以很容易地交换实现;例如,我们可以在不改变服务层的情况下从 JDBC 切换到 Hibernate。不太可能出现这种情况,但是有这种能力还是不错的。
这种方法最重要的原因是它简化了应用的测试。一般来说,数据访问很慢,所以我们必须尽可能快地运行我们的测试。一个单独的数据访问层使得创建我们的数据访问层的存根或模拟实现变得容易。
Spring 对数据访问层有很好的支持。例如,它提供了一种一致且透明的方式来处理各种数据访问框架(例如,JDBC、JPA 和 Hibernate)。对于这些技术中的每一项,Spring 都提供了对以下能力的广泛支持。
事务管理
资源处理
异常翻译
事务管理在其支持的每种技术中都是透明的。事务管理器处理事务,它支持 JTA (Java Transaction API ),支持分布式或全局事务(跨越多个资源的事务,如数据库和 JMS 代理)。这种出色的事务支持意味着事务管理器也可以为您管理资源。我们不再担心数据库连接或文件句柄被关闭;这都是为你处理的。支持的实现可以在org.springframework.jdbc
和org.springframework.orm
包中找到。
Tip
Spring Data 项目 7 提供了与几种技术的更深层次的集成。在一些用例中,它消除了编写我们自己的数据访问对象(DAO)或存储库的实现的需要。
Spring 框架包含了另一个强大的特性,作为其数据访问支持的一部分:异常翻译。Spring 为其支持的所有技术提供了广泛的异常翻译支持。这个特性将特定于技术的异常转换成org.springframework.dao.DataAccessException
的子类。对于数据库驱动技术,它考虑数据库供应商、版本和从数据库接收的错误代码。异常层次从java.lang.RuntimeException
开始扩展;因此,它不必被捕获,因为它不是一个检查过的异常。有关数据访问支持的更多信息,请参见 Pro Spring 5 (Apress,2017)或在线 Spring 框架文档。
清单 3-4 展示了数据访问对象或存储库的外观。注意,该接口没有引用或提及我们使用的任何数据访问技术(我们在示例应用中使用 JPA)。此外,服务层不关心数据是如何持久存储的,也不关心数据在哪里持久存储;它只是想知道如何存储或检索它。
package com.apress.prospringmvc.bookstore.repository;
import com.apress.prospringmvc.bookstore.domain.Account;
public interface AccountRepository extends CrudRepository<Account, Long> {
Account findByUsername(String username);
}
Listing 3-4A Sample AccountRepository using Spring Data
123456789101112
更多通往罗马的道路
这里讨论的架构并不是唯一的应用架构。哪种架构最适合给定的应用取决于应用的大小、开发团队的经验以及应用的生命周期。团队越大或者应用存在的时间越长,具有独立层的干净架构就变得越重要。
从单个静态页面开始的 web 应用可能不需要任何架构。然而,随着应用的增长,越来越重要的是,我们不要试图把所有东西都放在一个页面上,因为这将使维护或理解应用变得非常困难,更不用说测试了。
随着应用的规模和年龄的增长,我们需要重构它的设计,并记住每一层或每一个组件都应该有一个单独的职责。如果我们发现一些关注点应该在不同的层或者涉及多个组件,我们应该把它转换成应用的一个方面(横切关注点),并使用 AOP 把它应用到代码中。
当决定如何构建我们的层时,我们应该尝试为我们的系统确定一个清晰的 API(通过 Java 接口公开)。为我们的系统考虑一个 API 让我们考虑我们的设计和一个有用的和可用的 API。一般来说,如果一个 API 很难使用,它也很难测试和维护。因此,干净的 API 非常重要。此外,使用不同层之间的接口允许单独的层被独立地构建和测试。这在较大的开发团队(或者由多个较小的团队组成的团队)中是一个很大的优势。它允许我们专注于我们正在处理的功能,而不是底层或更高级别的组件。
在设计和构建应用时,使用良好的面向对象实践和模式来解决问题也很重要。例如,我们应该利用多态和继承,我们应该使用 AOP 来应用系统范围的关注点。Spring 框架还可以帮助我们在运行时将应用连接在一起。总的来说,本章描述的特性和方法可以帮助我们保持代码的整洁,并为我们的应用实现最佳的架构。
摘要
在这一章中,我们讨论了 MVC 模式,包括它的起源和它解决的问题。我们还简要讨论了 MVC 模式的三个组成部分:模型、视图和控制器。接下来,我们讨论了 Model 2 MVC 模式,以及使用前端控制器如何将其与 Model 1 MVC 模式区别开来。在 Spring MVC 中,这个前端控制器是org.springframework.web.servlet.DispatcherServlet
。
接下来,我们简要介绍了一般的 web 应用架构。我们确定了 web 应用中通常可用的五个不同的层:域、用户界面、web、服务和数据访问。这些层在我们的应用中扮演着重要的角色,我们讨论了这些角色是什么以及它们是如何组合在一起的。我们还介绍了 Spring 如何在应用的不同层帮助我们。
本章的主要内容是 MVC 模式中的各种层和组件可以分离不同的关注点。每一层都应该有一个单一的职责,无论是业务逻辑还是 HTTP 世界和服务层之间的绑定器。关注点的分离有助于我们实现一个干净的架构和创建可维护的代码。最后,清晰的分层使得测试我们的应用更加容易。
下一章将深入探讨 Spring MVC。具体来说,它探索了DispatcherServlet
servlet,包括它如何工作以及如何配置它。它还进一步研究了本章中描述的不同组件在 Spring MVC 应用中是如何工作的。
Footnotes 1
https://en.wikipedia.org/wiki/JSP_model_2_architecturel
2
https://www.thymeleaf.org
3
https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html
4
https://martinfowler.com/bliki/AnemicDomainModel.html
5
https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-atconfigurable
6
https://docs.spring.io/spring/docs/current/spring-framework-reference/index.html
7
https://spring.io/projects/spring-data
四、Spring MVC 架构
本章深入 Spring MVC 的内部,仔细观察org.springframework.web.servlet.DispatcherServlet
。首先,学习 servlet 如何处理传入的请求,并确定哪些组件在请求处理中起作用。在确定了这些组件之后,我们将更深入地研究它们的角色、功能和实现。您还将学习如何配置org.springframework.web.servlet.DispatcherServlet
,部分是通过检查 Spring Boot 的默认配置和扩展配置。
DispatcherServlet 请求处理工作流
在前一章中,你学习了前端控制器在 Model 2 MVC 模式中扮演的重要角色。前端控制器负责将传入的请求分派给正确的处理程序,并准备将响应呈现为用户希望看到的内容。Spring MVC 中前端控制器的角色由org.springframework.web.servlet.
DispatcherServlet
扮演。这个 servlet 使用几个组件来完成它的角色。所有这些组件都表示为接口,对于这些接口,有一个或多个实现是可用的。下一节将探讨这些组件在请求处理工作流中扮演的一般角色。下一节将介绍接口的不同实现。
我们特意使用了处理者这个术语。DispatcherServlet 非常灵活且可定制,它可以处理比org.springframework.web.servlet.mvc.Controller
实现或org.springframework.stereotype.Controller
注释类更多类型的处理程序。
工作流程
图 4-1 显示了请求处理工作流程的高级概述。
图 4-1
请求处理工作流
在前面的章节中,您学习了关注点分离的重要性。在 Spring 框架中,应用了相同的规则。考虑到可扩展性和关注点的分离,许多支持组件被设计为接口。虽然图 4-1 中的高层概述是正确的,但幕后发生的更多。图 4-2 显示了请求处理工作流程的完整视图。
图 4-2
请求处理工作流
图 4-2 提供了DispatcherServlet
内部请求处理工作流程的全局概览。以下部分将详细介绍这个流程中的不同步骤。
准备请求
在DispatcherServlet
开始分派和处理请求之前,它准备并预处理请求。servlet 通过使用org.springframework.web.servlet.LocaleResolver
确定和公开当前请求的当前java.util.Locale
来启动。接下来,它在org.springframework.web.context.request.RequestContextHolder
中准备并公开当前请求。这使得框架代码很容易访问当前请求,而不是传递它。
接下来,servlet 构造了org.springframework.web.servlet.FlashMap implementation
。它通过调用试图解析输入FlashMap
的org.springframework.web.servlet.FlashMapManager
来做到这一点。这个映射包含在前一个请求中显式存储的属性。一般来说,这在重定向到下一页时使用。这个主题将在第五章中进行深入讨论。
接下来,检查传入的请求以确定它是否是一个多部分 HTTP 请求(这在进行文件上传时使用)。如果是这样,请求通过一个org.springframework.web.multipart.MultipartResolver
组件被包装在org.springframework.web.multipart.MultipartHttpServletRequest
中。在此之后,请求准备好被分派给正确的处理程序。图 4-3 显示了请求处理工作流程第一部分的流程图。
图 4-3
请求处理流程的开始
确定处理程序执行链
几个组件参与分派请求(见图 4-4 )。当请求准备好分派时,DispatcherServlet
咨询一个或多个org.springframework.web.servlet.HandlerMapping
实现来确定哪个处理程序可以处理该请求。如果没有找到处理程序,HTTP 404 响应将被发送回客户端。HandlerMapping 返回org.springframework.web.servlet.HandlerExecutionChain
(您将在下一节了解更多)。当处理程序确定后,servlet 试图找到org.springframework.web.servlet.HandlerAdapter
来执行找到的处理程序。如果找不到合适的HandlerAdapter
,则抛出javax.servlet.ServletException
。
图 4-4
分派请求
执行处理程序执行链
为了处理请求,DispatcherServlet
使用HandlerExecutionChain class
来决定执行什么。该类包含对需要调用的实际处理程序的引用;然而,它也(可选地)引用在处理程序执行之前(preHandle
方法)和之后(postHandle
方法)执行的org.springframework.web.servlet.HandlerInterceptor
实现。这些拦截器可以应用横切功能(参见第六章了解更多关于这个主题的信息)。如果代码执行成功,拦截器会以相反的顺序再次被调用;最后,当需要时,视图被渲染(见图 4-5 )。
图 4-5
处理请求
处理程序的执行被委托给在上一步中确定的选定的HandlerAdapter
。它知道如何执行选定的处理程序,并将响应翻译成org.springframework.web.servlet.ModelAndView
。
如果返回的model and view
中没有视图,则根据传入的请求查询org.springframework.web.servlet.RequestToViewNameTranslator
以生成视图名称。
处理程序异常
当在处理请求的过程中抛出异常时,DispatcherServlet
咨询已配置的org.springframework.web.servlet.HandlerExceptionResolver
实例来处理抛出的异常。解析器可以将异常转换成视图向用户显示。例如,如果有一个与数据库错误相关的异常,您可以显示一个页面,指示数据库关闭。如果异常没有得到解决,它将被重新抛出并由 servlet 容器处理,这通常会导致 HTTP 500 响应代码(内部服务器错误)。图 4-6 显示了请求处理工作流程的这一部分。
图 4-6
异常处理
渲染视图
如果在请求处理工作流程中选择了一个视图,DispatcherServlet
首先检查它是否是一个视图引用(如果视图是java.lang.String
就是这种情况)。如果是这样的话,那么将参考已配置的org.springframework.web.servlet.ViewResolver
bean 来解析对实际org.springframework.web.servlet.View
实现的视图引用。如果没有观点和一个不能解决,javax.servlet.ServletException
被抛出。图 4-7 显示了视图渲染过程。
图 4-7
视图渲染过程
完成加工
每个传入的请求都经过请求处理流程的这一步,不管是否有异常。如果一个handler execution chain
可用,拦截器的afterCompletion
方法被调用。只有成功调用了preHandle
方法的拦截器才会调用它们的afterCompletion
方法。接下来,这些拦截器以调用它们的preHandle
方法的相反顺序执行。这模拟了 servlet 过滤器中的行为,其中第一个被调用的过滤器也是最后一个被调用的过滤器。
最后,DispatcherServlet
使用 Spring 框架中的事件机制来触发org.springframework.web.context.support.RequestHandledEvent
(见图 4-8 )。您可以创建并配置org.springframework.context.ApplicationListener
来接收和记录这些事件。
图 4-8
完成加工
请求处理摘要
DispatcherServlet
是使用 Spring MVC 处理请求的关键组件。它也是高度灵活和可配置的。这种灵活性来自于这样一个事实,即 servlet 使用许多不同的组件来完成它的角色,并且这些组件被表示为接口。表 4-1 给出了请求处理工作流中涉及的所有主要组件类型的概述。
表 4-1
请求处理工作流中使用的 DispatcherServlet 组件
|
组件类型
|
描述
|
| — | — |
| org.springframework.web.multipart.MultipartResolver
| 处理多部分表单处理的策略接口 |
| org.springframework.web.servlet.LocaleResolver
| 区域解析和修改策略 |
| org.springframework.web.servlet.ThemeResolver
| 主题解析和修改的策略 |
| org.springframework.web.servlet.HandlerMapping
| 将传入请求映射到处理程序对象的策略 |
| org.springframework.web.servlet.HandlerAdapter
| 处理程序对象类型执行处理程序的策略 |
| org.springframework.web.servlet.HandlerExceptionResolver
| 处理处理程序执行期间引发的异常的策略 |
| org.springframework.web.servlet.RequestToViewNameTranslator
| 处理程序返回 none 时确定视图名称的策略 |
| org.springframework.web.servlet.ViewResolver
| 将视图名称转换为实际视图实现的策略 |
| org.springframework.web.servlet.FlashMapManager
| 模拟 flash 范围的策略 |
在接下来的章节中,您将看到如何配置DispatcherServlet
。您还将进一步了解各种组件的不同实现。
前端控制器
像任何 servlet 一样,org.springframework.web.servlet.DispatcherServlet
需要进行配置,以便 web 容器可以引导和映射 servlet。这样,它可以处理请求。配置DispatcherServlet
是一个双向过程。首先,您需要告诉容器加载一个 servlet,并将其映射到一个或多个 URL 模式。
在引导之后,servlet 使用创建的org.springframework.web.context.WebApplicationContext
来配置自己。servlet 试图从这个应用上下文中检测所需的组件,如果没有找到,它将使用默认值(在大多数情况下)。
引导调度程序 Servlet
servlet 规范(从版本 3.0 开始)有几个配置和注册 servlet 的选项。
选项 1:使用一个web.xml
文件(参见清单 4-1 )。
选项 2:使用一个web-fragment.xml
文件(参见清单 4-2 )。
选项 3:使用javax.servlet.ServletContainerInitializer
(见清单 4-3 )。
选项 4:示例应用使用 Spring 5.2,因此您可以通过实现org.springframework.web.WebApplicationInitializer
接口获得第四个选项。
选项 5:使用 Spring Boot 自动配置DispatcherServlet.
dispatcher servlet 需要一个web application context
,它应该包含使 dispatcher servlet 能够配置自身的所有 beans。默认情况下,dispatcher servlet 创建org.springframework.web.context.support.XmlWebApplicationContext
。
接下来的部分中的所有样本加载org.springframework.web.servlet.DispatcherServlet
并将其映射到所有传入的请求(/
)。所有这些配置都导致 servlet 的相同运行时设置。只是你做这件事的机制不同。本书的其余部分使用选项 4 来配置示例应用。
org.springframework.web.context.WebApplicationContext
是org.springframework.context.ApplicationContext
的专门扩展,在网络环境中是需要的(更多信息见第二章)。
您在本书中构建的示例应用尽可能多地使用选项 5 来配置环境和应用。然而,您将学习配置 servlet 的所有四个选项的基本设置。
使用 web.xml
自从 servlet 规范出现以来,web.xml
文件就一直存在。它是一个 XML 文件,包含引导 servlet、监听器和/或过滤器所需的所有配置。清单 4-1 显示了引导DispatcherServlet
所需的最小 web.xml 配置。web.xml
文件必须在 web 应用的WEB-INF
目录中(这由 servlet 规范决定)。
<web-app xmlns:="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0" metadata-complete="true">
<servlet>
<servlet-name>bookstore</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>bookstore</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Listing 4-1The web.xml Configuration (Servlet 4.0)
123456789101112131415161718192021
默认情况下,dispatcher servlet 从WEB-INF
目录加载一个名为[servletname]-servlet.xml
的文件。
web-app 元素中的metadata-complete
属性指示 servlet 容器不要扫描javax.servlet.ServletContainerInitializer
实现的类路径;它也不扫描web-fragment.xml
文件。将这个属性添加到您的web.xml
中会大大增加启动时间,因为它会扫描类路径,这在大型应用中需要时间。
使用 web-fragment.xml
web-fragment.xml 特性从 servlet 规范的 3.0 版本开始就可用了,它允许对 web 应用进行更加模块化的配置。web-fragment.xml
必须在 jar 文件的META-INF
目录中。它不会在 web 应用的META-INF
中被检测到;它必须在一个 jar 文件中。web-fragment.xml
可以包含与web.xml
相同的元素(参见清单 4-2 )。
这种方法的好处是打包成 jar 文件的每个模块都有助于 web 应用的配置。这也被认为是一个缺点,因为现在你已经将你的配置分散到你的代码库,这在更大的项目中可能是麻烦的。
<web-fragment xmlns:="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-fragment_4_0.xsd"
version="4.0" metadata-complete="true">
<servlet>
<servlet-name>bookstore</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>bookstore</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-fragment>
Listing 4-2The web-fragment.xml Configuration (Servlet 4.0)
1234567891011121314151617181920
使用 ServletContainerInitializer
servlet 规范的 3.0 版本引入了使用基于 Java 的方法来配置 web 环境的选项(参见清单 4-3 )。Servlet 3.0+兼容容器扫描类路径,寻找实现javax.servlet.ServletContainerInitializer
接口的类,并调用这些类的onStartup
方法。通过在这些类上添加一个javax.servlet.annotation.HandlesTypes
注释,您还可以得到进一步配置 web 应用所需的类(这是允许第四个选项使用org.springframework.web.WebApplicationInitializer
的机制)。
像 web 片段一样,ServletContainerInitializer
允许 web 应用的模块化配置,但是现在是以基于 Java 的方式。使用 Java 给你带来了使用 Java 语言代替 XML 的所有好处。此时,您有了强类型,可以影响 servlet 的构造,并且有了配置 servlet 的更简单的方法(在 XML 文件中,这是通过在 XML 文件中添加 init-param 和/或 context-param 元素来完成的)。
package com.apress.prospringmvc.bookstore.web;
import java.util.Set;
// javax.servlet imports omitted.
import org.springframework.web.servlet.DispatcherServlet;
public class BookstoreServletContainerInitializer
implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> classes, ServletContext servletContext)
throws ServletException {
ServletRegistration.Dynamic registration;
registration = servletContext.addServlet("ds", DispatcherServlet.class);
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
Listing 4-3A Java-based Configuration
123456789101112131415161718192021222324
使用 WebApplicationInitializer
现在是时候看看在使用 Spring 时配置应用的选项 4 了。Spring 提供了一个ServletContainerInitializer
实现(org.springframework.web.SpringServletContainerInitializer)
,让生活变得更简单一些(参见清单 4-4 )。Spring 框架提供的实现检测并实例化所有实例of org.springframework.web.
WebApplicationInitializer
and calls the onStartup
这些实例的方法。
package com.apress.prospringmvc.bookstore.web;
// javax.servlet imports omitted
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.servlet.DispatcherServlet;
public class BookstoreWebApplicationInitializer
implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext)
throws ServletException {
ServletRegistration.Dynamic registration
registration = servletContext.addServlet("dispatcher", DispatcherServlet.class);
registration.addMapping("/");
registration.setLoadOnStartup(1);
}
}
Listing 4-4The WebApplicationInitializer Configuration
1234567891011121314151617181920212223
使用这个特性会影响应用的启动时间!首先,servlet 容器需要扫描所有javax.servlet.ServletContainerInitializer
实现的类路径。其次,扫描类路径中的org.springframework.web.WebApplicationInitializer
实现。在大型应用中,这种扫描可能需要一些时间。
不要直接实现WebApplicationInitializer,
,而是使用 Spring 的一个类。
使用 Spring Boot
使用 Spring Boot 时,不需要手动配置DispatcherServlet
。Spring Boot 根据检测到的配置自动进行配置。表 4-2 中提到的属性大多可以通过spring.mvc
名称空间中的属性进行配置。基本样品见清单 4-5 。
package com.apress.prospringmvc.bookstore;
@SpringBootApplication
public class BookstoreApplication {
public static void main(String[] args) {
SpringApplication.run(BookstoreApplication.class, args);
}
}
Listing 4-5BookstoreApplication Using Spring Boot
123456789101112
在经典的战争应用中使用 Spring Boot 时,需要一个专门的WebApplicationInitializer
。Spring Boot 为此提供了SpringBootServletInitializer
。样本见清单 4-6 。
package com.apress.prospringmvc.bookstore;
@SpringBootApplication
public class BookstoreApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(BookstoreApplication.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(BookstoreApplication.class);
}
}
Listing 4-6BookstoreApplication Using Spring Boot in a WAR
1234567891011121314151617
配置 DispatcherServlet
配置org.springframework.web.servlet.DispatcherServlet
是一个两步过程。第一步是通过直接在 dispatcher servlet(声明)上设置属性来配置 servlet 的行为。第二步是在应用上下文中配置组件(初始化)。
dispatcher servlet 附带了许多组件的默认设置。这使您不必为基本行为做大量的配置,并且您可以根据需要覆盖和扩展配置。除了 dispatcher servlet 的默认配置之外,Spring MVC 也有一个默认配置。这可以通过使用org.springframework.web.servlet.config.annotation.EnableWebMvc
注释来启用(参见第二章中的“启用功能”一节)。
使用 Spring Boot 时,不需要添加@EnableWebMvc
,因为当 Spring Boot 在类路径上检测到 Spring MVC 时,它默认是启用的。
DispatcherServlet 属性
dispatcher servlet 有几个可以设置的属性。所有这些属性都有一个 setter 方法,并且都可以通过编程或包含 servlet 初始化参数来设置。表 4-2 列出并描述了 dispatcher servlet 上可用的属性。
表 4-2
DispatcherServlet 的属性
|
财产
|
默认
|
描述
|
| — | — | — |
| cleanupAfterInclude
| 真实的 | 指示是否在包含请求后清除请求属性。通常,缺省值就足够了,只有在特殊情况下才应该将该属性设置为 false。 |
| contextAttribute
| 空 | 存储此 servlet 的应用上下文。如果应用上下文是通过 servlet 本身之外的某种方式创建的,这将非常有用。 |
| contextClass
| org.springframework.web.context.support.XmlWebApplicationContext
| 配置 servlet 要构造的org.springframework.web.context.WebApplicationContext
的类型(它需要一个默认的构造函数)。使用给定的contextConfigLocation
进行配置。如果使用构造函数传入应用上下文,则不需要它。 |
| contextConfigLocation
| [servlet-name]-servlet.xml
| 指示指定应用上下文类的配置文件的位置。 |
| contextId
| 空 | 提供应用上下文 ID。例如,这在上下文被记录或发送到System.out
时使用。 |
| contextInitializerscontextInitializerClasses
| Null
| 使用可选的org.springframework.context.ApplicationContextInitializer
类为应用上下文执行一些初始化逻辑,比如激活某个概要文件。 |
| detectAllHandlerAdapters
| True
| 从应用上下文中检测所有的org.springframework.web.servlet.HandlerAdapter
实例。当设置为false
时,使用特殊名称handlerAdapter
检测单个信号。 |
| detectAllHandlerExceptionResolvers
| True
| 从应用上下文中检测所有的org.springframework.web.servlet.HandlerExceptionResolver
实例。当设置为false
时,使用特殊名称handlerExceptionResolver
检测单个信号。 |
| detectAllHandlerMappings
| True
| 从应用上下文中检测所有的org.springframework.web.servlet.HandlerMapping
bean。当设置为false
时,使用特殊名称handlerMapping
检测单个信号。 |
| detectAllViewResolvers
| True
| 从应用上下文中检测所有的org.springframework.web.servlet.ViewResolver
bean。当设置为false
时,使用特殊名称viewResolver
检测单个信号。 |
| dispatchOptionsRequest
| False
| 指示是否处理 HTTP 选项请求。默认为false
;当设置为true
时,还可以处理 HTTP OPTIONS 请求。 |
| dispatchTraceRequest
| False
| 指示是否处理 HTTP 跟踪请求。默认值为 false 当设置为true
时,还可以处理 HTTP 跟踪请求。 |
| environment
| org.springframework.web.context.support.StandardServletEnvironment
| 为这个 servlet 配置org.springframework.core.env.Environment
。环境指定哪个配置文件是活动的,并且可以保存特定于该环境的属性。 |
| 命名空间 | [servletname]-servlet
| 使用此命名空间来配置应用上下文。 |
| publishContext
| True
| 指示 servlet 的应用上下文是否被发布到javax.servlet.ServletContext
。对于生产,我们建议您将此设置为false
。 |
| publishEvents
| True
| 指示请求处理后是否触发org.springframework.web.context.support.ServletRequestHandledEvent
。您可以使用org.springframework.context.ApplicationListener
来接收这些事件。 |
| threadContextInheritable
| False
| 指示是否向从请求处理线程创建的子线程公开LocaleContext
和RequestAttributes
。 |
应用上下文
org.springframework.web.servlet.DispatcherServlet
需要org.springframework.web.context.WebApplicationContext
用需要的组件来配置自己。您可以让 servlet 自己构造一个,或者使用构造函数来传递应用上下文。在基于 XML 的配置文件中,使用第一个选项(因为无法构造应用上下文)。在基于 Java 的配置中,使用第二个选项。
在示例应用中,com.apress.prospringmvc.bookstore.web.BookstoreWebApplicationInitializer class
引导应用。要启用基于 Java 的配置,您需要指示 servlet 使用基于 Java 的应用上下文(默认情况下是基于 XML 的上下文),并向它传递配置类。您使用org.springframework.web.context.support.AnnotationConfigWebApplicationContext
类来设置应用和配置 servlet。清单 4-7 中的变更以粗体突出显示。
package com.apress.prospringmvc.bookstore.web;
// javax.servlet imports omitted.
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import com.apress.prospringmvc.bookstore.web.config.WebMvcContextConfiguration;
public class BookstoreWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(final ServletContext servletContext) throws ServletException {
registerDispatcherServlet(servletContext);
}
private void registerDispatcherServlet(final ServletContext servletContext) {
WebApplicationContext dispatcherContext = createContext(WebMvcContextConfiguration.class);
DispatcherServlet dispatcherServlet = new DispatcherServlet(dispatcherContext);
ServletRegistration.Dynamic dispatcher;
dispatcher = servletContext.addServlet("dispatcher", dispatcherServlet);
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}
private WebApplicationContext createContext(final Class<?>... annotatedClasses) {
AnnotationConfigWebApplicationContext
context = new AnnotationConfigWebApplicationContext();
context.register(annotatedClasses);
return context;
}
}
Listing 4-7The BookstoreWebApplicationInitializer with ApplicationContext
1234567891011121314151617181920212223242526272829303132333435363738
清单 4-7 展示了如何构造org.springframework.web.servlet.DispatcherServlet
并传递给它一个应用上下文。这是配置 servlet 的最基本的方式。
第二章封面人物简介。要选择一个概要文件,您可以包含一个 servlet 初始化参数(参见第二章);然而,为了更加动态,您可以使用org.springframework.context.ApplicationContextInitializer
。这种初始化器在加载所有 beans 之前初始化应用上下文。
当您想要配置或设置想要使用的配置文件时,这在 web 应用中非常有用(更多信息,请参见第二章)。例如,您可能需要设置一个自定义系统属性。或者,您可以通过读取文件系统上的某个文件或选择基于操作系统的配置文件来检测配置文件。你有几乎无限多的选择。
packag* org.cloudfoundry.reconfiguration.spring;
// Other imports omitted
import org.cloudfoundry.runtime.env.CloudEnvironment;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
public final class CloudApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
private static final Log logger = LogFactory.getLog(CloudApplicationContextInitializer.class);
private static final int DEFAULT_ORDER = 0;
private ConfigurableEnvironment springEnvironment;
private CloudEnvironment cloudFoundryEnvironment;
public CloudApplicationContextInitializer() {
cloudFoundryEnvironment = new CloudEnvironment();
}
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
if (!cloudFoundryEnvironment.isCloudFoundry()) {
logger.info("Not running on Cloud Foundry.");
return;
}
try {
logger.info("Initializing Spring Environment for Cloud Foundry");
springEnvironment = applicationContext.getEnvironment();
addPropertySource(buildPropertySource());
addActiveProfile("cloud");
} catch(Throwable t) {
// be safe
logger.error("Unexpected exception on initialization: " + t.getMessage**(), t);
}
}
// Other methods omitted
}
Listing 4-8The CloudApplicationContextInitializer
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
组件分辨率
当 servlet 被配置时,它从 servlet 容器接收一个初始化请求。当 servlet 初始化时,它使用逻辑来检测所需的组件(参见图 4-9 )。
图 4-9
DispatcherServlet 的组件发现
有些组件是通过类型来检测的,而有些是通过名称来检测的。对于类型可检测的组件,您可以指定(见表 4-2 )您不想这样做。在这种情况下,组件由一个众所周知的名称来检测。表 4-3 列出了请求处理中涉及的不同组件以及用于检测它的 bean 名称。该表还指示 dispatcher servlet 是否自动检测多个实例(如果可以禁用 yes,则按照表中指定的名称检测单个 bean)。
表 4-3
组件及其名称
|
成分
|
默认 Bean 名称
|
检测多个
|
| — | — | — |
| org.springframework.web.multipart.MultipartResolver
| multipartResolver
| 不 |
| org.springframework.web.servlet.LocaleResolver
| localeResolver
| 不 |
| org.springframework.web.servlet.ThemeResolver
| themeResolver
| 不 |
| org.springframework.web.servlet.HandlerMapping
| handlerMapping
| 是 |
| org.springframework.web.servlet.HandlerAdapter
| handlerAdapter
| 是 |
| org.springframework.web.servlet.HandlerExceptionResolver
| handlerExceptionResolver
| 是 |
| org.springframework.web.servlet.RequestToViewNameTranslator
| requestToViewNameTranslator
| 不 |
| org.springframework.web.servlet.ViewResolver
| viewResolver
| 是 |
| org.springframework.web.servlet.FlashMapManager
| flashMapManager
| 不 |
DispatcherServlet 的默认配置
您可能会对处理请求所涉及的所有组件感到有点不知所措。您甚至可能想知道是否需要显式地配置它们。幸运的是,Spring MVC 有一些合理的缺省值,在很多情况下,这些缺省值足够了——或者至少足够开始使用了。正如您在表 4-4 中看到的,dispatcher servlet 有一些默认设置。您可以在下一节找到关于不同实现的更多信息。
表 4-4
DispatcherServlet 的默认组件
|
成分
|
默认实施
|
| — | — |
| MultipartResolver
| 不需要默认的显式配置 |
| LocaleResolver
| org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
|
| ThemeResolver
| org.springframework.web.servlet.theme.FixedThemeResolver
|
| HandlerMapping
| org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping
、org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
、org.springframework.web.servlet.function.support.RouterFunctionMapping
|
| HandlerAdapter
| org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter
、org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter
、org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
、org.springframework.web.servlet.function.support.HandlerFunctionAdapter
|
| HandlerExceptionResolver
| org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver
、org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver
、org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
|
| RequestToViewNameTranslator
| org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
|
| ViewResolver
| org.springframework.web.servlet.view.InternalResourceViewResolver
|
| FlashMapManager
| org.springframework.web.servlet.support.SessionFlashMapManager
|
Spring Boot 违约
Spring Boot 继承了上一节提到的大部分默认配置。然而,它在某些部分确实有所不同。
Spring Boot 默认使能org.springframework.web.multipart.support.StandardServletMultipartResolver
。这可以通过声明自己的MultipartResolver
或将spring.servlet.multipart.enabled
属性设置为false
来禁用。spring.servlet.multipart
名称空间中的其他属性可以配置文件上传。
接下来,它向列表中添加了两个ViewResolver
。它增加了org.springframework.web.servlet.view.BeanNameViewResolver
和org.springframework.web.servlet.view.ContentNegotiatingViewResolver
。它仍然有InternalResourceViewResolver
,可以通过使用spring.mvc.view.prefix
和spring.mvc.view.suffix
属性对其进行部分配置。
Spring MVC 组件
在前面的章节中,您了解了请求处理工作流以及其中使用的组件。您还学习了如何配置org.springframework.web.servlet.DispatcherServlet
。在本节中,您将仔细查看请求处理工作流中涉及的所有组件。例如,您探索不同组件的 API,并查看 Spring 框架附带了哪些实现。
的配置
Handler mapping
决定将传入的请求分派给哪个处理程序。可以用来映射传入请求的标准是 URL 然而,实现(见图 4-10 )可以自由选择使用什么标准来确定映射。
org.springframework.web.servlet.
HandlerMapping
的 API 由一个方法组成(参见清单 4-9 )。这个方法被DispatcherServlet
调用来确定org.springframework.web.servlet.HandlerExecutionChain
。可以配置多个处理程序映射。servlet 依次调用不同的处理程序映射,直到其中一个不返回 null。
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
public interface HandlerMapping {
HandlerExecutionChain getHandler(HttpServletRequest request)
throws Exception;
}
Listing 4-9The HandlerMapping API
12345678910111213
图 4-10
HandlerMapping
实施
开箱即用,Spring MVC 提供了四种不同的实现。大多数都是基于 URL 映射的。其中一个实现提供了更复杂的映射策略,稍后您将了解到这一点。然而,在查看不同的实现之前,请仔细查看 URL,看看哪些部分是重要的。
请求 URL 由几个部分组成。我们来解剖一下 http://www.example.org/bookstore/app/home
这个网址。一个 URL 由四部分组成(见图 4-11 )。
图 4-11
URL 映射
服务器的主机名,由协议+ ://
+主机名或域名+ :
+端口组成
应用的名称(如果是根应用,则为 none)
servlet 映射的名称(在示例应用中,它被映射到/)
servlet 内部的路径
默认情况下,所有提供的处理程序映射实现都使用 servlet 内部相对于 servlet 上下文的路径(servlet 上下文相对路径)来解析处理程序。将alwaysUseFullPath
属性设置为 true 可以改变这种行为。然后包含 servlet 映射,这(对于手边的例子)导致 /app/home 解析请求处理程序;否则,使用 /home 。
所有实现共有的最后一个特性是可以配置默认的处理程序。这是通过设置defaultHandler
属性来完成的。当找不到传入请求的处理程序时,它总是被映射到默认处理程序。这是可选的,应该谨慎使用,尤其是在链接多个处理程序映射时。只有最后一个处理程序映射应该指定一个默认的处理程序,否则链会断开。
beannomeler 映射
org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping
实现是 dispatcher servlet 使用的默认策略之一。该实现将任何名称以/开头的 bean 视为潜在的请求处理程序。一个 bean 可以有多个名称,名称也可以包含一个通配符,用*表示。
这个实现使用 ant 样式的正则表达式将传入请求的 URL 与 bean 的名称进行匹配。它遵循这个算法。
尝试精确匹配;如果找到,退出。
在所有注册的路径中搜索匹配项;最具体的获胜。
如果没有找到匹配项,则返回映射到/*或默认处理程序(如果已配置)的处理程序。
bean 的名称不同于 ID。过去,它是由 XML 规范定义的,不能包含特殊字符,如/。这意味着您需要使用 bean 的名称。您可以通过在org.springframework.context.annotation.Bean
注释上设置 name 属性来提供 bean 的名称。一个 bean 可以有多个名字,名字可以写成 ant 风格的正则表达式。
清单 4-10 展示了如何使用 bean 名称并将其映射到/index.htm
URL。在示例应用中,您现在可以使用 http://localhost:8080/chapter 4-book store/index . htm 来调用这个控制器。
package com.apress.prospringmvc.bookstore.web.config;
import java.util.Properties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.apress.prospringmvc.bookstore.web.IndexController;
@Configuration
public class WebMvcContextConfiguration {
@Bean(name = { "/index.htm" })
public IndexController indexController() {
return new IndexController();
}
}
Listing 4-10The BeanNameUrlHandlerMapping
sample Configuration
1234567891011121314151617181920
SimpleUrlHandlerMapping
与org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping
相反,这种实现需要显式配置,并且它不会自动检测映射。清单 4-11 显示了一个示例配置。同样,将控制器映射到/index.htm。
package com.apress.prospringmvc.bookstore.web.config;
// Other imports omitted see Listing 4-10
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
@Configuration
public class WebMvcContextConfiguration {
@Bean
public IndexController indexController() {
return new IndexController();
}
@Bean
public HandlerMapping simpleUrlHandlerMapping() {
var mappings = new Properties();
mappings.put("/index.htm", "indexController");
var urlMapping = new SimpleUrlHandlerMapping();
urlMapping.setMappings(mappings);
return urlMapping;
}
}
Listing 4-11The SimpleUrlHandlerMapping
Sample Configuration
1234567891011121314151617181920212223242526272829
您需要显式配置SimpleUrlHandlerMapping
并向其传递映射(参见粗体代码)。您将/index.htm
URL 映射到名为 indexController 的控制器。如果您有很多控制器,这种配置会大大增加。这种方法的优点是所有的映射都在一个位置。
RequestMappingHandlerMapping
RequestMappingHandlerMapping
的实现更加复杂。它使用注释来配置映射。注释可以在类和/或方法级别。为了将com.apress.prospringmvc.bookstore.web.IndexController
映射到/index.htm
,您需要添加@RequestMapping
注释。清单 4-12 是控制器,清单 4-13 显示了示例配置。
package com.apress.prospringmvc.bookstore.web.config;
// Other imports omitted see Listing 4-10
@Configuration
public class WebMvcContextConfiguration {
@Bean
public IndexController indexController() {
return new IndexController();
}
}
Listing 4-13An annotation-based sample Configuration
123456789101112131415
package com.apress.prospringmvc.bookstore.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class IndexController {
@RequestMapping(value = "/index.htm")
public ModelAndView indexPage() {
return new ModelAndView("/WEB-INF/views/index.jsp");
}
}
Listing 4-12The IndexController with RequestMapping
1234567891011121314151617
RouterFunctionMapping
org.springframework.web.servlet.function.support.HandlerFunctionAdapter
实现是定义处理程序的函数方式。清单 4-14 展示了编写处理程序来呈现索引页面的函数风格。
package com.apress.prospringmvc.bookstore.web.config;
// Other imports omitted see Listing 4-10
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
@Configuration
public class WebMvcContextConfiguration {
@Bean
public RouterFunction<ServerResponse> routes() {
return route()
.GET("/", response -> ok().render("index"))
.build();
}
}
Listing 4-14A Functional-Style Sample Configuration
1234567891011121314151617181920
处理器适配器
org.springframework.web.servlet.
HandlerAdapter
是 dispatcher servlet 和所选 handler 之间的绑定器。它从 dispatcher servlet 中删除了实际的执行逻辑,这使得 dispatcher servlet 具有无限的可扩展性。将该组件视为 servlet 和实际处理程序实现之间的绑定器。清单 4-15 显示了HandlerAdapter
API。
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
long getLastModified(HttpServletRequest request, Object handler);
}
Listing 4-15The HandlerAdapter API
1234567891011121314
如清单 4-15 所示,API 由三个方法组成。dispatcher servlet 在上下文中的每个处理程序上调用supports
方法;这样做是为了确定哪个HandlerAdapter
可以执行所选的处理程序。如果处理程序适配器可以执行该处理程序,则调用handle
方法来执行所选的处理程序。处理程序的执行会导致org.springframework.web.servlet.ModelAndView
被返回。然而,一些实现总是返回null
,表明响应已经发送到客户端。
如果传入的请求是 GET 或 HEAD 请求,则调用getLastModified
方法来确定底层资源最后一次被修改的时间(–1 表示总是重新生成内容)。结果作为Last-Modified
请求头发送回客户端,并与If-Modified-Since
请求头进行比较。如果有修改,内容会重新生成并重新发送给客户端;否则,HTTP 响应代码 304(未修改)被发送回客户端。这在 dispatcher servlet 提供静态资源时特别有用,这样可以节省带宽。
开箱即用,Spring MVC 提供了 HandlerAdapter 的五个实现(见图 4-12 )。
图 4-12
HandlerAdapter 实现
HttpRequestHandlerAdapter
org.springframework.web.servlet.mvc.
HttpRequestHandlerAdapter
知道如何执行org.springframework.web.HttpRequestHandler
实例。Spring Remoting 主要使用这个处理程序适配器来支持一些 HTTP remoting 选项。然而,您也可以使用org.springframework.web.HttpRequestHandler
接口的两个实现。一个服务静态资源,另一个将传入的请求转发给 servlet 容器的默认 servlet(更多信息见第五章)。
SimpleControllerHandlerAdapter
org.springframework.web.servlet.mvc.
SimpleControllerHandlerAdapter
知道如何执行org.springframework.web.servlet.mvc.Controller
实现。它从控制器实例的handleRequest
方法中返回org.springframework.web.servlet.ModelAndView
。
simplieservlethandleradapter
在应用上下文中配置javax.servlet.Servlet
实例并把它们放在 dispatcher servlet 后面会很方便。要执行这些 servlets,您需要org.springframework.web.servlet.handler.
SimpleServletHandlerAdapter
。它知道如何执行javax.servlet.Servlet
,并且总是返回null
,因为它期望 servlet 自己处理响应。
HandlerFunctionAdapter
org.springframework.web.servlet.function.support.
HandlerFunctionAdapter
知道如何执行org.springframework.web.servlet.function.HandlerFunction
实例。它根据 h andler function
的org.springframework.web.servlet.function.ServerResponse
返回org.springframework.web.servlet.ModelAndView
。
requestmappingchandleradapter
org.springframework.web.servlet.mvc.method.annotation.
RequestMappingHandlerAdapter
执行用org.springframework.web.bind.annotation.RequestMapping
标注的方法。它转换方法参数并提供对请求参数的简单访问。方法的返回值被转换或添加到这个处理程序适配器内部创建的org.springframework.web.servlet.ModelAndView
实现中。整个绑定和转换过程是可配置的、灵活的;在第 5 和 6 章节中解释了这些可能性。
多重解析器
org.springframework.web.multipart.
MultipartResolver
策略接口确定传入请求是否是多部分文件请求(用于文件上传),如果是,它将传入请求包装在org.springframework.web.multipart.MultipartHttpServletRequest
中。包装后的请求可以轻松地从表单访问底层的多部分文件。文件上传在第七章中说明。清单 4-16 显示了MultipartResolver
API。
package org.springframework.web.multipart;
import javax.servlet.http.HttpServletRequest;
public interface MultipartResolver {
boolean isMultipart(HttpServletRequest request);
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request)
throws MultipartException;
void cleanupMultipart(MultipartHttpServletRequest request);
}
Listing 4-16The MultipartResolver API
123456789101112131415161718
在准备和清理请求的过程中,会调用org.springframework.web.multipart.MultipartResolver
组件’s
的三个方法。调用isMultipart
方法来确定一个传入的请求是否是一个多部分请求。如果是,那么调用resolveMultipart
方法,将原始请求包装在MultipartHttpServletRequest
中。最后,当请求被处理后,调用cleanupMultipart
方法来清理所有被使用的资源。图 4-13 显示了MultipartResolver
的两种现成实现。
图 4-13
多解析器实现
CommonsMultipartResolver
org.springframework.web.multipart.commons.
CommonsMultipartResolver
使用 Commons FileUpload 库 1 来处理多部分文件。它可以轻松配置 Commons FileUpload 库的几个方面。
StandardServletMultipartResolver
Servlet 3.0 规范引入了处理多部分表单的标准方式。org.springframework.web.multipart.support.
StandardServletMultipartResolver
仅仅作为这个标准方法的包装器,所以它是透明公开的。
LocaleResolver
org.springframework.web.servlet.
LocaleResolver
策略接口决定哪个java.util.Locale
渲染页面。在大多数情况下,它解析应用中的验证消息或标签。不同的实现如图 4-14 所示,并在以下小节中描述。
图 4-14
LocaleResolver 实现
清单 4-17 显示了 org . spring framework . web . servlet . locale solver 的 API。
package org.springframework.web.servlet;
import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface LocaleResolver {
Locale resolveLocale(HttpServletRequest request);
void setLocale(HttpServletRequest request,HttpServletResponse response,
Locale locale);
}
Listing 4-17The LocaleResolver API
123456789101112131415
API 由两个方法组成,每个方法都在存储和检索当前的java.util.Locale
中发挥作用。当您想要更改当前的语言环境时,会调用setLocale
方法。如果实现不支持这一点,就会抛出java.lang.UnsupportedOperationException
。Spring Framework uses the resolveLocale method
——通常在内部——解析当前的语言环境。
AcceptHeaderLocaleResolver
org.springframework.web.servlet.i18n.
AcceptHeaderLocaleResolver 实现简单地委托给当前javax.servlet.HttpServletRequest
的getLocale
方法。它使用Accept-Language
HTTP 头来确定语言。客户端设置此头值;此解析程序不支持更改区域设置。
库克埃勒索尔弗
org.springframework.web.servlet.i18n.CookieLocaleResolver
实现使用javax.servlet.http.Cookie
来存储要使用的语言环境。这在您希望应用尽可能无状态的情况下特别有用。实际值存储在客户端,并在每次请求时发送给您。这个解析器允许更改区域设置(你可以在第六章找到更多信息)。这个解析器还允许您配置 cookie 的名称和要使用的默认区域设置。如果不能为当前请求确定任何值(即,既没有 cookie 也没有默认的区域设置),这个解析器就退回到请求的区域设置(见AcceptHeaderLocaleResolver
)。
FixedLocaleResolver
org.springframework.web.servlet.i18n.
FixedLocaleResolver
是org.springframework.web.servlet.LocaleResolver
的最基本实现。它允许您配置在整个应用中使用的区域设置。这种配置是固定的;因此,这是无法改变的。
SessionLocaleResolver
org.springframework.web.servlet.i18n.SessionLocaleResolver
实现使用javax.servlet.http.HttpSession
来存储区域设置的值。可以配置属性的名称以及默认的语言环境。如果不能为当前请求确定任何值(即,既没有值存储在会话中,也没有默认的区域设置),那么它将返回到请求的区域设置(见AcceptHeaderLocaleResolver
)。这个解析器还允许你改变区域设置(更多信息见第六章)。
主题解析器
org.springframework.web.servlet.
ThemeResolver
策略界面决定页面呈现哪个主题。有几种实现方式;这些如图 4-15 所示,并在以下小节中解释。如何应用主题在第八章中有解释。如果没有主题名称可以解析,那么这个解析器使用硬编码的默认主题。
图 4-15
ThemeResolver
实施
清单 4-18 显示了org.springframework.web.servlet.ThemeResolver
的 API,它类似于org.springframework.web.servlet.LocaleResolver
API。
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface ThemeResolver {
String resolveThemeName(HttpServletRequest request);
void setThemeName(HttpServletRequest request, HttpServletResponse response,
String themeName);
}
Listing 4-18The ThemeResolver API
1234567891011121314
当你想改变当前的主题时,调用setThemeName
方法。如果不支持改变主题,它抛出java.lang.UnsupportedOperationException
。Spring 框架在需要解析当前主题时会调用resolveThemeName
方法。这主要是通过使用主题 JSP 标签来完成的。
CookieThemeResolver
org.springframework.web.servlet.theme.
CookieThemeResolver
使用javax.servlet.http.Cookie
来存储要使用的主题。这在您希望应用尽可能无状态的情况下特别有用。实际值存储在客户端,并在每次请求时发送给您。此解析程序允许更改主题;你可以在第 6 和 8 章节中找到更多相关信息。这个解析器还允许您配置 cookie 的名称和要使用的主题区域设置。
FixedThemeResolver
org.springframework.web.servlet.theme.
FixedThemeResolver
是org.springframework.web.servlet.ThemeResolver
的最基本实现。它允许你配置一个在整个应用中使用的主题。这种配置是固定的;因此,这是无法改变的。
SessionThemeResolver
org.springframework.web.servlet.theme.
SessionThemeResolver
使用javax.servlet.http.HttpSession
存储主题的值。可以配置属性的名称和默认主题。
处理器异常解析器
在大多数情况下,您希望控制如何处理请求处理过程中发生的异常。您可以为此使用一个HandlerExceptionResolver
。API(参见清单 4-19 )由一个方法组成,这个方法在由 dispatcher servlet 检测到的org.springframework.web.servlet.
HandlerExceptionResolvers
上被调用。解析器可以选择自己处理异常,或者返回一个包含要呈现的视图和模型的org.springframework.web.servlet.ModelAndView implementation
(通常包含抛出的异常)。
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface HandlerExceptionResolver {
ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler,
Exception ex);
}
Listing 4-19The HandlerExceptionResolver API
1234567891011121314
图 4-16 显示了 Spring 框架提供的不同实现。每个都以稍微不同的方式工作,就像每个都有不同的配置一样(更多信息见第六章)。
图 4-16
HandlerExceptionResolver 实现
org.springframework.web.servlet.handler.HandlerExceptionResolverComposite
实现由 Spring MVC 内部使用。它将几个org.springframework.web.servlet.HandlerExceptionResolver
实现链接在一起。此解析程序不提供实际的实现或附加功能;相反,它仅仅充当多个实现的包装器(当配置了多个实现时)。
RequestToViewNameTranslator
当处理程序没有返回视图实现或视图名称,并且没有向客户端发送响应本身时,那么org.springframework.web.servlet.
RequestToViewNameTranslator
试图从传入的请求中确定视图名称。默认的实现(见图 4-17)org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
只是简单地获取 URL,去掉后缀和上下文路径,然后使用剩余部分作为视图名(即http://localhost:8080/bookstore/admin/index.html
变成了admin/index
)。你可以在第八章找到更多关于视图的信息。
图 4-17
RequstToViewNameTranslator 层次结构
清单 4-20 中显示了RequestToViewNameTranslator
API。
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
public interface RequestToViewNameTranslator {
String getViewName(HttpServletRequest request) throws Exception;
}
Listing 4-20The RequestToViewNameTranslator API
123456789101112
视图解析器
Spring MVC 提供了非常灵活的视图解析机制。它只是获取从处理程序返回的视图名称,并尝试将其解析为实际的视图实现(如果没有返回具体的org.springframework.web.servlet.View
)。实际的实现可以是 JSP,但也可以是 Excel 电子表格或 PDF 文件。有关视图解析的更多信息,请参阅第八章。
这个 API(参见清单 4-21 )非常简单,由一个方法组成。该方法采用视图名称和当前选择的区域设置(参见LocaleResolver
)。这可以解析一个实际的视图实现。当配置了多个org.springframework.web.servlet.ViewResolvers
时,dispatcher servlet 依次调用它们,直到其中一个返回一个视图进行渲染。
package org.springframework.web.servlet;
import java.util.Locale;
public interface ViewResolver {
View resolveViewName(String viewName, Locale locale) throws Exception;
}
Listing 4-21The ViewResolver API
123456789101112
ViewResolver
的实现如图 4-18 所示。开箱即用,Spring 提供了几个实现(更多信息参见第八章)。
图 4-18
ViewResolver 实现
FlashMapManager
org.springframework.web.servlet.
FlashMapManager
在 Spring MVC 应用中启用 flash“作用域”。您可以使用这种机制将属性放在一个 flash map 中,然后在重定向后检索这些属性(flash map 在请求/响应周期后仍然存在)。渲染视图后,会清除 flash 贴图。Spring 提供了一个单一的实现,org.springframework.web.servlet.support.SessionFlashMapManager
(参见图 4-19 )。
图 4-19
FlashMapManager 层次结构
清单 4-22 显示了FlashMapManager
API。
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public interface FlashMapManager {
FlashMap retrieveAndUpdate(HttpServletRequest request,
HttpServletResponse response);
void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request,
HttpServletResponse response);
}
Listing 4-22The FlashMapManager API
1234567891011121314151617
摘要
本章从查看请求处理工作流开始,确定哪些组件起作用。可以认为DispatcherServlet
是 Spring MVC 中的主要组件。它扮演着最重要的角色——前端控制器。Spring MVC 中的 MVC 模式是显式的;您有一个模型、一个视图和一个控制器(处理程序)。控制器处理请求,填充模型,并选择要呈现的视图。
在处理请求时,DispatcherServlet
使用许多不同的组件来扮演它的角色。最重要的部件是HandlerMapping
和HandlerAdapter
;这些组件分别是用于映射和处理请求的核心组件。要应用横切关注点,可以使用HandlerInterceptor
。处理完请求后,需要呈现一个视图。一个处理程序可以返回一个View
或者一个要渲染的视图的名称。在后一种情况下,这个名称被传递给一个ViewResolver
来解析一个实际的视图实现。
还有对 flash 范围的变量的基本支持。要让这成为可能,就有FlashMapManager
。有时,请求处理不会按照您希望的方式进行。例如,您可能会遇到异常。要处理这些,您可以使用HandlerExceptionResolver
。最后起作用的组件是LocaleResolver
和ThemeResolver
。总之,这些支持应用中的国际化和主题化。
接下来的章节将解释如何构建控制器来处理请求,并进一步研究如何通过 Spring Boot 来配置 Spring MVC。
Footnotes 1
https://commons.apache.org/fileupload/