比价网站怎么做的,工商局企业信息查询系统官网,网站seo优化服务,女教师遭网课入侵视频大全集原文#xff1a;Pro Spring MVC with WebFlux 协议#xff1a;CC BY-NC-SA 4.0 一、建立本地开发环境
Spring 于 2002 年 10 月发布#xff0c;是一个使用 Java 开发的开源框架和控制反转(IoC)容器#xff0c;它是为 Java 平台而构建的。它已经从一个小型的库集合转变为一… 原文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%\bin添加到系统的常规路径中(Windows 用户使用))。这背后的原因是为了确保用 Java 编写的任何其他开发应用都使用这个版本的 Java并防止开发过程中出现奇怪的不兼容错误。如果您想从终端运行构建您肯定使用了预期的 Java 版本。
重新启动终端并通过打开终端(Windows 中的命令提示符或 macOS 和 Linux 上安装的任何类型的终端)并键入以下命令验证操作系统看到的 Java 版本是否是您安装的版本。 java -version # to check the runtime
然后是下面。 javac -version # to check the compiler
您应该会看到类似下面的输出。 java -version
java version 14.0.2 2020-07-14
Java(TM) SE Runtime Environment (build 14.0.212-46)
Java HotSpot(TM) 64-Bit Server VM (build 14.0.212-46, mixed mode, sharing) javac -version
javac 14.0.2
安装 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%\bin)添加到系统的常规路径中以便您可以在终端中构建项目。
Gradle 被选为本书源代码的构建工具因为它设置简单配置文件小定义执行任务灵活Spring 团队目前使用它来构建所有的 Spring 项目。
要验证操作系统是否看到您刚刚安装的 Gradle 版本请打开一个终端(Windows 中的命令提示符以及安装在 macOS 和 Linux 上的任何类型的终端)并键入
gradle -version
您应该会看到类似下面的内容。
gradle -version------------------------------------------------------------
Gradle 6.7
------------------------------------------------------------Build time: 2020-08-04 22:01:06 UTC
Revision: 00a2948da9ea69c523b6b094331593e6be6d92bcKotlin: 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.212-46)
OS: Mac OS X 10.15.6 x86_64
运行这个命令还可以验证 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-dateListing 1-1Building the Project for This Book
一旦在终端中构建了项目您就可以验证您拥有正确的项目和正确的工具。现在是时候在 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 (Wrox2002)为专家一对一 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 (Apress2017) 3 或 Spring 5 Recipes4thEdition(Apress2017) 4 。
除了 Spring 框架复习之外我们还将触及 Spring Boot 的基础知识。关于 Spring Boot 更深入的信息我们建议优秀的 Spring Boot 参考指南 5 或 Spring Boot 2 食谱 (Apress2018) 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 访问支持 | | 同 JavaMessageServiceJava 消息服务 | 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();Overridepublic 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
从清单 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
前面的代码不是特别干净例如想象来自不同上下文的多个依赖项。代码会很快变得混乱越来越难以进行单元测试。
为了解决对象构造器中的构造/查找逻辑我们可以使用依赖注入。我们只是传递给对象完成工作所需的依赖关系。这使得我们的代码干净、解耦并且易于测试(参见清单 2-3 )。依赖注入是一个过程其中对象指定它们所使用的依赖。IoC 容器使用该规范当它构造一个对象时它也注入它的依赖项。这样我们的代码更干净我们不再用构造逻辑来增加类的负担。维护更容易进行单元和/或集成测试也更容易。测试更容易因为我们可以注入一个存根或模拟对象来验证我们的对象的行为。
package com.apress.prospringmvc.moneytransfer.constructor;// import statements ommittedpublic 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
顾名思义基于构造函数的依赖注入使用构造函数来注入对象中的依赖。清单 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 ommittedpublic 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
最后还有使用注释的基于字段的依赖注入(参见清单 2-5 )。我们不需要指定构造函数参数或 setter 方法来设置依赖关系。我们首先定义一个可以保存依赖关系的类级字段。接下来我们在该字段上添加了一个注释以表达我们将该依赖项注入到对象中的意图。Spring 接受几种不同的注释:Autowired、Resource和Inject。所有这些注释或多或少都以相同的方式工作。深入解释这些注释之间的差异不在本书的范围内所以如果你想了解更多我们建议使用 Spring Boot 参考指南或 Pro Spring 5 (Apress2017)。主要区别在于Autowired注释来自 Spring 框架而Resource和Inject是 Java 标准注释。
package com.apress.prospringmvc.moneytransfer.annotation;import org.springframework.beans.factory.annotation.Autowired;//other imports omittedpublic class MoneyTransferServiceImpl implements MoneyTransferService {Autowiredprivate AccountRepository accountRepository;Autowiredprivate TransactionRepository transactionRepository;//transfer method omitted, same as listing 2.1}Listing 2-5A MoneyTransferService Implementation with Field-Based Dependency Injection
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 {Beanpublic AccountRepository accountRepository() {return new MapBasedAccountRepository();}Beanpublic TransactionRepository transactionRepository() {return new MapBasedTransactionRepository();}Beanpublic MoneyTransferService moneyTransferService() {return new MoneyTransferServiceImpl();}
}Listing 2-6The ApplicationContextConfiguration Configuration File
配置类可以是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
最后请注意应用上下文可以在一个层次结构中。我们可以有一个应用上下文作为另一个上下文的父上下文(见图 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
看一下清单 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 {Beanpublic AccountRepository accountRepository() {return new MapBasedAccountRepository();}Beanpublic MoneyTransferService moneyTransferService() {return new MoneyTransferServiceImpl();}ConfigurationProfile(value test)public static class TestContextConfiguration {Beanpublic TransactionRepository transactionRepository() {return new StubTransactionRepository();}}ConfigurationProfile(value local)public static class LocalContextConfiguration {Beanpublic TransactionRepository transactionRepository() {return new MapBasedTransactionRepository();}}
}Listing 2-9ApplicationContextConfiguration with Profiles
清单 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
你可能想知道为什么我们应该使用概要文件。一个原因是它允许灵活的配置。这意味着我们的整个配置都在版本控制之下并且在相同的源代码中而不是分散在不同的服务器、工作站等等上。当然我们仍然可以加载包含一些属性(如用户名和密码)的附加文件。如果公司的安全策略不允许我们将这些属性置于版本控制之下那么这可能是有用的。当我们讨论测试和部署到云时我们广泛地使用概要文件因为这两个任务需要不同的数据源配置。
启用功能
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 {Overridepublic 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
让我们通过添加一个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-11public class BookstoreWebApplicationInitializer implements WebApplicationInitializer {Overridepublic void onStartup(final ServletContext servletContext) throws ServletException {registerListener(servletContext);registerDispatcherServlet(servletContext);}// registerDispatcherServlet method ommitted see Listing 2-11
// createContext method omitted see Listing 2-11private void registerListener(final ServletContext servletContext) {AnnotationConfigWebApplicationContext rootContext createContext(InfrastructureContextConfiguration.class);servletContext.addListener(new ContextLoaderListener(rootContext));}
}Listing 2-12The Modifcationfor the BookstoreWebApplicationInitializer Class
清单 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 {Beanpublic LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {LocalContainerEntityManagerFactoryBean emfb new LocalContainerEntityManagerFactoryBean();emfb.setDataSource(dataSource);emfb.setJpaVendorAdapter(jpaVendorAdapter());return emfb;}Beanpublic JpaVendorAdapter jpaVendorAdapter() {return new HibernateJpaVendorAdapter();}Beanpublic PlatformTransactionManager transactionManager(EntityManagerFactory emf) {return new JpaTransactionManager(emf);}Beanpublic DataSource dataSource() {return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();}
}Listing 2-13The InfrastructureContextConfiguration Source File
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);}Overrideprotected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {return builder.sources(BookstoreApplication.class);}
}Listing 2-14The BookstoreApplication
BookstoreApplication类有SpringBootApplication,可以自动配置检测到的特性和第三方库。在这种情况下它扩展了SpringBootServletInitializer,因为应用被打包成一个 WAR 并部署到一个容器中。Spring Boot 没有编写我们自己的WebApplicationInitializer而是提供了一个现成的。它在一个经典容器中实现了大多数 Spring Boot 功能。
配置属性可以在一个application.properties或application.yml文件中给出(见清单 2-15 )以配置缺省值不适用时所需的特性。有关最常见功能的列表请查看 Spring Boot 参考指南的附录 A10。
server.port8080 # 8080 is also the default servlet port
spring.application.namebookstoreListing 2-15application.properties
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)在设计上是无状态的所以保持一个模型是很困难的。对于 WebMVC 模式被实现为模型 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 设计和开发 (Wrox2002)对应用的分布和扩展进行了很好的讨论。
图 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
这个清单被认为是粗粒度的因为它需要从客户端调用一个简单的方法来完成一个用例。这与清单 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
如果可能的话我们不应该调用一系列方法来执行一个系统函数。我们应该尽可能地屏蔽用户的数据访问和 POJO 交互。在理想情况下粗粒度函数应该代表一个成功或失败的工作单元。用户可以使用不同的客户端(例如网络应用、网络服务或桌面应用)然而这些客户端应该执行相同的业务逻辑。因此服务层应该是我们实际系统(即业务逻辑)的单一入口点。
在服务层使用单一入口点和粗粒度方法的额外好处是我们可以在这一层简单地应用事务和安全性。我们不必让应用的不同客户端承担安全和事务性需求。它现在是系统核心的一部分一般通过 AOP 来应用。
在基于 web 的环境中我们可能有多个用户同时操作服务。服务必须是无状态的因此将服务设为单例是一个好的做法。在领域模型中应该尽可能地保留状态。保持服务层的无状态提供了一个额外的好处:它还使得服务层是线程安全的。
将服务层保持在单个入口点保持层的无状态并在该层上应用事务和安全性这使得 Spring 框架的其他特性能够将服务层公开给不同的客户端。例如我们可以使用配置轻松地通过 RMI 或 JMS 公开我们的服务层。有关 Spring Framework 远程支持的更多信息我们建议使用 Pro Spring 5 (Apress2017)或在线 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 {ListBook findBooksByCategory(Category category);Book findBook(long id);Order findOrder(long id);ListBook findRandomBooks();ListOrder findOrdersForAccount(Account account);Order store(Order order);ListBook findBooks(BookSearchCriteria bookSearchCriteria);Order createOrder(Cart cart, Account account);ListCategory findAllCategories();}Listing 3-3The BookstoreService Interface
如清单 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 (Apress2017)或在线 Spring 框架文档。
清单 3-4 展示了数据访问对象或存储库的外观。注意该接口没有引用或提及我们使用的任何数据访问技术(我们在示例应用中使用 JPA)。此外服务层不关心数据是如何持久存储的也不关心数据在哪里持久存储它只是想知道如何存储或检索它。
package com.apress.prospringmvc.bookstore.repository;import com.apress.prospringmvc.bookstore.domain.Account;public interface AccountRepository extends CrudRepositoryAccount, Long {Account findByUsername(String username);}Listing 3-4A Sample AccountRepository using Spring Data
更多通往罗马的道路
这里讨论的架构并不是唯一的应用架构。哪种架构最适合给定的应用取决于应用的大小、开发团队的经验以及应用的生命周期。团队越大或者应用存在的时间越长具有独立层的干净架构就变得越重要。
从单个静态页面开始的 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.ViewResolverbean 来解析对实际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/javaeexmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsdversion4.0 metadata-completetrueservletservlet-namebookstore/servlet-nameservlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-classload-on-startup1/load-on-startup/servletservlet-mappingservlet-namebookstore/servlet-nameurl-pattern//url-pattern/servlet-mapping/web-appListing 4-1The web.xml Configuration (Servlet 4.0)
默认情况下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/javaeexmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-fragment_4_0.xsdversion4.0 metadata-completetrueservletservlet-namebookstore/servlet-nameservlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-classload-on-startup1/load-on-startup/servletservlet-mappingservlet-namebookstore/servlet-nameurl-pattern/*/url-pattern/servlet-mapping/web-fragmentListing 4-2The web-fragment.xml Configuration (Servlet 4.0)
使用 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 BookstoreServletContainerInitializerimplements ServletContainerInitializer {Overridepublic void onStartup(SetClass? 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
使用 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 omittedimport org.springframework.web.WebApplicationInitializer;
import org.springframework.web.servlet.DispatcherServlet;public class BookstoreWebApplicationInitializerimplements WebApplicationInitializer {Overridepublic void onStartup(ServletContext servletContext)throws ServletException {ServletRegistration.Dynamic registrationregistration servletContext.addServlet(dispatcher, DispatcherServlet.class);registration.addMapping(/);registration.setLoadOnStartup(1);}
}Listing 4-4The WebApplicationInitializer Configuration
使用这个特性会影响应用的启动时间首先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
在经典的战争应用中使用 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);}Overrideprotected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {return builder.sources(BookstoreApplication.class);}
}Listing 4-6BookstoreApplication Using Spring Boot in a WAR
配置 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.HandlerMappingbean。当设置为false时使用特殊名称handlerMapping检测单个信号。 | | detectAllViewResolvers | True | 从应用上下文中检测所有的org.springframework.web.servlet.ViewResolverbean。当设置为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 {Overridepublic 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) {AnnotationConfigWebApplicationContextcontext new AnnotationConfigWebApplicationContext();context.register(annotatedClasses);return context;}
}Listing 4-7The BookstoreWebApplicationInitializer with ApplicationContext
清单 4-7 展示了如何构造org.springframework.web.servlet.DispatcherServlet并传递给它一个应用上下文。这是配置 servlet 的最基本的方式。
第二章封面人物简介。要选择一个概要文件您可以包含一个 servlet 初始化参数(参见第二章)然而为了更加动态您可以使用org.springframework.context.ApplicationContextInitializer。这种初始化器在加载所有 beans 之前初始化应用上下文。
当您想要配置或设置想要使用的配置文件时这在 web 应用中非常有用(更多信息请参见第二章)。例如您可能需要设置一个自定义系统属性。或者您可以通过读取文件系统上的某个文件或选择基于操作系统的配置文件来检测配置文件。你有几乎无限多的选择。
packag* org.cloudfoundry.reconfiguration.spring;// Other imports omittedimport 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 ApplicationContextInitializerConfigurableApplicationContext, 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();}Overridepublic 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 safelogger.error(Unexpected exception on initialization: t.getMessage**(), t);}}// Other methods omitted
}Listing 4-8The CloudApplicationContextInitializer
组件分辨率
当 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 图 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 BeanNameUrlHandlerMappingsample Configuration
SimpleUrlHandlerMapping
与org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping相反这种实现需要显式配置并且它不会自动检测映射。清单 4-11 显示了一个示例配置。同样将控制器映射到/index.htm。
package com.apress.prospringmvc.bookstore.web.config;// Other imports omitted see Listing 4-10import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;Configuration
public class WebMvcContextConfiguration {Beanpublic IndexController indexController() {return new IndexController();}Beanpublic HandlerMapping simpleUrlHandlerMapping() {var mappings new Properties();mappings.put(/index.htm, indexController);var urlMapping new SimpleUrlHandlerMapping();urlMapping.setMappings(mappings);return urlMapping;}
}Listing 4-11The SimpleUrlHandlerMappingSample Configuration
您需要显式配置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-10Configuration
public class WebMvcContextConfiguration {Beanpublic IndexController indexController() {return new IndexController();}
}Listing 4-13An annotation-based sample Configuration
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
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 {Beanpublic RouterFunctionServerResponse routes() {return route().GET(/, response - ok().render(index)).build();}
}Listing 4-14A Functional-Style Sample Configuration
处理器适配器
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
如清单 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
在准备和清理请求的过程中会调用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
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
当你想改变当前的主题时调用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
图 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
视图解析器
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
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
摘要
本章从查看请求处理工作流开始确定哪些组件起作用。可以认为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/