精通-Spring5-全-

精通 Spring5(全)

原文:zh.annas-archive.org/md5/73290E1F786F5BAA832E07A902070E3F

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Spring 5.0 即将推出,将带来许多新的令人兴奋的功能,将改变我们迄今为止使用该框架的方式。本书将向您展示这一演变——从解决可测试应用程序的问题到在云端构建分布式应用程序。

本书以介绍 Spring 5.0 的新功能开始,并向您展示如何使用 Spring MVC 构建应用程序。然后,您将深入了解如何使用 Spring Framework 构建和扩展微服务。您还将了解如何构建和部署云应用程序。您将意识到应用程序架构是如何从单体架构演变为围绕微服务构建的。还将涵盖 Spring Boot 的高级功能,并通过强大的示例展示。

通过本书,您将掌握使用 Spring Framework 开发应用程序的知识和最佳实践。

本书涵盖内容

第一章《Evolution to Spring Framework 5.0》带您了解 Spring Framework 的演变,从最初的版本到 Spring 5.0。最初,Spring 被用来使用依赖注入和核心模块开发可测试的应用程序。最近的 Spring 项目,如 Spring Boot、Spring Cloud、Spring Cloud Data Flow,涉及应用程序基础设施和将应用程序迁移到云端。我们将概述不同的 Spring 模块和项目。

第二章《Dependency Injection》深入探讨了依赖注入。我们将看看 Spring 中可用的不同类型的依赖注入方法,以及自动装配如何简化您的生活。我们还将快速了解单元测试。

第三章《使用 Spring MVC 构建 Web 应用程序》快速概述了使用 Spring MVC 构建 Web 应用程序。

第四章《演变为微服务和云原生应用程序》解释了过去十年应用程序架构的演变。我们将了解为什么需要微服务和云原生应用程序,并快速概述帮助我们构建云原生应用程序的不同 Spring 项目。

第五章《使用 Spring Boot 构建微服务》讨论了 Spring Boot 如何简化创建生产级 Spring 应用程序的复杂性。它使得使用基于 Spring 的项目变得更加容易,并提供了与第三方库的轻松集成。在本章中,我们将带领学生一起使用 Spring Boot。我们将从实现基本的 Web 服务开始,然后逐步添加缓存、异常处理、HATEOAS 和国际化,同时利用 Spring Framework 的不同功能。

第六章《扩展微服务》专注于为我们在第四章中构建的微服务添加更多高级功能。

第七章《Spring Boot 高级功能》介绍了 Spring Boot 的高级功能。您将学习如何使用 Spring Boot Actuator 监视微服务。然后,您将把微服务部署到云端。您还将学习如何使用 Spring Boot 提供的开发者工具更有效地开发。

第八章《Spring Data》讨论了 Spring Data 模块。我们将开发简单的应用程序,将 Spring 与 JPA 和大数据技术集成在一起。

第九章《Spring Cloud》讨论了云中的分布式系统存在的常见问题,包括配置管理、服务发现、断路器和智能路由。在本章中,您将了解 Spring Cloud 如何帮助您为这些常见模式开发解决方案。这些解决方案应该在云端和开发人员的本地系统上都能很好地运行。

第十章《Spring Cloud 数据流》讨论了 Spring Cloud 数据流,它提供了一系列关于基于微服务的分布式流式处理和批处理数据管道的模式和最佳实践。在本章中,我们将了解 Spring Cloud 数据流的基础知识,并使用它构建基本的数据流使用案例。

第十一章《响应式编程》探讨了使用异步数据流进行编程。在本章中,我们将了解响应式编程,并快速了解 Spring Framework 提供的功能。

第十二章《Spring 最佳实践》帮助您了解与单元测试、集成测试、维护 Spring 配置等相关的 Spring 企业应用程序开发的最佳实践。

第十三章《在 Spring 中使用 Kotlin》向您介绍了一种快速流行的 JVM 语言——Kotlin。我们将讨论如何在 Eclipse 中设置 Kotlin 项目。我们将使用 Kotlin 创建一个新的 Spring Boot 项目,并实现一些基本的服务,并进行单元测试和集成测试。

本书所需内容

为了能够运行本书中的示例,您需要以下工具:

  • Java 8

  • Eclipse IDE

  • Postman

我们将使用嵌入到 Eclipse IDE 中的 Maven 来下载所有需要的依赖项。

本书适合对象

本书适用于有经验的 Java 开发人员,他们了解 Spring 的基础知识,并希望学习如何使用 Spring Boot 构建应用程序并将其部署到云端。

惯例

在本书中,您会发现一些区分不同类型信息的文本样式。以下是一些这些样式的示例及其含义解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL 和用户输入显示如下:"在您的pom.xml文件中配置spring-boot-starter-parent"。

代码块设置如下:

<properties>
  <mockito.version>1.10.20</mockito.version>
</properties>

任何命令行输入或输出都以以下方式编写:

mvn clean install

新术语重要词汇以粗体显示。您在屏幕上看到的词语,例如菜单或对话框中的词语,会在文本中以这种方式出现:"提供详细信息并单击生成项目"。

警告或重要说明会出现在这样的框中。

提示和技巧会以这种方式出现。

第一章:Spring Framework 5.0 的演变

Spring Framework 1.0 的第一个版本于 2004 年 3 月发布。在十五年多的时间里,Spring Framework 一直是构建 Java 应用程序的首选框架。

在 Java 框架相对年轻和动态的世界中,十年是很长的时间。

在本章中,我们将从理解 Spring Framework 的核心特性开始。我们将看看 Spring Framework 为什么变得受欢迎以及它如何适应以保持首选框架。在快速了解 Spring Framework 中的重要模块之后,我们将进入 Spring 项目的世界。我们将通过查看 Spring Framework 5.0 中的新功能来结束本章。

本章将回答以下问题:

  • Spring Framework 为什么受欢迎?

  • Spring Framework 如何适应应用程序架构的演变?

  • Spring Framework 中的重要模块是什么?

  • Spring Framework 在 Spring 项目的伞下适用于哪些方面?

  • Spring Framework 5.0 中的新功能是什么?

Spring Framework

Spring 网站(projects.spring.io/spring-framework/)对 Spring Framework 的定义如下:Spring Framework 为现代基于 Java 的企业应用程序提供了全面的编程和配置模型

Spring Framework 用于连接企业 Java 应用程序。Spring Framework 的主要目标是处理连接应用程序不同部分所需的所有技术细节。这使程序员可以专注于他们的工作核心--编写业务逻辑。

EJB 的问题

Spring Framework 于 2004 年 3 月发布。在 Spring Framework 的第一个版本发布时,开发企业应用程序的流行方式是使用 EJB 2.1。

开发和部署 EJB 是一个繁琐的过程。虽然 EJB 使组件的分发变得更容易,但开发、单元测试和部署它们并不容易。EJB 的初始版本(1.0、2.0、2.1)具有复杂的应用程序接口(API),导致人们(在大多数应用程序中是真的)认为引入的复杂性远远超过了好处:

  • 难以进行单元测试。实际上,在 EJB 容器之外进行测试也很困难。

  • 需要实现多个接口,具有许多不必要的方法。

  • 繁琐和乏味的异常处理。

  • 不方便的部署描述符。

Spring Framework 最初是作为一个旨在简化开发 Java EE 应用程序的轻量级框架而推出的。

Spring Framework 为什么受欢迎?

Spring Framework 的第一个版本于 2004 年 3 月发布。在随后的十五年中,Spring Framework 的使用和受欢迎程度只增不减。

Spring Framework 受欢迎的重要原因如下:

  • 简化单元测试--因为依赖注入

  • 减少样板代码

  • 架构灵活性

  • 跟上时代的变化

让我们详细讨论每一个。

简化单元测试

早期版本的 EJB 非常难以进行单元测试。事实上,很难在容器之外运行 EJB(截至 2.1 版本)。测试它们的唯一方法是将它们部署在容器中。

Spring Framework 引入了“依赖注入”的概念。我们将在第二章“依赖注入”中详细讨论依赖注入。

依赖注入使得单元测试变得容易,可以通过将依赖项替换为它们的模拟来进行单元测试。我们不需要部署整个应用程序来进行单元测试。

简化单元测试有多重好处:

  • 程序员更加高效

  • 缺陷可以更早地发现,因此修复成本更低

  • 应用程序具有自动化的单元测试,可以在持续集成构建中运行,以防止未来的缺陷

减少样板代码

在 Spring Framework 之前,典型的 J2EE(或现在称为 Java EE)应用程序包含大量的管道代码。例如:获取数据库连接、异常处理代码、事务管理代码、日志记录代码等等。

让我们看一个使用预编译语句执行查询的简单例子:

    PreparedStatement st = null;
    try {
          st = conn.prepareStatement(INSERT_TODO_QUERY);
          st.setString(1, bean.getDescription());
          st.setBoolean(2, bean.isDone());
          st.execute();
        } 
    catch (SQLException e) {
          logger.error("Failed : " + INSERT_TODO_QUERY, e);
     } finally {
                if (st != null) {
           try {
           st.close();
          } catch (SQLException e) {
           // Ignore - nothing to do..
          }
       }
     }

在前面的例子中,有四行业务逻辑和超过 10 行管道代码。

使用 Spring Framework,相同的逻辑可以应用在几行代码中:

    jdbcTemplate.update(INSERT_TODO_QUERY, 
    bean.getDescription(), bean.isDone());

Spring Framework 是如何做到这一点的?

在前面的例子中,Spring JDBC(以及 Spring 总体)将大多数已检查异常转换为未检查异常。通常,当查询失败时,我们无法做太多事情 - 除了关闭语句并使事务失败。我们可以在每个方法中实现异常处理,也可以使用 Spring 面向方面的编程AOP)进行集中式异常处理并将其注入。

Spring JDBC 消除了创建所有涉及获取连接、创建预编译语句等管道代码的需要。jdbcTemplate类可以在 Spring 上下文中创建,并在需要时注入到数据访问对象DAO)类中。

与前面的例子类似,Spring JMS、Spring AOP 和其他 Spring 模块有助于减少大量的管道代码。

Spring Framework 让程序员专注于程序员的主要工作 - 编写业务逻辑。

避免所有管道代码还有另一个很大的好处 - 减少代码重复。由于所有事务管理、异常处理等代码(通常是所有横切关注点)都在一个地方实现,因此更容易维护。

架构灵活性

Spring Framework 是模块化的。它是建立在核心 Spring 模块之上的一组独立模块。大多数 Spring 模块都是独立的 - 您可以使用其中一个而无需使用其他模块。

让我们看几个例子:

  • 在 Web 层,Spring 提供了自己的框架 - Spring MVC。但是,Spring 对 Struts、Vaadin、JSF 或您选择的任何 Web 框架都有很好的支持。

  • Spring Beans 可以为您的业务逻辑提供轻量级实现。但是,Spring 也可以与 EJB 集成。

  • 在数据层,Spring 通过其 Spring JDBC 模块简化了 JDBC。但是,Spring 对您喜欢的任何首选数据层框架(JPA、Hibernate(带或不带 JPA)或 iBatis)都有很好的支持。

  • 您可以选择使用 Spring AOP 来实现横切关注点(日志记录、事务管理、安全等),或者可以集成一个完整的 AOP 实现,比如 AspectJ。

Spring Framework 不希望成为万能工具。在专注于减少应用程序不同部分之间的耦合并使它们可测试的核心工作的同时,Spring 与您选择的框架集成得很好。这意味着您在架构上有灵活性 - 如果您不想使用特定框架,可以轻松地用另一个替换它。

跟上时代的变化

Spring Framework 的第一个版本专注于使应用程序可测试。然而,随着时间的推移,出现了新的挑战。Spring Framework 设法演变并保持领先地位,提供了灵活性和模块。以下列举了一些例子:

  • 注解是在 Java 5 中引入的。Spring Framework(2.5 版 - 2007 年 11 月)在引入基于注解的 Spring MVC 控制器模型方面领先于 Java EE。使用 Java EE 的开发人员必须等到 Java EE 6(2009 年 12 月 - 2 年后)才能获得类似的功能。

  • Spring 框架在 Java EE 之前引入了许多抽象概念,以使应用程序与特定实现解耦。缓存 API 就是一个例子。Spring 在 Spring 3.1 中提供了透明的缓存支持。Java EE 在 2014 年提出了JSR-107用于 JCache——Spring 4.1 提供了对其的支持。

Spring 带来的另一个重要的东西是 Spring 项目的总称。Spring 框架只是 Spring 项目下的众多项目之一。我们将在单独的部分讨论不同的 Spring 项目。以下示例说明了 Spring 如何通过新的 Spring 项目保持领先地位:

  • Spring Batch定义了构建 Java 批处理应用程序的新方法。直到 Java EE 7(2013 年 6 月)我们才有了 Java EE 中可比较的批处理应用程序规范。

  • 随着架构向云和微服务发展,Spring 推出了新的面向云的 Spring 项目。Spring Cloud 有助于简化微服务的开发和部署。Spring Cloud Data Flow 提供了对微服务应用程序的编排。

Spring 模块

Spring 框架的模块化是其广泛使用的最重要原因之一。Spring 框架非常模块化,有 20 多个不同的模块,具有明确定义的边界。

下图显示了不同的 Spring 模块——按照它们通常在应用程序中使用的层进行组织:

我们将从讨论 Spring 核心容器开始,然后再讨论其他按照它们通常在应用程序层中使用的模块分组的模块。

Spring 核心容器

Spring 核心容器提供了 Spring 框架的核心功能——依赖注入、IoC(控制反转)容器和应用程序上下文。我们将在第二章“依赖注入”中更多地了解 DI 和 IoC 容器。

重要的核心 Spring 模块列在下表中:

模块/构件 用途
spring-core 其他 Spring 模块使用的实用工具。
spring-beans 支持 Spring beans。与 spring-core 结合使用,提供了 Spring 框架的核心功能——依赖注入。包括 BeanFactory 的实现。
spring-context 实现了 ApplicationContext,它扩展了 BeanFactory 并提供了加载资源和国际化等支持。
spring-expression 扩展了EL(来自 JSP 的表达式语言)并提供了一种用于访问和操作 bean 属性(包括数组和集合)的语言。

横切关注点

横切关注点适用于所有应用程序层——包括日志记录和安全性等。AOP通常用于实现横切关注点。

单元测试和集成测试属于这一类,因为它们适用于所有层。

与横切关注点相关的重要 Spring 模块如下所示:

模块/构件 用途
spring-aop 提供面向切面编程的基本支持——包括方法拦截器和切入点。
spring-aspects 提供与最流行和功能齐全的 AOP 框架 AspectJ 的集成。
spring-instrument 提供基本的仪器支持。
spring-test 提供对单元测试和集成测试的基本支持。

Web

Spring 提供了自己的 MVC 框架,Spring MVC,除了与流行的 Web 框架(如 Struts)进行良好的集成。

重要的构件/模块如下所示:

  • spring-web:提供基本的 Web 功能,如多部分文件上传。提供与其他 Web 框架(如 Struts)的集成支持。

  • spring-webmvc:提供了一个功能齐全的 Web MVC 框架——Spring MVC,其中包括实现 REST 服务的功能。

我们将在第三章使用 Spring MVC 构建 Web 应用程序和第五章使用 Spring Boot 构建微服务中详细介绍 Spring MVC 并开发 Web 应用程序和 REST 服务。

业务

业务层专注于执行应用程序的业务逻辑。在 Spring 中,业务逻辑通常是在普通的旧 Java 对象POJO)中实现的。

Spring 事务spring-tx)为 POJO 和其他类提供声明式事务管理。

数据

应用程序中的数据层通常与数据库和/或外部接口进行通信。

以下表格列出了与数据层相关的一些重要的 Spring 模块:

模块/组件 用途
spring-jdbc 提供对 JDBC 的抽象,避免样板代码。
spring-orm 与 ORM 框架和规范集成--包括 JPA 和 Hibernate 等。
spring-oxm 提供对象到 XML 映射集成。支持 JAXB、Castor 等框架。
spring-jms 提供对 JMS 的抽象,避免样板代码。

Spring 项目

虽然 Spring 框架为企业应用程序的核心功能(DI、Web、数据)提供了基础,但其他 Spring 项目探索了企业领域中的集成和解决方案--部署、云、大数据、批处理和安全等。

以下列出了一些重要的 Spring 项目:

  • Spring Boot

  • Spring Cloud

  • Spring Data

  • Spring Batch

  • Spring 安全

  • Spring HATEOAS

Spring Boot

在开发微服务和 Web 应用程序时遇到的一些挑战如下:

  • 制定框架选择和决定兼容的框架版本

  • 提供外部化配置的机制--可以从一个环境更改为另一个环境的属性

  • 健康检查和监控--如果应用程序的特定部分宕机,则提供警报

  • 决定部署环境并为其配置应用程序

Spring Boot 通过采取主观的观点来解决所有这些问题。

我们将在两章中深入研究 Spring Boot--第五章使用 Spring Boot 构建微服务和第七章高级 Spring Boot 功能

Spring Cloud

可以毫不夸张地说世界正在向云端迁移

云原生微服务和应用程序是当今的趋势。我们将在第四章向微服务和云原生应用的演进中详细讨论这一点。

Spring 正在迅速迈向使应用程序在云中开发变得更简单的方向,Spring Cloud 正在朝着这个方向迈进。

Spring Cloud 为分布式系统中的常见模式提供解决方案。Spring Cloud 使开发人员能够快速创建实现常见模式的应用程序。Spring Cloud 中实现的一些常见模式如下所示:

  • 配置管理

  • 服务发现

  • 断路器

  • 智能路由

我们将在第九章中更详细地讨论 Spring Cloud 及其各种功能,Spring Cloud

Spring Data

当今世界存在多个数据源--SQL(关系型)和各种 NOSQL 数据库。Spring Data 试图为所有这些不同类型的数据库提供一致的数据访问方法。

Spring Data 提供与各种规范和/或数据存储的集成:

  • JPA

  • MongoDB

  • Redis

  • Solr

  • 宝石缓存

  • Apache Cassandra

以下列出了一些重要的特性:

  • 通过从方法名称确定查询,提供关于存储库和对象映射的抽象

  • 简单的 Spring 集成

  • 与 Spring MVC 控制器的集成

  • 高级自动审计功能--创建者、创建日期、最后更改者和最后更改日期

我们将在第八章中更详细地讨论 Spring Data,Spring Data

Spring Batch

今天的企业应用程序使用批处理程序处理大量数据。这些应用程序的需求非常相似。Spring Batch 提供了解决高性能要求的高容量批处理程序的解决方案。

Spring Batch 中的重要功能如下:

  • 启动、停止和重新启动作业的能力--包括从失败点重新启动失败的作业的能力

  • 处理数据块的能力

  • 重试步骤或在失败时跳过步骤的能力

  • 基于 Web 的管理界面

Spring Security

认证是识别用户的过程。授权是确保用户有权访问资源执行已识别操作的过程。

认证和授权是企业应用程序的关键部分,包括 Web 应用程序和 Web 服务。Spring Security 为基于 Java 的应用程序提供声明性认证和授权。

Spring Security 中的重要功能如下:

  • 简化的认证和授权

  • 与 Spring MVC 和 Servlet API 的良好集成

  • 防止常见安全攻击的支持--跨站请求伪造CSRF)和会话固定

  • 可用于与 SAML 和 LDAP 集成的模块

我们将在第三章中讨论如何使用 Spring Security 保护 Web 应用程序,使用 Spring MVC 构建 Web 应用程序

我们将在第六章中讨论如何使用 Spring Security 保护基本的和 OAuth 身份验证机制的 REST 服务,扩展微服务

Spring HATEOAS

HATEOAS代表超媒体作为应用程序状态的引擎。尽管听起来复杂,但它是一个非常简单的概念。它的主要目的是解耦服务器(服务提供者)和客户端(服务消费者)。

服务提供者向服务消费者提供有关资源上可以执行的其他操作的信息。

Spring HATEOAS 提供了 HATEOAS 实现--特别是针对使用 Spring MVC 实现的 REST 服务。

Spring HATEOAS 中的重要功能如下:

  • 简化了指向服务方法的链接的定义,使链接更加稳固

  • 支持 JAXB(基于 XML)和 JSON 集成

  • 支持服务消费者(客户端)

我们将在第六章中讨论如何在扩展微服务中使用 HATEOAS。

Spring Framework 5.0 中的新功能

Spring Framework 5.0 是 Spring Framework 的首次重大升级,距离 Spring Framework 4.0 差不多四年。在这段时间内,Spring Boot 项目的主要发展之一就是演变。我们将在下一节讨论 Spring Boot 2.0 的新功能。

Spring Framework 5.0 最大的特点之一是响应式编程。Spring Framework 5.0 提供了核心响应式编程功能和对响应式端点的支持。重要变化的列表包括以下内容:

  • 基线升级

  • JDK 9 运行时兼容性

  • 在 Spring Framework 代码中使用 JDK 8 功能的能力

  • 响应式编程支持

  • 功能性 Web 框架

  • Jigsaw 的 Java 模块化

  • Kotlin 支持

  • 删除的功能

基线升级

Spring Framework 5.0 具有 JDK 8 和 Java EE 7 基线。基本上,这意味着不再支持以前的 JDK 和 Java EE 版本。

Spring Framework 5.0 的重要基线 Java EE 7 规范如下:

  • Servlet 3.1

  • JMS 2.0

  • JPA 2.1

  • JAX-RS 2.0

  • Bean Validation 1.1

多个 Java 框架的最低支持版本发生了许多变化。以下列表包含一些知名框架的最低支持版本:

  • Hibernate 5

  • Jackson 2.6

  • EhCache 2.10

  • JUnit 5

  • Tiles 3

以下列表显示了支持的服务器版本:

  • Tomcat 8.5+

  • Jetty 9.4+

  • WildFly 10+

  • Netty 4.1+(用于 Spring Web Flux 的 Web 响应式编程)

  • Undertow 1.4+(用于使用 Spring Web Flux 进行 Web 响应式编程)

使用之前版本的任何规范/框架的应用程序在使用 Spring Framework 5.0 之前,至少需要升级到前面列出的版本。

JDK 9 运行时兼容性

预计 JDK 9 将于 2017 年中期发布。Spring Framework 5.0 预计将与 JDK 9 具有运行时兼容性。

在 Spring Framework 代码中使用 JDK 8 特性

Spring Framework 4.x 的基线版本是 Java SE 6。这意味着它支持 Java 6、7 和 8。必须支持 Java SE 6 和 7 对 Spring Framework 代码施加了限制。框架代码无法使用 Java 8 的任何新特性。因此,虽然世界其他地方已经升级到 Java 8,Spring Framework 中的代码(至少是主要部分)仍受限于使用较早版本的 Java。

Spring Framework 5.0 的基线版本是 Java 8。Spring Framework 代码现在已升级以使用 Java 8 的新特性。这将导致更可读和更高性能的框架代码。使用的一些 Java 8 特性如下:

  • 核心 Spring 接口中的 Java 8 默认方法

  • 基于 Java 8 反射增强的内部代码改进

  • 在框架代码中使用函数式编程--lambda 和流

响应式编程支持

响应式编程是 Spring Framework 5.0 最重要的特性之一。

微服务架构通常是围绕基于事件的通信构建的。应用程序被构建为对事件(或消息)做出反应。

响应式编程提供了一种专注于构建对事件做出反应的应用程序的替代编程风格。

虽然 Java 8 没有内置对响应式编程的支持,但有许多框架提供了对响应式编程的支持:

  • 响应式流:语言中立的尝试定义响应式 API。

  • Reactor:由 Spring Pivotal 团队提供的 Reactive Streams 的 Java 实现。

  • Spring WebFlux:基于响应式编程开发 Web 应用程序的框架。提供类似于 Spring MVC 的编程模型。

我们将在《响应式编程》的第十一章中讨论响应式编程以及如何在 Spring Web Flux 中实现它。

功能性 Web 框架

在响应式特性的基础上,Spring 5 还提供了一个功能性 Web 框架。

功能性 Web 框架提供了使用函数式编程风格定义端点的功能。这里展示了一个简单的 hello world 示例:

    RouterFunction<String> route =
    route(GET("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")));

功能性 Web 框架还可以用于定义更复杂的路由,如下例所示:

    RouterFunction<?> route = route(GET("/todos/{id}"),
    request -> {
       Mono<Todo> todo = Mono.justOrEmpty(request.pathVariable("id"))
       .map(Integer::valueOf)
       .then(repository::getTodo);
       return Response.ok().body(fromPublisher(todo, Todo.class));
      })
     .and(route(GET("/todos"),
     request -> {
       Flux<Todo> people = repository.allTodos();
       return Response.ok().body(fromPublisher(people, Todo.class));
     }))
    .and(route(POST("/todos"),
    request -> {
      Mono<Todo> todo = request.body(toMono(Todo.class));
      return Response.ok().build(repository.saveTodo(todo));
    }));

需要注意的一些重要事项如下:

  • RouterFunction评估匹配条件以将请求路由到适当的处理程序函数

  • 我们正在定义三个端点,两个 GET 和一个 POST,并将它们映射到不同的处理程序函数

我们将在《响应式编程》的第十一章中更详细地讨论 Mono 和 Flux。

使用 Jigsaw 的 Java 模块化

直到 Java 8 之前,Java 平台并不是模块化的。由此产生了一些重要问题:

  • 平台膨胀:在过去的几十年中,Java 模块化并不是一个令人担忧的问题。然而,随着物联网IOT)和新的轻量级平台如 Node.js 的出现,迫切需要解决 Java 平台的膨胀问题。(JDK 的初始版本小于 10MB。最近的 JDK 版本需要超过 200MB。)

  • JAR Hell:另一个重要问题是 JAR Hell 的问题。当 Java ClassLoader 找到一个类时,它不会查看是否有其他可用于该类的定义。它会立即加载找到的第一个类。如果应用程序的两个不同部分需要来自不同 JAR 的相同类,它们无法指定必须从哪个 JAR 加载该类。

开放系统网关倡议OSGi)是 1999 年开始的倡议之一,旨在将模块化引入 Java 应用程序。

每个模块(称为捆绑包)定义如下:

  • imports: 模块使用的其他捆绑包

  • exports: 此捆绑包导出的包

每个模块都可以有自己的生命周期。它可以独立安装、启动和停止。

Jigsaw 是Java 社区进程JCP)下的一个倡议,从 Java 7 开始,旨在将模块化引入 Java。它有两个主要目标:

  • 为 JDK 定义和实现模块化结构

  • 为构建在 Java 平台上的应用程序定义模块系统

预计 Jigsaw 将成为 Java 9 的一部分,Spring Framework 5.0 预计将包括对 Jigsaw 模块的基本支持。

Kotlin 支持

Kotlin 是一种静态类型的 JVM 语言,可以编写富有表现力、简短和可读的代码。Spring Framework 5.0 对 Kotlin 有很好的支持。

考虑一个简单的 Kotlin 程序,演示如下所示的数据类:

    import java.util.*
    data class Todo(var description: String, var name: String, var  
    targetDate : Date)
    fun main(args: Array<String>) {
      var todo = Todo("Learn Spring Boot", "Jack", Date())
      println(todo)
        //Todo(description=Learn Spring Boot, name=Jack, 
        //targetDate=Mon May 22 04:26:22 UTC 2017)
      var todo2 = todo.copy(name = "Jill")
      println(todo2)
         //Todo(description=Learn Spring Boot, name=Jill, 
         //targetDate=Mon May 22 04:26:22 UTC 2017)
      var todo3 = todo.copy()
      println(todo3.equals(todo)) //true
    }  

在不到 10 行的代码中,我们创建并测试了一个具有三个属性和以下功能的数据 bean:

  • equals()

  • hashCode()

  • toString()

  • copy()

Kotlin 是强类型的。但是不需要显式指定每个变量的类型:

    val arrayList = arrayListOf("Item1", "Item2", "Item3") 
    // Type is ArrayList

命名参数允许您在调用方法时指定参数的名称,从而使代码更易读:

    var todo = Todo(description = "Learn Spring Boot", 
    name = "Jack", targetDate = Date())

Kotlin 通过提供默认变量(it)和诸如takedrop等方法来简化函数式编程:

    var first3TodosOfJack = students.filter { it.name == "Jack"   
     }.take(3)

您还可以在 Kotlin 中为参数指定默认值:

    import java.util.*
    data class Todo(var description: String, var name: String, var
    targetDate : Date = Date())
    fun main(args: Array<String>) {
      var todo = Todo(description = "Learn Spring Boot", name = "Jack")
    }

凭借其使代码简洁和表达力的所有功能,我们期望 Kotlin 成为要学习的语言。

我们将在第十三章“在 Spring 中使用 Kotlin”中更多地讨论 Kotlin。

已删除的功能

Spring Framework 5 是一个主要的 Spring 版本,基线版本大幅增加。随着 Java、Java EE 和其他一些框架的基线版本的增加,Spring Framework 5 取消了对一些框架的支持:

  • Portlet

  • Velocity

  • JasperReports

  • XMLBeans

  • JDO

  • Guava

如果您使用了上述任何框架,建议您计划迁移并继续使用直到 2019 年支持的 Spring Framework 4.3。

Spring Boot 2.0 的新功能

Spring Boot 的第一个版本于 2014 年发布。以下是预计在 Spring Boot 2.0 中的一些重要更新:

  • 基线 JDK 版本是 Java 8

  • Spring Framework 5.0 的基线版本是 Spring Framework 5.0

  • Spring Boot 2.0 支持使用 WebFlux 进行响应式 Web 编程

一些重要框架的最低支持版本如下所示:

  • Jetty 9.4

  • Tomcat 8.5

  • Hibernate 5.2

  • Gradle 3.4

我们将在第五章“使用 Spring Boot 构建微服务”和第七章“高级 Spring Boot 功能”中广泛讨论 Spring Boot。

摘要

在过去的十五年中,Spring Framework 显着改善了开发 Java 企业应用程序的体验。Spring Framework 5.0 带来了许多功能,同时显着增加了基线。

在随后的章节中,我们将介绍依赖注入,并了解如何使用 Spring MVC 开发 Web 应用程序。之后,我们将进入微服务的世界。在第五章“使用 Spring Boot 构建微服务”、第六章“扩展微服务”和第七章“高级 Spring Boot 功能”中,我们将介绍 Spring Boot 如何简化微服务的创建。然后,我们将把注意力转向使用 Spring Cloud 和 Spring Cloud Data Flow 在云中构建应用程序。

第二章:依赖注入

我们编写的任何 Java 类都依赖于其他类。类依赖的其他类是其依赖项。如果一个类直接创建依赖项的实例,它们之间建立了紧耦合。使用 Spring,创建和连接对象的责任被一个称为IoC 容器的新组件接管。类定义依赖关系,Spring 的控制反转IoC)容器创建对象并将依赖项连接在一起。这个革命性的概念,即创建和连接依赖项的控制被容器接管,被称为 IoC 或依赖注入DI)。

在本章中,我们首先探讨了 DI 的需求。我们使用一个简单的例子来说明 DI 的用法。我们将了解 DI 的重要优势--更容易维护,耦合度更低和改进的可测试性。我们将探索 Spring 中的 DI 选项。我们将结束本章,看一下 Java 的标准 DI 规范上下文和依赖注入CDI)以及 Spring 如何支持它。

本章将回答以下问题:

  • 什么是依赖注入?

  • 依赖注入的正确使用如何使应用程序可测试?

  • Spring 如何使用注解实现 DI?

  • 什么是组件扫描?

  • Java 和 XML 应用上下文之间有什么区别?

  • 如何为 Spring 上下文创建单元测试?

  • 模拟如何使单元测试更简单?

  • 不同的 bean 作用域是什么?

  • 什么是 CDI 以及 Spring 如何支持 CDI?

理解依赖注入

我们将看一个例子来理解依赖注入。我们将编写一个简单的业务服务,与一个数据服务交互。我们将使代码可测试,并看到正确使用 DI 如何使代码可测试。

以下是我们将遵循的步骤顺序:

  1. 编写一个业务服务与数据服务交互的简单示例。当业务服务直接创建数据服务的实例时,它们之间是紧密耦合的。单元测试将会很困难。

  2. 通过将创建数据服务的责任移出业务服务,使代码松耦合。

  3. 引入 Spring IoC 容器来实例化 bean 并将它们连接在一起。

  4. 探索 Spring 提供的 XML 和 Java 配置选项。

  5. 探索 Spring 单元测试选项。

  6. 使用模拟编写真正的单元测试。

理解依赖关系

我们将从编写一个简单的例子开始;一个业务服务与另一个数据服务交互。大多数 Java 类依赖于其他类。这些被称为该类的依赖项

看一个示例类BusinessServiceImpl,如下所示:

    public class BusinessServiceImpl { 
      public long calculateSum(User user) { 
        DataServiceImpl dataService = new DataServiceImpl(); 
        long sum = 0; 
        for (Data data : dataService.retrieveData(user)) { 
          sum += data.getValue(); 
        } 
        return sum; 
      }
    }

通常,所有设计良好的应用程序都有多个层。每个层都有明确定义的责任。业务层包含业务逻辑。数据层与外部接口和/或数据库交互以获取数据。在前面的例子中,DataServiceImpl类从数据库中获取与用户相关的一些数据。BusinessServiceImpl类是一个典型的业务服务,与数据服务DataServiceImpl交互获取数据,并在其上添加业务逻辑(在本例中,业务逻辑非常简单:计算数据服务返回的数据的总和)。

BusinessServiceImpl依赖于DataServiceImpl。因此,DataServiceImplBusinessServiceImpl的一个依赖项。

关注BusinessServiceImpl如何创建DataServiceImpl的实例。

    DataServiceImpl dataService = new DataServiceImpl();

BusinessServiceImpl自己创建一个实例。这是紧耦合。

想一想单元测试;如何在不涉及(或实例化)DataServiceImpl类的情况下对BusinessServiceImpl类进行单元测试?这很困难。人们可能需要做复杂的事情,比如使用反射来编写单元测试。因此,前面的代码是不可测试的。

当您可以轻松地为代码编写简单的单元测试时,代码(方法、一组方法或类)就是可测试的。单元测试中使用的方法之一是模拟依赖关系。我们将稍后更详细地讨论模拟。

这是一个需要思考的问题:我们如何使前面的代码可测试?我们如何减少BusinessServiceImplDataServiceImpl之间的紧耦合?

我们可以做的第一件事是为DataServiceImpl创建一个接口。我们可以在BusinessServiceImpl中使用DataServiceImpl的新创建接口,而不是直接使用该类。

以下代码显示了如何创建一个接口:

    public interface DataService { 
     List<Data> retrieveData(User user); 
    }

让我们更新BusinessServiceImpl中的代码以使用接口:

    DataService dataService = new DataServiceImpl();

使用接口有助于创建松散耦合的代码。我们可以将任何接口实现替换为一个明确定义的依赖关系。

例如,考虑一个需要进行一些排序的业务服务。

第一个选项是直接在代码中使用排序算法,例如冒泡排序。第二个选项是为排序算法创建一个接口并使用该接口。具体的算法可以稍后连接。在第一个选项中,当我们需要更改算法时,我们需要更改代码。在第二个选项中,我们只需要更改连接。

我们现在使用DataService接口,但BusinessServiceImpl仍然紧密耦合,因为它创建了DataServiceImpl的实例。我们如何解决这个问题?

BusinessServiceImpl不自己创建DataServiceImpl的实例怎么样?我们可以在其他地方创建DataServiceImpl的实例(稍后我们将讨论谁将创建实例)并将其提供给BusinessServiceImpl吗?

为了实现这一点,我们将更新BusinessServiceImpl中的代码,为DataService添加一个 setter。calculateSum方法也更新为使用此引用。更新后的代码如下:

    public class BusinessServiceImpl { 
      private DataService dataService; 
      public long calculateSum(User user) { 
        long sum = 0; 
        for (Data data : dataService.retrieveData(user)) { 
          sum += data.getValue(); 
         } 
        return sum; 
       } 
      public void setDataService(DataService dataService) { 
        this.dataService = dataService; 
       } 
    }

除了为数据服务创建一个 setter 之外,我们还可以创建一个接受数据服务作为参数的BusinessServiceImpl构造函数。这称为构造函数注入

您可以看到BusinessServiceImpl现在可以与DataService的任何实现一起工作。它与特定实现DataServiceImpl没有紧密耦合。

为了使代码更加松散耦合(在开始编写测试时),让我们为BusinessService创建一个接口,并更新BusinessServiceImpl以实现该接口:

    public interface BusinessService { 
      long calculateSum(User user); 
    } 
    public class BusinessServiceImpl implements BusinessService { 
      //.... Rest of code.. 
    }

现在我们已经减少了耦合,但仍然有一个问题;谁负责创建DataServiceImpl类的实例并将其连接到BusinessServiceImpl类?

这正是 Spring IoC 容器发挥作用的地方。

Spring IoC 容器

Spring IoC 容器根据应用程序开发人员创建的配置设置创建 bean 并将它们连接在一起。

需要回答以下问题:

  • 问题 1:Spring IoC 容器如何知道要创建哪些 bean?具体来说,Spring IoC 容器如何知道要为BusinessServiceImplDataServiceImpl类创建 bean?

  • 问题 2:Spring IoC 容器如何知道如何将 bean 连接在一起?具体来说,Spring IoC 容器如何知道将DataServiceImpl类的实例注入BusinessServiceImpl类?

  • 问题 3:Spring IoC 容器如何知道在哪里搜索 bean?在类路径中搜索所有包并不高效。

在我们专注于创建容器之前,让我们先专注于问题 1 和 2;如何定义需要创建哪些 bean 以及如何将它们连接在一起。

定义 bean 和装配

让我们先解决第一个问题;Spring IoC 容器如何知道要创建哪些 bean?

我们需要告诉 Spring IoC 容器要创建哪些 bean。这可以通过在需要创建 bean 的类上使用@Repository@Component@Service注解来完成。所有这些注解告诉 Spring 框架在定义这些注解的特定类中创建 bean。

@Component注解是定义 Spring bean 的最通用方式。其他注解具有更具体的上下文。@Service注解用于业务服务组件。@Repository注解用于数据访问对象DAO)组件。

我们在DataServiceImpl上使用@Repository注解,因为它与从数据库获取数据有关。我们在BusinessServiceImpl类上使用@Service注解,因为它是一个业务服务:

    @Repository 
    public class DataServiceImpl implements DataService 
    @Service 
    public class BusinessServiceImpl implements BusinessService

现在让我们把注意力转移到第二个问题上--Spring IoC 容器如何知道如何将 bean 装配在一起?DataServiceImpl类的 bean 需要注入到BusinessServiceImpl类的 bean 中。

我们可以通过在BusinessServiceImpl类中的DataService接口的实例变量上指定一个@Autowired注解来实现这一点:

    public class BusinessServiceImpl { 
      @Autowired 
      private DataService dataService;

现在我们已经定义了 bean 和它们的装配,为了测试这一点,我们需要一个DataService的实现。我们将创建一个简单的、硬编码的实现。DataServiceImpl返回一些数据:

    @Repository 
    public class DataServiceImpl implements DataService { 
      public List<Data> retrieveData(User user) { 
        return Arrays.asList(new Data(10), new Data(20)); 
      } 
    }

现在我们已经定义了我们的 bean 和依赖关系,让我们专注于如何创建和运行 Spring IoC 容器。

创建 Spring IoC 容器

创建 Spring IoC 容器有两种方式:

  • Bean 工厂

  • 应用程序上下文

Bean 工厂是所有 Spring IoC 功能的基础--bean 的生命周期和装配。应用程序上下文基本上是 Bean 工厂的超集,具有在企业环境中通常需要的附加功能。Spring 建议在所有情况下使用应用程序上下文,除非应用程序上下文消耗的额外几 KB 内存是关键的。

让我们使用应用程序上下文来创建一个 Spring IoC 容器。我们可以使用 Java 配置或 XML 配置来创建应用程序上下文。让我们首先使用 Java 应用程序配置。

应用程序上下文的 Java 配置

以下示例显示了如何创建一个简单的 Java 上下文配置:

    @Configuration 
    class SpringContext { 
    }

关键是@Configuration注解。这就是定义这个为 Spring 配置的地方。

还有一个问题;Spring IoC 容器如何知道在哪里搜索 bean?

我们需要告诉 Spring IoC 容器要搜索的包,通过定义一个组件扫描。让我们在之前的 Java 配置定义中添加一个组件扫描:

    @Configuration 
    @ComponentScan(basePackages = { "com.mastering.spring" }) 
     class SpringContext { 
     }

我们已经为com.mastering.spring包定义了一个组件扫描。它展示了我们到目前为止讨论的所有类是如何组织的。到目前为止,我们定义的所有类都按如下方式存在于这个包中:

快速回顾

让我们花一点时间回顾一下我们到目前为止所做的一切,以使这个例子工作起来:

  • 我们已经定义了一个 Spring 配置类SpringContext,带有@Configuration注解和一个对com.mastering.spring包的组件扫描

  • 我们有一些文件(在前面的包中):

  • BusinessServiceImpl带有@Service注解

  • DataServiceImpl带有@Repository注解

  • BusinessServiceImplDataService的实例上有@Autowired注解

当我们启动一个 Spring 上下文时,将会发生以下事情:

  • 它将扫描com.mastering.spring包,并找到BusinessServiceImplDataServiceImpl的 bean。

  • DataServiceImpl没有任何依赖。因此,将创建DataServiceImpl的 bean。

  • BusinessServiceImpl依赖于DataServiceDataServiceImplDataService接口的实现。因此,它符合自动装配的条件。因此,为BusinessServiceImpl创建了一个 bean,并且为DataServiceImpl创建的 bean 通过 setter 自动装配到它。

使用 Java 配置启动应用程序上下文

以下程序显示了如何启动 Java 上下文;我们使用主方法使用AnnotationConfigApplicationContext启动应用程序上下文:

    public class LaunchJavaContext { 
      private static final User DUMMY_USER = new User("dummy"); 
      public static Logger logger =  
      Logger.getLogger(LaunchJavaContext.class); 
      public static void main(String[] args) { 
        ApplicationContext context = new 
        AnnotationConfigApplicationContext( 
        SpringContext.class); 
        BusinessService service = 
        context.getBean(BusinessService.class); 
        logger.debug(service.calculateSum(DUMMY_USER)); 
      } 
     }

以下代码行创建应用程序上下文。我们希望基于 Java 配置创建应用程序上下文。因此,我们使用AnnotationConfigApplicationContext

    ApplicationContext context = new 
    AnnotationConfigApplicationContext( 
      SpringContext.class);

一旦上下文启动,我们将需要获取业务服务 bean。我们使用getBean方法,传递 bean 的类型(BusinessService.class)作为参数:

    BusinessService service = context.getBean(BusinessService.class );

我们已准备好通过运行LaunchJavaContext程序来启动应用程序上下文。

控制台日志

以下是使用LaunchJavaContext启动上下文后日志中的一些重要语句。让我们快速查看日志,以深入了解 Spring 正在做什么:

前几行显示了组件扫描的操作:

Looking for matching resources in directory tree [/target/classes/com/mastering/spring]

Identified candidate component class: file [/in28Minutes/Workspaces/SpringTutorial/mastering-spring-example-1/target/classes/com/mastering/spring/business/BusinessServiceImpl.class]

Identified candidate component class: file [/in28Minutes/Workspaces/SpringTutorial/mastering-spring-example-1/target/classes/com/mastering/spring/data/DataServiceImpl.class]

defining beans [******OTHERS*****,businessServiceImpl,dataServiceImpl];

Spring 现在开始创建 bean。它从businessServiceImpl开始,但它有一个自动装配的依赖项:

Creating instance of bean 'businessServiceImpl'Registered injected element on class [com.mastering.spring.business.BusinessServiceImpl]: AutowiredFieldElement for private com.mastering.spring.data.DataService com.mastering.spring.business.BusinessServiceImpl.dataService 

Processing injected element of bean 'businessServiceImpl': AutowiredFieldElement for private com.mastering.spring.data.DataService com.mastering.spring.business.BusinessServiceImpl.dataService

Spring 继续移动到dataServiceImpl并为其创建一个实例:

Creating instance of bean 'dataServiceImpl'
Finished creating instance of bean 'dataServiceImpl'

Spring 将dataServiceImpl自动装配到businessServiceImpl

Autowiring by type from bean name 'businessServiceImpl' to bean named 'dataServiceImpl'
Finished creating instance of bean 'businessServiceImpl'

应用程序上下文的 XML 配置

在上一个示例中,我们使用了 Spring Java 配置来启动应用程序上下文。Spring 也支持 XML 配置。

以下示例显示了如何使用 XML 配置启动应用程序上下文。这将有两个步骤:

  • 定义 XML Spring 配置

  • 使用 XML 配置启动应用程序上下文

定义 XML Spring 配置

以下示例显示了典型的 XML Spring 配置。此配置文件在src/main/resources目录中创建,名称为BusinessApplicationContext.xml

    <?xml version="1.0" encoding="UTF-8" standalone="no"?> 
    <beans>  <!-Namespace definitions removed--> 
      <context:component-scan base-package ="com.mastering.spring"/> 
    </beans>

使用context:component-scan定义组件扫描。

使用 XML 配置启动应用程序上下文

以下程序显示了如何使用 XML 配置启动应用程序上下文。我们使用主方法使用ClassPathXmlApplicationContext启动应用程序上下文:

    public class LaunchXmlContext { 
      private static final User DUMMY_USER = new User("dummy"); 
      public static Logger logger = 
      Logger.getLogger(LaunchJavaContext.class); 
      public static void main(String[] args) { 
         ApplicationContext context = new
         ClassPathXmlApplicationContext( 
         "BusinessApplicationContext.xml"); 
         BusinessService service =
         context.getBean(BusinessService.class); 
         logger.debug(service.calculateSum(DUMMY_USER)); 
        } 
     }

以下代码行创建应用程序上下文。我们希望基于 XML 配置创建应用程序上下文。因此,我们使用ClassPathXmlApplicationContext创建应用程序上下文:AnnotationConfigApplicationContext

    ApplicationContext context = new 
    ClassPathXmlApplicationContext (SpringContext.class);

一旦上下文启动,我们将需要获取对业务服务 bean 的引用。这与我们使用 Java 配置所做的非常相似。我们使用getBean方法,传递 bean 的类型(BusinessService.class)作为参数。

我们可以继续运行LaunchXmlContext类。您会注意到,我们得到的输出与使用 Java 配置运行上下文时非常相似。

使用 Spring 上下文编写 JUnit

在前面的部分中,我们看了如何从主方法启动 Spring 上下文。现在让我们将注意力转向从单元测试中启动 Spring 上下文。

我们可以使用SpringJUnit4ClassRunner.class作为运行器来启动 Spring 上下文:

    @RunWith(SpringJUnit4ClassRunner.class)

我们需要提供上下文配置的位置。我们将使用之前创建的 XML 配置。以下是您可以声明的方式:

    @ContextConfiguration(locations = {  
    "/BusinessApplicationContext.xml" })

我们可以使用@Autowired注解将上下文中的 bean 自动装配到测试中。BusinessService 是按类型自动装配的:

    @Autowired 
    private BusinessService service;

目前,已经自动装配的DataServiceImpl返回Arrays.asList(new Data(10)new Data(20))BusinessServiceImpl计算和返回10+20的和30。我们将使用assertEquals在测试方法中断言30

    long sum = service.calculateSum(DUMMY_USER); 
    assertEquals(30, sum);

为什么我们在书中这么早介绍单元测试?

实际上,我们认为我们已经迟了。理想情况下,我们会喜欢使用测试驱动开发TDD)并在编写代码之前编写测试。根据我的经验,进行 TDD 会导致简单、可维护和可测试的代码。

单元测试有许多优点:

  • 对未来缺陷的安全网

  • 早期发现缺陷

  • 遵循 TDD 会导致更好的设计

  • 良好编写的测试充当代码和功能的文档--特别是使用 BDD Given-When-Then 风格编写的测试

我们将编写的第一个测试实际上并不是一个单元测试。我们将在这个测试中加载所有的 bean。下一个使用模拟编写的测试将是一个真正的单元测试,其中被单元测试的功能是正在编写的特定代码单元。

测试的完整列表如下;它有一个测试方法:

    @RunWith(SpringJUnit4ClassRunner.class) 
    @ContextConfiguration(locations = {
      "/BusinessApplicationContext.xml" }) 
       public class BusinessServiceJavaContextTest { 
       private static final User DUMMY_USER = new User("dummy"); 
       @Autowired 
       private BusinessService service; 

       @Test 
       public void testCalculateSum() { 
         long sum = service.calculateSum(DUMMY_USER); 
         assertEquals(30, sum); 
        } 
     }

我们编写的JUnit存在一个问题。它不是一个真正的单元测试。这个测试使用了DataServiceImpl的真实(几乎)实现进行 JUnit 测试。因此,我们实际上正在测试BusinessServiceImplDataServiceImpl的功能。这不是单元测试。

现在的问题是;如何在不使用DataService的真实实现的情况下对BusinessServiceImpl进行单元测试?

有两个选项:

  • 创建数据服务的存根实现,在src\test\java文件夹中提供一些虚拟数据。使用单独的测试上下文配置来自动装配存根实现,而不是真正的DataServiceImpl类。

  • 创建一个DataService的模拟并将其自动装配到BusinessServiceImpl中。

创建存根实现意味着创建一个额外的类和一个额外的上下文。存根变得更难维护,因为我们需要更多的数据变化来进行单元测试。

在下一节中,我们将探讨使用模拟进行单元测试的第二个选项。随着模拟框架(特别是 Mockito)在过去几年中的进步,您将看到我们甚至不需要启动 Spring 上下文来执行单元测试。

使用模拟进行单元测试

让我们从理解模拟开始。模拟是创建模拟真实对象行为的对象。在前面的例子中,在单元测试中,我们希望模拟DataService的行为。

与存根不同,模拟可以在运行时动态创建。我们将使用最流行的模拟框架 Mockito。要了解有关 Mockito 的更多信息,我们建议查看github.com/mockito/mockito/wiki/FAQ上的 Mockito 常见问题解答。

我们将创建一个DataService的模拟。使用 Mockito 创建模拟有多种方法。让我们使用其中最简单的方法--注解。我们使用@Mock注解来创建DataService的模拟:

    @Mock 
    private DataService dataService;

创建模拟后,我们需要将其注入到被测试的类BusinessServiceImpl中。我们使用@InjectMocks注解来实现:

    @InjectMocks 
    private BusinessService service = 
    new BusinessServiceImpl();

在测试方法中,我们将需要存根模拟服务以提供我们想要提供的数据。有多种方法。我们将使用 Mockito 提供的 BDD 风格方法来模拟retrieveData方法:

    BDDMockito.given(dataService.retrieveData(
      Matchers.any(User.class))) 
      .willReturn(Arrays.asList(new Data(10),  
      new Data(15), new Data(25)));

在前面的代码中我们定义的是所谓的存根。与 Mockito 的任何东西一样,这是非常易读的。当在dataService模拟上调用retrieveData方法并传入任何User类型的对象时,它将返回一个具有指定值的三个项目的列表。

当我们使用 Mockito 注解时,我们需要使用特定的 JUnit 运行器,即MockitoJunitRunnerMockitoJunitRunner有助于保持测试代码的清晰,并在测试失败时提供清晰的调试信息。MockitoJunitRunner在执行每个测试方法后初始化带有@Mock注解的 bean,并验证框架的使用。

    @RunWith(MockitoJUnitRunner.class)

测试的完整列表如下。它有一个测试方法:

    @RunWith(MockitoJUnitRunner.class) 
    public class BusinessServiceMockitoTest { 
      private static final User DUMMY_USER = new User("dummy");
       @Mock 
      private DataService dataService; 
      @InjectMocks 
      private BusinessService service =  
      new BusinessServiceImpl(); 
      @Test 
      public void testCalculateSum() { 
        BDDMockito.given(dataService.retrieveData( 
        Matchers.any(User.class))) 
        .willReturn( 
           Arrays.asList(new Data(10),  
           new Data(15), new Data(25))); 
           long sum = service.calculateSum(DUMMY_USER); 
           assertEquals(10 + 15 + 25, sum); 
       } 
     }

容器管理的 bean

与其类自己创建其依赖项,我们之前的示例中看到了 Spring IoC 容器如何接管管理 bean 及其依赖项的责任。由容器管理的 bean 称为容器管理的 bean

将 bean 的创建和管理委托给容器有许多优点。其中一些列举如下:

  • 由于类不负责创建依赖项,它们之间松耦合且易于测试。这导致了良好的设计和较少的缺陷。

  • 由于容器管理 bean,可以以更通用的方式引入围绕 bean 的一些钩子。诸如日志记录、缓存、事务管理和异常处理等横切关注点可以使用面向方面的编程AOP)围绕这些 bean 进行编织。这导致了更易于维护的代码。

依赖注入类型

在前面的示例中,我们使用了 setter 方法来注入依赖项。经常使用的两种依赖注入类型是:

  • setter 注入

  • 构造函数注入

setter 注入

setter 注入用于通过 setter 方法注入依赖项。在以下示例中,DataService的实例使用了 setter 注入:

    public class BusinessServiceImpl { 
      private DataService dataService; 
      @Autowired 
      public void setDataService(DataService dataService) { 
        this.dataService = dataService; 
      } 
    }

实际上,为了使用 setter 注入,甚至不需要声明 setter 方法。如果在变量上指定了@Autowired,Spring 会自动使用 setter 注入。因此,以下代码就是您为DataService进行 setter 注入所需要的全部内容:

    public class BusinessServiceImpl { 
      @Autowired 
      private DataService dataService; 
    }

构造函数注入

构造函数注入,另一方面,使用构造函数来注入依赖项。以下代码显示了如何在DataService中使用构造函数进行注入:

    public class BusinessServiceImpl { 
      private DataService dataService; 
      @Autowired 
      public BusinessServiceImpl(DataService dataService) { 
        super(); 
        this.dataService = dataService; 
      } 
    }

当您运行具有前面BusinessServiceImpl实现的代码时,您将在日志中看到此语句,断言使用构造函数进行了自动装配:

    Autowiring by type from bean name 'businessServiceImpl' via 
    constructor to bean named 'dataServiceImpl'

构造函数与 setter 注入

最初,在基于 XML 的应用程序上下文中,我们使用构造函数注入来处理强制依赖项,使用 setter 注入来处理非强制依赖项。

然而,需要注意的一点是,当我们在字段或方法上使用@Autowired时,默认情况下依赖项是必需的。如果没有可用于@Autowired字段的候选项,自动装配将失败并抛出异常。因此,在 Java 应用程序上下文中,选择并不那么明显。

使用 setter 注入会导致对象在创建过程中状态发生变化。对于不可变对象的粉丝来说,构造函数注入可能是更好的选择。有时使用 setter 注入可能会隐藏一个类具有大量依赖项的事实。使用构造函数注入会使这一点显而易见,因为构造函数的大小会增加。

Spring bean 作用域

Spring bean 可以创建多种作用域。默认作用域是单例模式。

由于单例 bean 只有一个实例,因此不能包含特定于请求的任何数据。

可以在任何 Spring bean 上使用@Scope注解来提供作用域:

    @Service 
    @Scope("singleton") 
    public class BusinessServiceImpl implements BusinessService

以下表格显示了可用于 bean 的不同作用域类型:

作用域 用途
Singleton 默认情况下,所有 bean 都是单例作用域。每个 Spring IoC 容器实例只使用一次这样的 bean 实例。即使有多个对 bean 的引用,它也只在容器中创建一次。单个实例被缓存并用于使用此 bean 的所有后续请求。重要的是要指出,Spring 单例作用域是一个 Spring 容器中的一个对象。如果在单个 JVM 中有多个 Spring 容器,则可以有多个相同 bean 的实例。因此,Spring 单例作用域与典型的单例定义有些不同。
Prototype 每次从 Spring 容器请求 bean 时都会创建一个新实例。如果 bean 包含状态,建议您为其使用原型范围。
request 仅在 Spring Web 上下文中可用。为每个 HTTP 请求创建一个 bean 的新实例。一旦请求处理完成,bean 就会被丢弃。适用于保存特定于单个请求的数据的 bean。
session 仅在 Spring Web 上下文中可用。为每个 HTTP 会话创建一个 bean 的新实例。适用于特定于单个用户的数据,例如 Web 应用程序中的用户权限。
application 仅在 Spring Web 上下文中可用。每个 Web 应用程序一个 bean 实例。适用于特定环境的应用程序配置等内容。

Java 与 XML 配置

随着 Java 5 中注解的出现,基于 Java 的配置在基于 Spring 的应用程序中得到了广泛使用。如果必须在基于 Java 的配置和基于 XML 的配置之间进行选择,应该做出什么样的选择?

Spring 对基于 Java 和基于 XML 的配置提供了同样良好的支持。因此,选择权在于程序员及其团队。无论做出何种选择,都很重要的是在团队和项目之间保持一致。在做出选择时,可能需要考虑以下一些事项:

  • 注解导致 bean 定义更短、更简单。

  • 注解比基于 XML 的配置更接近其适用的代码。

  • 使用注解的类不再是简单的 POJO,因为它们使用了特定于框架的注解。

  • 使用注解时出现自动装配问题可能很难解决,因为连线不再是集中的,也没有明确声明。

  • 如果它被打包在应用程序包装之外--WAR 或 EAR,使用 Spring 上下文 XML 可能会有更灵活的连线优势。这将使我们能够为集成测试设置不同的设置,例如。

深入了解@Autowired 注解

当在依赖项上使用@Autowired时,应用程序上下文会搜索匹配的依赖项。默认情况下,所有自动装配的依赖项都是必需的。

可能的结果如下:

  • 找到一个匹配项:这就是你要找的依赖项

  • 找到多个匹配项:自动装配失败

  • 找不到匹配项:自动装配失败

可以通过两种方式解决找到多个候选项的情况:

  • 使用@Primary注解标记其中一个候选项作为要使用的候选项

  • 使用@Qualifier进一步限定自动装配

@Primary 注解

当在 bean 上使用@Primary注解时,它将成为在自动装配特定依赖项时可用的多个候选项中的主要候选项。

在以下示例中,有两种排序算法可用:QuickSortMergeSort。如果组件扫描找到它们两个,QuickSort将用于在SortingAlgorithm上连线任何依赖项,因为有@Primary注解:

    interface SortingAlgorithm { 
    } 
    @Component 
    class MergeSort implements SortingAlgorithm { 
      // Class code here 
    } 
   @Component 
   @Primary 
   class QuickSort implements SortingAlgorithm { 
     // Class code here 
   }

@Qualifier 注解

@Qualifier注解可用于给出对 Spring bean 的引用。该引用可用于限定需要自动装配的依赖项。

在以下示例中,有两种排序算法可用:QuickSortMergeSort。但由于SomeService类中使用了@Qualifier("mergesort"),因此MergeSort成为了自动装配选定的候选依赖项,因为它也在其上定义了mergesort限定符。

    @Component 
    @Qualifier("mergesort") 
    class MergeSort implements SortingAlgorithm { 
      // Class code here 
    } 
    @Component 
    class QuickSort implements SortingAlgorithm { 
     // Class code here 
    } 
    @Component 
    class SomeService { 
      @Autowired 
      @Qualifier("mergesort") 
      SortingAlgorithm algorithm; 
    }

其他重要的 Spring 注解

Spring 在定义 bean 和管理 bean 的生命周期方面提供了很大的灵活性。还有一些其他重要的 Spring 注解,我们将在下表中讨论。

注解 用途
@ScopedProxy 有时,我们需要将一个请求或会话作用域的 bean 注入到单例作用域的 bean 中。在这种情况下,@ScopedProxy注解提供了一个智能代理,可以注入到单例作用域的 bean 中。

@Component@Service@Controller@Repository | @Component是定义 Spring bean 的最通用方式。其他注解与它们关联的上下文更具体。

  • @Service 用于业务服务层

  • @Repository 用于数据访问对象DAO

  • @Controller 用于表示组件

|

@PostConstruct 在任何 Spring bean 上,可以使用@PostConstruct注解提供一个 post construct 方法。这个方法在 bean 完全初始化了依赖项后被调用。这将在 bean 生命周期中只被调用一次。
@PreDestroy 在任何 Spring bean 上,可以使用@PreDestroy注解提供一个 predestroy 方法。这个方法在 bean 从容器中移除之前被调用。这可以用来释放 bean 持有的任何资源。

探索上下文和依赖注入

CDI 是 Java EE 将 DI 引入到 Java EE 的尝试。虽然不像 Spring 那样功能齐全,但 CDI 旨在标准化 DI 的基本方式。Spring 支持JSR-330中定义的标准注解。在大多数情况下,这些注解与 Spring 注解的处理方式相同。

在我们使用 CDI 之前,我们需要确保已经包含了 CDI jar 的依赖项。以下是代码片段:

    <dependency> 
      <groupId>javax.inject</groupId> 
      <artifactId>javax.inject</artifactId> 
      <version>1</version> 
    </dependency>

在这个表中,让我们比较一下 CDI 注解和 Spring Framework 提供的注解。应该注意的是,@Value@Required@Lazy Spring 注解没有等价的 CDI 注解。

CDI 注解 与 Spring 注解的比较
@Inject 类似于@Autowired。一个微不足道的区别是@Inject上没有 required 属性。
@Named @Named类似于@Component。用于标识命名组件。此外,@Named也可以用于类似于@Qualifier Spring 注解的 bean 限定。在一个依赖项的自动装配中有多个候选项可用时,这是很有用的。
@Singleton 类似于 Spring 注解@Scope("singleton")。
@Qualifier 与 Spring 中同名的注解类似--@Qualifier

CDI 的一个例子

当我们使用 CDI 时,不同类上的注解看起来是这样的。在如何创建和启动 Spring 应用上下文方面没有变化。

CDI 对@Repository@Controller@Service@Component没有区别。我们使用@Named代替所有前面的注解。

在示例中,我们对DataServiceImplBusinessServiceImpl使用了@Named。我们使用@InjectdataService注入到BusinessServiceImpl中(而不是使用@Autowired):

    @Named //Instead of @Repository 
    public class DataServiceImpl implements DataService 
    @Named //Instead of @Service 
    public class BusinessServiceImpl { 
       @Inject //Instead of @Autowired 
       private DataService dataService;

总结

依赖注入(或 IoC)是 Spring 的关键特性。它使代码松散耦合且可测试。理解 DI 是充分利用 Spring Framework 的关键。

在本章中,我们深入研究了 DI 和 Spring Framework 提供的选项。我们还看了编写可测试代码的示例,并编写了一些单元测试。

在下一章中,我们将把注意力转向 Spring MVC,这是最流行的 Java Web MVC 框架。我们将探讨 Spring MVC 如何使 Web 应用程序的开发更加简单。

第三章:使用 Spring MVC 构建 Web 应用程序

Spring MVC 是用于开发 Java Web 应用程序的最流行的 Web 框架。Spring MVC 的优势在于其清晰的、松散耦合的架构。通过对控制器、处理程序映射、视图解析器和普通的 Java 对象POJO)命令 bean 的角色进行清晰定义,Spring MVC 利用了所有核心 Spring 功能--如依赖注入和自动装配--使得创建 Web 应用程序变得简单。它支持多种视图技术,也具有可扩展性。

虽然 Spring MVC 可以用于创建 REST 服务,但我们将在第五章中讨论使用 Spring Boot 构建微服务

在本章中,我们将重点介绍 Spring MVC 的基础知识,并提供简单的示例。

在本章中,我们将涵盖以下主题:

  • Spring MVC 架构

  • DispatcherServlet、视图解析器、处理程序映射和控制器所扮演的角色

  • 模型属性和会话属性

  • 表单绑定和验证

  • 与 Bootstrap 集成

  • Spring 安全的基础知识

  • 为控制器编写简单的单元测试

Java Web 应用程序架构

在过去的几十年里,我们开发 Java Web 应用程序的方式已经发生了变化。我们将讨论开发 Java Web 应用程序的不同架构方法,并看看 Spring MVC 适用于哪些方面:

  • 模型 1 架构

  • 模型 2 或 MVC 架构

  • 带有前端控制器的模型 2

模型 1 架构

模型 1 架构是用于开发基于 Java 的 Web 应用程序的初始架构风格之一。一些重要的细节如下:

  • JSP 页面直接处理来自浏览器的请求

  • JSP 页面使用包含简单 Java bean 的模型

  • 在这种架构风格的一些应用中,甚至 JSP 执行了对数据库的查询

  • JSP 还处理流程逻辑:下一个要显示的页面

以下图片代表典型的模型 1 架构:

这种方法存在许多缺点,导致快速搁置和其他架构的演变。以下是一些重要的缺点:

  • 几乎没有关注点分离:JSP 负责检索数据,显示数据,决定下一个要显示的页面(流程),有时甚至包括业务逻辑

  • 复杂的 JSP:因为 JSP 处理了很多逻辑,它们很庞大且难以维护

模型 2 架构

模型 2 架构出现是为了解决处理多个责任的复杂 JSP 所涉及的复杂性。这构成了 MVC 架构风格的基础。以下图片代表典型的模型 2 架构:

模型 2 架构在模型、视图和控制器之间有明确的角色分离。这导致了更易维护的应用程序。一些重要的细节如下:

  • 模型:表示用于生成视图的数据。

  • 视图:使用模型来呈现屏幕。

  • 控制器:控制流程。从浏览器获取请求,填充模型并重定向到视图。示例是前面图中的Servlet1Servlet2

模型 2 前端控制器架构

在模型 2 架构的基本版本中,浏览器的请求直接由不同的 servlet(或控制器)处理。在许多业务场景中,我们希望在处理请求之前在 servlet 中执行一些常见的操作。例如,确保已登录的用户有权执行请求。这是一个常见的功能,您不希望在每个 servlet 中实现。

在模型 2前端控制器架构中,所有请求都流入一个称为前端控制器的单个控制器。

下面的图片代表典型的模型 2 前端控制器架构:

以下是典型前端控制器的一些职责:

  • 它决定了哪个控制器执行请求

  • 它决定了要渲染哪个视图

  • 它提供了添加更多常见功能的规定

  • Spring MVC 使用带有 Front Controller 的 MVC 模式。前端控制器称为DispatcherServlet。我们稍后将讨论 DispatcherServlet。

基本流程

Spring MVC 使用了修改版的 Model 2 Front Controller 架构。在我们深入了解 Spring MVC 的工作原理之前,我们将专注于使用 Spring MVC 创建一些简单的 Web 流程。在本节中,我们将使用 Spring MVC 创建六种典型的 Web 应用程序流程。流程如下所示:

  • 流程 1:没有视图的控制器;自己提供内容

  • 流程 2:带有视图(JSP)的控制器

  • 流程 3:带有视图并使用 ModelMap 的控制器

  • 流程 4:带有视图并使用 ModelAndView 的控制器

  • 流程 5:简单表单的控制器

  • 流程 6:带有验证的简单表单的控制器

在每个流程结束时,我们将讨论如何对控制器进行单元测试。

基本设置

在我们开始第一个流程之前,我们需要设置应用程序以使用 Spring MVC。在下一节中,我们将开始了解如何在 Web 应用程序中设置 Spring MVC。

我们使用 Maven 来管理我们的依赖关系。设置一个简单的 Web 应用程序涉及以下步骤:

  1. 添加 Spring MVC 的依赖。

  2. 将 DispatcherServlet 添加到web.xml中。

  3. 创建一个 Spring 应用上下文。

添加 Spring MVC 的依赖

让我们从在pom.xml中添加 Spring MVC 依赖开始。以下代码显示了要添加的依赖项。由于我们使用 Spring BOM,我们不需要指定 artifact 版本:

    <dependency> 
      <groupId>org.springframework</groupId> 
      <artifactId>spring-webmvc</artifactId> 
    </dependency>

DispatcherServlet 是 Front Controller 模式的一种实现。Spring MVC 的任何请求都将由前端控制器 DispatcherServlet 处理。

将 DispatcherServlet 添加到 web.xml

为了实现这一点,我们需要将 DispatcherServlet 添加到web.xml中。让我们看看如何做到这一点:

    <servlet> 
      <servlet-name>spring-mvc-dispatcher-servlet</servlet-name>    
      <servlet-class> 
        org.springframework.web.servlet.DispatcherServlet 
      </servlet-class> 
      <init-param> 
        <param-name>contextConfigLocation</param-name> 
        <param-value>/WEB-INF/user-web-context.xml</param-value> 
      </init-param> 
        <load-on-startup>1</load-on-startup> 
    </servlet> 
    <servlet-mapping> 
      <servlet-name>spring-mvc-dispatcher-servlet</servlet-name> 
      <url-pattern>/</url-pattern> 
    </servlet-mapping>

第一部分是定义一个 servlet。我们还定义了一个上下文配置位置,/WEB-INF/user-web-context.xml。我们将在下一步中定义一个 Spring 上下文。在第二部分中,我们正在定义一个 servlet 映射。我们正在将 URL /映射到 DispatcherServlet。因此,所有请求都将由 DispatcherServlet 处理。

创建 Spring 上下文

现在我们在web.xml中定义了 DispatcherServlet,我们可以继续创建我们的 Spring 上下文。最初,我们将创建一个非常简单的上下文,而不是真正定义任何具体内容:

    <beans > <!-Schema Definition removed --> 
       <context:component-scan  
       base-package="com.mastering.spring.springmvc"  /> 
       <mvc:annotation-driven /> 
    </beans>

我们正在为com.mastering.spring.springmvc包定义一个组件扫描,以便在此包中创建和自动装配所有的 bean 和控制器。

使用<mvc:annotation-driven/>初始化了 Spring MVC 支持的许多功能,例如:

  • 请求映射

  • 异常处理

  • 数据绑定和验证

  • 当使用@RequestBody注解时,自动转换(例如 JSON)

这就是我们需要设置 Spring MVC 应用程序的所有设置。我们已经准备好开始第一个流程了。

流程 1 - 没有视图的简单控制器流程

让我们从一个简单的流程开始,通过在屏幕上显示一些简单的文本来输出 Spring MVC 控制器的内容。

创建一个 Spring MVC 控制器

让我们创建一个简单的 Spring MVC 控制器,如下所示:

    @Controller 
    public class BasicController { 
      @RequestMapping(value = "/welcome") 
      @ResponseBody 
    public String welcome() { 
      return "Welcome to Spring MVC"; 
     } 
   }

这里需要注意的一些重要事项如下:

  • @Controller:这定义了一个 Spring MVC 控制器,可以包含请求映射--将 URL 映射到控制器方法。

  • @RequestMapping(value = "/welcome"):这定义了 URL /welcomewelcome方法的映射。当浏览器发送请求到/welcome时,Spring MVC 会执行welcome方法。

  • @ResponseBody:在这个特定的上下文中,welcome方法返回的文本被发送到浏览器作为响应内容。@ResponseBody做了很多魔术--特别是在 REST 服务的上下文中。我们将在第五章中讨论这个问题,使用 Spring Boot 构建微服务

运行 Web 应用程序

我们使用 Maven 和 Tomcat 7 来运行这个 Web 应用程序。

Tomcat 7 服务器默认在 8080 端口启动。

我们可以通过调用mvn tomcat7:run命令来运行服务器。

当在浏览器上访问http://localhost:8080/welcomeURL 时,屏幕上的显示如下截图所示:

单元测试

单元测试是开发可维护应用程序的一个非常重要的部分。我们将使用 Spring MVC Mock 框架来对本章中编写的控制器进行单元测试。我们将添加 Spring 测试框架的依赖来使用 Spring MVC Mock 框架:

    <dependency> 
      <groupId>org.springframework</groupId> 
      <artifactId>spring-test</artifactId> 
      <scope>test</scope> 
    </dependency>

我们将采取以下方法:

  1. 设置要测试的控制器。

  2. 编写测试方法。

设置要测试的控制器

我们要测试的控制器是BasicController。创建单元测试的约定是类名后缀为Test。我们将创建一个名为BasicControllerTest的测试类。

基本设置如下所示:

    public class BasicControllerTest { 
      private MockMvc mockMvc; 
      @Before 
      public void setup() { 
        this.mockMvc = MockMvcBuilders.standaloneSetup( 
        new BasicController()) 
        .build(); 
      } 
     }

需要注意的一些重要事项如下:

  • mockMvc:这个变量可以在不同的测试中使用。因此,我们定义了MockMvc类的一个实例变量。

  • @Before setup:这个方法在每个测试之前运行,以初始化MockMvc

  • MockMvcBuilders.standaloneSetup(new BasicController()).build():这行代码构建了一个MockMvc实例。它初始化 DispatcherServlet 来为配置的控制器(在这个例子中是BasicController)提供请求服务。

编写测试方法

完整的Test方法如下所示:

    @Test 
    public void basicTest() throws Exception { 
      this.mockMvc 
      .perform( 
      get("/welcome") 
      .accept(MediaType.parseMediaType 
      ("application/html;charset=UTF-8"))) 
      .andExpect(status().isOk()) 
      .andExpect( content().contentType 
      ("application/html;charset=UTF-8")) 
      .andExpect(content(). 
       string("Welcome to Spring MVC")); 
    }

需要注意的一些重要事项如下:

  • MockMvc mockMvc.perform:这个方法执行请求并返回一个 ResultActions 的实例,允许链式调用。在这个例子中,我们正在链接 andExpect 调用来检查期望。

  • get("/welcome").accept(MediaType.parseMediaType("application/html;charset=UTF-8")):这创建了一个接受application/html媒体类型响应的 HTTP get 请求。

  • andExpect:这个方法用于检查期望。如果期望没有被满足,这个方法将使测试失败。

  • status().isOk():这使用 ResultMatcher 来检查响应状态是否是成功请求的状态-200。

  • content().contentType("application/html;charset=UTF-8")):这使用 ResultMatcher 来检查响应的内容类型是否与指定的内容类型相匹配。

  • content().string("Welcome to Spring MVC"):这使用 ResultMatcher 来检查响应内容是否包含指定的字符串。

流程 2 - 带有视图的简单控制器流程

在前面的流程中,要在浏览器上显示的文本是在控制器中硬编码的。这不是一个好的做法。在浏览器上显示的内容通常是从视图生成的。最常用的选项是 JSP。

在这个流程中,让我们从控制器重定向到一个视图。

Spring MVC 控制器

与前面的例子类似,让我们创建一个简单的控制器。考虑一个控制器的例子:

    @Controller 
    public class BasicViewController { 
      @RequestMapping(value = "/welcome-view") 
      public String welcome() { 
        return "welcome"; 
       } 
    }

需要注意的一些重要事项如下:

  • @RequestMapping(value = "/welcome-view"):我们正在映射一个 URL/welcome-view

  • public String welcome():这个方法上没有@RequestBody注解。所以,Spring MVC 尝试将返回的字符串welcome与一个视图匹配。

创建一个视图-JSP

让我们在src/main/webapp/WEB-INF/views/welcome.jsp文件夹中创建welcome.jsp,内容如下:

    <html> 
      <head> 
        <title>Welcome</title> 
      </head> 
      <body> 
        <p>Welcome! This is coming from a view - a JSP</p> 
      </body> 
    </html>

这是一个简单的 HTML,包含头部、主体和主体中的一些文本。

Spring MVC 必须将从welcome方法返回的字符串映射到/WEB-INF/views/welcome.jsp的实际 JSP。这个魔术是如何发生的呢?

视图解析器

视图解析器将视图名称解析为实际的 JSP 页面。

此示例中的视图名称为welcome,我们希望它解析为/WEB-INF/views/welcome.jsp

可以在 spring 上下文/WEB-INF/user-web-context.xml中配置视图解析器。以下是代码片段:

    <bean class="org.springframework.web.
    servlet.view.InternalResourceViewResolver"> 
     <property name="prefix"> 
       <value>/WEB-INF/views/</value> 
     </property> 
     <property name="suffix"> 
       <value>.jsp</value> 
     </property> 
    </bean>

需要注意的几个重要点:

  • org.springframework.web.servlet.view.InternalResourceViewResolver:支持 JSP 的视图解析器。通常使用JstlView。它还支持使用TilesView的 tiles。

  • <property name="prefix"> <value>/WEB-INF/views/</value> </property><property name="suffix"> <value>.jsp</value> </property>:将前缀和后缀映射到视图解析器使用的值。视图解析器从控制器方法中获取字符串并解析为视图:prefix + viewname + suffix。因此,视图名称 welcome 解析为/WEB-INF/views/welcome.jsp

以下是当 URL 被访问时屏幕上的截图:

单元测试

MockMvc 框架的独立设置创建了 DispatcherServlet 所需的最低基础设施。如果提供了视图解析器,它可以执行视图解析。但是,它不会执行视图。因此,在独立设置的单元测试中,我们无法验证视图的内容。但是,我们可以检查是否传递了正确的视图。

在这个单元测试中,我们想要设置BasicViewController,执行一个对/welcome-view的 get 请求,并检查返回的视图名称是否为welcome。在以后的部分中,我们将讨论如何执行集成测试,包括视图的渲染。就这个测试而言,我们将限制我们的范围以验证视图名称。

设置要测试的控制器

这一步与之前的流程非常相似。我们想要测试BasicViewController。我们使用BasicViewController实例化 MockMvc。我们还配置了一个简单的视图解析器:

    public class BasicViewControllerTest { 
      private MockMvc mockMvc; 
      @Before 
      public void setup() { 
        this.mockMvc = MockMvcBuilders.standaloneSetup 
        (new BasicViewController()) 
        .setViewResolvers(viewResolver()).build(); 
       } 
      private ViewResolver viewResolver() { 
        InternalResourceViewResolver viewResolver =  
        new InternalResourceViewResolver(); 
        viewResolver.setViewClass(JstlView.class); 
        viewResolver.setPrefix("/WEB-INF/jsp/"); 
        viewResolver.setSuffix(".jsp"); 
       return viewResolver; 
      } 
    }

编写测试方法

完整的测试方法如下所示:

    @Test 
    public void testWelcomeView() throws Exception { 
      this.mockMvc 
      .perform(get("/welcome-view") 
      .accept(MediaType.parseMediaType( 
      "application/html;charset=UTF-8"))) 
      .andExpect(view().name("welcome")); 
    }

需要注意的几个重要事项如下:

  • get("/welcome-model-view"):执行对指定 URL 的 get 请求

  • view().name("welcome"):使用 Result Matcher 来检查返回的视图名称是否与指定的相同

流程 3 - 控制器重定向到具有模型的视图

通常,为了生成视图,我们需要向其传递一些数据。在 Spring MVC 中,可以使用模型将数据传递给视图。在这个流程中,我们将使用一个简单的属性设置模型,并在视图中使用该属性。

Spring MVC 控制器

让我们创建一个简单的控制器。考虑以下示例控制器:

    @Controller 
    public class BasicModelMapController { 
      @RequestMapping(value = "/welcome-model-map") 
      public String welcome(ModelMap model) { 
        model.put("name", "XYZ"); 
      return "welcome-model-map"; 
     } 
   }

需要注意的几个重要事项如下:

  • @RequestMapping(value = "/welcome-model-map"):映射的 URI 为/welcome-model-map

  • public String welcome(ModelMap model):添加的新参数是ModelMap model。Spring MVC 将实例化一个模型,并使其对此方法可用。放入模型中的属性将可以在视图中使用。

  • model.put("name", "XYZ"):向模型中添加一个名为name值为XYZ的属性。

创建一个视图

让我们使用在控制器中设置的模型属性name创建一个视图。让我们在WEB-INF/views/welcome-model-map.jsp路径下创建一个简单的 JSP:

    Welcome ${name}! This is coming from a model-map - a JSP

需要注意的一件事是:

  • ${name}:使用表达式语言EL)语法来访问模型中的属性。

以下是当 URL 被访问时屏幕上的截图:

单元测试

在这个单元测试中,我们想要设置BasicModelMapController,执行一个对/welcome-model-map的 get 请求,并检查模型是否具有预期的属性,以及返回的视图名称是否符合预期。

设置要测试的控制器

这一步与上一个流程非常相似。我们使用BasicModelMapController实例化 Mock MVC:

    this.mockMvc = MockMvcBuilders.standaloneSetup 
      (new BasicModelMapController()) 
      .setViewResolvers(viewResolver()).build();

编写测试方法

完整的测试方法如下所示:

    @Test 
    public void basicTest() throws Exception { 
      this.mockMvc 
      .perform( 
      get("/welcome-model-map") 
      .accept(MediaType.parseMediaType 
      ("application/html;charset=UTF-8"))) 
      .andExpect(model().attribute("name", "XYZ")) 
      .andExpect(view().name("welcome-model-map")); 
    }

需要注意的几个重要事项:

  • get("/welcome-model-map"):执行对指定 URL 的get请求

  • model().attribute("name", "XYZ"):结果匹配器,用于检查模型是否包含指定属性name和指定值XYZ

  • view().name("welcome-model-map"):结果匹配器,用于检查返回的视图名称是否与指定的相同

流程 4 - 控制器重定向到带有 ModelAndView 的视图

在上一个流程中,我们返回了一个视图名称,并在模型中填充了要在视图中使用的属性。Spring MVC 提供了一种使用ModelAndView的替代方法。控制器方法可以返回一个带有视图名称和模型中适当属性的ModelAndView对象。在这个流程中,我们将探讨这种替代方法。

Spring MVC 控制器

看一下下面的控制器:

    @Controller 
    public class BasicModelViewController { 
     @RequestMapping(value = "/welcome-model-view") 
      public ModelAndView welcome(ModelMap model) { 
        model.put("name", "XYZ"); 
        return new ModelAndView("welcome-model-view", model); 
      } 
   }

需要注意的几个重要事项如下:

  • @RequestMapping(value = "/welcome-model-view"):映射的 URI 是/welcome-model-view

  • public ModelAndView welcome(ModelMap model):请注意,返回值不再是 String。它是ModelAndView

  • return new ModelAndView("welcome-model-view", model):使用适当的视图名称和模型创建ModelAndView对象。

创建一个视图

让我们使用在控制器中设置的模型属性name创建一个视图。在/WEB-INF/views/welcome-model-view.jsp路径下创建一个简单的 JSP:

    Welcome ${name}! This is coming from a model-view - a JSP

当 URL 被访问时,屏幕上会显示如下截图:

单元测试

对于这个流程的单元测试与上一个流程类似。我们需要检查是否返回了预期的视图名称。

流程 5 - 控制器重定向到带有表单的视图

现在让我们把注意力转移到创建一个简单的表单,以从用户那里获取输入。

需要以下步骤:

  • 创建一个简单的 POJO。我们想创建一个用户。我们将创建一个 POJO 用户。

  • 创建一对控制器方法——一个用于显示表单,另一个用于捕获表单中输入的详细信息。

  • 创建一个带有表单的简单视图。

创建一个命令或表单备份对象

POJO 代表普通的旧 Java 对象。通常用于表示遵循典型 JavaBean 约定的 bean。通常,它包含具有 getter 和 setter 的私有成员变量和一个无参数构造函数。

我们将创建一个简单的 POJO 作为命令对象。类的重要部分列在下面:

    public class User { 
      private String guid; 
      private String name; 
      private String userId; 
      private String password; 
      private String password2; 
      //Constructor 
      //Getters and Setters   
      //toString 
    }

需要注意的几个重要事项如下:

  • 这个类没有任何注释或与 Spring 相关的映射。任何 bean 都可以充当表单备份对象。

  • 我们将在表单中捕获name用户 ID密码。我们有一个密码确认字段password2和唯一标识符字段 guid。

  • 为简洁起见,构造函数、getter、setter 和 toString 方法未显示。

显示表单的控制器方法

让我们从创建一个带有记录器的简单控制器开始:

    @Controller 
    public class UserController { 
      private Log logger = LogFactory.getLog 
      (UserController.class); 
     }

让我们在控制器中添加以下方法:

    @RequestMapping(value = "/create-user",  
    method = RequestMethod.GET) 
    public String showCreateUserPage(ModelMap model) { 
      model.addAttribute("user", new User()); 
      return "user"; 
   }

需要注意的重要事项如下:

  • @RequestMapping(value = "/create-user", method = RequestMethod.GET):我们正在映射一个/create-user URI。这是第一次使用 method 属性指定Request方法。此方法仅在 HTTP Get 请求时调用。HTTP Get请求通常用于显示表单。这不会被其他类型的 HTTP 请求调用,比如 Post。

  • public String showCreateUserPage(ModelMap model):这是一个典型的控制方法。

  • model.addAttribute("user", new User()):这用于使用空表单备份对象设置模型。

创建带有表单的视图

Java Server Pages 是 Spring Framework 支持的视图技术之一。Spring Framework 通过提供标签库,使得使用 JSP 轻松创建视图变得容易。这包括各种表单元素、绑定、验证、设置主题和国际化消息的标签。在本例中,我们将使用 Spring MVC 标签库以及标准的 JSTL 标签库来创建我们的视图。

让我们从创建/WEB-INF/views/user.jsp文件开始。

首先,让我们添加要使用的标签库的引用:

    <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> 
    <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> 
    <%@ taglib uri="http://www.springframework.org/tags/form"  
      prefix="form"%> 
    <%@ taglib uri="http://www.springframework.org/tags"
      prefix="spring"%>

前两个条目是 JSTL 核心和格式化标签库。我们将广泛使用 Spring 表单标签。我们提供一个prefix作为引用标签的快捷方式。

让我们先创建一个只有一个字段的表单:

    <form:form method="post" modelAttribute="user"> 
     <fieldset> 
       <form:label path="name">Name</form:label> 
       <form:input path="name"  
       type="text" required="required" /> 
     </fieldset> 
   </form:form>

需要注意的重要事项如下:

  • <form:form method="post" modelAttribute="user">:这是 Spring 表单标签库中的form标签。指定了两个属性。表单中的数据使用 post 方法发送。第二个属性modelAttribute指定了模型中充当表单后备对象的属性。在模型中,我们添加了一个名为 user 的属性。我们使用该属性作为modelAttribute

  • <fieldset>:这是 HTML 元素,用于对一组相关控件(标签、表单字段和验证消息)进行分组。

  • <form:label path="name">Name</form:label>:这是 Spring 表单标签,用于显示标签。path 属性指定了该标签应用于的字段名称(来自 bean)。

  • <form:input path="name" type="text" required="required" />:这是 Spring 表单标签,用于创建文本输入字段。path属性指定了该输入字段要映射到的 bean 中的字段名称。required 属性表示这是一个required字段。

当我们使用 Spring 表单标签时,表单后备对象(modelAttribute="user")中的值会自动绑定到表单上,并且在提交表单时,表单中的值会自动绑定到表单后备对象上。

包括名称和用户 ID 字段在内的更完整的表单标签列表如下:

    <form:form method="post" modelAttribute="user"> 
    <form:hidden path="guid" /> 
    <fieldset> 
      <form:label path="name">Name</form:label> 
      <form:input path="name"  
       type="text" required="required" /> 
    </fieldset> 
    <fieldset> 
      <form:label path="userId">User Id</form:label> 
      <form:input path="userId"  
       type="text" required="required" /> 
    </fieldset> 
    <!-password and password2 fields not shown for brewity--> 
    <input class="btn btn-success" type="submit" value="Submit" /> 
    </form:form>

控制器获取方法来处理表单提交

当用户提交表单时,浏览器会发送一个 HTTP POST请求。现在让我们创建一个方法来处理这个请求。为了保持简单,我们将记录表单对象的内容。该方法的完整列表如下:

    @RequestMapping(value = "/create-user", method = 
    RequestMethod.POST) 
    public String addTodo(User user) { 
      logger.info("user details " + user); 
      return "redirect:list-users"; 
    }

一些重要的细节如下:

  • @RequestMapping(value = "/create-user", method = RequestMethod.POST):由于我们要处理表单提交,我们使用RequestMethod.POST方法。

  • public String addTodo(User user):我们使用表单后备对象作为参数。Spring MVC 将自动将表单中的值绑定到表单后备对象。

  • logger.info("user details " + user):记录用户的详细信息。

  • 返回redirect:list-users:通常,在提交表单后,我们会将数据库的详细信息保存并将用户重定向到不同的页面。在这里,我们将用户重定向到/list-users。当我们使用redirect时,Spring MVC 会发送一个带有状态302的 HTTP 响应;也就是说,REDIRECT到新的 URL。浏览器在处理302响应时,会将用户重定向到新的 URL。虽然POST/REDIRECT/GET模式并不是解决重复表单提交问题的完美方法,但它确实减少了发生的次数,特别是在视图渲染后发生的次数。

列出用户的代码非常简单,如下所示:

    @RequestMapping(value = "/list-users",  
    method = RequestMethod.GET) 
    public String showAllUsers() { 
      return "list-users"; 
    }

单元测试

当我们在下一个流程中添加验证时,我们将讨论单元测试。

流程 6 - 在上一个流程中添加验证

在上一个流程中,我们添加了一个表单。但是,我们没有验证表单中的值。虽然我们可以编写 JavaScript 来验证表单内容,但在服务器上进行验证总是更安全的。在本流程中,让我们使用 Spring MVC 在服务器端对我们之前创建的表单添加验证。

Spring MVC 与 Bean Validation API 提供了很好的集成。 JSR 303JSR 349分别定义了 Bean Validation API 的规范(版本 1.0 和 1.1),而 Hibernate Validator 是参考实现。

Hibernate Validator 依赖

让我们从将 Hibernate Validator 添加到我们的项目pom.xml开始:

    <dependency> 
      <groupId>org.hibernate</groupId> 
      <artifactId>hibernate-validator</artifactId> 
      <version>5.0.2.Final</version> 
    </dependency>

Bean 上的简单验证

Bean Validation API 指定了可以在 bean 的属性上指定的一些验证。看一下以下列表:

   @Size(min = 6, message = "Enter at least 6 characters") 
   private String name; 
   @Size(min = 6, message = "Enter at least 6 characters") 
   private String userId; 
   @Size(min = 8, message = "Enter at least 8 characters") 
   private String password; 
   @Size(min = 8, message = "Enter at least 8 characters") 
   private String password2;

需要注意的一件重要的事情如下:

  • @Size(min = 6, message = "Enter at least 6 characters"):指定字段至少应有六个字符。如果验证未通过,则使用消息属性中的文本作为验证错误消息。

使用 Bean Validation 可以执行的其他验证如下:

  • @NotNull:它不应为 null

  • @Size(min =5, max = 50):最大 50 个字符,最小 5 个字符。

  • @Past:应该是过去的日期

  • @Future:应该是未来的日期

  • @Pattern:应该匹配提供的正则表达式

  • @Max:字段的最大值

  • @Min:字段的最小值

现在让我们专注于使控制器方法在提交时验证表单。完整的方法列表如下:

    @RequestMapping(value = "/create-user-with-validation",  
    method = RequestMethod.POST) 
    public String addTodo(@Valid User user, BindingResult result) { 
      if (result.hasErrors()) { 
        return "user"; 
       } 
      logger.info("user details " + user); 
      return "redirect:list-users"; 
    }

以下是一些重要的事项:

  • public String addTodo(@Valid User user, BindingResult result):当使用@Valid注释时,Spring MVC 验证 bean。验证的结果在BindingResult实例 result 中可用。

  • if (result.hasErrors()):检查是否有任何验证错误。

  • return "user":如果有验证错误,我们将用户发送回用户页面。

我们需要增强user.jsp以在验证错误时显示验证消息。其中一个字段的完整列表如下所示。其他字段也必须类似地更新:

    <fieldset> 
      <form:label path="name">Name</form:label> 
      <form:input path="name" type="text" required="required" /> 
      <form:errors path="name" cssClass="text-warning"/> 
    </fieldset>

<form:errors path="name" cssClass="text-warning"/>:这是 Spring 表单标签,用于显示与指定路径中的字段名称相关的错误。我们还可以分配用于显示验证错误的 CSS 类。

自定义验证

可以使用@AssertTrue注释实现更复杂的自定义验证。以下是添加到User类的示例方法列表:

    @AssertTrue(message = "Password fields don't match") 
    private boolean isValid() { 
      return this.password.equals(this.password2); 
    }

@AssertTrue(message = "Password fields don't match")是在验证失败时要显示的消息。

可以在这些方法中实现具有多个字段的复杂验证逻辑。

单元测试

此部分的单元测试重点是检查验证错误。我们将为一个空表单编写一个测试,触发四个验证错误。

控制器设置

控制器设置非常简单:

    this.mockMvc = MockMvcBuilders.standaloneSetup( 
    new UserValidationController()).build();

测试方法

完整的Test方法如下所示:

    @Test 
    public void basicTest_WithAllValidationErrors() throws Exception { 
      this.mockMvc 
        .perform( 
           post("/create-user-with-validation") 
           .accept(MediaType.parseMediaType( 
           "application/html;charset=UTF-8"))) 
           .andExpect(status().isOk()) 
           .andExpect(model().errorCount(4)) 
           .andExpect(model().attributeHasFieldErrorCode 
           ("user", "name", "Size")); 
    }

这里需要注意的一些要点如下:

  • post("/create-user-with-validation"):创建到指定 URI 的 HTTP POST请求。由于我们没有传递任何请求参数,所有属性都为 null。这将触发验证错误。

  • model().errorCount(4):检查模型上是否有四个验证错误。

  • model().attributeHasFieldErrorCode("user", "name", "Size"):检查user属性是否具有名为Size的验证错误的name字段。

Spring MVC 概述

现在我们已经看了 Spring MVC 的一些基本流程,我们将把注意力转向理解这些流程是如何工作的。Spring MVC 是如何实现魔术的?

重要特性

在处理不同的流程时,我们看了 Spring MVC 框架的一些重要特性。这些包括以下内容:

  • 具有明确定义的独立角色的松散耦合架构。

  • 高度灵活的控制器方法定义。控制器方法可以具有各种参数和返回值。这使程序员可以灵活选择满足其需求的定义。

  • 允许重用域对象作为表单后备对象。减少了需要单独的表单对象。

  • 带有本地化支持的内置标签库(Spring,spring-form)。

  • Model 使用具有键值对的 HashMap。允许与多个视图技术集成。

  • 灵活的绑定。绑定时的类型不匹配可以作为验证错误而不是运行时错误来处理。

  • 模拟 MVC 框架以对控制器进行单元测试。

它是如何工作的

Spring MVC 架构中的关键组件如下图所示:

让我们看一个示例流程,并了解执行流程涉及的不同步骤。我们将采取流程4,返回ModelAndView作为具体示例。流程4的 URL 是http://localhost:8080/welcome-model-view。不同的步骤详细说明如下:

  1. 浏览器向特定 URL 发出请求。DispatcherServlet 是前端控制器,处理所有请求。因此,它接收请求。

  2. Dispatcher Servlet 查看 URI(在示例中为/welcome-model-view),并需要确定正确的控制器来处理它。为了帮助找到正确的控制器,它与处理程序映射进行通信。

  3. 处理程序映射返回处理请求的特定处理程序方法(在示例中,BasicModelViewController中的welcome方法)。

  4. DispatcherServlet 调用特定的处理程序方法(public ModelAndView welcome(ModelMap model))。

  5. 处理程序方法返回模型和视图。在这个例子中,返回了 ModelAndView 对象。

  6. DispatcherServlet 具有逻辑视图名称(来自 ModelAndView;在这个例子中是welcome-model-view)。它需要找出如何确定物理视图名称。它检查是否有任何可用的视图解析器。它找到了配置的视图解析器(org.springframework.web.servlet.view.InternalResourceViewResolver)。它调用视图解析器,将逻辑视图名称(在这个例子中是welcome-model-view)作为输入。

  7. View 解析器执行逻辑以将逻辑视图名称映射到物理视图名称。在这个例子中,welcome-model-view被翻译为/WEB-INF/views/welcome-model-view.jsp

  8. DispatcherServlet 执行 View。它还使 Model 可用于 View。

  9. View 将返回要发送回 DispatcherServlet 的内容。

  10. DispatcherServlet 将响应发送回浏览器。

Spring MVC 背后的重要概念

现在我们已经完成了一个 Spring MVC 示例,我们准备理解 Spring MVC 背后的重要概念。

RequestMapping

正如我们在之前的示例中讨论的,RequestMapping用于将 URI 映射到 Controller 或 Controller 方法。它可以在类和/或方法级别完成。可选的方法参数允许我们将方法映射到特定的请求方法(GETPOST等)。

请求映射的示例

即将出现的几个示例将说明各种变化。

示例 1

在以下示例中,showPage方法中只有一个RequestMappingshowPage方法将映射到GETPOST和 URI/show-page的任何其他请求类型:

    @Controller 
    public class UserController { 
      @RequestMapping(value = "/show-page") 
      public String showPage() { 
        /* Some code */ 
       } 
    }

示例 2

在以下示例中,定义了一个RequestMapping--RequestMethod.GET的方法。showPage方法将仅映射到 URI/show-pageGET请求。所有其他请求方法类型都会抛出“方法不受支持异常”:

    @Controller 
    public class UserController { 
      @RequestMapping(value = "/show-page" , method = 
      RequestMethod.GET) 
      public String showPage() { 
        /* Some code */ 
       } 
    }

示例 3

在以下示例中,有两个RequestMapping方法--一个在类中,另一个在方法中。使用两种RequestMapping方法的组合来确定 URI。showPage方法将仅映射到 URI/user/show-pageGET请求:

    @Controller 
    @RequestMapping("/user") 
    public class UserController { 
      @RequestMapping(value = "/show-page" , method =   
       RequestMethod.GET) 
       public String showPage() { 
         /* Some code */ 
       } 
    }

请求映射方法-支持的方法参数

以下是在具有 RequestMapping 的 Controller 方法中支持的一些参数类型:

参数类型/注释 用途
java.util.Map / org.springframework.ui.Model / org.springframework.ui.ModelMap 作为模型(MVC),用于容纳暴露给视图的值。
命令或表单对象 用于将请求参数绑定到 bean。还支持验证。
org.springframework.validation.Errors / org.springframework.validation.BindingResult 验证命令或表单对象的结果(表单对象应该是前一个方法参数)。
@PreDestroy 在任何 Spring bean 上,可以使用@PreDestroy注解提供预销毁方法。该方法在 bean 从容器中移除之前调用。它可以用于释放 bean 持有的任何资源。
@RequestParam 访问特定 HTTP 请求参数的注解。
@RequestHeader 访问特定 HTTP 请求头的注解。
@SessionAttribute 访问 HTTP 会话中的属性的注解。
@RequestAttribute 访问特定 HTTP 请求属性的注解。
@PathVariable 允许从 URI 模板中访问变量的注解。/owner/{ownerId}。当我们讨论微服务时,我们将深入研究这个问题。

RequestMapping 方法-支持的返回类型

RequestMapping方法支持各种返回类型。从概念上讲,请求映射方法应该回答两个问题:

  • 视图是什么?

  • 视图需要什么模型?

然而,使用 Spring MVC 时,视图和模型不一定需要始终明确声明:

  • 如果视图没有明确定义为返回类型的一部分,则它是隐式定义的。

  • 同样,任何模型对象始终按照以下规则进行丰富。

Spring MVC 使用简单的规则来确定确切的视图和模型。以下列出了一些重要的规则:

  • 模型的隐式丰富:如果模型是返回类型的一部分,则它将与命令对象(包括命令对象验证的结果)一起丰富。此外,带有@ModelAttribute注解的方法的结果也会添加到模型中。

  • 视图的隐式确定:如果返回类型中没有视图名称,则使用DefaultRequestToViewNameTranslator确定。默认情况下,DefaultRequestToViewNameTranslator会从 URI 中删除前导和尾随斜杠以及文件扩展名;例如,display.html变成了 display。

以下是在带有请求映射的控制器方法上支持的一些返回类型:

返回类型 发生了什么?
ModelAndView 该对象包括对模型和视图名称的引用。
模型 只返回模型。视图名称使用DefaultRequestToViewNameTranslator确定。
Map 一个简单的映射来暴露模型。
视图 隐式定义模型的视图。
String 视图名称的引用。

视图解析

Spring MVC 提供非常灵活的视图解析。它提供多个视图选项:

  • 与 JSP、Freemarker 集成。

  • 多种视图解析策略。以下列出了其中一些:

  • XmlViewResolver:基于外部 XML 配置的视图解析

  • ResourceBundleViewResolver:基于属性文件的视图解析

  • UrlBasedViewResolver:将逻辑视图名称直接映射到 URL

  • ContentNegotiatingViewResolver:根据接受请求头委托给其他视图解析器

  • 支持显式定义首选顺序的视图解析器的链接。

  • 使用内容协商直接生成 XML、JSON 和 Atom。

配置 JSP 视图解析器

以下示例显示了配置 JSP 视图解析器使用InternalResourceViewResolver的常用方法。使用JstlView,通过配置的前缀和后缀确定逻辑视图名称的物理视图名称:

    <bean id="jspViewResolver" class=  
      "org.springframework.web.servlet.view.
      InternalResourceViewResolver"> 
      <property name="viewClass"  
        value="org.springframework.web.servlet.view.JstlView"/> 
      <property name="prefix" value="/WEB-INF/jsp/"/> 
      <property name="suffix" value=".jsp"/> 
    </bean>

还有其他使用属性和 XML 文件进行映射的方法。

配置 Freemarker

以下示例显示了配置 Freemarker 视图解析器的典型方法。

首先,freemarkerConfig bean 用于加载 Freemarker 模板:

    <bean id="freemarkerConfig"
      class="org.springframework.web.servlet.view.
      freemarker.FreeMarkerConfigurer"> 
      <property name="templateLoaderPath" value="/WEB-
      INF/freemarker/"/> 
    </bean>

以下是如何配置 Freemarker 视图解析器的 bean 定义:

    <bean id="freemarkerViewResolver"  
     class="org.springframework.web.servlet.view.
     freemarker.FreeMarkerViewResolver"> 
       <property name="cache" value="true"/> 
       <property name="prefix" value=""/> 
       <property name="suffix" value=".ftl"/> 
    </bean>

与 JSP 一样,视图解析可以使用属性或 XML 文件来定义。

处理程序映射和拦截器

在 Spring 2.5 之前的版本(在支持注解之前),URL 和控制器(也称为处理程序)之间的映射是使用称为处理程序映射的东西来表达的。今天几乎是一个历史事实。注解的使用消除了对显式处理程序映射的需求。

HandlerInterceptors 可用于拦截对处理程序(或控制器)的请求。有时,您希望在请求之前和之后进行一些处理。您可能希望记录请求和响应的内容,或者您可能想找出特定请求花费了多少时间。

创建 HandlerInterceptor 有两个步骤:

  1. 定义 HandlerInterceptor。

  2. 将 HandlerInterceptor 映射到要拦截的特定处理程序。

定义 HandlerInterceptor

以下是您可以在HandlerInterceptorAdapter中重写的方法:

  • public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler): 在调用处理程序方法之前调用

  • public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView): 在调用处理程序方法后调用

  • public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex): 在请求处理完成后调用

以下示例实现显示了如何创建 HandlerInterceptor。让我们从创建一个扩展HandlerInterceptorAdapter的新类开始:

    public class HandlerTimeLoggingInterceptor extends 
    HandlerInterceptorAdapter {

preHandle方法在调用处理程序之前被调用。让我们在请求上放置一个属性,指示处理程序调用的开始时间:

    @Override 
    public boolean preHandle(HttpServletRequest request, 
      HttpServletResponse response, Object handler) throws Exception { 
      request.setAttribute( 
      "startTime", System.currentTimeMillis()); 
      return true; 
    }

postHandle方法在调用处理程序后被调用。让我们在请求上放置一个属性,指示处理程序调用的结束时间:

    @Override 
    public void postHandle(HttpServletRequest request, 
    HttpServletResponse response, Object handler, 
    ModelAndView modelAndView) throws Exception { 
       request.setAttribute( 
       "endTime", System.currentTimeMillis()); 
     }

afterCompletion方法在请求处理完成后被调用。我们将使用我们之前设置到请求中的属性来识别处理程序中花费的时间:

    @Override 
    public void afterCompletion(HttpServletRequest request, 
    HttpServletResponse response, Object handler, Exception ex) 
    throws Exception { 
      long startTime = (Long) request.getAttribute("startTime"); 
      long endTime = (Long) request.getAttribute("endTime"); 
      logger.info("Time Spent in Handler in ms : "  
      + (endTime - startTime)); 
    }

将 HandlerInterceptor 映射到处理程序

HandlerInterceptors 可以映射到您希望拦截的特定 URL。以下示例显示了一个示例 XML 上下文配置。默认情况下,拦截器将拦截所有处理程序(控制器):

    <mvc:interceptors> 
      <bean class="com.mastering.spring.springmvc.
      controller.interceptor.HandlerTimeLoggingInterceptor" /> 
    </mvc:interceptors>

我们可以配置精确的 URI 进行拦截。在下面的示例中,除了以/secure/开头的 URI 映射的处理程序之外,所有处理程序都会被拦截:

    <mvc:interceptors> 
      <mapping path="/**"/> 
      <exclude-mapping path="/secure/**"/> 
      <bean class="com.mastering.spring.springmvc.
       controller.interceptor.HandlerTimeLoggingInterceptor" /> 
    </mvc:interceptors>

模型属性

常见的 Web 表单包含许多下拉值--州的列表,国家的列表等等。这些值列表需要在模型中可用,以便视图可以显示列表。这些常见的东西通常使用标有@ModelAttribute注解的方法填充到模型中。

有两种可能的变体。在下面的示例中,该方法返回需要放入模型中的对象:

    @ModelAttribute 
    public List<State> populateStateList() { 
      return stateService.findStates(); 
     }

这个示例中的方法用于向模型添加多个属性:

    @ModelAttribute 
    public void populateStateAndCountryList() { 
      model.addAttribute(stateService.findStates()); 
      model.addAttribute(countryService.findCountries()); 
     }

需要注意的重要事项是,可以标记为@ModelAttribute注解的方法数量没有限制。

使用 Controller Advice 可以使模型属性在多个控制器中变得通用。我们将在本节后面讨论 Controller Advice。

会话属性

到目前为止,我们讨论的所有属性和值都是在单个请求中使用的。但是,可能存在值(例如特定的 Web 用户配置)在请求之间不会发生变化。这些类型的值通常将存储在 HTTP 会话中。Spring MVC 提供了一个简单的类型级别(类级别)注释@SessionAttributes,用于指定要存储在会话中的属性。

看一下以下示例:

    @Controller 
    @SessionAttributes("exampleSessionAttribute") 
    public class LoginController {

将属性放入会话中

一旦我们在@SessionAttributes注释中定义了一个属性,如果将相同的属性添加到模型中,它将自动添加到会话中。

在前面的示例中,如果我们将一个名为exampleSessionAttribute的属性放入模型中,它将自动存储到会话对话状态中:

    model.put("exampleSessionAttribute", sessionValue);

从会话中读取属性

首先在类型级别指定@SessionAttributes注释,然后可以在其他控制器中访问此值:

    @Controller 
    @SessionAttributes("exampleSessionAttribute") 
    public class SomeOtherController {

会话属性的值将直接提供给所有模型对象。因此,可以从模型中访问:

   Value sessionValue =(Value)model.get("exampleSessionAttribute");

从会话中删除属性

当不再需要会话中的值时,将其从会话中删除非常重要。我们可以通过两种方式从会话对话状态中删除值。第一种方式在以下代码片段中进行了演示。它使用WebRequest类中可用的removeAttribute方法:

    @RequestMapping(value="/some-method",method = RequestMethod.GET) 
    public String someMethod(/*Other Parameters*/  
    WebRequest request, SessionStatus status) { 
      status.setComplete(); 
      request.removeAttribute("exampleSessionAttribute",
      WebRequest.SCOPE_SESSION); 
       //Other Logic
    }

此示例显示了使用SessionAttributeStore中的cleanUpAttribute方法的第二种方法:

    @RequestMapping(value = "/some-other-method",  
    method = RequestMethod.GET) 
    public String someOtherMethod(/*Other Parameters*/ 
    SessionAttributeStore store, SessionStatus status) { 
      status.setComplete(); 
      store.cleanupAttribute(request, "exampleSessionAttribute"); 
      //Other Logic 
    }

InitBinders

典型的 Web 表单包含日期、货币和金额。表单中的值需要绑定到表单后端对象。可以使用@InitBinder注释引入绑定发生的自定义。

可以使用 Handler Advice 在特定控制器或一组控制器中进行自定义。此示例显示了如何设置用于表单绑定的默认日期格式:

    @InitBinder 
    protected void initBinder(WebDataBinder binder) { 
      SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy"); 
      binder.registerCustomEditor(Date.class, new CustomDateEditor( 
      dateFormat, false)); 
    }

@ControllerAdvice 注释

我们在控制器级别定义的一些功能可能在整个应用程序中是通用的。例如,我们可能希望在整个应用程序中使用相同的日期格式。因此,我们之前定义的@InitBinder可以适用于整个应用程序。我们如何实现?@ControllerAdvice可以帮助我们使功能在默认情况下在所有请求映射中通用。

例如,考虑此处列出的 Controller 建议示例。我们在类上使用@ControllerAdvice注释,并在此类中使用@InitBinder定义方法。默认情况下,此方法中定义的绑定适用于所有请求映射:

    @ControllerAdvice 
    public class DateBindingControllerAdvice { 
      @InitBinder 
      protected void initBinder(WebDataBinder binder) { 
        SimpleDateFormat dateFormat = new  
        SimpleDateFormat("dd/MM/yyyy"); 
        binder.registerCustomEditor(Date.class,  
        new CustomDateEditor( 
          dateFormat, false)); 
        } 
     }

Controller 建议还可以用于定义公共模型属性(@ModelAttribute)和公共异常处理(@ExceptionHandler)。您只需要创建带有适当注释的方法。我们将在下一节讨论异常处理。

Spring MVC - 高级功能

在本节中,我们将讨论与 Spring MVC 相关的高级功能,包括以下内容:

  • 如何为 Web 应用程序实现通用异常处理?

  • 如何国际化消息?

  • 如何编写集成测试?

  • 如何公开静态内容并与前端框架(如 Bootstrap)集成?

  • 如何使用 Spring Security 保护我们的 Web 应用程序?

异常处理

异常处理是任何应用程序的关键部分之一。在整个应用程序中拥有一致的异常处理策略非常重要。一个流行的误解是,只有糟糕的应用程序才需要异常处理。事实并非如此。即使设计良好、编写良好的应用程序也需要良好的异常处理。

在 Spring Framework 出现之前,由于受检异常的广泛使用,需要在应用程序代码中处理异常处理代码。例如,大多数 JDBC 方法抛出受检异常,需要在每个方法中使用 try catch 来处理异常(除非您希望声明该方法抛出 JDBC 异常)。使用 Spring Framework,大多数异常都变成了未经检查的异常。这确保除非需要特定的异常处理,否则可以在整个应用程序中通用地处理异常。

在本节中,我们将看一下异常处理的几个示例实现:

  • 所有控制器中的通用异常处理

  • 控制器的特定异常处理

跨控制器的通用异常处理

Controller Advice 也可以用于实现跨控制器的通用异常处理。

看一下以下代码:

    @ControllerAdvice 
    public class ExceptionController { 
      private Log logger =  
      LogFactory.getLog(ExceptionController.class); 
      @ExceptionHandler(value = Exception.class) 
      public ModelAndView handleException 
      (HttpServletRequest request, Exception ex) { 
         logger.error("Request " + request.getRequestURL() 
         + " Threw an Exception", ex); 
         ModelAndView mav = new ModelAndView(); 
         mav.addObject("exception", ex); 
         mav.addObject("url", request.getRequestURL()); 
         mav.setViewName("common/spring-mvc-error"); 
         return mav; 
        } 
     }

以下是一些需要注意的事项:

  • @ControllerAdvice:Controller Advice,默认情况下适用于所有控制器。

  • @ExceptionHandler(value = Exception.class):当控制器中抛出指定类型(Exception.class)或子类型的异常时,将调用带有此注解的任何方法。

  • public ModelAndView handleException (HttpServletRequest request, Exception ex):抛出的异常被注入到 Exception 变量中。该方法声明为 ModelAndView 返回类型,以便能够返回一个带有异常详细信息和异常视图的模型。

  • mav.addObject("exception", ex):将异常添加到模型中,以便在视图中显示异常详细信息。

  • mav.setViewName("common/spring-mvc-error"):异常视图。

错误视图

每当发生异常时,ExceptionController在填充模型的异常详细信息后将用户重定向到ExceptionController的 spring-mvc-error 视图。以下代码片段显示了完整的 jsp/WEB-INF/views/common/spring-mvc-error.jsp

    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 
    <%@page isErrorPage="true"%> 
    <h1>Error Page</h1> 
     URL: ${url} 
    <BR /> 
    Exception: ${exception.message} 
   <c:forEach items="${exception.stackTrace}"  
      var="exceptionStackTrace">     
      ${exceptionStackTrace}  
   </c:forEach>

重要注意事项如下:

  • URL: ${url}:显示模型中的 URL。

  • Exception: ${exception.message}:显示异常消息。异常是从ExceptionController中填充到模型中的。

  • forEach around ${exceptionStackTrace}:显示特定于ExceptionController的异常处理的堆栈跟踪。

控制器中的特定异常处理

在某些情况下,控制器需要特定的异常处理。可以通过实现一个带有@ExceptionHandler(value = Exception.class)注解的方法来轻松处理这种情况。

如果只需要针对特定异常进行特定异常处理,则可以将特定异常类提供为注解的 value 属性的值。

国际化

当我们开发应用程序时,希望它们能够在多个区域设置中使用。您希望根据用户的位置和语言定制向用户显示的文本。这称为国际化。国际化,i18n,也称为本地化

可以使用两种方法实现:

  • SessionLocaleResolver

  • CookieLocaleResolver

SessionLocaleResolver的情况下,用户选择的区域设置存储在用户会话中,因此仅对用户会话有效。但是,在CookieLocaleResolver的情况下,选择的区域设置存储为 cookie。

消息包设置

首先,让我们设置一个消息绑定器。来自 spring 上下文的代码片段如下:

    <bean id="messageSource"  class=  
      "org.springframework.context.support.
      ReloadableResourceBundleMessageSource"> 
      <property name="basename" value="classpath:messages" /> 
      <property name="defaultEncoding" value="UTF-8" /> 
    </bean>

重要注意事项如下:

  • class="org.springframework.context.support.ReloadableResourceBundleMessageSource":我们正在配置一个可重新加载的资源包。通过 cacheSeconds 设置支持重新加载属性。

  • <property name="basename" value="classpath:messages" />:配置从messages.propertiesmessages_{locale}.properties文件中加载属性。我们将很快讨论语言环境。

让我们配置一些属性文件,并使它们在src/main/resources文件夹中可用:

    message_en.properties 
    welcome.caption=Welcome in English 
    message_fr.properties 
    welcome.caption=Bienvenue - Welcome in French

我们可以使用spring:message标签在视图中显示来自消息包的消息:

    <spring:message code="welcome.caption" />

配置 SessionLocaleResolver

配置SessionLocaleResolver有两个部分。第一个是配置localeResolver。第二个是配置拦截器来处理语言环境的更改:

    <bean id="springMVCLocaleResolver" 
      class="org.springframework.web.servlet.i18n.
      SessionLocaleResolver"> 
      <property name="defaultLocale" value="en" /> 
    </bean> 
    <mvc:interceptors> 
      <bean id="springMVCLocaleChangeInterceptor" 
      class="org.springframework.web.servlet.
      i18n.LocaleChangeInterceptor"> 
        <property name="paramName" value="language" /> 
      </bean> 
    </mvc:interceptors>

需要注意的重要事项如下:

  • <property name="defaultLocale" value="en" />:默认情况下使用en语言环境。

  • <mvc:interceptors>LocaleChangeInterceptor被配置为 HandlerInterceptor。它将拦截所有处理程序请求并检查语言环境。

  • <property name="paramName" value="language" />LocaleChangeInterceptor被配置为使用名为 language 的请求参数来指示语言环境。因此,任何http://server/uri?language={locale}格式的 URL 都会触发语言环境的更改。

  • 如果您在任何 URL 后附加language=en,则会在会话期间使用en语言环境。如果您在任何 URL 后附加language=fr,则会使用法语语言环境。

配置 CookieLocaleResolver

在以下示例中,我们使用CookieLocaleResolver

    <bean id="localeResolver" 
     class="org.springframework.web.servlet.
     i18n.CookieLocaleResolver"> 
       <property name="defaultLocale" value="en" /> 
       <property name="cookieName" value="userLocaleCookie"/> 
       <property name="cookieMaxAge" value="7200"/> 
    </bean>

需要注意的重要事项如下:

  • <property name="cookieName" value="userLocaleCookie"/>:存储在浏览器中的 cookie 的名称将是userLocaleCookie

  • <property name="cookieMaxAge" value="7200"/>:cookie 的生存期为 2 小时(7200秒)。

  • 由于我们在前一个示例中使用了LocaleChangeInterceptor,如果您在任何 URL 后附加language=en,则会在 2 小时内(或直到语言环境更改)使用en语言环境。如果您在任何 URL 后附加language=fr,则会在 2 小时内(或直到语言环境更改)使用法语语言环境。

集成测试 Spring 控制器

在我们讨论的流程中,我们考虑使用真正的单元测试--只加载正在测试的特定控制器。

另一种可能性是加载整个 Spring 上下文。但是,这将更多地是一个集成测试,因为我们将加载整个上下文。以下代码向您展示了如何启动 Spring 上下文,启动所有控制器:

    @RunWith(SpringRunner.class) 
    @WebAppConfiguration 
    @ContextConfiguration("file:src/main/webapp/
    WEB-INF/user-web-context.xml") 
    public class BasicControllerSpringConfigurationIT { 
      private MockMvc mockMvc; 
      @Autowired 
      private WebApplicationContext wac; 
      @Before 
      public void setup() { 
        this.mockMvc =  
        MockMvcBuilders.webAppContextSetup 
        (this.wac).build(); 
      } 
       @Test 
       public void basicTest() throws Exception { 
        this.mockMvc 
        .perform( 
           get("/welcome") 
          .accept(MediaType.parseMediaType 
          ("application/html;charset=UTF-8"))) 
          .andExpect(status().isOk()) 
          .andExpect(content().string 
          ("Welcome to Spring MVC")); 
        } 
      }

需要注意的一些事项如下:

  • @RunWith(SpringRunner.class)SpringRunner帮助我们启动 Spring 上下文。

  • @WebAppConfiguration:用于使用 Spring MVC 启动 Web 应用程序上下文

  • @ContextConfiguration("file:src/main/webapp/WEB-INF/user-web-context.xml"):指定 spring 上下文 XML 的位置。

  • this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build():在之前的示例中,我们使用了独立设置。但是,在这个示例中,我们想要启动整个 Web 应用程序。因此,我们使用webAppContextSetup

  • 测试的执行与我们之前的测试非常相似。

提供静态资源

如今,大多数团队都有专门的团队提供前端和后端内容。前端使用现代的 JavaScript 框架开发,如 AngularJs,Backbone 等。后端是通过基于 Spring MVC 等框架构建的 Web 应用程序或 REST 服务。

随着前端框架的发展,找到正确的解决方案来版本化和交付前端静态内容非常重要。

以下是 Spring MVC 框架提供的一些重要功能:

  • 他们从 Web 应用程序根目录中的文件夹中公开静态内容

  • 它们启用了缓存

  • 他们启用了静态内容的 GZip 压缩

公开静态内容

Web 应用程序通常具有大量静态内容。Spring MVC 提供了从 Web 应用程序根目录中的文件夹或类路径上的位置公开静态内容的选项。以下代码片段显示了 war 中的内容可以公开为静态内容:

    <mvc:resources  
    mapping="/resources/**"  
    location="/static-resources/"/>

需要注意的事项如下:

  • location="/static-resources/":位置指定 war 或类路径中要公开为静态内容的文件夹。在此示例中,我们希望将根目录中static-resources文件夹中的所有内容公开为静态内容。我们可以指定多个逗号分隔的值以在相同的外部 URI 下公开多个文件夹。

  • mapping="/resources/**":映射指定外部 URI 路径。因此,静态资源文件夹中名为app.css的 CSS 文件可以使用/resources/app.css URI 进行访问。

相同配置的完整 Java 配置在此处显示:

    @Configuration 
    @EnableWebMvc 
    public class WebConfig extends WebMvcConfigurerAdapter { 
      @Override 
      public void addResourceHandlers 
     (ResourceHandlerRegistry registry) { 
        registry 
       .addResourceHandler("/static-resources/**") 
       .addResourceLocations("/static-resources/"); 
      } 
    }

缓存静态内容

可以启用静态资源的缓存以提高性能。浏览器将缓存为指定时间段提供的资源。可以使用cache-period属性或setCachePeriod方法来指定基于使用的配置类型的缓存间隔(以秒为单位)。以下代码片段显示了详细信息:

这是 Java 配置:

    registry 
   .addResourceHandler("/resources/**") 
   .addResourceLocations("/static-resources/") 
   .setCachePeriod(365 * 24 * 60 * 60);

这是 XML 配置:

    <mvc:resources  
     mapping="/resources/**"  
     location="/static-resources/"  
     cache-period="365 * 24 * 60 * 60"/>

将发送Cache-Control: max-age={specified-max-age}响应头到浏览器。

启用静态内容的 GZip 压缩

压缩响应是使 Web 应用程序更快的一种简单方法。所有现代浏览器都支持 GZip 压缩。可以发送压缩文件作为响应,而不是发送完整的静态内容文件。浏览器将解压并使用静态内容。

浏览器可以指定它可以接受压缩内容的请求头。如果服务器支持,它可以传递压缩内容-再次标记为响应头。

浏览器发送的请求头如下:

Accept-Encoding: gzip, deflate

来自 Web 应用程序的响应头如下:

Content-Encoding: gzip

以下代码片段显示了如何添加 Gzip 解析器以提供压缩的静态内容:

    registry 
      .addResourceHandler("/resources/**") 
      .addResourceLocations("/static-resources/") 
      .setCachePeriod(365 * 24 * 60 * 60) 
      .resourceChain(true) 
      .addResolver(new GzipResourceResolver()) 
      .addResolver(new PathResourceResolver()); 

需要注意的事项如下:

  • resourceChain(true):我们希望启用 Gzip 压缩,但希望在请求完整文件时返回完整文件。因此,我们使用资源链(资源解析器的链接)。

  • addResolver(new PathResourceResolver()): PathResourceResolver:这是默认解析器。它根据配置的资源处理程序和位置进行解析。

  • addResolver(new GzipResourceResolver()): GzipResourceResolver:当请求时启用 Gzip 压缩。

将 Spring MVC 与 Bootstrap 集成

在 Web 应用程序中使用 Bootstrap 的一种方法是下载 JavaScript 和 CSS 文件,并将它们放在各自的文件夹中。但是,这意味着每次有新版本的 Bootstrap 时,我们都需要下载并将其作为源代码的一部分提供。问题是这样的-是否有办法可以通过 Maven 等依赖管理引入 Bootstrap 或任何其他静态(JS 或 CSS)库?

答案是 WebJars。WebJars 是打包成 JAR 文件的客户端 JS 或 CSS 库。我们可以使用 Java 构建工具(Maven 或 Gradle)来下载并使它们可用于应用程序。最大的优势是 WebJars 可以解析传递依赖关系。

现在让我们使用 Bootstrap WebJar 并将其包含在我们的 Web 应用程序中。涉及的步骤如下:

  • 将 Bootstrap WebJars 作为 Maven 依赖项添加

  • 配置 Spring MVC 资源处理程序以从 WebJar 提供静态内容

  • 在 JSP 中使用 Bootstrap 资源(CSS 和 JS)

Bootstrap WebJar 作为 Maven 依赖项

让我们将其添加到pom.xml文件中:

    <dependency> 
      <groupId>org.webjars</groupId> 
      <artifactId>bootstrap</artifactId> 
      <version>3.3.6</version> 
    </dependency>

配置 Spring MVC 资源处理程序以提供 WebJar 静态内容

这很简单。我们需要将以下映射添加到 spring 上下文中:

    <mvc:resources mapping="/webjars/**" location="/webjars/"/>

通过此配置,ResourceHttpRequestHandler使来自 WebJars 的内容可用作静态内容。

如静态内容部分所讨论的,如果我们想要缓存内容,我们可以特别缓存一段时间。

在 JSP 中使用引导资源

我们可以像 JSP 中的其他静态资源一样添加引导资源:

    <script src= 
     "webjars/bootstrap/3.3.6/js/bootstrap.min.js"> 
    </script> 
   <link  
    href="webjars/bootstrap/3.3.6/css/bootstrap.min.css" 
    rel="stylesheet">

Spring Security

Web 应用程序的关键部分是身份验证和授权。身份验证是建立用户身份的过程,验证用户是否是他/她声称的人。授权是检查用户是否有权执行特定操作。授权指定用户的访问权限。用户能否查看页面?用户能否编辑页面?用户能否删除页面?

最佳实践是在应用程序的每个页面上强制进行身份验证和授权。在执行对 Web 应用程序的任何请求之前,应验证用户凭据和授权。

Spring Security 为 Java EE 企业应用程序提供了全面的安全解决方案。虽然为基于 Spring(和基于 Spring MVC 的)应用程序提供了很好的支持,但它也可以与其他框架集成。

以下列表突出显示了 Spring Security 支持的广泛范围的身份验证机制中的一些:

  • 基于表单的身份验证:基本应用程序的简单集成

  • LDAP:通常在大多数企业应用程序中使用

  • Java 身份验证和授权服务(JAAS):身份验证和授权标准;Java EE 标准规范的一部分

  • 容器管理的身份验证

  • 自定义身份验证系统

让我们考虑一个简单的示例,在简单的 Web 应用程序上启用 Spring Security。我们将使用内存配置。

涉及的步骤如下:

  1. 添加 Spring Security 依赖。

  2. 配置拦截所有请求。

  3. 配置 Spring Security。

  4. 添加注销功能。

添加 Spring Security 依赖

我们将从向pom.xml添加 Spring Security 依赖开始:

    <dependency> 
      <groupId>org.springframework.security</groupId> 
      <artifactId>spring-security-web</artifactId> 
    </dependency> 
    <dependency> 
      <groupId>org.springframework.security</groupId> 
      <artifactId>spring-security-config</artifactId> 
    </dependency>

添加的依赖是spring-security-webspring-security-config

配置过滤器以拦截所有请求

在实施安全性时的最佳实践是验证所有传入请求。我们希望我们的安全框架查看传入请求,对用户进行身份验证,并仅在用户有权执行操作时才允许执行操作。我们将使用过滤器拦截和验证请求。以下示例显示了更多细节。

我们希望配置 Spring Security 拦截对 Web 应用程序的所有请求。我们将使用一个过滤器DelegatingFilterProxy,它委托给一个 Spring 管理的 beanFilterChainProxy

    <filter> 
      <filter-name>springSecurityFilterChain</filter-name> 
      <filter-class> 
        org.springframework.web.filter.DelegatingFilterProxy 
      </filter-class> 
    </filter> 
    <filter-mapping> 
      <filter-name>springSecurityFilterChain</filter-name> 
      <url-pattern>/*</url-pattern> 
    </filter-mapping>

现在,所有对我们 Web 应用程序的请求都将通过过滤器。但是,我们尚未配置与安全相关的任何内容。让我们使用一个简单的 Java 配置示例:

    @Configuration 
    @EnableWebSecurity 
    public class SecurityConfiguration extends  
    WebSecurityConfigurerAdapter { 
      @Autowired 
      public void configureGlobalSecurity 
      (AuthenticationManagerBuilder auth) throws Exception { 
      auth 
      .inMemoryAuthentication() 
      .withUser("firstuser").password("password1") 
      .roles("USER", "ADMIN"); 
     } 
     @Override 
     protected void configure(HttpSecurity http)  
     throws Exception { 
       http 
      .authorizeRequests() 
      .antMatchers("/login").permitAll() 
      .antMatchers("/*secure*/**") 
      .access("hasRole('USER')") 
      .and().formLogin(); 
      } 
    }

需要注意的事项如下:

  • @EnableWebSecurity:此注解使任何配置类能够包含 Spring 配置的定义。在这种特定情况下,我们重写了一些方法,以提供我们特定的 Spring MVC 配置。

  • WebSecurityConfigurerAdapter:此类提供了创建 Spring 配置(WebSecurityConfigurer)的基类。

  • protected void configure(HttpSecurity http): 此方法为不同 URL 提供安全需求。

  • antMatchers("/*secure*/**").access("hasRole('USER')": 您需要具有用户角色才能访问包含子字符串secure的任何 URL。

  • antMatchers("/login").permitAll(): 允许所有用户访问登录页面。

  • public void configureGlobalSecurity(AuthenticationManagerBuilder auth): 在此示例中,我们使用内存身份验证。这可以用于连接到数据库(auth.jdbcAuthentication()),或 LDAP(auth.ldapAuthentication()),或自定义身份验证提供程序(扩展AuthenticationProvider创建)。

  • withUser("firstuser").password("password1"): 配置内存中有效的用户 ID 和密码组合。

  • .roles("USER", "ADMIN"): 为用户分配角色。

当我们尝试访问任何安全的 URL 时,我们将被重定向到登录页面。Spring Security 提供了自定义逻辑页面以及重定向的方式。只有具有正确角色的经过认证的用户才能访问受保护的应用程序页面。

注销

Spring Security 提供了功能,使用户能够注销并被重定向到指定页面。LogoutController的 URI 通常映射到 UI 中的注销链接。LogoutController的完整列表如下:

    @Controller 
    public class LogoutController { 
      @RequestMapping(value = "/secure/logout",  
      method = RequestMethod.GET) 
      public String logout(HttpServletRequest request, 
      HttpServletResponse response) { 
        Authentication auth =  
        SecurityContextHolder.getContext() 
        .getAuthentication(); 
        if (auth != null) { 
            new SecurityContextLogoutHandler() 
           .logout(request, response, auth); 
            request.getSession().invalidate(); 
          } 
        return "redirect:/secure/welcome"; 
       } 
     }

需要注意的是:

  • if (auth != null): 如果有有效的认证,那么结束会话

  • new SecurityContextLogoutHandler().logout(request, response, auth): SecurityContextLogoutHandler通过从SecurityContextHolder中删除认证信息来执行注销

  • return "redirect:/secure/welcome": 重定向到安全的欢迎页面

总结

在本章中,我们讨论了使用 Spring MVC 开发 Web 应用程序的基础知识。我们还讨论了实现异常处理、国际化以及使用 Spring Security 保护我们的应用程序。

Spring MVC 也可以用来构建 REST 服务。我们将在接下来的章节中讨论与 REST 服务相关的内容。

在下一章中,我们将把注意力转向微服务。我们将尝试理解为什么世界对微服务如此关注。我们还将探讨应用程序成为云原生的重要性。

第四章:向微服务和云原生应用的演进

在过去的十年中,Spring 框架已经发展成为开发 Java 企业应用程序的最流行框架。Spring 框架使开发松耦合、可测试的应用程序变得容易。它简化了横切关注点的实现。

然而,今天的世界与十年前大不相同。随着时间的推移,应用程序变得庞大而难以管理。由于这些问题,新的架构开始演变。最近的热词是 RESTful 服务、微服务和云原生应用程序。

在本章中,我们将从回顾 Spring 框架在过去十年中解决的问题开始。我们将了解单片应用程序的问题,并介绍更小、独立部署的组件的世界。

我们将探讨为什么世界正在向微服务和云原生应用程序发展。我们将结束本章,看看 Spring 框架和 Spring 项目如何发展以解决当今的问题。

本章将涵盖以下主题:

  • 基于 Spring 的典型应用程序架构

  • Spring 框架在过去十年中解决的问题

  • 我们开发应用程序时的目标是什么?

  • 单片应用程序存在哪些挑战?

  • 什么是微服务?

  • 微服务的优势是什么?

  • 微服务存在哪些挑战?

  • 有哪些好的实践可以帮助将微服务部署到云中?

  • 有哪些 Spring 项目可以帮助我们开发微服务和云原生应用程序?

具有 Spring 的典型 Web 应用程序架构

在过去的十五年中,Spring 一直是连接 Java 企业应用程序的首选框架。应用程序使用分层架构,所有横切关注点都使用面向方面的编程进行管理。以下图表显示了使用 Spring 开发的 Web 应用程序的典型架构:

这样的应用程序中的典型层在这里列出。我们将把横切关注点列为一个单独的层,尽管在现实中,它们适用于所有层:

  • Web 层:通常负责控制 Web 应用程序流程(控制器和/或前端控制器)并呈现视图。

  • 业务层:这是您的所有业务逻辑所在。大多数应用程序从业务层开始进行事务管理。

  • 数据层:它还负责与数据库通信。这负责将 Java 对象中的数据持久化/检索到数据库中的表中。

  • 集成层:应用程序与其他应用程序通信,可以通过队列或调用 Web 服务来实现。集成层与其他应用程序建立这样的连接。

  • 横切关注点:这些是跨不同层的关注点--日志记录、安全性、事务管理等。由于 Spring IoC 容器管理 bean,它可以通过面向方面的编程AOP)在 bean 周围编织这些关注点。

让我们更详细地讨论每个层和使用的框架。

Web 层

Web 层取决于您希望如何向最终用户公开业务逻辑。它是 Web 应用程序吗?还是您正在公开 RESTful Web 服务?

Web 应用程序-呈现 HTML 视图

这些 Web 应用程序使用 Web MVC 框架,如 Spring MVC 或 Struts。视图可以使用 JSP、JSF 或基于模板的框架(如 Freemarker)进行呈现。

RESTful 服务

用于开发 RESTful Web 服务的两种典型方法:

  • JAX-RS:用于 REST 服务的 Java API。这是 Java EE 规范的标准。Jersey 是参考实现。

  • Spring MVC 或 Spring REST:Restful 服务也可以使用 Spring MVC 开发。

Spring MVC 没有实现 JAX-RS,因此选择比较棘手。JAX-RS 是一个 Java EE 标准。但是 Spring MVC 更具创新性,更有可能帮助您更快地构建新功能。

业务层

业务层通常包含应用程序中的所有业务逻辑。在这一层中,使用 Spring 框架来连接 bean。

这也是事务管理边界开始的地方。事务管理可以使用 Spring AOP 或 AspectJ 来实现。十年前,企业 Java BeanEJB)是实现业务层的最流行方法。由于其轻量级特性,Spring 现在是业务层的首选框架。

EJB3 比 EJB2 简单得多。然而,EJB3 发现很难赶上 Spring 失去的地位。

数据层

大多数应用程序与数据库通信。数据层负责将 Java 对象的数据存储到数据库中,反之亦然。以下是构建数据层的最流行方法:

  • JPAJava 持久化 API帮助您将 Java 对象(POJOs)映射到数据库表。Hibernate 是 JPA 最流行的实现。JPA 通常适用于所有事务性应用程序。JPA 不是批处理和报告应用程序的最佳选择。

  • MyBatis:MyBatis(以前是 iBatis)是一个简单的数据映射框架。正如其网站(www.mybatis.org/mybatis-3/)所说,MyBatis 是一个支持自定义 SQL、存储过程和高级映射的一流持久化框架。MyBatis 几乎消除了所有的 JDBC 代码和手动设置参数以及检索结果。MyBatis 可以考虑用于更常用 SQL 和存储过程的批处理和报告应用程序。

  • Spring JDBC:JDBC 和 Spring JDBC 不再那么常用。

我们将在《第八章》Spring Data中详细讨论 JDBC、Spring JDBC、MyBatis 和 JPA 的优缺点。

集成层

集成层通常是我们与其他应用程序交流的地方。可能有其他应用程序通过 HTTP(Web)或 MQ 公开 SOAP 或 RESTful 服务。

  • Spring JMS 通常用于在队列或服务总线上发送或接收消息。

  • Spring MVC RestTemplate 可用于调用 RESTful 服务。

  • Spring WS 可用于调用基于 SOAP 的 Web 服务。

  • Spring Integration 提供了一个更高级的抽象层,用于构建企业集成解决方案。它通过清晰地分离应用程序和集成代码的关注点,实现了可测试性。它支持所有流行的企业集成模式。我们将在《第十章》Spring Cloud Data Flow中更多地讨论 Spring Integration。

横切关注点

横切关注点是通常适用于应用程序的多个层的关注点--日志记录、安全性和事务管理等。让我们快速讨论其中一些:

  • 日志记录:可以使用面向方面的编程(Spring AOP 或 AspectJ)在多个层实现审计日志记录。

  • 安全性:通常使用 Spring Security 框架来实现安全性。正如前一章所讨论的,Spring Security 使安全性的实现变得非常简单。

  • 事务管理:Spring 框架为事务管理提供了一致的抽象。更重要的是,Spring 框架为声明式事务管理提供了很好的支持。以下是 Spring 框架支持的一些事务 API:

  • Java 事务 APIJTA)是事务管理的标准。它是 Java EE 规范的一部分。

  • JDBC。

  • JPA(包括 Hibernate)。

  • 错误处理:Spring 提供的大多数抽象使用未检查的异常,因此除非业务逻辑需要,否则在暴露给客户(用户或其他应用程序)的层中实现错误处理就足够了。Spring MVC 提供了 Controller Advice 来实现整个应用程序中一致的错误处理。

Spring 框架在应用程序架构中扮演着重要角色。Spring IoC 用于将不同层中的 bean 连接在一起。Spring AOP 用于在 bean 周围编织交叉关注点。除此之外,Spring 还与不同层的框架提供了很好的集成。

在接下来的部分中,我们将快速回顾 Spring 在过去十年左右解决的一些重要问题。

Spring 解决的问题

Spring 是连接企业 Java 应用程序的首选框架。它解决了自 EJB2 以来企业 Java 应用程序面临的许多问题。以下是其中的一些:

  • 松耦合和可测试性

  • 管道代码

  • 轻量级架构

  • 架构灵活性

  • 简化交叉关注点的实现

  • 最佳的免费设计模式

松耦合和可测试性

通过依赖注入,Spring 实现了类之间的松耦合。虽然松耦合对于长期应用的可维护性是有益的,但首先实现的好处是它带来的可测试性。

在 Spring 之前,Java EE(或当时称为 J2EE)并不擅长可测试性。测试 EJB2 应用程序的唯一方法是在容器中运行它们。对它们进行单元测试非常困难。

这正是 Spring 框架要解决的问题。正如我们在前面的章节中看到的,如果使用 Spring 来连接对象,编写单元测试会变得更容易。我们可以轻松地存根或模拟依赖项并将它们连接到对象中。

管道代码

20 世纪 90 年代末和 21 世纪初到中期的开发人员会熟悉必须编写大量管道代码来执行通过 JDBC 进行简单查询并将结果填充到 Java 对象中的情况。你必须执行 Java 命名和目录接口(JNDI)查找,获取连接并填充结果。这导致了重复的代码。通常,问题在每个方法中都会重复出现异常处理代码。而且这个问题并不仅限于 JDBC。

Spring 框架解决的问题之一是通过消除所有管道代码。通过 Spring JDBC、Spring JMS 和其他抽象,开发人员可以专注于编写业务逻辑。Spring 框架处理了繁琐的细节。

轻量级架构

使用 EJB 使应用程序变得复杂,并非所有应用程序都需要那种复杂性。Spring 提供了一种简化、轻量级的应用程序开发方式。如果需要分发,可以随后添加。

架构灵活性

Spring 框架用于在不同层中连接应用程序中的对象。尽管它一直存在,但 Spring 框架并没有限制应用架构师和开发人员的灵活性或选择框架的选择。以下是一些示例:

  • Spring 框架在 Web 层提供了很大的灵活性。如果你想使用 Struts 或 Struts 2 而不是 Spring MVC,是可以配置的。你可以选择与更广泛的视图和模板框架集成。

  • 另一个很好的例子是数据层,你可以通过 JPA、JDBC 和映射框架(如 MyBatis)来连接。

简化交叉关注点的实现

当 Spring 框架用于管理 bean 时,Spring IoC 容器管理 bean 的生命周期——创建、使用、自动连接和销毁。这使得更容易在 bean 周围编织额外的功能,比如交叉关注点。

免费的设计模式

Spring Framework 默认鼓励使用许多设计模式。一些例子如下:

  • 依赖注入或控制反转:这是 Spring Framework 建立的基本设计模式。它实现了松散耦合和可测试性。

  • 单例:所有 Spring bean 默认都是单例。

  • 工厂模式:使用 bean 工厂来实例化 bean 是工厂模式的一个很好的例子。

  • 前端控制器:Spring MVC 使用 DispatcherServlet 作为前端控制器。因此,当我们使用 Spring MVC 开发应用程序时,我们使用前端控制器模式。

  • 模板方法:帮助我们避免样板代码。许多基于 Spring 的类--JdbcTemplate 和 JmsTemplate--都是这种模式的实现。

应用程序开发目标

在我们转向 REST 服务、微服务和云原生应用程序的概念之前,让我们花些时间了解我们开发应用程序时的共同目标。了解这些目标将有助于我们理解为什么应用程序正在向微服务架构转变。

首先,我们应该记住,软件行业仍然是一个相对年轻的行业。在我十五年的软件开发、设计和架构经验中,一直有一件事是不变的,那就是事物的变化。今天的需求不是明天的需求。今天的技术不是明天我们将使用的技术。虽然我们可以尝试预测未来会发生什么,但我们经常是错误的。

在软件开发的最初几十年中,我们做的一件事是为未来构建软件系统。设计和架构被复杂化,以应对未来的需求。

在过去的十年中,随着敏捷极限编程,重点转向了精益和构建足够好的系统,遵循基本的设计原则。重点转向了演进式设计。思考过程是这样的:如果一个系统对今天的需求有良好的设计,并且不断发展并且有良好的测试,它可以很容易地重构以满足明天的需求

虽然我们不知道我们的方向,但我们知道在开发应用程序时的大部分目标并没有改变。

对于大量应用程序的软件开发的关键目标可以用“规模上的速度和安全”来描述。

我们将在下一节中讨论这些元素。

速度

交付新需求和创新的速度越来越成为一个关键的区分因素。快速开发(编码和测试)已经不够了。快速交付(到生产环境)变得很重要。现在已经普遍认识到,世界上最好的软件组织每天多次将软件交付到生产环境。

技术和商业环境是不断变化和不断发展的。关键问题是“一个应用程序能够多快地适应这些变化?”。这里强调了技术和商业环境中的一些重要变化:

  • 新的编程语言

  • Go

  • Scala

  • 闭包

  • 新的编程范式

  • 函数式编程

  • 响应式编程

  • 新框架

  • 新工具

  • 开发

  • 代码质量

  • 自动化测试

  • 部署

  • 容器化

  • 新的流程和实践

  • 敏捷

  • 测试驱动开发

  • 行为驱动开发

  • 持续集成

  • 持续交付

  • DevOps

  • 新设备和机会

  • 移动

安全

速度没有安全有什么用?谁会想要乘坐一辆可以以每小时 300 英里的速度行驶但没有适当安全功能的汽车呢?

让我们考虑一个安全应用程序的几个特点:

可靠性

可靠性是系统功能的准确度的度量。

要问的关键问题如下:

  • 系统是否满足其功能要求?

  • 在不同的发布阶段泄漏了多少缺陷?

可用性

大多数外部面向客户的应用程序都希望全天候可用。可用性是衡量应用程序对最终用户可用的时间百分比。

安全性

应用程序和数据的安全对组织的成功至关重要。应该有明确的程序进行身份验证(你是你声称的那个人吗?)、授权(用户有什么访问权限?)和数据保护(接收或发送的数据是否准确?数据是否安全,不会被意外用户拦截?)。

我们将在《第六章》中更多地讨论如何使用 Spring Security 实现安全性,扩展微服务

性能

如果一个 Web 应用程序在几秒内没有响应,你的应用程序的用户很有可能会感到失望。性能通常指的是系统在为定义数量的用户提供约定的响应时间的能力。

高弹性

随着应用程序变得分布式,故障的概率增加。应用程序在出现局部故障或中断的情况下会如何反应?它能否在完全崩溃的情况下提供基本操作?

应用程序在出现意外故障时提供最低限度的服务水平的行为被称为弹性。

随着越来越多的应用程序向云迁移,应用程序的弹性变得重要。

我们将在《第九章》中讨论如何使用Spring Cloud 和 Spring Data Flow构建高度弹性的微服务,Spring Cloud和《第十章》Spring Cloud Data Flow

可伸缩性

可伸缩性是衡量应用在其可用资源被扩展时的反应能力。如果一个应用程序在给定基础设施支持 10,000 用户,它能否在双倍基础设施的情况下支持至少 20,000 用户?

如果一个 Web 应用程序在几秒内没有响应,你的应用程序的用户很有可能会感到失望。性能通常指的是系统在为定义数量的用户提供约定的响应时间的能力。

在云的世界中,应用程序的可伸缩性变得更加重要。很难猜测一个创业公司可能会有多成功。Twitter 或 Facebook 在孵化时可能没有预料到这样的成功。他们的成功在很大程度上取决于他们如何能够适应用户基数的多倍增长而不影响性能。

我们将在《第九章》中讨论如何使用 Spring Cloud 和 Spring Data Flow 构建高度可伸缩的微服务,Spring Cloud和《第十章》Spring Cloud Data Flow

单体应用的挑战

在过去的几年里,除了与几个小应用程序一起工作,我还有机会在不同领域的四个不同的单体应用程序上工作--保险、银行和医疗保健。所有这些应用程序都面临着非常相似的挑战。在本节中,我们将首先看一下单体应用的特征,然后再看看它们带来的挑战。

首先:什么是单体应用?一个有很多代码的应用--可能超过 10 万行代码?是的。

对我来说,单体应用是那些在将发布推向生产环境时面临巨大挑战的应用。属于这一类别的应用有许多用户需求是迫切需要的,但这些应用可能每隔几个月才能发布新功能。有些应用甚至每季度发布一次功能,有时甚至少至一年两次。

通常,所有的单体应用都具有这些特征:

  • 体积庞大:大多数这些单片应用有超过 10 万行的代码。有些代码库超过 100 万行代码。

  • 团队庞大:团队规模可能从 20 到 300 不等。

  • 多种做同一件事的方式:由于团队庞大,存在沟通障碍。这导致应用程序不同部分对同一问题有多种解决方案。

  • 缺乏自动化测试:大多数这些应用几乎没有单元测试,也完全缺乏集成测试。这些应用高度依赖手动测试。

由于这些特点,这些单片应用面临许多挑战。

发布周期长

在单片的一个部分进行代码更改可能会影响单片的其他部分。大多数代码更改都需要完整的回归周期。这导致发布周期很长。

由于缺乏自动化测试,这些应用依赖手动测试来发现缺陷。将功能上线是一个重大挑战。

难以扩展

通常,大多数单片应用不是云原生的,这意味着它们不容易部署在云上。它们依赖手动安装和手动配置。通常在将新应用实例添加到集群之前,运维团队需要投入大量工作。这使得扩展规模成为一个重大挑战。

另一个重要的挑战是大型数据库。通常,单片应用的数据库容量达到TB级别。当扩展规模时,数据库成为瓶颈。

调整新技术

大多数单片应用使用旧技术。将新技术添加到单片中只会使其更难以维护。架构师和开发人员不愿引入任何新技术。

调整新方法

敏捷等新方法需要小型(四至七人的团队)。单片的重要问题是:我们如何防止团队互相干扰?我们如何创建能够使团队独立工作的岛屿?这是一个难以解决的挑战。

现代开发实践的调整

现代开发实践,如测试驱动开发TDD)、行为驱动开发BDD)需要松耦合、可测试的架构。如果单片应用具有紧密耦合的层和框架,很难进行单元测试。这使得调整现代开发实践具有挑战性。

了解微服务

单片应用的挑战导致组织寻找解决方案。我们如何能够更频繁地上线更多功能?

许多组织尝试了不同的架构和实践来寻找解决方案。

在过去几年中,所有成功做到这一点的组织中出现了一个共同模式。从中产生了一种被称为微服务架构的架构风格。

正如 Sam Newman 在《构建微服务》一书中所说:许多组织发现,通过拥抱细粒度、微服务架构,他们可以更快地交付软件并采用更新的技术。

什么是微服务?

我在软件中喜欢的一个原则是保持小型。无论你在谈论什么,这个原则都适用——变量的范围、方法、类、包或组件的大小。你希望所有这些都尽可能小。

微服务是这一原则的简单延伸。它是一种专注于构建小型基于能力的独立可部署服务的架构风格。

没有一个单一的微服务定义。我们将看一些流行的定义:

“微服务是小型、自治的服务,彼此协同工作”

  • Sam Newman,Thoughtworks

“松耦合的面向服务的架构与有界上下文”

  • Adrian Cockcroft, Battery Ventures

“微服务是有界范围内的独立部署组件,通过基于消息的通信支持互操作性。微服务架构是一种由能力对齐的微服务组成的高度自动化、可演进的软件系统的工程风格”在《微服务架构》一书中

  • Irakli Nadareishvili, ‎Ronnie Mitra, ‎Matt McLarty

虽然没有公认的定义,但所有微服务定义中通常具有一些特征。在我们看微服务的特征之前,我们将尝试了解整体情况-我们将看看没有微服务的架构与使用微服务的架构相比如何。

微服务架构

单体应用程序-即使是模块化的-也有一个可部署的单元。下图显示了一个具有三个模块的单体应用程序的示例,模块 1、2 和 3。这些模块可以是单体应用程序的一部分的业务能力。在购物应用程序中,其中一个模块可能是产品推荐。

以下图显示了使用微服务架构开发的前一个单体应用程序的样子:

需要注意的一些重要事项如下:

  • 模块是基于业务能力进行识别的。模块提供了什么功能?

  • 每个模块都可以独立部署。在下面的示例中,模块 1、2 和 3 是单独的可部署单元。如果模块 3 的业务功能发生变化,我们可以单独构建和部署模块 3。

微服务特征

在前一节中,我们看了一个微服务架构的例子。对于成功适应微服务架构风格的组织的经验评估表明,团队和架构共享了一些特征。让我们看看其中一些:

小型和轻量级微服务

良好的微服务提供了业务能力。理想情况下,微服务应遵循“单一责任原则”。因此,微服务通常规模较小。通常,我使用的一个经验法则是应该能够在 5 分钟内构建和部署一个微服务。如果构建和部署需要更长时间,很可能正在构建一个比推荐的微服务更大的服务。

一些小型和轻量级微服务的例子如下:

  • 产品推荐服务

  • 电子邮件通知服务

  • 购物车服务

基于消息的通信的互操作性

微服务的关键重点是互操作性-使用不同技术之间的系统通信。实现互操作性的最佳方式是使用基于消息的通信。

能力对齐的微服务

微服务必须有清晰的边界是至关重要的。通常,每个微服务都有一个单一的业务能力,它能够很好地提供。团队发现成功地采用了 Eric J Evans 在《领域驱动设计》一书中提出的“有界上下文”概念。

基本上,对于大型系统来说,创建一个领域模型非常困难。Evans 谈到了将系统拆分为不同的有界上下文。确定正确的有界上下文是微服务架构成功的关键。

独立部署单元

每个微服务都可以单独构建和部署。在前面讨论的示例中,模块 1、2 和 3 可以分别构建和部署。

无状态

理想的微服务没有状态。它不在请求之间存储任何信息。创建响应所需的所有信息都包含在请求中。

自动化构建和发布过程

微服务具有自动化的构建和发布流程。看一下下面的图。它展示了微服务的简单构建和发布流程:

当一个微服务被构建和发布时,微服务的一个版本被存储在仓库中。部署工具有能力从仓库中选择正确的微服务版本,将其与特定环境所需的配置(来自配置仓库)匹配,并将微服务部署到特定环境中。

一些团队进一步将微服务包与运行微服务所需的基础设施结合起来。部署工具将复制此映像,并将其与特定环境的配置匹配以创建环境。

事件驱动架构

微服务通常采用事件驱动架构构建。让我们考虑一个简单的例子。每当有新客户注册时,需要执行三件事:

  • 将客户信息存储到数据库中

  • 发送欢迎套件

  • 发送电子邮件通知

让我们看看设计这个的两种不同方法。

方法 1 - 顺序方法

让我们考虑三个服务--CustomerInformationServiceMailServiceEmailService,它们可以提供前面列出的功能。我们可以使用以下步骤创建NewCustomerService

  1. 调用CustomerInformationService将客户信息保存到数据库中。

  2. 调用MailService发送欢迎套件。

  3. 调用EmailService发送电子邮件通知。

NewCustomerService成为所有业务逻辑的中心。想象一下,如果我们在创建新客户时需要做更多的事情。所有这些逻辑将开始累积并使NewCustomerService变得臃肿。

方法 2 - 事件驱动方法

在这种方法中,我们使用消息代理。NewCustomerService将创建一个新事件并将其发布到消息代理。下图显示了一个高层表示:

三个服务--CustomerInformationServiceMailServiceEmailService--将在消息代理上监听新事件。当它们看到新的客户事件时,它们会处理它并执行该特定服务的功能。

事件驱动方法的关键优势在于没有所有业务逻辑的集中磁铁。添加新功能更容易。我们可以创建一个新服务来监听消息代理上的事件。还有一点需要注意的是,我们不需要对任何现有服务进行更改。

独立团队

开发微服务的团队通常是独立的。它包含了开发、测试和部署微服务所需的所有技能。它还负责在生产中支持微服务。

微服务优势

微服务有几个优势。它们有助于跟上技术并更快地为您的客户提供解决方案。

更快的上市时间

更快的上市时间是确定组织成功的关键因素之一。

微服务架构涉及创建小型、独立部署的组件。微服务的增强更容易,更不脆弱,因为每个微服务都专注于单一的业务能力。流程中的所有步骤--构建、发布、部署、测试、配置管理和监控--都是自动化的。由于微服务的责任是有界的,因此可以编写出色的自动化单元和集成测试。

所有这些因素导致应用程序能够更快地对客户需求做出反应。

技术演进

每天都有新的语言、框架、实践和自动化可能性出现。应用程序架构必须具备灵活性,以适应新的可能性。以下图显示了不同服务是如何使用不同技术开发的:

微服务架构涉及创建小型服务。在某些边界内,大多数组织都允许个体团队做出一些技术决策。这使团队能够尝试新技术并更快地创新。这有助于应用程序适应并与技术的演进保持一致。

可用性和扩展性

应用程序的不同部分的负载通常非常不同。例如,在航班预订应用程序的情况下,顾客通常在决定是否预订航班之前进行多次搜索。搜索模块的负载通常会比预订模块的负载多很多倍。微服务架构提供了设置多个搜索服务实例和少量预订服务实例的灵活性。

以下图显示了如何根据负载扩展特定微服务:

微服务23共享一个盒子(部署环境)。负载更大的微服务1被部署到多个盒子中。

另一个例子是初创公司的需求。当初创公司开始运营时,他们通常不知道自己可能会增长到何种程度。如果应用程序的需求增长得非常快会发生什么?如果他们采用微服务架构,它可以使他们在需要时更好地扩展。

团队动态

敏捷等开发方法倡导小型、独立的团队。由于微服务很小,围绕它们建立小团队是可能的。团队是跨职能的,对特定微服务拥有端到端的所有权。

微服务架构非常适合敏捷和其他现代开发方法。

微服务挑战

微服务架构具有显著的优势。但是,也存在显著的挑战。确定微服务的边界是一个具有挑战性但重要的决定。由于微服务很小,在大型企业中可能会有数百个微服务,因此具有良好的自动化和可见性至关重要。

自动化需求增加

使用微服务架构,你将一个大型应用程序拆分成多个微服务,因此构建、发布和部署的数量会成倍增加。对于这些步骤采用手动流程将非常低效。

测试自动化对于实现更快的上市时间至关重要。团队应该专注于识别可能出现的自动化可能性。

定义子系统的边界

微服务应该是智能的。它们不是弱的 CRUD 服务。它们应该模拟系统的业务能力。它们在一个有界上下文中拥有所有的业务逻辑。话虽如此,微服务不应该很大。决定微服务的边界是一个挑战。第一次确定正确的边界可能会很困难。团队对业务上下文的了解越多,知识就会流入架构中,并确定新的边界。通常,找到微服务的正确边界是一个演进的过程。

以下是需要注意的几个重要点:

  • 松耦合和高内聚对于任何编程和架构决策都是基本的。当系统松耦合时,对一个部分的更改不应该需要其他部分的更改。

  • 有界上下文代表着具体业务能力的自治业务模块。

正如 Sam Newman 在书中所说的“构建微服务--”:“通过明确的边界强制执行特定的责任”。始终思考,“我们为域的其他部分提供了哪些能力?”。

可见性和监控

使用微服务,一个应用程序被拆分成多个微服务。为了征服与多个微服务和异步基于事件的协作相关的复杂性,具有良好的可见性是很重要的。

确保高可用性意味着每个微服务都应该受到监控。自动化的微服务健康管理变得很重要。

调试问题需要洞察多个微服务背后发生的情况。通常使用集中日志记录,从不同微服务中聚合日志和指标。需要使用诸如关联 ID 之类的机制来隔离和调试问题。

容错性

假设我们正在构建一个购物应用程序。如果推荐微服务宕机会发生什么?应用程序如何反应?会完全崩溃吗?还是会让顾客继续购物?随着我们适应微服务架构,这种情况会更加频繁发生。

随着我们将服务变得更小,服务宕机的可能性增加。应用程序如何应对这些情况成为一个重要问题。在前面的例子中,一个容错应用程序会显示一些默认的推荐,同时让顾客继续购物。

随着我们进入微服务架构,应用程序应该更具有容错性。应用程序应该能够在服务宕机时提供降级行为。

最终一致性

在组织中,微服务之间的一定程度的一致性是很重要的。微服务之间的一致性使得整个组织能够在开发、测试、发布、部署和运营过程中实现类似的流程。这使得不同的开发人员和测试人员在跨团队移动时能够保持高效。在一定程度上保持灵活性,而不是过于死板,以避免扼杀创新,这是很重要的。

共享能力(企业级)

让我们看看在企业级必须标准化的一些能力。

  • 硬件:我们使用什么硬件?我们使用云吗?

  • 代码管理:我们使用什么版本控制系统?我们在分支和提交代码方面的做法是什么?

  • 构建和部署:我们如何构建?我们使用什么工具来自动化部署?

  • 数据存储:我们使用什么类型的数据存储?

  • 服务编排:我们如何编排服务?我们使用什么样的消息代理?

  • 安全和身份:我们如何对用户和服务进行身份验证和授权?

  • 系统可见性和监控:我们如何监控我们的服务?我们如何在整个系统中提供故障隔离?

运维团队需求增加

随着我们进入微服务世界,运维团队的责任发生了明显的转变。责任转移到识别自动化机会,而不是手动操作,比如执行发布和部署。

随着多个微服务和系统不同部分之间通信的增加,运维团队变得至关重要。重要的是在初始阶段就将运维团队纳入团队,以便他们能够找到简化运维的解决方案。

云原生应用

云正在改变世界。出现了以前从未可能的许多可能性。组织能够按需提供计算、网络和存储设备。这在许多行业中有很高的潜力来降低成本。

考虑零售行业,在某些时段需求很高(黑色星期五,假日季等)。为什么他们要在整年都支付硬件费用,而不是按需提供呢?

虽然我们希望从云的可能性中受益,但这些可能性受到架构和应用程序性质的限制。

我们如何构建可以轻松部署到云上的应用程序?这就是云原生应用程序的作用。

云原生应用程序是那些可以轻松部署到云上的应用程序。这些应用程序共享一些共同的特征。我们将首先看一下 Twelve-Factor 应用程序--云原生应用程序中常见模式的组合。

Twelve-Factor 应用程序

Twelve-Factor 应用程序是由 Heroku 的工程师的经验演变而来的。这是一份在云原生应用程序架构中使用的模式列表。

重要的是要注意,这里的应用程序是指一个单独的可部署单元。基本上,每个微服务都是一个应用程序(因为每个微服务都可以独立部署)。

维护一个代码库

每个应用程序在修订控制中有一个代码库。可以部署应用程序的多个环境。但是,所有这些环境都使用来自单个代码库的代码。一个反模式的例子是从多个代码库构建可部署的应用程序。

依赖项

所有依赖项必须明确声明和隔离。典型的 Java 应用程序使用构建管理工具,如 Maven 和 Gradle 来隔离和跟踪依赖项。

下图显示了典型的 Java 应用程序使用 Maven 管理依赖项:

下图显示了 pom.xml,其中管理了 Java 应用程序的依赖项:

配置

所有应用程序的配置在不同环境之间都有所不同。配置可以在多个位置找到;应用程序代码、属性文件、数据库、环境变量、JNDI 和系统变量都是一些例子。

Twelve-Factor 应用程序

应用程序应在环境中存储配置。虽然在 Twelve-Factor 应用程序中建议使用环境变量来管理配置,但对于更复杂的系统,应考虑其他替代方案,例如为应用程序配置建立一个集中存储库。

无论使用何种机制,我们建议您执行以下操作:

在应用程序代码之外管理配置(独立于应用程序的可部署单元)

使用标准化的配置方式

后备服务

应用程序依赖于其他可用的服务--数据存储和外部服务等。Twelve-Factor 应用程序将后备服务视为附加资源。后备服务通常通过外部配置声明。

与后备服务的松耦合具有许多优势,包括能够优雅地处理后备服务的中断。

构建、发布、运行

构建、发布和运行阶段的描述如下。我们应该在这三个阶段之间保持清晰的分离:

  • 构建:从代码创建可执行包(EAR、WAR 或 JAR),以及可以部署到多个环境的依赖项

  • 发布:将可执行包与特定环境配置结合起来,在环境中部署

  • 运行:使用特定发布在执行环境中运行应用程序

以下截图突出显示了构建和发布阶段:

一个反模式是构建针对每个环境特定的单独可执行包。

无状态

Twelve-Factor 应用程序没有状态。它需要的所有数据都存储在持久存储中。

粘性会话是一种反模式。

端口绑定

Twelve-Factor 应用程序通过端口绑定公开所有服务。虽然可能有其他机制来公开服务,但这些机制是依赖于实现的。端口绑定可以完全控制接收和处理消息,无论应用程序部署在何处。

并发

十二要素应用通过水平扩展实现更多的并发。垂直扩展有其限制。水平扩展提供了无限扩展的机会。

可处置性

十二要素应用应该促进弹性扩展。因此,它们应该是可处置的。它们可以在需要时启动和停止。

十二要素应用应该做到以下几点:

  • 具有最小的启动时间。长时间的启动意味着应用程序在能够接受请求之前有很长的延迟。

  • 优雅地关闭。

  • 优雅地处理硬件故障。

环境一致性

所有环境——开发、测试、暂存和生产——应该是相似的。它们应该使用相同的流程和工具。通过持续部署,它们应该非常频繁地具有相似的代码。这使得查找和修复问题更容易。

日志作为事件流

对于十二要素应用来说,可见性至关重要。由于应用部署在云上并且自动扩展,重要的是你能够集中查看应用程序不同实例中发生的情况。

将所有日志视为流使得可以将日志流路由到不同的目的地以进行查看和存档。这个流可以用于调试问题、执行分析,并基于错误模式创建警报系统。

没有管理流程的区别

十二要素应用将管理任务(迁移、脚本)视为正常应用程序流程的一部分。

Spring 项目

随着世界朝着云原生应用和微服务迈进,Spring 项目也紧随其后。有许多新的 Spring 项目——Spring Boot、Spring Cloud 等,解决了新兴世界的问题。

Spring Boot

在单体架构时代,我们有时间为应用程序设置框架的奢侈。然而,在微服务时代,我们希望更快地创建单独的组件。Spring Boot 项目旨在解决这个问题。

正如官方网站强调的那样,Spring Boot 使得创建独立的、生产级别的基于 Spring 的应用程序变得容易,你可以直接运行。我们对 Spring 平台和第三方库采取了一种有主见的观点,这样你就可以尽量少地开始。

Spring Boot 旨在采取一种有主见的观点——基本上为我们做出许多决定——以开发基于 Spring 的项目。

在接下来的几章中,我们将看看 Spring Boot 以及不同的功能,使我们能够更快地创建适用于生产的应用程序。

Spring Cloud

Spring Cloud 旨在为在云上构建系统时遇到的一些常见模式提供解决方案:

  • 配置管理:正如我们在十二要素应用部分讨论的那样,管理配置是开发云原生应用的重要部分。Spring Cloud 为微服务提供了一个名为 Spring Cloud Config 的集中式配置管理解决方案。

  • 服务发现:服务发现促进了服务之间的松耦合。Spring Cloud 与流行的服务发现选项(如 Eureka、ZooKeeper 和 Consul)集成。

  • 断路器:云原生应用必须具有容错能力。它们应该能够优雅地处理后端服务的故障。断路器在故障时提供默认的最小服务起着关键作用。Spring Cloud 与 Netflix Hystrix 容错库集成。

  • API 网关:API 网关提供集中的聚合、路由和缓存服务。Spring Cloud 与 API 网关库 Netflix Zuul 集成。

总结

在本章中,我们看到了世界是如何向微服务和云原生应用发展的。我们了解到 Spring 框架和项目如何发展以满足当今世界的需求,例如 Spring Boot、Spring Cloud 和 Spring Data 等项目。

在下一章中,我们将开始关注 Spring Boot。我们将看看 Spring Boot 如何简化微服务的开发。

Spring 框架 1.0 的第一个版本于 2004 年 3 月发布。十五年多来,Spring 框架一直是构建 Java 应用程序的首选框架。

在相对年轻和充满活力的 Java 框架世界中,十年是很长的时间。

在本章中,我们将开始了解 Spring 框架的核心特性。我们将看看 Spring 框架为何变得受欢迎以及如何适应保持首选框架。在快速了解 Spring 框架的重要模块后,我们将进入 Spring 项目的世界。最后,我们将看看 Spring 框架 5.0 中的新功能。

本章将回答以下问题:

  • Spring 框架为何受欢迎?

  • Spring 框架如何适应应用架构的演变?

  • Spring 框架中的重要模块是什么?

  • Spring 框架在 Spring 项目的伞下适用于哪里?

  • Spring 框架 5.0 中的新功能是什么?

Spring 框架

Spring 网站(projects.spring.io/spring-framework/)将 Spring 框架定义如下:Spring 框架为现代基于 Java 的企业应用程序提供了全面的编程和配置模型

Spring 框架用于连接企业 Java 应用程序。Spring 框架的主要目的是处理连接应用程序不同部分所需的所有技术细节。这使程序员能够专注于他们的工作核心--编写业务逻辑。

EJB 的问题

Spring 框架于 2004 年 3 月发布。当 Spring 框架的第一个版本发布时,开发企业应用程序的流行方式是使用 EJB 2.1。

开发和部署 EJB 是一个繁琐的过程。虽然 EJB 使组件的分发变得更容易,但开发、单元测试和部署它们并不容易。EJB 的初始版本(1.0、2.0、2.1)具有复杂的应用程序接口(API),导致人们认为(在大多数应用程序中是真的)引入的复杂性远远超过了好处:

  • 难以进行单元测试。实际上,在 EJB 容器外进行测试很困难。

  • 需要实现多个接口,其中包含许多不必要的方法。

  • 繁琐和乏味的异常处理。

  • 不方便的部署描述符。

Spring 框架被引入作为一个轻量级框架,旨在简化开发 Java EE 应用程序。

Spring 框架为什么受欢迎?

Spring 框架的第一个版本于 2004 年 3 月发布。在随后的十五年中,Spring 框架的使用和受欢迎程度不断增长。

Spring 框架受欢迎的重要原因如下:

  • 简化单元测试--由于依赖注入

  • 减少了管道代码

  • 架构灵活性

  • 跟上时代的变化

让我们详细讨论每一个。

简化单元测试

早期版本的 EJB 非常难以进行单元测试。事实上,很难在容器外运行 EJB(截至 2.1 版本)。测试它们的唯一方法是在容器中部署它们。

Spring 框架引入了依赖注入的概念。我们将在第二章“依赖注入”中详细讨论依赖注入。

依赖注入通过轻松替换依赖项为其模拟使单元测试变得容易。我们不需要部署整个应用程序来进行单元测试。

简化单元测试具有多重好处:

  • 程序员更加高效

  • 缺陷被更早地发现,因此修复成本更低

  • 应用程序具有自动化单元测试,可以在持续集成构建中运行,以防止未来的缺陷

减少管道代码

在 Spring Framework 之前,典型的 J2EE(或现在称为 Java EE)应用程序包含大量的管道代码。例如:获取数据库连接、异常处理代码、事务管理代码、日志记录代码等等。

让我们看一个使用准备语句执行查询的简单例子:

    PreparedStatement st = null;
    try {
          st = conn.prepareStatement(INSERT_TODO_QUERY);
          st.setString(1, bean.getDescription());
          st.setBoolean(2, bean.isDone());
          st.execute();
        } 
    catch (SQLException e) {
          logger.error("Failed : " + INSERT_TODO_QUERY, e);
     } finally {
                if (st != null) {
           try {
           st.close();
          } catch (SQLException e) {
           // Ignore - nothing to do..
          }
       }
     }

在前面的例子中,有四行业务逻辑和超过 10 行的管道代码。

使用 Spring Framework,相同的逻辑可以应用在几行代码中:

    jdbcTemplate.update(INSERT_TODO_QUERY, 
    bean.getDescription(), bean.isDone());

Spring Framework 是如何做到这一点的呢?

在前面的例子中,Spring JDBC(以及 Spring 总体)将大多数已检查异常转换为未检查异常。通常,当查询失败时,我们除了关闭语句并使事务失败之外,没有太多可以做的事情。我们可以集中处理异常并使用 Spring 面向切面编程AOP)进行注入,而不是在每个方法中实现异常处理。

Spring JDBC 消除了创建所有涉及获取连接、创建准备语句等管道代码的需要。jdbcTemplate类可以在 Spring 上下文中创建,并在需要时注入到数据访问对象DAO)类中。

与前面的例子类似,Spring JMS、Spring AOP 和其他 Spring 模块有助于减少大量的管道代码。

Spring Framework 让程序员专注于程序员的主要工作--编写业务逻辑。

避免所有管道代码还有另一个很大的好处--减少代码重复。由于所有事务管理、异常处理等代码(通常是所有横切关注点)都在一个地方实现,因此更容易维护。

架构灵活性

Spring Framework 是模块化的。它是建立在核心 Spring 模块之上的一组独立模块。大多数 Spring 模块都是独立的--您可以使用其中一个而不必使用其他模块。

让我们看几个例子:

  • 在 Web 层,Spring 提供了自己的框架--Spring MVC。但是,Spring 对 Struts、Vaadin、JSF 或您选择的任何 Web 框架都有很好的支持。

  • Spring Beans 可以为您的业务逻辑提供轻量级实现。但是,Spring 也可以与 EJB 集成。

  • 在数据层,Spring 通过其 Spring JDBC 模块简化了 JDBC。但是,Spring 对您喜欢的任何数据层框架--JPA、Hibernate(带有或不带有 JPA)或 iBatis 都有很好的支持。

  • 您可以选择使用 Spring AOP 实现横切关注点(日志记录、事务管理、安全等)。或者,您可以集成一个完整的 AOP 实现,比如 AspectJ。

Spring Framework 不想成为万能工具。在专注于减少应用程序不同部分之间的耦合并使它们可测试的核心工作的同时,Spring 与您选择的框架进行了很好的集成。这意味着您在架构上有灵活性--如果您不想使用特定的框架,可以轻松地用另一个替换它。

跟上时代的变化

Spring Framework 的第一个版本专注于使应用程序可测试。然而,随着时间的推移,出现了新的挑战。Spring Framework 设法通过提供的灵活性和模块来不断发展并保持领先地位。以下列举了一些例子:

  • 注解是在 Java 5 中引入的。Spring Framework(版本 2.5 - 2007 年 11 月)在引入基于注解的 Spring MVC 控制器模型方面领先于 Java EE。使用 Java EE 的开发人员必须等到 Java EE 6(2009 年 12 月 - 2 年后)才能获得可比较的功能。

  • Spring Framework 在 Java EE 之前引入了许多抽象概念,以使应用程序与特定实现解耦。 缓存 API 就是一个例子。 Spring 在 Spring 3.1 中提供了透明的缓存支持。 Java EE 推出了JSR-107用于 JCache(2014 年)--Spring 4.1 提供了对其的支持。

Spring 带来的另一个重要事项是 Spring 项目的总称。 Spring Framework 只是 Spring 项目下的众多项目之一。 我们将在单独的部分讨论不同的 Spring 项目。 以下示例说明了 Spring 如何通过新的 Spring 项目保持领先地位:

  • Spring Batch定义了构建 Java 批处理应用程序的新方法。 我们不得不等到 Java EE 7(2013 年 6 月)才有了 Java EE 中可比的批处理应用程序规范。

  • 随着架构向云和微服务发展,Spring 推出了新的面向云的 Spring 项目。 Spring Cloud 有助于简化微服务的开发和部署。 Spring Cloud Data Flow 提供了围绕微服务应用程序的编排。

Spring 模块

Spring Framework 的模块化是其广泛使用的最重要原因之一。 Spring Framework 非常模块化,有 20 多个不同的模块--具有明确定义的边界。

下图显示了不同的 Spring 模块--按照它们通常在应用程序中使用的层进行组织:

我们将从讨论 Spring 核心容器开始,然后再讨论其他模块,这些模块按照它们通常在应用程序层中使用的方式进行分组。

Spring 核心容器

Spring Core Container 提供了 Spring Framework 的核心功能--依赖注入,IoC(控制反转)容器和应用程序上下文。 我们将在第二章“依赖注入”中更多地了解 DI 和 IoC 容器。

重要的核心 Spring 模块列在以下表中:

模块/构件 用途
spring-core 其他 Spring 模块使用的实用程序。
spring-beans 支持 Spring beans。 与 spring-core 结合使用,提供了 Spring Framework 的核心功能--依赖注入。 包括 BeanFactory 的实现。
spring-context 实现了 ApplicationContext,它扩展了 BeanFactory,并提供了加载资源和国际化等支持。
spring-expression 扩展了 JSP 的EL(表达式语言)并提供了一种用于访问和操作 bean 属性(包括数组和集合)的语言。

横切关注点

横切关注点适用于所有应用程序层--包括日志记录和安全性等。 AOP通常用于实现横切关注点。

单元测试和集成测试属于这个类别,因为它们适用于所有层。

与横切关注点相关的重要 Spring 模块列在以下表中:

模块/构件 用途
spring-aop 提供面向方面的编程的基本支持--具有方法拦截器和切入点。
spring-aspects 提供与最受欢迎和功能齐全的 AOP 框架 AspectJ 的集成。
spring-instrument 提供基本的仪器支持。
spring-test 提供基本的单元测试和集成测试支持。

Web

Spring 除了与流行的 Web 框架(如 Struts)提供良好的集成外,还提供了自己的 MVC 框架 Spring MVC。

重要的构件/模块列在以下表中:

  • spring-web: 提供基本的网络功能,如多部分文件上传。 提供与其他 Web 框架(如 Struts)集成的支持。

  • spring-webmvc: 提供了一个功能齐全的 Web MVC 框架--Spring MVC,其中包括实现 REST 服务的功能。

我们将在第三章使用 Spring MVC 构建 Web 应用程序和第五章使用 Spring Boot 构建微服务中介绍 Spring MVC 并开发 Web 应用程序和 REST 服务。

业务

业务层专注于执行应用程序的业务逻辑。使用 Spring,业务逻辑通常在普通的旧 Java 对象POJO)中实现。

Spring Transactions (spring-tx)为 POJO 和其他类提供声明式事务管理。

数据

应用程序中的数据层通常与数据库和/或外部接口通信。

以下是与数据层相关的一些重要的 Spring 模块:

模块/构件 用途
spring-jdbc 提供对 JDBC 的抽象,避免样板代码。
spring-orm 与 ORM 框架和规范集成--包括 JPA 和 Hibernate 等。
spring-oxm 提供对象到 XML 映射集成。支持诸如 JAXB、Castor 等框架。
spring-jms 提供对 JMS 的抽象,避免样板代码。

Spring 项目

虽然 Spring 框架为企业应用程序的核心功能(DI、Web、数据)提供了基础,但其他 Spring 项目探索了企业领域的集成和解决其他问题的解决方案--部署、云端、大数据、批处理和安全等。

以下是一些重要的 Spring 项目:

  • Spring Boot

  • Spring Cloud

  • Spring Data

  • Spring Batch

  • Spring Security

  • Spring HATEOAS

Spring Boot

在开发微服务和 Web 应用程序时遇到的一些挑战如下:

  • 做框架选择和决定兼容的框架版本

  • 提供外部化配置的机制--可以从一个环境更改为另一个环境的属性

  • 健康检查和监控--如果应用程序的特定部分宕机,则提供警报

  • 决定部署环境并为其配置应用程序

Spring Boot 通过采取主观的观点来解决所有这些问题。

我们将在两章中深入研究 Spring Boot--第五章,使用 Spring Boot 构建微服务和第七章,高级 Spring Boot 功能

Spring Cloud

可以毫不夸张地说世界正在向云端迁移

云原生微服务和应用程序是当今的趋势。我们将在第四章向微服务和云原生应用程序的演变中详细讨论这一点。

Spring 正在快速迈向使云端应用程序开发更简单的方向。

Spring Cloud 为分布式系统中的常见模式提供解决方案。Spring Cloud 使开发人员能够快速创建实现常见模式的应用程序。Spring Cloud 中实现的一些常见模式如下所示:

  • 配置管理

  • 服务发现

  • 断路器

  • 智能路由

我们将在第九章中更详细地讨论 Spring Cloud 及其各种功能,Spring Cloud

Spring Data

当今世界有多个数据来源--SQL(关系型)和各种 NOSQL 数据库。Spring Data 试图为所有这些不同类型的数据库提供一致的数据访问方法。

Spring Data 提供与各种规范和/或数据存储的集成:

  • JPA

  • MongoDB

  • Redis

  • Solr

  • Gemfire

  • Apache Cassandra

以下是一些重要特性:

  • 通过从方法名称确定查询来提供对存储库和对象映射的抽象

  • 简单的 Spring 集成

  • 与 Spring MVC 控制器集成

  • 高级自动审计功能--创建者、创建日期、最后更改者和最后更改日期

我们将在第八章中更详细地讨论 Spring Data,Spring Data

Spring Batch

今天的企业应用程序使用批处理程序处理大量数据。这些应用程序的需求非常相似。Spring Batch 为具有高性能要求的高容量批处理程序提供解决方案。

Spring Batch 中的重要功能如下:

  • 启动、停止和重新启动作业的能力,包括重新启动失败的作业从失败的地方重新开始

  • 处理数据的能力

  • 重试步骤或在失败时跳过步骤的能力

  • 基于 Web 的管理界面

Spring Security

身份验证是识别用户的过程。授权是确保用户有权访问资源执行已识别的操作的过程。

身份验证和授权是企业应用程序的关键部分,包括 Web 应用程序和 Web 服务。Spring Security 为基于 Java 的应用程序提供声明性身份验证和授权。

Spring Security 中的重要功能如下:

  • 简化的身份验证和授权

  • 与 Spring MVC 和 Servlet API 的良好集成

  • 支持防止常见的安全攻击--跨站请求伪造CSRF)和会话固定

  • 可用于与 SAML 和 LDAP 集成的模块

我们将在第三章中讨论如何使用 Spring Security 保护 Web 应用程序,使用 Spring MVC 构建 Web 应用程序

我们将在《第六章》中讨论如何使用 Spring Security 来保护基本和 OAuth 身份验证机制的 REST 服务,扩展微服务

Spring HATEOAS

HATEOAS代表超媒体作为应用程序状态的引擎。尽管听起来很复杂,但它是一个非常简单的概念。它的主要目的是将服务器(服务提供者)与客户端(服务消费者)解耦。

服务提供者向服务消费者提供有关资源上可以执行的其他操作的信息。

Spring HATEOAS 提供了 HATEOAS 实现,特别是针对使用 Spring MVC 实现的 REST 服务。

Spring HATEOAS 中的重要功能如下:

  • 简化指向服务方法的链接的定义,使链接更加稳固

  • 支持 JAXB(基于 XML)和 JSON 集成

  • 对服务消费者(客户端)的支持

我们将在《第六章》中讨论如何使用 HATEOAS,扩展微服务

Spring Framework 5.0 中的新功能

Spring Framework 5.0 是 Spring Framework 的首次重大升级,距离 Spring Framework 4.0 已经有将近四年的时间。在这段时间内,Spring Boot 项目的主要发展之一就是 Spring Boot 项目的发展。我们将在下一节中讨论 Spring Boot 2.0 中的新功能。

Spring Framework 5.0 最大的特性之一是响应式编程。Spring Framework 5.0 具有核心响应式编程功能,并且支持响应式端点。重要变化的列表包括以下内容:

  • 基线升级

  • JDK 9 运行时兼容性

  • 在 Spring Framework 代码中使用 JDK 8 功能

  • 响应式编程支持

  • 功能性的 Web 框架

  • Jigsaw 中的 Java 模块化

  • Kotlin 支持

  • 删除的功能

基线升级

Spring Framework 5.0 具有 JDK 8 和 Java EE 7 基线。基本上,这意味着不再支持以前的 JDK 和 Java EE 版本。

Spring Framework 5.0 的一些重要基线 Java EE 7 规范如下所示:

  • Servlet 3.1

  • JMS 2.0

  • JPA 2.1

  • JAX-RS 2.0

  • Bean Validation 1.1

许多 Java 框架的最低支持版本发生了许多变化。以下列表包含一些知名框架的最低支持版本:

  • Hibernate 5

  • Jackson 2.6

  • EhCache 2.10

  • JUnit 5

  • Tiles 3

以下列表显示了支持的服务器版本:

  • Tomcat 8.5+

  • Jetty 9.4+

  • WildFly 10+

  • Netty 4.1+(用于使用 Spring Web Flux 进行 Web 响应式编程)

  • Undertow 1.4+(用于使用 Spring Web Flux 进行 Web 响应式编程)

使用之前版本的任何上述规范/框架的应用程序在使用 Spring Framework 5.0 之前,至少需要升级到前面列出的版本。

JDK 9 运行时兼容性

预计 JDK 9 将于 2017 年中期发布。Spring Framework 5.0 预计将与 JDK 9 具有运行时兼容性。

在 Spring Framework 代码中使用 JDK 8 的特性

Spring Framework 4.x 的基线版本是 Java SE 6。这意味着它支持 Java 6、7 和 8。必须支持 Java SE 6 和 7 会对 Spring Framework 代码造成限制。框架代码不能使用 Java 8 的任何新功能。因此,尽管世界其他地方升级到了 Java 8,Spring Framework 中的代码(至少是主要部分)仍受限于使用较早版本的 Java。

在 Spring Framework 5.0 中,基线版本是 Java 8。Spring Framework 代码现在已升级以使用 Java 8 的新功能。这将导致更易读和更高性能的框架代码。其中使用的一些 Java 8 特性如下:

  • 核心 Spring 接口中的 Java 8 默认方法

  • 基于 Java 8 反射增强的内部代码改进

  • 在框架代码中使用函数式编程--lambda 和 streams

响应式编程支持

响应式编程是 Spring Framework 5.0 最重要的特性之一。

微服务架构通常建立在基于事件的通信之上。应用程序被构建为对事件(或消息)做出反应。

响应式编程提供了一种专注于构建对事件做出反应的应用程序的替代编程风格。

虽然 Java 8 没有内置对响应式编程的支持,但有许多框架提供了对响应式编程的支持:

  • Reactive Streams:语言中立的尝试定义响应式 API。

  • Reactor:由 Spring Pivotal 团队提供的 Reactive Streams 的 Java 实现。

  • Spring WebFlux:支持基于响应式编程的 Web 应用程序开发。提供类似于 Spring MVC 的编程模型。

我们将在第十一章中讨论响应式编程以及如何在 Spring Web Flux 中实现它,响应式编程

功能性 Web 框架

基于响应式特性,Spring 5 还提供了一个功能性的 Web 框架。

功能性 Web 框架提供了使用函数式编程风格定义端点的功能。下面是一个简单的 hello world 示例:

    RouterFunction<String> route =
    route(GET("/hello-world"),
    request -> Response.ok().body(fromObject("Hello World")));

功能性 Web 框架还可以用于定义更复杂的路由,如下面的示例所示:

    RouterFunction<?> route = route(GET("/todos/{id}"),
    request -> {
       Mono<Todo> todo = Mono.justOrEmpty(request.pathVariable("id"))
       .map(Integer::valueOf)
       .then(repository::getTodo);
       return Response.ok().body(fromPublisher(todo, Todo.class));
      })
     .and(route(GET("/todos"),
     request -> {
       Flux<Todo> people = repository.allTodos();
       return Response.ok().body(fromPublisher(people, Todo.class));
     }))
    .and(route(POST("/todos"),
    request -> {
      Mono<Todo> todo = request.body(toMono(Todo.class));
      return Response.ok().build(repository.saveTodo(todo));
    }));

需要注意的一些重要事项如下:

  • RouterFunction评估匹配条件以将请求路由到适当的处理程序函数

  • 我们正在定义三个端点,两个 GET 和一个 POST,并将它们映射到不同的处理程序函数

我们将在第十一章中更详细地讨论 Mono 和 Flux,响应式编程

使用 Jigsaw 的 Java 模块化

直到 Java 8,Java 平台并不是模块化的。由此产生了一些重要问题:

  • 平台膨胀:在过去的几十年里,Java 模块化并不是一个令人担忧的问题。然而,随着物联网IOT)和新的轻量级平台如 Node.js 的出现,迫切需要解决 Java 平台的膨胀问题。(JDK 的初始版本小于 10MB。最近的 JDK 版本需要超过 200MB。)

  • JAR 地狱:另一个重要问题是 JAR 地狱的问题。当 Java ClassLoader 找到一个类时,它不会查看是否有其他可用的类定义。它会立即加载找到的第一个类。如果应用程序的两个不同部分需要来自不同 JAR 的相同类,它们无法指定要从哪个 JAR 加载类。

开放系统网关倡议(OSGi)是 1999 年开始的倡议之一,旨在为 Java 应用程序带来模块化。

每个模块(称为捆绑包)定义如下:

  • 导入:模块使用的其他捆绑包

  • 导出:此捆绑包导出的包

每个模块都可以有自己的生命周期。它可以独立安装、启动和停止。

Jigsaw 是 Java 社区进程(JCP)的一个倡议,从 Java 7 开始,旨在为 Java 带来模块化。它有两个主要目标:

  • 为 JDK 定义和实现模块化结构

  • 为构建在 Java 平台上的应用程序定义模块系统

Jigsaw 预计将成为 Java 9 的一部分,Spring Framework 5.0 预计将包括对 Jigsaw 模块的基本支持。

Kotlin 支持

Kotlin 是一种静态类型的 JVM 语言,可以编写富有表现力、简短和可读的代码。Spring Framework 5.0 对 Kotlin 有很好的支持。

考虑一个简单的 Kotlin 程序,演示一个数据类,如下所示:

    import java.util.*
    data class Todo(var description: String, var name: String, var  
    targetDate : Date)
    fun main(args: Array<String>) {
      var todo = Todo("Learn Spring Boot", "Jack", Date())
      println(todo)
        //Todo(description=Learn Spring Boot, name=Jack, 
        //targetDate=Mon May 22 04:26:22 UTC 2017)
      var todo2 = todo.copy(name = "Jill")
      println(todo2)
         //Todo(description=Learn Spring Boot, name=Jill, 
         //targetDate=Mon May 22 04:26:22 UTC 2017)
      var todo3 = todo.copy()
      println(todo3.equals(todo)) //true
    }  

在不到 10 行代码的情况下,我们创建并测试了一个具有三个属性和以下函数的数据 bean:

  • equals()

  • hashCode()

  • toString()

  • copy()

Kotlin 是强类型的。但是不需要明确指定每个变量的类型:

    val arrayList = arrayListOf("Item1", "Item2", "Item3") 
    // Type is ArrayList

命名参数允许您在调用方法时指定参数的名称,从而使代码更易读:

    var todo = Todo(description = "Learn Spring Boot", 
    name = "Jack", targetDate = Date())

Kotlin 通过提供默认变量(it)和诸如 takedrop 等方法,使函数式编程更简单:

    var first3TodosOfJack = students.filter { it.name == "Jack"   
     }.take(3)

您还可以在 Kotlin 中为参数指定默认值:

    import java.util.*
    data class Todo(var description: String, var name: String, var
    targetDate : Date = Date())
    fun main(args: Array<String>) {
      var todo = Todo(description = "Learn Spring Boot", name = "Jack")
    }

凭借其简洁和表达力的特点,我们期望 Kotlin 成为一个需要学习的语言。

我们将在第十三章《在 Spring 中使用 Kotlin》中更多地讨论 Kotlin。

已删除的功能

Spring Framework 5 是一个重要的 Spring 发布版本,基线版本大幅增加。随着 Java、Java EE 和其他一些框架的基线版本增加,Spring Framework 5 移除了对一些框架的支持:

  • Portlet

  • Velocity

  • JasperReports

  • XMLBeans

  • JDO

  • Guava

如果您使用了上述任何框架,建议您计划迁移并继续使用支持到 2019 年的 Spring Framework 4.3。

Spring Boot 2.0 新功能

Spring Boot 的第一个版本于 2014 年发布。以下是 Spring Boot 2.0 中预期的一些重要更新:

  • 基线 JDK 版本是 Java 8

  • 基线 Spring 版本是 Spring Framework 5.0

  • Spring Boot 2.0 具有对 WebFlux 的响应式 Web 编程的支持

一些重要框架的最低支持版本如下所列:

  • Jetty 9.4

  • Tomcat 8.5

  • Hibernate 5.2

  • Gradle 3.4

我们将在第五章《使用 Spring Boot 构建微服务》和第七章《高级 Spring Boot 功能》中广泛讨论 Spring Boot。

摘要

在过去的十五年中,Spring Framework 显著改善了开发 Java 企业应用程序的体验。Spring Framework 5.0 带来了许多功能,同时显著提高了基线。

在接下来的章节中,我们将介绍依赖注入,并了解如何使用 Spring MVC 开发 Web 应用程序。之后,我们将进入微服务的世界。在第五章《使用 Spring Boot 构建微服务》、第六章《扩展微服务》和第七章《高级 Spring Boot 功能》中,我们将介绍 Spring Boot 如何简化微服务的创建。然后我们将把注意力转向使用 Spring Cloud 和 Spring Cloud Data Flow 在云中构建应用程序。

第五章:使用 Spring Boot 构建微服务

正如我们在上一章中讨论的,我们正在朝着具有更小、可以独立部署的微服务的架构发展。这意味着将会开发大量更小的微服务。

一个重要的结果是,我们需要能够快速上手并运行新组件。

Spring Boot 旨在解决快速启动新组件的问题。在本章中,我们将开始了解 Spring Boot 带来的能力。我们将回答以下问题:

  • 为什么选择 Spring Boot?

  • Spring Boot 提供了哪些功能?

  • 什么是自动配置?

  • Spring Boot 不是什么?

  • 当您使用 Spring Boot 时,后台会发生什么?

  • 如何使用 Spring Initializr 创建新的 Spring Boot 项目?

  • 如何使用 Spring Boot 创建基本的 RESTful 服务?

什么是 Spring Boot?

首先,让我们开始澄清关于 Spring Boot 的一些误解:

  • Spring Boot 不是一个代码生成框架。它不会生成任何代码。

  • Spring Boot 既不是应用服务器,也不是 Web 服务器。它与不同范围的应用程序和 Web 服务器集成良好。

  • Spring Boot 不实现任何特定的框架或规范。

这些问题仍然存在:

  • 什么是 Spring Boot?

  • 为什么在过去几年中变得如此流行?

为了回答这些问题,让我们构建一个快速的示例。让我们考虑一个您想要快速原型的示例应用程序。

为微服务快速创建原型

假设我们想要使用 Spring MVC 构建一个微服务,并使用 JPA(使用 Hibernate 作为实现)来连接数据库。

让我们考虑设置这样一个应用程序的步骤:

  1. 决定使用哪个版本的 Spring MVC、JPA 和 Hibernate。

  2. 设置 Spring 上下文以将所有不同的层连接在一起。

  3. 使用 Spring MVC 设置 Web 层(包括 Spring MVC 配置):

  • 为 DispatcherServlet、处理程序、解析器、视图解析器等配置 bean
  1. 在数据层设置 Hibernate:
  • 为 SessionFactory、数据源等配置 bean
  1. 决定并实现如何存储应用程序配置,这在不同环境之间会有所不同。

  2. 决定您希望如何进行单元测试。

  3. 决定并实现您的事务管理策略。

  4. 决定并实现如何实现安全性。

  5. 设置您的日志框架。

  6. 决定并实现您希望如何在生产中监视应用程序。

  7. 决定并实现一个度量管理系统,以提供有关应用程序的统计信息。

  8. 决定并实现如何将应用程序部署到 Web 或应用程序服务器。

至少有几个提到的步骤必须在我们开始构建业务逻辑之前完成。这可能需要至少几周的时间。

当我们构建微服务时,我们希望能够快速启动。所有前面的步骤都不会使开发微服务变得容易。这就是 Spring Boot 旨在解决的问题。

以下引用是从 Spring Boot 网站中提取的(docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-documentation):

Spring Boot 使得创建独立的、生产级别的基于 Spring 的应用程序变得容易,您可以“只需运行”。我们对 Spring 平台和第三方库持有一种看法,因此您可以尽量少地开始。大多数 Spring Boot 应用程序几乎不需要 Spring 配置。

Spring Boot 使开发人员能够专注于微服务背后的业务逻辑。它旨在处理开发微服务涉及的所有琐碎技术细节。

主要目标

Spring Boot 的主要目标如下:

  • 快速启动基于 Spring 的项目。

  • 持有观点。根据常见用法进行默认假设。提供配置选项以处理与默认值不同的偏差。

  • 提供了各种非功能特性。

  • 不要使用代码生成,避免使用大量的 XML 配置。

非功能特性

Spring Boot 提供的一些非功能特性如下:

  • 默认处理各种框架、服务器和规范的版本控制和配置

  • 应用程序安全的默认选项

  • 默认应用程序指标,并有扩展的可能性

  • 使用健康检查进行基本应用程序监控

  • 多种外部化配置选项

Spring Boot Hello World

我们将从本章开始构建我们的第一个 Spring Boot 应用程序。我们将使用 Maven 来管理依赖项。

启动 Spring Boot 应用程序涉及以下步骤:

  1. 在您的pom.xml文件中配置spring-boot-starter-parent

  2. 使用所需的起始项目配置pom.xml文件。

  3. 配置spring-boot-maven-plugin以便能够运行应用程序。

  4. 创建您的第一个 Spring Boot 启动类。

让我们从第 1 步开始:配置起始项目。

配置 spring-boot-starter-parent

让我们从一个简单的带有spring-boot-starter-parentpom.xml文件开始:

    <project 

     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
     http://maven.apache.org/xsd/maven-4.0.0.xsd">   
    <modelVersion>4.0.0</modelVersion> 
    <groupId>com.mastering.spring</groupId> 
    <artifactId>springboot-example</artifactId> 
    <version>0.0.1-SNAPSHOT</version> 
    <name>First Spring Boot Example</name> 
    <packaging>war</packaging>
    <parent> 
      <groupId>org.springframework.boot</groupId> 
      <artifactId>spring-boot-starter-parent</artifactId>  
      <version>2.0.0.M1</version>
    </parent>
    <properties> 
      <java.version>1.8</java.version> 
    </properties>

   <repositories>
    <repository>
      <id>spring-milestones</id>
      <name>Spring Milestones</name>
      <url>https://repo.spring.io/milestone</url>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </repository>
   </repositories>

   <pluginRepositories>
    <pluginRepository>
      <id>spring-milestones</id>
      <name>Spring Milestones</name>
      <url>https://repo.spring.io/milestone</url>
        <snapshots>
          <enabled>false</enabled>
        </snapshots>
     </pluginRepository>
    </pluginRepositories>

</project>

第一个问题是:为什么我们需要spring-boot-starter-parent

spring-boot-starter-parent依赖项包含要使用的 Java 的默认版本,Spring Boot 使用的依赖项的默认版本以及 Maven 插件的默认配置。

spring-boot-starter-parent依赖是为基于 Spring Boot 的应用程序提供依赖项和插件管理的父 POM。

让我们看一下spring-boot-starter-parent内部的一些代码,以更深入地了解spring-boot-starter-parent

spring-boot-starter-parent

spring-boot-starter-parent依赖项继承自顶部 POM 中定义的spring-boot-dependencies。以下代码片段显示了从spring-boot-starter-parent中提取的内容:

    <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>2.0.0.M1</version>
      <relativePath>../../spring-boot-dependencies</relativePath>
   </parent>

spring-boot-dependencies为 Spring Boot 使用的所有依赖项提供了默认的依赖项管理。以下代码显示了在spring-boot-dependencies中配置的各种依赖项的不同版本:

<activemq.version>5.13.4</activemq.version>
<aspectj.version>1.8.9</aspectj.version>
<ehcache.version>2.10.2.2.21</ehcache.version>
<elasticsearch.version>2.3.4</elasticsearch.version>
<gson.version>2.7</gson.version>
<h2.version>1.4.192</h2.version>
<hazelcast.version>3.6.4</hazelcast.version>
<hibernate.version>5.0.9.Final</hibernate.version>
<hibernate-validator.version>5.2.4.Final</hibernate
  validator.version>
<hsqldb.version>2.3.3</hsqldb.version>
<htmlunit.version>2.21</htmlunit.version>
<jackson.version>2.8.1</jackson.version>
<jersey.version>2.23.1</jersey.version>
<jetty.version>9.3.11.v20160721</jetty.version>
<junit.version>4.12</junit.version>
<mockito.version>1.10.19</mockito.version>
<selenium.version>2.53.1</selenium.version>
<servlet-api.version>3.1.0</servlet-api.version>
<spring.version>4.3.2.RELEASE</spring.version>
<spring-amqp.version>1.6.1.RELEASE</spring-amqp.version>
<spring-batch.version>3.0.7.RELEASE</spring-batch.version>
<spring-data-releasetrain.version>Hopper-SR2</spring-
  data-releasetrain.version>
<spring-hateoas.version>0.20.0.RELEASE</spring-hateoas.version>
<spring-restdocs.version>1.1.1.RELEASE</spring-restdocs.version>
<spring-security.version>4.1.1.RELEASE</spring-security.version>
<spring-session.version>1.2.1.RELEASE</spring-session.version>
<spring-ws.version>2.3.0.RELEASE</spring-ws.version>
<thymeleaf.version>2.1.5.RELEASE</thymeleaf.version>
<tomcat.version>8.5.4</tomcat.version>
<xml-apis.version>1.4.01</xml-apis.version>

如果我们想要覆盖特定依赖项的版本,可以通过在我们应用程序的pom.xml文件中提供正确名称的属性来实现。以下代码片段显示了配置我们的应用程序以使用 Mockito 的 1.10.20 版本的示例:

    <properties>
     <mockito.version>1.10.20</mockito.version>
    </properties>

以下是spring-boot-starter-parent中定义的一些其他内容:

  • 默认的 Java 版本为<java.version>1.8</java.version>

  • Maven 插件的默认配置:

  • maven-failsafe-plugin

  • maven-surefire-plugin

  • git-commit-id-plugin

不同版本框架之间的兼容性是开发人员面临的主要问题之一。我如何找到与特定版本 Spring 兼容的最新 Spring Session 版本?通常的答案是阅读文档。但是,如果我们使用 Spring Boot,这就变得简单了,因为有了spring-boot-starter-parent。如果我们想升级到更新的 Spring 版本,我们只需要找到该 Spring 版本的spring-boot-starter-parent依赖项。一旦我们升级我们的应用程序以使用该特定版本的spring-boot-starter-parent,我们将所有其他依赖项升级到与新 Spring 版本兼容的版本。开发人员少了一个问题要处理。总是让我很开心。

使用所需的起始项目配置 pom.xml

每当我们想要在 Spring Boot 中构建应用程序时,我们需要开始寻找起始项目。让我们专注于理解什么是起始项目。

理解起始项目

启动器是为不同目的定制的简化的依赖描述符。例如,spring-boot-starter-web是用于构建 Web 应用程序(包括使用 Spring MVC 的 RESTful)的启动器。它使用 Tomcat 作为默认的嵌入式容器。如果我想使用 Spring MVC 开发 Web 应用程序,我们只需要在依赖项中包含spring-boot-starter-web,就会自动预配置如下内容:

  • Spring MVC

  • 兼容的 jackson-databind 版本(用于绑定)和 hibernate-validator 版本(用于表单验证)

  • spring-boot-starter-tomcat(Tomcat 的启动项目)

以下代码片段显示了在spring-boot-starter-web中配置的一些依赖项:

    <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>
        <dependency>
          <groupId>org.hibernate</groupId>
          <artifactId>hibernate-validator</artifactId>
        </dependency>
        <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-web</artifactId>
        </dependency>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-webmvc</artifactId>
       </dependency>
    </dependencies>

正如我们在前面的代码片段中所看到的

spring-boot-starter-web,我们得到了许多自动配置的框架。

对于我们想要构建的 Web 应用程序,我们还希望进行一些良好的单元测试并将其部署在 Tomcat 上。以下代码片段显示了我们需要的不同启动器依赖项。我们需要将其添加到我们的pom.xml文件中:

    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
     </dependency>
     <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
       <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-tomcat</artifactId>
       <scope>provided</scope>
     </dependency>
    </dependencies>

我们添加了三个启动项目:

  • 我们已经讨论了spring-boot-starter-web。它为我们提供了构建使用 Spring MVC 的 Web 应用程序所需的框架。

  • spring-boot-starter-test依赖项提供了所需的单元测试框架:

  • JUnit:基本的单元测试框架

  • Mockito:用于模拟

  • HamcrestAssertJ:用于可读的断言

  • Spring Test:用于基于 spring-context 的应用程序的单元测试框架

  • spring-boot-starter-tomcat依赖是运行 Web 应用程序的默认值。我们为了清晰起见包含它。spring-boot-starter-tomcat是使用 Tomcat 作为嵌入式 servlet 容器的启动器。

我们现在已经配置了我们的pom.xml文件,其中包含了启动器父级和所需的启动器项目。现在让我们添加spring-boot-maven-plugin,这将使我们能够运行 Spring Boot 应用程序。

配置 spring-boot-maven-plugin

当我们使用 Spring Boot 构建应用程序时,可能会出现几种情况:

  • 我们希望在原地运行应用程序,而不需要构建 JAR 或 WAR

  • 我们希望为以后部署构建一个 JAR 和一个 WAR

spring-boot-maven-plugin依赖项为上述两种情况提供了功能。以下代码片段显示了如何在应用程序中配置spring-boot-maven-plugin

    <build>
     <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
     </plugins>
    </build>

spring-boot-maven-plugin依赖项为 Spring Boot 应用程序提供了几个目标。最受欢迎的目标是 run(可以在项目的根文件夹中的命令提示符上执行mvn spring-boot:run)。

创建您的第一个 Spring Boot 启动类

以下类说明了如何创建一个简单的 Spring Boot 启动类。它使用SpringApplication类的静态 run 方法,如下面的代码片段所示:

    package com.mastering.spring.springboot; 
    import org.springframework.boot.SpringApplication; 
    import org.springframework.boot.
    autoconfigure.SpringBootApplication; 
    import org.springframework.context.ApplicationContext; 
    @SpringBootApplication public class Application {
       public static void main(String[] args)
        { 
         ApplicationContext ctx = SpringApplication.run 
         (Application.class,args); 
        }
     }

前面的代码是一个简单的 Java main方法,执行SpringApplication类上的静态run方法。

SpringApplication 类

SpringApplication类可用于从 Java main方法引导和启动 Spring 应用程序。

以下是 Spring Boot 应用程序引导时通常执行的步骤:

  1. 创建 Spring 的ApplicationContext实例。

  2. 启用接受命令行参数并将它们公开为 Spring 属性的功能。

  3. 根据配置加载所有 Spring bean。

@SpringBootApplication注解

@SpringBootApplication注解是三个注解的快捷方式:

  • @Configuration:指示这是一个 Spring 应用程序上下文配置文件。

  • @EnableAutoConfiguration:启用自动配置,这是 Spring Boot 的一个重要特性。我们将在后面的单独部分讨论自动配置。

  • @ComponentScan:启用在此类的包和所有子包中扫描 Spring bean。

运行我们的 Hello World 应用程序

我们可以以多种方式运行 Hello World 应用程序。让我们从最简单的选项开始运行--作为 Java 应用程序运行。在您的 IDE 中,右键单击应用程序类,并将其作为 Java 应用程序运行。以下截图显示了运行我们的Hello World应用程序的一些日志:

以下是需要注意的关键事项:

  • Tomcat 服务器在端口 8080 上启动--Tomcat started on port(s): 8080 (http)

  • DispatcherServlet 已配置。这意味着 Spring MVC 框架已准备好接受请求--Mapping servlet: 'dispatcherServlet' to [/]

  • 默认启用四个过滤器--characterEncodingFilterhiddenHttpMethodFilterhttpPutFormContentFilterrequestContextFilter

  • 已配置默认错误页面--Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)

  • WebJars 已自动配置。正如我们在第三章使用 Spring MVC 构建 Web 应用程序中讨论的那样,WebJars 可以为静态依赖项(如 Bootstrap 和 query)提供依赖项管理--Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]

以下截图显示了当前应用程序布局。我们只有两个文件,pom.xmlApplication.java

通过一个简单的pom.xml文件和一个 Java 类,我们能够启动 Spring MVC 应用程序,并具有前述所有功能。关于 Spring Boot 最重要的是要了解后台发生了什么。理解前述启动日志是第一步。让我们看一下 Maven 依赖项,以获得更深入的了解。

以下截图显示了在我们创建的pom.xml文件中配置的基本配置的一些依赖项:

Spring Boot 做了很多魔术。一旦您配置并运行了应用程序,我建议您尝试玩耍,以获得更深入的理解,这在您调试问题时将会很有用。

正如蜘蛛侠所说,伴随着强大的力量,也伴随着巨大的责任。这在 Spring Boot 的情况下绝对是真实的。在未来的时间里,最好的 Spring Boot 开发人员将是那些了解后台发生情况的人--依赖项和自动配置。

自动配置

为了让我们更好地理解自动配置,让我们扩展我们的应用程序类,包括更多的代码行:

    ApplicationContext ctx = SpringApplication.run(Application.class, 
     args);
    String[] beanNames = ctx.getBeanDefinitionNames();
    Arrays.sort(beanNames);

   for (String beanName : beanNames) {
     System.out.println(beanName);
    }

我们获取在 Spring 应用程序上下文中定义的所有 bean,并打印它们的名称。当Application.java作为 Java 程序运行时,它会打印出 bean 的列表,如下面的输出所示:

application
basicErrorController
beanNameHandlerMapping
beanNameViewResolver
characterEncodingFilter
conventionErrorViewResolver
defaultServletHandlerMapping
defaultViewResolver
dispatcherServlet
dispatcherServletRegistration
duplicateServerPropertiesDetector
embeddedServletContainerCustomizerBeanPostProcessor
error
errorAttributes
errorPageCustomizer
errorPageRegistrarBeanPostProcessor
faviconHandlerMapping
faviconRequestHandler
handlerExceptionResolver
hiddenHttpMethodFilter
httpPutFormContentFilter
httpRequestHandlerAdapter
jacksonObjectMapper
jacksonObjectMapperBuilder
jsonComponentModule
localeCharsetMappingsCustomizer
mappingJackson2HttpMessageConverter
mbeanExporter
mbeanServer
messageConverters
multipartConfigElement
multipartResolver
mvcContentNegotiationManager
mvcConversionService
mvcPathMatcher
mvcResourceUrlProvider
mvcUriComponentsContributor
mvcUrlPathHelper
mvcValidator
mvcViewResolver
objectNamingStrategy
autoconfigure.AutoConfigurationPackages
autoconfigure.PropertyPlaceholderAutoConfiguration
autoconfigure.condition.BeanTypeRegistry
autoconfigure.context.ConfigurationPropertiesAutoConfiguration
autoconfigure.info.ProjectInfoAutoConfiguration
autoconfigure.internalCachingMetadataReaderFactory
autoconfigure.jackson.JacksonAutoConfiguration
autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration
autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperBuilderConfiguration
autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperConfiguration
autoconfigure.jmx.JmxAutoConfiguration
autoconfigure.web.DispatcherServletAutoConfiguration
autoconfigure.web.DispatcherServletAutoConfiguration$DispatcherServletConfiguration
autoconfigure.web.DispatcherServletAutoConfiguration$DispatcherServletRegistrationConfiguration
autoconfigure.web.EmbeddedServletContainerAutoConfiguration
autoconfigure.web.EmbeddedServletContainerAutoConfiguration$EmbeddedTomcat
autoconfigure.web.ErrorMvcAutoConfiguration
autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration
autoconfigure.web.HttpEncodingAutoConfiguration
autoconfigure.web.HttpMessageConvertersAutoConfiguration
autoconfigure.web.HttpMessageConvertersAutoConfiguration$StringHttpMessageConverterConfiguration
autoconfigure.web.JacksonHttpMessageConvertersConfiguration
autoconfigure.web.JacksonHttpMessageConvertersConfiguration$MappingJackson2HttpMessageConverterConfiguration
autoconfigure.web.MultipartAutoConfiguration
autoconfigure.web.ServerPropertiesAutoConfiguration
autoconfigure.web.WebClientAutoConfiguration
autoconfigure.web.WebClientAutoConfiguration$RestTemplateConfiguration
autoconfigure.web.WebMvcAutoConfiguration
autoconfigure.web.WebMvcAutoConfiguration$EnableWebMvcConfiguration
autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter
autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter$FaviconConfiguration
autoconfigure.websocket.WebSocketAutoConfiguration
autoconfigure.websocket.WebSocketAutoConfiguration$TomcatWebSocketConfiguration
context.properties.ConfigurationPropertiesBindingPostProcessor
context.properties.ConfigurationPropertiesBindingPostProcessor.store
annotation.ConfigurationClassPostProcessor.enhancedConfigurationProcessor
annotation.ConfigurationClassPostProcessor.importAwareProcessor
annotation.internalAutowiredAnnotationProcessor
annotation.internalCommonAnnotationProcessor
annotation.internalConfigurationAnnotationProcessor
annotation.internalRequiredAnnotationProcessor
event.internalEventListenerFactory
event.internalEventListenerProcessor
preserveErrorControllerTargetClassPostProcessor
propertySourcesPlaceholderConfigurer
requestContextFilter
requestMappingHandlerAdapter
requestMappingHandlerMapping
resourceHandlerMapping
restTemplateBuilder
serverProperties
simpleControllerHandlerAdapter
spring.http.encoding-autoconfigure.web.HttpEncodingProperties
spring.http.multipart-autoconfigure.web.MultipartProperties
spring.info-autoconfigure.info.ProjectInfoProperties
spring.jackson-autoconfigure.jackson.JacksonProperties
spring.mvc-autoconfigure.web.WebMvcProperties
spring.resources-autoconfigure.web.ResourceProperties
standardJacksonObjectMapperBuilderCustomizer
stringHttpMessageConverter
tomcatEmbeddedServletContainerFactory
viewControllerHandlerMapping
viewResolver
websocketContainerCustomizer

需要考虑的重要事项如下:

  • 这些 bean 在哪里定义?

  • 这些 bean 是如何创建的?

这就是 Spring 自动配置的魔力。

每当我们向 Spring Boot 项目添加新的依赖项时,Spring Boot 自动配置会自动尝试根据依赖项配置 bean。

例如,当我们在spring-boot-starter-web中添加依赖项时,将自动配置以下 bean:

  • basicErrorControllerhandlerExceptionResolver:基本异常处理。当异常发生时,显示默认错误页面。

  • beanNameHandlerMapping:用于解析到处理程序(控制器)的路径。

  • characterEncodingFilter:提供默认的字符编码 UTF-8。

  • dispatcherServlet:DispatcherServlet 是 Spring MVC 应用程序的前端控制器。

  • jacksonObjectMapper:在 REST 服务中将对象转换为 JSON 和 JSON 转换为对象。

  • messageConverters:默认消息转换器,用于将对象转换为 XML 或 JSON,反之亦然。

  • multipartResolver:提供了在 Web 应用程序中上传文件的支持。

  • mvcValidator:支持对 HTTP 请求进行验证。

  • viewResolver:将逻辑视图名称解析为物理视图。

  • propertySourcesPlaceholderConfigurer:支持应用配置的外部化。

  • requestContextFilter:为请求默认过滤器。

  • restTemplateBuilder:用于调用 REST 服务。

  • tomcatEmbeddedServletContainerFactory:Tomcat 是 Spring Boot 基于 Web 应用程序的默认嵌入式 Servlet 容器。

在下一节中,让我们看一些起始项目和它们提供的自动配置。

Starter 项目

以下表格显示了 Spring Boot 提供的一些重要的起始项目:

Starter 描述
spring-boot-starter-web-services 这是一个用于开发基于 XML 的 Web 服务的起始项目。
spring-boot-starter-web 这是一个用于构建基于 Spring MVC 的 Web 应用程序或 RESTful 应用程序的起始项目。它使用 Tomcat 作为默认的嵌入式 Servlet 容器。
spring-boot-starter-activemq 这支持在 ActiveMQ 上使用 JMS 进行基于消息的通信。
spring-boot-starter-integration 这支持 Spring Integration Framework,提供了企业集成模式的实现。
spring-boot-starter-test 这提供了对各种单元测试框架的支持,如 JUnit、Mockito 和 Hamcrest matchers。
spring-boot-starter-jdbc 这提供了使用 Spring JDBC 的支持。它默认配置了 Tomcat JDBC 连接池。
spring-boot-starter-validation 这提供了对 Java Bean 验证 API 的支持。它的默认实现是 hibernate-validator。
spring-boot-starter-hateoas HATEOAS 代表超媒体作为应用程序状态的引擎。使用 HATEOAS 的 RESTful 服务返回与当前上下文相关的附加资源的链接。
spring-boot-starter-jersey JAX-RS 是开发 REST API 的 Java EE 标准。Jersey 是默认实现。这个起始项目提供了构建基于 JAX-RS 的 REST API 的支持。
spring-boot-starter-websocket HTTP 是无状态的。WebSockets 允许您在服务器和浏览器之间保持连接。这个起始项目提供了对 Spring WebSockets 的支持。
spring-boot-starter-aop 这提供了面向切面编程的支持。它还提供了对高级面向切面编程的 AspectJ 的支持。
spring-boot-starter-amqp 以 RabbitMQ 为默认,这个起始项目提供了使用 AMQP 进行消息传递的支持。
spring-boot-starter-security 这个起始项目启用了 Spring Security 的自动配置。
spring-boot-starter-data-jpa 这提供了对 Spring Data JPA 的支持。其默认实现是 Hibernate。
spring-boot-starter 这是 Spring Boot 应用程序的基本起始项目。它提供了自动配置和日志记录的支持。
spring-boot-starter-batch 这提供了使用 Spring Batch 开发批处理应用程序的支持。
spring-boot-starter-cache 这是使用 Spring Framework 进行缓存的基本支持。
spring-boot-starter-data-rest 这是使用 Spring Data REST 公开 REST 服务的支持。

到目前为止,我们已经建立了一个基本的 Web 应用程序,并了解了与 Spring Boot 相关的一些重要概念:

  • 自动配置

  • Starter 项目

  • spring-boot-maven-plugin

  • spring-boot-starter-parent

  • 注解@SpringBootApplication

现在让我们把重点转移到理解 REST 是什么,并构建一个 REST 服务。

REST 是什么?

表述状态转移REST)基本上是 Web 的一种架构风格。REST 指定了一组约束。这些约束确保客户端(服务消费者和浏览器)可以以灵活的方式与服务器交互。

让我们首先了解一些常见的术语:

  • 服务器:服务提供者。提供可以被客户端消费的服务。

  • 客户端:服务的消费者。可以是浏览器或其他系统。

  • 资源:任何信息都可以是资源:一个人,一张图片,一个视频,或者你想要销售的产品。

  • 表示:资源可以以特定的方式表示。例如,产品资源可以使用 JSON、XML 或 HTML 表示。不同的客户端可能会请求资源的不同表示。

以下列出了一些重要的 REST 约束:

  • 客户端-服务器:应该有一个服务器(服务提供者)和一个客户端(服务消费者)。这使得服务器和客户端可以独立地发展,从而实现松耦合。

  • 无状态:每个服务应该是无状态的。后续的请求不应依赖于从先前请求中临时存储的某些数据。消息应该是自描述的。

  • 统一接口:每个资源都有一个资源标识符。在 Web 服务的情况下,我们使用这个 URI 示例:/users/Jack/todos/1。在这个 URI 中,Jack 是用户的名字。1是我们想要检索的待办事项的 ID。

  • 可缓存:服务响应应该是可缓存的。每个响应都应指示它是否可缓存。

  • 分层系统:服务的消费者不应假定与服务提供者直接连接。由于请求可以被缓存,客户端可能会从中间层获取缓存的响应。

  • 通过表示来操作资源:一个资源可以有多种表示。应该可以通过任何这些表示的消息来修改资源。

  • 超媒体作为应用状态的引擎HATEOAS):RESTful 应用的消费者应该只知道一个固定的服务 URL。所有后续的资源都应该可以从资源表示中包含的链接中发现。

以下是带有 HATEOAS 链接的示例响应。这是对检索所有待办事项的请求的响应:

    {  
    "_embedded":{ 
    "todos":[  
            {  
               "user":"Jill",
               "desc":"Learn Hibernate",
               "done":false,
               "_links":{  
                 "self":{  
                        "href":"http://localhost:8080/todos/1"
                  },
                    "todo":{  
                        "href":"http://localhost:8080/todos/1"
                    }
                }
           }
        ]
     },
    "_links":{  
        "self":{  
            "href":"http://localhost:8080/todos"
        },
        "profile":{  
            "href":"http://localhost:8080/profile/todos"
        },
        "search":{  
            "href":"http://localhost:8080/todos/search"
        }
      }
    }

前面的响应包括以下链接:

  • 特定的待办事项(http://localhost:8080/todos/1

  • 搜索资源(http://localhost:8080/todos/search

如果服务的消费者想要进行搜索,它可以从响应中获取搜索 URL 并将搜索请求发送到该 URL。这将减少服务提供者和服务消费者之间的耦合。

我们开发的初始服务不会遵循所有这些约束。随着我们进入下一章,我们将向您介绍这些约束的细节,并将它们添加到服务中,使其更具有 RESTful 特性。

第一个 REST 服务

让我们从创建一个简单的 REST 服务返回欢迎消息开始。我们将创建一个简单的 POJO WelcomeBean类,其中包含一个名为 message 的成员字段和一个参数构造函数,如下面的代码片段所示:

    package com.mastering.spring.springboot.bean;

    public class WelcomeBean {
      private String message;

       public WelcomeBean(String message) {
         super();
         this.message = message;
       }

      public String getMessage() {
        return message;
      }
    }

返回字符串的简单方法

让我们从创建一个简单的 REST 控制器方法返回一个字符串开始:

    @RestController
    public class BasicController {
      @GetMapping("/welcome")
      public String welcome() {
        return "Hello World";
      }
    }

以下是一些需要注意的重要事项:

  • @RestController@RestController注解提供了@ResponseBody@Controller注解的组合。这通常用于创建 REST 控制器。

  • @GetMapping("welcome")@GetMapping@RequestMapping(method = RequestMethod.GET)的快捷方式。这个注解是一个可读性更好的替代方法。带有这个注解的方法将处理对welcome URI 的 Get 请求。

如果我们将Application.java作为 Java 应用程序运行,它将启动嵌入式的 Tomcat 容器。我们可以在浏览器中打开 URL,如下面的屏幕截图所示:

单元测试

让我们快速编写一个单元测试来测试前面的controller方法:

    @RunWith(SpringRunner.class)
    @WebMvcTest(BasicController.class)
    public class BasicControllerTest {

      @Autowired
      private MockMvc mvc;

      @Test
      public void welcome() throws Exception {
        mvc.perform(
        MockMvcRequestBuilders.get("/welcome")
       .accept(MediaType.APPLICATION_JSON))
       .andExpect(status().isOk())
       .andExpect(content().string(
       equalTo("Hello World")));
      }
    }

在前面的单元测试中,我们将使用BasicController启动一个 Mock MVC 实例。以下是一些需要注意的事项:

  • @RunWith(SpringRunner.class): SpringRunner 是SpringJUnit4ClassRunner注解的快捷方式。这为单元测试启动了一个简单的 Spring 上下文。

  • @WebMvcTest(BasicController.class): 这个注解可以与 SpringRunner 一起使用,用于编写 Spring MVC 控制器的简单测试。这将只加载使用 Spring-MVC 相关注解注释的 bean。在这个例子中,我们正在启动一个 Web MVC 测试上下文,测试的类是 BasicController。

  • @Autowired private MockMvc mvc: 自动装配可以用于发出请求的 MockMvc bean。

  • mvc.perform(MockMvcRequestBuilders.get("/welcome").accept(MediaType.APPLICATION_JSON)): 使用Accept头值application/json执行对/welcome的请求。

  • andExpect(status().isOk()): 期望响应的状态为 200(成功)。

  • andExpect(content().string(equalTo("Hello World"))): 期望响应的内容等于"Hello World"。

集成测试

当我们进行集成测试时,我们希望启动嵌入式服务器,并加载所有配置的控制器和 bean。这段代码片段展示了我们如何创建一个简单的集成测试:

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = Application.class, 
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    public class BasicControllerIT {

      private static final String LOCAL_HOST = 
      "http://localhost:";

      @LocalServerPort
      private int port;

      private TestRestTemplate template = new TestRestTemplate();

      @Test
      public void welcome() throws Exception {
        ResponseEntity<String> response = template
       .getForEntity(createURL("/welcome"), String.class);
        assertThat(response.getBody(), equalTo("Hello World"));
       }

      private String createURL(String uri) {
        return LOCAL_HOST + port + uri;
      }
    }

需要注意的一些重要事项如下:

  • @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT): 提供了在 Spring TestContext 之上的额外功能。提供支持以配置端口来完全运行容器和 TestRestTemplate(执行请求)。

  • @LocalServerPort private int port: SpringBootTest会确保容器运行的端口被自动装配到端口变量中。

  • private String createURL(String uri): 用于将本地主机 URL 和端口附加到 URI 以创建完整 URL 的方法。

  • private TestRestTemplate template = new TestRestTemplate(): TestRestTemplate通常用于集成测试。它提供了在 RestTemplate 之上的额外功能,在集成测试环境中特别有用。它不会遵循重定向,这样我们就可以断言响应位置。

  • template.getForEntity(createURL("/welcome"), String.class): 执行对给定 URI 的 get 请求。

  • assertThat(response.getBody(), equalTo("Hello World")): 断言响应主体内容为"Hello World"。

返回对象的简单 REST 方法

在前面的方法中,我们返回了一个字符串。让我们创建一个返回正确的 JSON 响应的方法。看一下下面的方法:

    @GetMapping("/welcome-with-object")
    public WelcomeBean welcomeWithObject() {
      return new WelcomeBean("Hello World");
    }

这个先前的方法返回一个简单的WelcomeBean,它初始化了一个消息:"Hello World"。

执行请求

让我们发送一个测试请求,看看我们得到什么响应。下面的截图显示了输出:

http://localhost:8080/welcome-with-object URL 的响应如下所示:

    {"message":"Hello World"}

需要回答的问题是:我们返回的WelcomeBean对象是如何转换为 JSON 的?

再次,这是 Spring Boot 自动配置的魔力。如果 Jackson 在应用程序的类路径上,Spring Boot 会自动配置默认的对象到 JSON(反之亦然)转换器的实例。

单元测试

让我们快速编写一个单元测试,检查 JSON 响应。让我们将测试添加到BasicControllerTest中:

    @Test
    public void welcomeWithObject() throws Exception {
      mvc.perform(
       MockMvcRequestBuilders.get("/welcome-with-object")
      .accept(MediaType.APPLICATION_JSON))
      .andExpect(status().isOk())
      .andExpect(content().string(containsString("Hello World")));
    }

这个测试与之前的单元测试非常相似,只是我们使用containsString来检查内容是否包含子字符串"Hello World"。稍后我们将学习如何编写正确的 JSON 测试。

集成测试

让我们把注意力转移到编写一个集成测试。让我们向BasicControllerIT中添加一个方法,如下面的代码片段所示:

    @Test
    public void welcomeWithObject() throws Exception {
      ResponseEntity<String> response = 
      template.getForEntity(createURL("/welcome-with-object"), 
      String.class);
      assertThat(response.getBody(), 
      containsString("Hello World"));
    }

这个方法与之前的集成测试类似,只是我们使用String方法来断言子字符串。

带有路径变量的 Get 方法

让我们把注意力转移到路径变量上。路径变量用于将 URI 中的值绑定到控制器方法上的变量。在以下示例中,我们希望对名称进行参数化,以便我们可以使用名称定制欢迎消息:

    private static final String helloWorldTemplate = "Hello World, 
    %s!";

   @GetMapping("/welcome-with-parameter/name/{name}")
   public WelcomeBean welcomeWithParameter(@PathVariable String name) 
    {
       return new WelcomeBean(String.format(helloWorldTemplate, name));
    }

需要注意的几个重要事项如下:

  • @GetMapping("/welcome-with-parameter/name/{name}"){name}表示这个值将是变量。我们可以在 URI 中有多个变量模板。

  • welcomeWithParameter(@PathVariable String name)@PathVariable确保从 URI 中的变量值绑定到变量名称。

  • String.format(helloWorldTemplate, name):一个简单的字符串格式,用名称替换模板中的%s

执行请求

让我们发送一个测试请求,看看我们得到什么响应。以下截图显示了响应:

http://localhost:8080/welcome-with-parameter/name/Buddy URL 的响应如下:

    {"message":"Hello World, Buddy!"}

如预期,URI 中的名称用于形成响应中的消息。

单元测试

让我们快速为前面的方法编写一个单元测试。我们希望将名称作为 URI 的一部分传递,并检查响应是否包含名称。以下代码显示了我们如何做到这一点:

    @Test
    public void welcomeWithParameter() throws Exception {
      mvc.perform(
      MockMvcRequestBuilders.get("/welcome-with-parameter/name/Buddy")
     .accept(MediaType.APPLICATION_JSON))
     .andExpect(status().isOk())
     .andExpect(
     content().string(containsString("Hello World, Buddy")));
    }

需要注意的几个重要事项如下:

  • MockMvcRequestBuilders.get("/welcome-with-parameter/name/Buddy"):这与 URI 中的变量模板匹配。我们传入名称Buddy

  • .andExpect(content().string(containsString("Hello World, Buddy”))):我们期望响应包含带有名称的消息。

集成测试

前面方法的集成测试非常简单。看一下以下测试方法:

    @Test
    public void welcomeWithParameter() throws Exception {
      ResponseEntity<String> response = 
      template.getForEntity(
      createURL("/welcome-with-parameter/name/Buddy"), String.class);
      assertThat(response.getBody(), 
      containsString("Hello World, Buddy"));
    }

需要注意的几个重要事项如下:

  • createURL("/welcome-with-parameter/name/Buddy"):这与 URI 中的变量模板匹配。我们传入名称 Buddy。

  • assertThat(response.getBody(), containsString("Hello World, Buddy”)):我们期望响应包含带有名称的消息。

在本节中,我们看了使用 Spring Boot 创建简单 REST 服务的基础知识。我们还确保我们有良好的单元测试和集成测试。虽然这些都非常基础,但它们为我们在下一节中构建更复杂的 REST 服务奠定了基础。

我们实施的单元测试和集成测试可以使用 JSON 比较而不是简单的子字符串比较来进行更好的断言。我们将在下一节中为我们创建的 REST 服务编写的测试中专注于这一点。

创建待办事项资源

我们将专注于为基本待办事项管理系统创建 REST 服务。我们将为以下内容创建服务:

  • 检索给定用户的待办事项列表

  • 检索特定待办事项的详细信息

  • 为用户创建待办事项

请求方法、操作和 URI

REST 服务的最佳实践之一是根据我们执行的操作使用适当的 HTTP 请求方法。在我们暴露的服务中,我们使用了GET方法,因为我们专注于读取数据的服务。

以下表格显示了基于我们执行的操作的适当 HTTP 请求方法:

HTTP 请求方法 操作
GET 读取--检索资源的详细信息
POST 创建--创建新项目或资源
PUT 更新/替换
PATCH 更新/修改资源的一部分
DELETE 删除

让我们快速将我们要创建的服务映射到适当的请求方法:

  • 检索给定用户的待办事项列表:这是读取。我们将使用GET。我们将使用 URI:/users/{name}/todos。另一个良好的实践是在 URI 中对静态内容使用复数形式:users,todo 等。这会导致更可读的 URI。

  • 检索特定待办事项的详细信息:同样,我们将使用GET。我们将使用 URI /users/{name}/todos/{id}。您可以看到这与我们之前为待办事项列表决定的 URI 是一致的。

  • 为用户创建待办事项:对于创建操作,建议的 HTTP 请求方法是POST。要创建一个新的待办事项,我们将发布到URI /users/{name}/todos

Beans and services

为了能够检索和存储待办事项的详细信息,我们需要一个 Todo bean 和一个用于检索和存储详细信息的服务。

让我们创建一个 Todo Bean:

    public class Todo {
      private int id;
      private String user;

      private String desc;

      private Date targetDate;
      private boolean isDone;

      public Todo() {}

      public Todo(int id, String user, String desc, 
      Date targetDate, boolean isDone) { 
        super();
        this.id = id;
        this.user = user;
        this.desc = desc;
        this.targetDate = targetDate;
        this.isDone = isDone;
      }

       //ALL Getters
    }

我们创建了一个简单的 Todo bean,其中包含 ID、用户名称、待办事项描述、待办事项目标日期和完成状态指示器。我们为所有字段添加了构造函数和 getter。

现在让我们添加TodoService

   @Service
   public class TodoService {
     private static List<Todo> todos = new ArrayList<Todo>();
     private static int todoCount = 3;

     static {
       todos.add(new Todo(1, "Jack", "Learn Spring MVC", 
       new Date(), false));
       todos.add(new Todo(2, "Jack", "Learn Struts", new Date(), 
       false));
       todos.add(new Todo(3, "Jill", "Learn Hibernate", new Date(), 
       false));
      }

     public List<Todo> retrieveTodos(String user) {
       List<Todo> filteredTodos = new ArrayList<Todo>();
       for (Todo todo : todos) {
         if (todo.getUser().equals(user))
         filteredTodos.add(todo);
        }
      return filteredTodos;
     }

    public Todo addTodo(String name, String desc, 
    Date targetDate, boolean isDone) {
      Todo todo = new Todo(++todoCount, name, desc, targetDate, 
      isDone);
      todos.add(todo);
      return todo;
    }

    public Todo retrieveTodo(int id) {
      for (Todo todo : todos) {
      if (todo.getId() == id)
        return todo;
      }
      return null;
     }
   }

需要注意的快速事项如下:

  • 为了保持简单,该服务不与数据库通信。它维护一个待办事项的内存数组列表。该列表使用静态初始化程序进行初始化。

  • 我们公开了一些简单的检索方法和一个添加待办事项的方法。

现在我们的服务和 bean 已经准备好了,我们可以创建我们的第一个服务来为用户检索待办事项列表。

检索待办事项列表

我们将创建一个名为TodoController的新RestController注解。检索待办事项方法的代码如下所示:

    @RestController
    public class TodoController {
     @Autowired
     private TodoService todoService;

     @GetMapping("/users/{name}/todos")
     public List<Todo> retrieveTodos(@PathVariable String name) {
       return todoService.retrieveTodos(name);
     }
    }

需要注意的一些事项如下:

  • 我们使用@Autowired注解自动装配了待办事项服务

  • 我们使用@GetMapping注解将"/users/{name}/todos" URI 的 Get 请求映射到retrieveTodos方法

执行服务

让我们发送一个测试请求,看看我们将得到什么响应。下图显示了输出:

http://localhost:8080/users/Jack/todos的响应如下:

   [
    {"id":1,"user":"Jack","desc":"Learn Spring    
     MVC","targetDate":1481607268779,"done":false},  
    {"id":2,"user":"Jack","desc":"Learn 
    Struts","targetDate":1481607268779, "done":false}
   ]

单元测试

用于单元测试TodoController类的代码如下所示:

   @RunWith(SpringRunner.class)
   @WebMvcTest(TodoController.class)
   public class TodoControllerTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private TodoService service;

    @Test
    public void retrieveTodos() throws Exception {
     List<Todo> mockList = Arrays.asList(new Todo(1, "Jack",
     "Learn Spring MVC", new Date(), false), new Todo(2, "Jack",
     "Learn Struts", new Date(), false));

     when(service.retrieveTodos(anyString())).thenReturn(mockList);

     MvcResult result = mvc
    .perform(MockMvcRequestBuilders.get("/users
    /Jack/todos").accept(MediaType.APPLICATION_JSON))
    .andExpect(status().isOk()).andReturn();

    String expected = "["
     + "{id:1,user:Jack,desc:\"Learn Spring MVC\",done:false}" +","
     + "{id:2,user:Jack,desc:\"Learn Struts\",done:false}" + "]";

     JSONAssert.assertEquals(expected, result.getResponse()
      .getContentAsString(), false);
     }
    }

一些重要的事情需要注意:

  • 我们正在编写一个单元测试。因此,我们只想测试TodoController类中的逻辑。因此,我们使用@WebMvcTest(TodoController.class)初始化一个仅包含TodoController类的 Mock MVC 框架。

  • @MockBean private TodoService service:我们使用@MockBean注解模拟了 TodoService。在使用 SpringRunner 运行的测试类中,使用@MockBean定义的 bean 将被使用 Mockito 框架创建的模拟对象替换。

  • when(service.retrieveTodos(anyString())).thenReturn(mockList):我们模拟了 retrieveTodos 服务方法以返回模拟列表。

  • MvcResult result = ..:我们将请求的结果接受到一个 MvcResult 变量中,以便我们可以对响应执行断言。

  • JSONAssert.assertEquals(expected, result.getResponse().getContentAsString(), false): JSONAssert 是一个非常有用的框架,用于对 JSON 执行断言。它将响应文本与期望值进行比较。JSONAssert 足够智能,可以忽略未指定的值。另一个优点是在断言失败时提供清晰的失败消息。最后一个参数 false 表示使用非严格模式。如果将其更改为 true,则期望值应与结果完全匹配。

集成测试

用于对TodoController类执行集成测试的代码如下所示。它启动了整个 Spring 上下文,其中定义了所有控制器和 bean:

   @RunWith(SpringJUnit4ClassRunner.class)
   @SpringBootTest(classes = Application.class, webEnvironment =     
   SpringBootTest.WebEnvironment.RANDOM_PORT)
   public class TodoControllerIT {

    @LocalServerPort
    private int port;

    private TestRestTemplate template = new TestRestTemplate();

    @Test
    public void retrieveTodos() throws Exception {
     String expected = "["
     + "{id:1,user:Jack,desc:\"Learn Spring MVC\",done:false}" + ","
     + "{id:2,user:Jack,desc:\"Learn Struts\",done:false}" + "]";

     String uri = "/users/Jack/todos";

     ResponseEntity<String> response =
     template.getForEntity(createUrl(uri), String.class);

     JSONAssert.assertEquals(expected, response.getBody(), false);
    }

     private String createUrl(String uri) {
     return "http://localhost:" + port + uri;
    }
  }

这个测试与BasicController的集成测试非常相似,只是我们使用JSONAssert来断言响应。

检索特定待办事项的详细信息

现在我们将添加检索特定待办事项的方法:

    @GetMapping(path = "/users/{name}/todos/{id}")
    public Todo retrieveTodo(@PathVariable String name, @PathVariable 
    int id) {
      return todoService.retrieveTodo(id);
    }

需要注意的一些事项如下:

  • 映射的 URI 是/users/{name}/todos/{id}

  • 我们为nameid定义了两个路径变量

执行服务

让我们发送一个测试请求,看看我们将得到什么响应,如下图所示:

http://localhost:8080/users/Jack/todos/1的响应如下所示:

    {"id":1,"user":"Jack","desc":"Learn Spring MVC", 
    "targetDate":1481607268779,"done":false}

单元测试

retrieveTodo进行单元测试的代码如下:

     @Test
     public void retrieveTodo() throws Exception {
       Todo mockTodo = new Todo(1, "Jack", "Learn Spring MVC", 
       new Date(), false);

       when(service.retrieveTodo(anyInt())).thenReturn(mockTodo);

       MvcResult result = mvc.perform(
       MockMvcRequestBuilders.get("/users/Jack/todos/1")
       .accept(MediaType.APPLICATION_JSON))
       .andExpect(status().isOk()).andReturn();

       String expected = "{id:1,user:Jack,desc:\"Learn Spring
       MVC\",done:false}";

      JSONAssert.assertEquals(expected, 
       result.getResponse().getContentAsString(), false);

     }

需要注意的几个重要事项如下:

  • when(service.retrieveTodo(anyInt())).thenReturn(mockTodo):我们正在模拟 retrieveTodo 服务方法返回模拟的待办事项。

  • MvcResult result = ..:我们将请求的结果接受到 MvcResult 变量中,以便我们对响应执行断言。

  • JSONAssert.assertEquals(expected, result.getResponse().getContentAsString(), false):断言结果是否符合预期。

集成测试

在以下代码片段中显示了对TodoController中的retrieveTodos进行集成测试的代码。这将添加到TodoControllerIT类中:

     @Test
     public void retrieveTodo() throws Exception {
       String expected = "{id:1,user:Jack,desc:\"Learn Spring   
       MVC\",done:false}";
       ResponseEntity<String> response = template.getForEntity(
       createUrl("/users/Jack/todos/1"), String.class);
       JSONAssert.assertEquals(expected, response.getBody(), false);
     }

添加待办事项

现在我们将添加创建新待办事项的方法。用于创建的 HTTP 方法是Post。我们将发布到"/users/{name}/todos" URI:

    @PostMapping("/users/{name}/todos")
    ResponseEntity<?> add(@PathVariable String name,
    @RequestBody Todo todo) { 
      Todo createdTodo = todoService.addTodo(name, todo.getDesc(),
      todo.getTargetDate(), todo.isDone());
      if (createdTodo == null) {
         return ResponseEntity.noContent().build();
      }

     URI location = ServletUriComponentsBuilder.fromCurrentRequest()

    .path("/{id}").buildAndExpand(createdTodo.getId()).toUri();
    return ResponseEntity.created(location).build();
   }

需要注意的几件事如下:

  • @PostMapping("/users/{name}/todos")@PostMapping注解将add()方法映射到具有POST方法的 HTTP 请求。

  • ResponseEntity<?> add(@PathVariable String name, @RequestBody Todo todo):HTTP POST 请求应该理想地返回创建资源的 URI。我们使用ResourceEntity来实现这一点。@RequestBody将请求的正文直接绑定到 bean。

  • ResponseEntity.noContent().build():用于返回资源创建失败的情况。

  • ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(createdTodo.getId()).toUri():形成可以在响应中返回的已创建资源的 URI。

  • ResponseEntity.created(location).build():返回201(CREATED)状态,并附带资源的链接。

Postman

如果您使用的是 Mac,您可能还想尝试 Paw 应用程序。

让我们发送一个测试请求,看看我们得到什么响应。以下截图显示了响应:

我们将使用 Postman 应用程序与 REST 服务进行交互。您可以从网站www.getpostman.com/安装它。它适用于 Windows 和 Mac。还提供 Google Chrome 插件。

执行 POST 服务

使用POST创建新的待办事项,我们需要在请求的正文中包含待办事项的 JSON。以下截图显示了我们如何使用 Postman 应用程序创建请求以及执行请求后的响应:

需要注意的几个重要事项如下:

  • 我们正在发送 POST 请求。因此,我们从左上角的下拉菜单中选择 POST。

  • 要将 Todo JSON 作为请求正文的一部分发送,我们在 Body 选项卡中选择原始选项(用蓝点标出)。我们选择 JSON(application/json)作为内容类型。

  • 一旦请求成功执行,您可以在屏幕中间的栏中看到请求的状态:状态:201 已创建。

  • 位置是http://localhost:8080/users/Jack/todos/5。这是在响应中收到的新创建的待办事项的 URI。

http://localhost:8080/users/Jack/todos的请求的完整细节如下所示:

    Header
    Content-Type:application/json

   Body
    {
      "user": "Jack",
      "desc": "Learn Spring Boot",
       "done": false
     }

单元测试

对创建的待办事项进行单元测试的代码如下所示:

    @Test
    public void createTodo() throws Exception {
     Todo mockTodo = new Todo(CREATED_TODO_ID, "Jack", 
     "Learn Spring MVC", new Date(), false);
     String todo = "{"user":"Jack","desc":"Learn Spring MVC",     
     "done":false}";

    when(service.addTodo(anyString(), anyString(),   
    isNull(),anyBoolean()))
    .thenReturn(mockTodo);

   mvc
    .perform(MockMvcRequestBuilders.post("/users/Jack/todos")
    .content(todo)
    .contentType(MediaType.APPLICATION_JSON)
    )
    .andExpect(status().isCreated())
    .andExpect(
      header().string("location",containsString("/users/Jack/todos/"
     + CREATED_TODO_ID)));
   }

需要注意的几个重要事项如下:

  • String todo = "{"user":"Jack","desc":"Learn Spring MVC","done":false}":要发布到创建待办事项服务的 Todo 内容。

  • when(service.addTodo(anyString(), anyString(), isNull(), anyBoolean())).thenReturn(mockTodo):模拟服务返回一个虚拟的待办事项。

  • MockMvcRequestBuilders.post("/users/Jack/todos").content(todo).contentType(MediaType.APPLICATION_JSON)):使用给定的内容类型创建给定 URI 的 POST。

  • andExpect(status().isCreated()):期望状态为已创建。

  • andExpect(header().string("location",containsString("/users/Jack/todos/" + CREATED_TODO_ID))): 期望标头包含创建资源的 URI 的location

集成测试

TodoController中执行对创建的 todo 的集成测试的代码如下所示。这将添加到TodoControllerIT类中,如下所示:

    @Test
    public void addTodo() throws Exception {
      Todo todo = new Todo(-1, "Jill", "Learn Hibernate", new Date(),  
      false);
      URI location = template
     .postForLocation(createUrl("/users/Jill/todos"),todo);
      assertThat(location.getPath(), 
      containsString("/users/Jill/todos/4"));
    }

还有一些重要的事项需要注意:

  • URI location = template.postForLocation(createUrl("/users/Jill/todos"), todo): postForLocation是一个实用方法,特别适用于测试,用于创建新资源。我们正在将 todo 发布到给定的 URI,并从标头中获取位置。

  • assertThat(location.getPath(), containsString("/users/Jill/todos/4")): 断言位置包含到新创建资源的路径。

Spring Initializr

您想要自动生成 Spring Boot 项目吗?您想要快速开始开发您的应用程序吗?Spring Initializr 就是答案。

Spring Initializr 托管在start.spring.io。以下截图显示了网站的外观:

Spring Initializr 在创建项目时提供了很大的灵活性。您可以选择以下选项:

  • 选择您的构建工具:Maven 或 Gradle。

  • 选择您要使用的 Spring Boot 版本。

  • 为您的组件配置 Group ID 和 Artifact ID。

  • 选择您的项目所需的启动器(依赖项)。您可以单击屏幕底部的链接“切换到完整版本”来查看您可以选择的所有启动器项目。

  • 选择如何打包您的组件:JAR 或 WAR。

  • 选择您要使用的 Java 版本。

  • 选择要使用的 JVM 语言。

当您展开(单击链接)到完整版本时,Spring Initializr 提供的一些选项如下截图所示:

创建您的第一个 Spring Initializr 项目

我们将使用完整版本并输入值,如下所示:

需要注意的事项如下:

  • 构建工具:Maven

  • Spring Boot 版本:选择最新可用的版本

  • Group:com.mastering.spring

  • Artifact: first-spring-initializr

  • 选择的依赖项:选择Web, JPA, Actuator and Dev Tools。在文本框中输入每个依赖项,然后按Enter选择它们。我们将在下一节中了解有关 Actuator 和 Dev Tools 的更多信息

  • Java 版本:1.8

继续并单击生成项目按钮。这将创建一个.zip文件,您可以将其下载到您的计算机上。

以下截图显示了创建的项目的结构:

现在我们将此项目导入到您的 IDE 中。在 Eclipse 中,您可以执行以下步骤:

  1. 启动 Eclipse。

  2. 导航到文件|导入。

  3. 选择现有的 Maven 项目。

  4. 浏览并选择 Maven 项目的根目录(包含pom.xml文件的目录)。

  5. 继续使用默认值,然后单击完成。

这将把项目导入到 Eclipse 中。以下截图显示了 Eclipse 中项目的结构:

让我们来看一下生成项目中的一些重要文件。

pom.xml

以下代码片段显示了声明的依赖项:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>

还有一些其他重要的观察结果如下:

  • 此组件的打包为.jar

  • org.springframework.boot:spring-boot-starter-parent被声明为父 POM

  • <java.version>1.8</java.version>: Java 版本为 1.8

  • Spring Boot Maven 插件(org.springframework.boot:spring-boot-maven-plugin)被配置为插件

FirstSpringInitializrApplication.java 类

FirstSpringInitializrApplication.java是 Spring Boot 的启动器:

    package com.mastering.spring;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure
    .SpringBootApplication;

    @SpringBootApplication
    public class FirstSpringInitializrApplication {
       public static void main(String[] args) {
        SpringApplication.run(FirstSpringInitializrApplication.class,   
        args);
      }
    }

FirstSpringInitializrApplicationTests 类

FirstSpringInitializrApplicationTests包含了可以用来开始编写测试的基本上下文,当我们开始开发应用程序时:

    package com.mastering.spring;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class FirstSpringInitializrApplicationTests {

      @Test
      public void contextLoads() {
      }
   }

快速了解自动配置

自动配置是 Spring Boot 最重要的功能之一。在本节中,我们将快速了解 Spring Boot 自动配置的工作原理。

大部分 Spring Boot 自动配置的魔力来自于spring-boot-autoconfigure-{version}.jar。当我们启动任何 Spring Boot 应用程序时,会自动配置许多 bean。这是如何发生的?

以下截图显示了来自spring-boot-autoconfigure-{version}.jarspring.factories的摘录。出于空间的考虑,我们已经过滤掉了一些配置:

每当启动 Spring Boot 应用程序时,上述自动配置类的列表都会运行。让我们快速看一下其中一个:

org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration

这是一个小片段:

@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class,
WebMvcConfigurerAdapter.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter(DispatcherServletAutoConfiguration.class)
public class WebMvcAutoConfiguration {

一些重要的要点如下:

  • @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurerAdapter.class }):如果类路径中有提到的任何类,则启用此自动配置。当我们添加 web 启动器项目时,会带入所有这些类的依赖项。因此,此自动配置将被启用。

  • @ConditionalOnMissingBean(WebMvcConfigurationSupport.class): 只有在应用程序没有明确声明WebMvcConfigurationSupport.class类的 bean 时,才启用此自动配置。

  • @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10): 这指定了这个特定自动配置的优先级。

让我们看另一个小片段,显示了同一类中的一个方法:

    @Bean
    @ConditionalOnBean(ViewResolver.class)
    @ConditionalOnMissingBean(name = "viewResolver", 
    value = ContentNegotiatingViewResolver.class)
    public ContentNegotiatingViewResolver 
    viewResolver(BeanFactory beanFactory) {
      ContentNegotiatingViewResolver resolver = new 
      ContentNegotiatingViewResolver();
      resolver.setContentNegotiationManager
      (beanFactory.getBean(ContentNegotiationManager.class));
      resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
      return resolver;
     }

视图解析器是由WebMvcAutoConfiguration类配置的 bean 之一。上述片段确保如果应用程序没有提供视图解析器,则 Spring Boot 会自动配置默认的视图解析器。以下是一些重要要点:

  • @ConditionalOnBean(ViewResolver.class): 如果ViewResolver.class在类路径上,则创建此 bean

  • @ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class): 如果没有明确声明名称为viewResolver且类型为ContentNegotiatingViewResolver.class的 bean,则创建此 bean。

  • 方法的其余部分在视图解析器中配置

总之,所有自动配置逻辑都在 Spring Boot 应用程序启动时执行。如果类路径上有特定依赖项或启动器项目的特定类可用,则会执行自动配置类。这些自动配置类查看已经配置的 bean。根据现有的 bean,它们启用默认 bean 的创建。

总结

Spring Boot 使得开发基于 Spring 的应用程序变得容易。它使我们能够从项目的第一天起创建生产就绪的应用程序。

在本章中,我们介绍了 Spring Boot 和 REST 服务的基础知识。我们讨论了 Spring Boot 的不同特性,并创建了一些带有很好测试的 REST 服务。我们通过深入了解自动配置来了解后台发生了什么。

在下一章中,我们将把注意力转向为 REST 服务添加更多功能。

第六章:扩展微服务

在第五章《使用 Spring Boot 构建微服务》中,我们构建了一个基本组件,提供了一些服务。在本章中,我们将重点放在添加更多功能,使我们的微服务能够投入生产。

我们将讨论如何将这些功能添加到我们的微服务中:

  • 异常处理

  • HATEOAS

  • 缓存

  • 国际化

我们还将讨论如何使用 Swagger 文档化我们的微服务。我们将了解使用 Spring Security 保护微服务的基础知识。

异常处理

异常处理是开发 Web 服务的重要部分之一。当出现问题时,我们希望向服务使用者返回有关出现问题的良好描述。您不希望服务在不返回任何有用信息给服务使用者的情况下崩溃。

Spring Boot 提供了良好的默认异常处理。我们将从查看 Spring Boot 提供的默认异常处理功能开始,然后再进行自定义。

Spring Boot 默认异常处理

为了了解 Spring Boot 提供的默认异常处理,让我们从向不存在的 URL 发送请求开始。

不存在的资源

让我们使用一个头部(Content-Type:application/json)向http://localhost:8080/non-existing-resource发送一个GET请求。

当我们执行请求时,下面的截图显示了响应:

响应如下代码片段所示:

    {
      "timestamp": 1484027734491,
      "status": 404,
      "error": "Not Found",
      "message": "No message available",
      "path": "/non-existing-resource"
    }

一些重要的事情需要注意:

  • 响应头具有 HTTP 状态码404 - 资源未找到

  • Spring Boot 返回一个有效的 JSON;响应,其中说明资源未找到

资源抛出异常

让我们创建一个抛出异常的资源,并向其发送一个GET请求,以了解应用程序对运行时异常的反应。

让我们创建一个抛出异常的虚拟服务。下面的代码片段显示了一个简单的服务:

    @GetMapping(path = "/users/dummy-service")
    public Todo errorService() {
      throw new RuntimeException("Some Exception Occured");
    }

一些重要的事情需要注意:

  • 我们正在创建一个带有 URI /users/dummy-serviceGET服务。

  • 该服务抛出RuntimeException。我们选择了RuntimeException以便能够轻松创建异常。如果需要,我们可以轻松替换为自定义异常。

让我们使用 Postman 向前述服务发送一个GET请求,网址为http://localhost:8080/users/dummy-service。响应如下所示的代码:

    {
      "timestamp": 1484028119553,
      "status": 500,
      "error": "Internal Server Error",
      "exception": "java.lang.RuntimeException",
      "message": "Some Exception Occured",
      "path": "/users/dummy-service"
   }

一些重要的事情需要注意:

  • 响应头具有 HTTP 状态码500内部服务器错误

  • Spring Boot 还返回抛出异常的消息

正如我们在前面的两个例子中所看到的,Spring Boot 提供了良好的默认异常处理。在下一节中,我们将重点关注应用程序对自定义异常的反应。

抛出自定义异常

让我们创建一个自定义异常,并从服务中抛出它。看一下下面的代码:

    public class TodoNotFoundException extends RuntimeException {
      public TodoNotFoundException(String msg) {
        super(msg);
      }
    }

这是一个非常简单的代码片段,定义了TodoNotFoundException

现在让我们增强我们的TodoController类,当找不到具有给定 ID 的todo时抛出TodoNotFoundException

    @GetMapping(path = "/users/{name}/todos/{id}")
    public Todo retrieveTodo(@PathVariable String name, 
    @PathVariable int id) {
      Todo todo = todoService.retrieveTodo(id);
      if (todo == null) {
        throw new TodoNotFoundException("Todo Not Found");
       }

     return todo;
    }

如果todoService返回一个空的todo,我们抛出TodoNotFoundException

当我们向一个不存在的todohttp://localhost:8080/users/Jack/todos/222)发送一个GET请求时,我们得到了下面代码片段中显示的响应:

    {
      "timestamp": 1484029048788,
      "status": 500,
      "error": "Internal Server Error",
      "exception":    
      "com.mastering.spring.springboot.bean.TodoNotFoundException",
      "message": "Todo Not Found",
      "path": "/users/Jack/todos/222"
    }

正如我们所看到的,清晰的异常响应被发送回服务使用者。然而,还有一件事情可以进一步改进——响应状态。当找不到资源时,建议返回404 - 资源未找到状态。我们将在下一个示例中看看如何自定义响应状态。

自定义异常消息

让我们看看如何自定义前面的异常并返回带有自定义消息的适当响应状态。

让我们创建一个 bean 来定义我们自定义异常消息的结构:

    public class ExceptionResponse {
      private Date timestamp = new Date();
      private String message;
      private String details;

      public ExceptionResponse(String message, String details) {
        super();
        this.message = message;
        this.details = details;
       }

      public Date getTimestamp() {
        return timestamp;
      }

      public String getMessage() {
        return message;
      }

      public String getDetails() {
        return details;
      }
     }

我们已经创建了一个简单的异常响应 bean,其中包含自动填充的时间戳和一些额外属性,即消息和详细信息。

当抛出TodoNotFoundException时,我们希望使用ExceptionResponse bean 返回响应。以下代码显示了如何为TodoNotFoundException.class创建全局异常处理:

    @ControllerAdvice
    @RestController
    public class RestResponseEntityExceptionHandler 
      extends  ResponseEntityExceptionHandler 
      {
        @ExceptionHandler(TodoNotFoundException.class)
        public final ResponseEntity<ExceptionResponse> 
        todoNotFound(TodoNotFoundException ex) {
           ExceptionResponse exceptionResponse = 
           new ExceptionResponse(  ex.getMessage(), 
           "Any details you would want to add");
           return new ResponseEntity<ExceptionResponse>
           (exceptionResponse, new HttpHeaders(), 
           HttpStatus.NOT_FOUND);
         }
     }

需要注意的一些重要事项如下:

  • RestResponseEntityExceptionHandler 扩展 ResponseEntityExceptionHandler:我们正在扩展ResponseEntityExceptionHandler,这是 Spring MVC 为中心化异常处理ControllerAdvice类提供的基类。

  • @ExceptionHandler(TodoNotFoundException.class): 这定义了接下来要处理特定异常TodoNotFoundException.class的方法。任何其他未定义自定义异常处理的异常将遵循 Spring Boot 提供的默认异常处理。

  • ExceptionResponse exceptionResponse = new ExceptionResponse(ex.getMessage(), "您想要添加的任何细节"):这创建了一个自定义异常响应。

  • new ResponseEntity<ExceptionResponse>(exceptionResponse,new HttpHeaders(), HttpStatus.NOT_FOUND): 这是返回404 资源未找到响应的定义,其中包括先前定义的自定义异常。

当我们使用GET请求执行服务到一个不存在的todohttp://localhost:8080/users/Jack/todos/222)时,我们会得到以下响应:

    {
      "timestamp": 1484030343311,
      "message": "Todo Not Found",
      "details": "Any details you would want to add"
    }

如果要为所有异常创建通用异常消息,我们可以向RestResponseEntityExceptionHandler添加一个带有@ExceptionHandler(Exception.class)注解的方法。

以下代码片段显示了我们如何做到这一点:

    @ExceptionHandler(Exception.class)
    public final ResponseEntity<ExceptionResponse> todoNotFound(
    Exception ex) {
       //Customize and return the response
    }

任何未定义自定义异常处理程序的异常将由前面的方法处理。

响应状态

在 REST 服务中要关注的重要事情之一是错误响应的响应状态。以下表格显示了要使用的场景和错误响应状态:

情况 响应状态
请求体不符合 API 规范。它没有足够的细节或包含验证错误。 ;400 错误请求
认证或授权失败。 401 未经授权
用户由于各种因素无法执行操作,例如超出限制。 403 禁止
资源不存在。 404 未找到
不支持的操作,例如,在只允许GET的资源上尝试 POST。; 405 方法不允许
服务器上的错误。理想情况下,这不应该发生。消费者将无法修复这个问题。; 500 内部服务器错误

在这一部分,我们看了 Spring Boot 提供的默认异常处理以及我们如何进一步定制以满足我们的需求。

HATEOAS

HATEOAS超媒体作为应用状态的引擎)是 REST 应用程序架构的约束之一。

让我们考虑一种情况,即服务消费者从服务提供者那里消费大量服务。开发这种类型的系统的最简单方法是让服务消费者存储他们从服务提供者那里需要的每个资源的资源 URI。然而,这将在服务提供者和服务消费者之间创建紧密耦合。每当服务提供者上的任何资源 URI 发生变化时,服务消费者都需要进行更新。

考虑一个典型的 Web 应用程序。假设我导航到我的银行账户详情页面。几乎所有银行网站都会在屏幕上显示我在银行账户上可以进行的所有交易的链接,以便我可以通过链接轻松导航。

如果我们可以将类似的概念引入 RESTful 服务,使得服务不仅返回有关请求资源的数据,还提供其他相关资源的详细信息,会怎么样?

HATEOAS 将这个概念引入了 RESTful 服务中,即为给定的资源显示相关链接。当我们返回特定资源的详细信息时,我们还返回可以对该资源执行的操作的链接,以及相关资源的链接。如果服务消费者可以使用响应中的链接执行事务,那么它就不需要硬编码所有链接。

Roy Fielding(roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven)提出的与 HATEOAS 相关的约束摘录如下:

REST API 不得定义固定的资源名称或层次结构(客户端和服务器的明显耦合)。服务器必须有自由控制自己的命名空间。相反,允许服务器指示客户端如何构造适当的 URI,例如在 HTML 表单和 URI 模板中所做的那样,通过在媒体类型和链接关系中定义这些指令。 REST API 应该在没有先前知识的情况下进入(书签)和一组适用于预期受众的标准化媒体类型(即,预计任何可能使用 API 的客户端都能理解)。从那时起,所有应用程序状态转换必须由客户端选择接收到的表示中存在的服务器提供的选择来驱动,或者由用户对这些表示的操作所暗示。转换可以由客户端对媒体类型和资源通信机制的知识确定(或受限于),这两者都可以即时改进(例如,按需代码)。

这里显示了一个带有 HATEOAS 链接的示例响应。这是对/todos请求的响应,以便检索所有 todos:

    {
      "_embedded" : {
        "todos" : [ {
          "user" : "Jill",
          "desc" : "Learn Hibernate",
          "done" : false,
         "_links" : {
          "self" : {
                 "href" : "http://localhost:8080/todos/1"
                 },
          "todo" : {
                 "href" : "http://localhost:8080/todos/1"
                  }
            }
     } ]
    },
     "_links" : {
     "self" : {
              "href" : "http://localhost:8080/todos"
              },
     "profile" : {
              "href" : "http://localhost:8080/profile/todos"
              },
     "search" : {
              "href" : "http://localhost:8080/todos/search"
              }
       },
     }

上述响应包括以下链接:

  • 特定的todos(http://localhost:8080/todos/1)

  • 搜索资源(http://localhost:8080/todos/search)

如果服务消费者想要进行搜索,它可以选择从响应中获取搜索 URL 并将搜索请求发送到该 URL。这将减少服务提供者和服务消费者之间的耦合。

在响应中发送 HATEOAS 链接

现在我们了解了 HATEOAS 是什么,让我们看看如何在响应中发送与资源相关的链接。

Spring Boot starter HATEOAS

Spring Boot 有一个专门的 HATEOAS 启动器,称为spring-boot-starter-hateoas。我们需要将其添加到pom.xml文件中。

以下代码片段显示了依赖块:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-hateoas</artifactId>
    </dependency>

spring-boot-starter-hateoas的一个重要依赖是spring-hateoas,它提供了 HATEOAS 功能:

    <dependency>
      <groupId>org.springframework.hateoas</groupId>
      <artifactId>spring-hateoas</artifactId>
    </dependency>

让我们增强retrieveTodo资源(/users/{name}/todos/{id})以在响应中返回检索所有todos(/users/{name}/todos)的链接:

    @GetMapping(path = "/users/{name}/todos/{id}")
    public Resource<Todo> retrieveTodo(
    @PathVariable String name, @PathVariable int id) {
    Todo todo = todoService.retrieveTodo(id);
      if (todo == null) {
           throw new TodoNotFoundException("Todo Not Found");
        }

     Resource<Todo> todoResource = new Resource<Todo>(todo);
     ControllerLinkBuilder linkTo = 
     linkTo(methodOn(this.getClass()).retrieveTodos(name));
     todoResource.add(linkTo.withRel("parent"));

     return todoResource;
    }

需要注意的一些重要点如下:

  • ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveTodos(name)):我们想要获取当前类中retrieveTodos方法的链接

  • linkTo.withRel("parent"):当前资源的关系是 parent

以下片段显示了向http://localhost:8080/users/Jack/todos/1发送GET请求时的响应:

   {
     "id": 1,
     "user": "Jack",
     "desc": "Learn Spring MVC",
     "targetDate": 1484038262110,
     "done": false,
     "_links": {
               "parent": {
               "href": "http://localhost:8080/users/Jack/todos"
               }
        }
   }

_links部分将包含所有链接。目前,我们有一个带有关系 parent 和hrefhttp://localhost:8080/users/Jack/todos的链接。

如果您在执行上述请求时遇到问题,请尝试使用 Accept 标头--application/json

HATEOAS 并不是今天大多数资源中常用的东西。然而,它有潜力在服务提供者和消费者之间减少耦合。

验证

一个好的服务在处理数据之前总是验证数据。在本节中,我们将研究 Bean Validation API,并使用其参考实现来在我们的服务中实现验证。

Bean Validation API 提供了许多注释,可用于验证 bean。JSR 349规范定义了 Bean Validation API 1.1。Hibernate-validator 是参考实现。两者已经在spring-boot-web-starter项目中定义为依赖项:

  • hibernate-validator-5.2.4.Final.jar

  • validation-api-1.1.0.Final.jar

我们将为 createTodo 服务方法创建一个简单的验证。

创建验证包括两个步骤:

  1. 在控制器方法上启用验证。

  2. 在 bean 上添加验证。

在控制器方法上启用验证

在控制器方法上启用验证非常简单。以下代码片段显示了一个示例:

    @RequestMapping(method = RequestMethod.POST, 
    path = "/users/{name}/todos")
    ResponseEntity<?> add(@PathVariable String name
    @Valid @RequestBody Todo todo) {

@Valid(包 javax.validation)注释用于标记要验证的参数。在执行add方法之前,将执行Todo bean 中定义的任何验证。

在 bean 上定义验证

让我们在Todo bean 上定义一些验证:

   public class Todo {
     private int id; 

     @NotNull
     private String user;

     @Size(min = 9, message = "Enter atleast 10 Characters.")
     private String desc;

需要注意的一些重要点如下:

  • @NotNull:验证用户字段不为空

  • @Size(min = 9, message = "Enter atleast 10 Characters."):检查desc字段是否至少有九个字符。

还有许多其他注释可用于验证 bean。以下是一些 Bean Validation 注释:

  • @AssertFalse@AssertTrue:对于布尔元素。检查被注释的元素。

  • @AssertFalse:检查是否为 false。@Assert检查是否为 true。

  • @Future:被注释的元素必须是将来的日期。

  • @Past:被注释的元素必须是过去的日期。

  • @Max:被注释的元素必须是一个数字,其值必须小于或等于指定的最大值。

  • @Min:被注释的元素必须是一个数字,其值必须大于或等于指定的最小值。

  • @NotNull:被注释的元素不能为空。

  • @Pattern:被注释的{@code CharSequence}元素必须与指定的正则表达式匹配。正则表达式遵循 Java 正则表达式约定。

  • @Size:被注释的元素大小必须在指定的边界内。

单元测试验证

以下示例显示了如何对我们添加的验证进行单元测试:

     @Test
     public void createTodo_withValidationError() throws Exception {
       Todo mockTodo = new Todo(CREATED_TODO_ID, "Jack", 
       "Learn Spring MVC", new Date(), false);

       String todo = "{"user":"Jack","desc":"Learn","done":false}";

       when( service.addTodo(
         anyString(), anyString(), isNull(), anyBoolean()))
        .thenReturn(mockTodo);

         MvcResult result = mvc.perform(
         MockMvcRequestBuilders.post("/users/Jack/todos")
        .content(todo)
        .contentType(MediaType.APPLICATION_JSON))
        .andExpect(
           status().is4xxClientError()).andReturn();
     }

需要注意的一些重要点如下:

  • "desc":"Learn":我们使用长度为5的 desc 值。这将导致@Size(min = 9, message = "Enter atleast 10 Characters.")检查失败。

  • .andExpect(status().is4xxClientError()):检查验证错误状态。

REST 服务文档

在服务提供者可以使用服务之前,他们需要一个服务合同。服务合同定义了有关服务的所有细节:

  • 我如何调用服务?服务的 URI 是什么?

  • 请求格式应该是什么?

  • 我应该期望什么样的响应?

有多种选项可用于为 RESTful 服务定义服务合同。在过去几年中最受欢迎的是Swagger。Swagger 在过去几年中得到了很多支持,得到了主要供应商的支持。在本节中,我们将为我们的服务生成 Swagger 文档。

来自 Swagger 网站(swagger.io)的以下引用定义了 Swagger 规范的目的:

Swagger 规范为您的 API 创建 RESTful 合同,详细说明了所有资源和操作,以人类和机器可读的格式进行易于开发、发现和集成。

生成 Swagger 规范

RESTful 服务开发在过去几年中的一个有趣发展是工具的演变,可以从代码生成服务文档(规范)。这确保了代码和文档始终保持同步。

Springfox Swagger可以用来从 RESTful 服务代码生成 Swagger 文档。此外,还有一个名为Swagger UI的精彩工具,当集成到应用程序中时,提供人类可读的文档。

以下代码片段显示了如何将这两个工具添加到pom.xml文件中:

    <dependency>
     <groupId>io.springfox</groupId>
     <artifactId>springfox-swagger2</artifactId>
     <version>2.4.0</version>
    </dependency>

    <dependency>
     <groupId>io.springfox</groupId>
     <artifactId>springfox-swagger-ui</artifactId>
     <version>2.4.0</version>
    </dependency>

下一步是添加配置类以启用和生成 Swagger 文档。以下代码片段显示了如何做到这一点:

    @Configuration
    @EnableSwagger2
    public class SwaggerConfig {
      @Bean
      public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
        .select()
        .apis(RequestHandlerSelectors.any())
        .paths(PathSelectors.any()).build();
      }
    }

需要注意的一些重要点如下:

  • @Configuration:定义一个 Spring 配置文件

  • @EnableSwagger2:启用 Swagger 支持的注解

  • Docket:一个简单的构建器类,用于使用 Swagger Spring MVC 框架配置 Swagger 文档的生成

  • new Docket(DocumentationType.SWAGGER_2):配置 Swagger 2 作为要使用的 Swagger 版本

  • .apis(RequestHandlerSelectors.any()).paths(PathSelectors.any()):包括文档中的所有 API 和路径

当我们启动服务器时,我们可以启动 API 文档 URL(http://localhost:8080/v2/api-docs)。以下截图显示了一些生成的文档:

让我们来看一些生成的文档。这里列出了检索todos服务的文档:

    "/users/{name}/todos": {
      "get": {
      "tags": [
             "todo-controller"
             ],
      "summary": "retrieveTodos",
      "operationId": "retrieveTodosUsingGET",
      "consumes": [
               "application/json"
               ],
      "produces": [
               "*/*"
               ],
      "parameters": [
              {
                "name": "name",
                "in": "path",
                "description": "name",
                "required": true,
                "type": "string"
              }
             ],
       "responses": {
       "200": {
              "description": "OK",
              "schema": {
                      "type": "array",
                      items": {
                          "$ref": "#/definitions/Todo"
                        }
                       }
               },
       "401": {
                "description": "Unauthorized"
               },
       "403": {
                "description": "Forbidden"
              },
       "404": {
                "description": "Not Found"
              } 
        }
     }

服务定义清楚地定义了服务的请求和响应。还定义了服务在不同情况下可以返回的不同响应状态。

以下代码片段显示了Todo bean 的定义:

    "Resource«Todo»": {
      "type": "object",
      "properties": {
      "desc": {
               "type": "string"
             },
     "done": {
               "type": "boolean"
             },
     "id": {
              "type": "integer",
              "format": "int32"
           },
     "links": {
              "type": "array",
              "items": {
                         "$ref": "#/definitions/Link"
                       }
              },
     "targetDate": {
                    "type": "string",
                    "format": "date-time"
                },
     "user": {
              "type": "string"
            }
        }
      }

它定义了Todo bean 中的所有元素,以及它们的格式。

Swagger UI

Swagger UI(http://localhost:8080/swagger-ui.html)也可以用来查看文档。Swagger UI 是通过在上一步中添加到我们的pom.xml中的依赖项(io.springfox:springfox-swagger-ui)启用的。

Swagger UI(petstore.swagger.io)也可以在线使用。我们可以使用 Swagger UI 可视化任何 Swagger 文档(swagger JSON)。

以下截图显示了公开控制器服务的列表。当我们点击任何控制器时,它会展开显示每个控制器支持的请求方法和 URI 列表:

以下截图显示了在 Swagger UI 中为创建用户的todo服务的 POST 服务的详细信息:

需要注意的一些重要事项如下:

  • 参数显示了所有重要的参数,包括请求体

  • 参数类型 body(对于todo参数)显示了请求体的预期结构

  • 响应消息部分显示了服务返回的不同 HTTP 状态代码

Swagger UI 提供了一种出色的方式来在不需要太多额外工作的情况下公开 API 的服务定义。

使用注解自定义 Swagger 文档

Swagger UI 还提供了注解来进一步自定义您的文档。

这里列出了检索todos服务的一些文档:

    "/users/{name}/todos": {
      "get": {
      "tags": [
             "todo-controller"
             ],
      "summary": "retrieveTodos",
      "operationId": "retrieveTodosUsingGET",
      "consumes": [
               "application/json"
               ],
      "produces": [
                "*/*"
               ],

如您所见,生成的文档非常原始。我们可以在文档中改进许多内容,以更好地描述服务。以下是一些示例:

  • 提供更好的摘要

  • 添加 application/JSON 到 produces

Swagger 提供了我们可以添加到我们的 RESTful 服务中的注解,以自定义文档。让我们在控制器中添加一些注解以改进文档:

    @ApiOperation(
      value = "Retrieve all todos for a user by passing in his name", 
      notes = "A list of matching todos is returned. Current pagination   
      is not supported.",
      response = Todo.class, 
      responseContainer = "List", 
      produces = "application/json")
      @GetMapping("/users/{name}/todos")
      public List<Todo> retrieveTodos(@PathVariable String name) {
        return todoService.retrieveTodos(name);
     }

需要注意的几个重要点如下:

  • @ApiOperation(value = "Retrieve all todos for a user by passing in his name"):在文档中作为服务摘要生成

  • notes = "A list of matching todos is returned. Current pagination is not supported.":在文档中生成作为服务描述的说明

  • produces = "application/json”:自定义服务文档的produces部分

以下是更新后的文档摘录:

    get": {
         "tags": [
                   "todo-controller"
                 ],
         "summary": "Retrieve all todos for a user by passing in his 
          name",
         "description": "A list of matching todos is returned. Current 
          pagination is not supported.",
         "operationId": "retrieveTodosUsingGET",
         "consumes": [
                     "application/json"
                   ],
         "produces": [
                     "application/json",
                     "*/*"
                   ],

Swagger 提供了许多其他注解来自定义文档。以下列出了一些重要的注解:

  • @Api:将类标记为 Swagger 资源

  • @ApiModel:提供有关 Swagger 模型的附加信息

  • @ApiModelProperty:添加和操作模型属性的数据

  • @ApiOperation:描述针对特定路径的操作或 HTTP 方法

  • @ApiParam:为操作参数添加附加元数据

  • @ApiResponse:描述操作的示例响应

  • @ApiResponses:允许多个ApiResponse对象的列表包装器。

  • @Authorization:声明要在资源或操作上使用的授权方案

  • @AuthorizationScope:描述 OAuth 2 授权范围

  • @ResponseHeader:表示可以作为响应的一部分提供的标头

Swagger 提供了一些 Swagger 定义注解,可以用来自定义有关一组服务的高级信息--联系人、许可和其他一般信息。以下是一些重要的注解:

  • @SwaggerDefinition:要添加到生成的 Swagger 定义的定义级属性

  • @Info:Swagger 定义的一般元数据

  • @Contact:用于描述 Swagger 定义的联系人的属性

  • @License:用于描述 Swagger 定义的许可证的属性

使用 Spring Security 保护 REST 服务

到目前为止,我们创建的所有服务都是不安全的。消费者不需要提供任何凭据即可访问这些服务。然而,在现实世界中,所有服务通常都是受保护的。

在本节中,我们将讨论验证 REST 服务的两种方式:

  • 基本身份验证

  • OAuth 2.0 身份验证

我们将使用 Spring Security 实现这两种类型的身份验证。

Spring Boot 提供了一个用于 Spring Security 的启动器;spring-boot-starter-security。我们将从向我们的pom.xml文件中添加 Spring Security 启动器开始。

添加 Spring Security 启动器

将以下依赖项添加到您的文件pom.xml中:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

Spring-boot-starter-security依赖项;引入了三个重要的 Spring Security 依赖项:

  • spring-security-config

  • spring-security-core

  • spring-security-web

基本身份验证

Spring-boot-starter-security依赖项;还默认为所有服务自动配置基本身份验证。

如果我们现在尝试访问任何服务,我们将收到"拒绝访问"的消息。

当我们发送请求到http://localhost:8080/users/Jack/todos时的响应如下代码片段中所示:

    {
      "timestamp": 1484120815039,
      "status": 401,
      "error": "Unauthorized",
      "message": "Full authentication is required to access this 
       resource",
       "path": "/users/Jack/todos"
    }

响应状态为401 - 未经授权

当资源受基本身份验证保护时,我们需要发送用户 ID 和密码来验证我们的请求。由于我们没有配置用户 ID 和密码,Spring Boot 会自动配置默认的用户 ID 和密码。默认用户 ID 是user。默认密码通常会打印在日志中。

以下代码片段中显示了一个示例:

2017-01-11 13:11:58.696 INFO 3888 --- [ restartedMain] b.a.s.AuthenticationManagerConfiguration :

Using default security password: 3fb5564a-ce53-4138-9911-8ade17b2f478

2017-01-11 13:11:58.771 INFO 3888 --- [ restartedMain] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: Ant [pattern='/css/**'], []

在上述代码片段中划线的是日志中打印的默认安全密码。

我们可以使用 Postman 发送带有基本身份验证的请求。以下截图显示了如何在请求中发送基本身份验证详细信息:

正如你所看到的,身份验证成功,我们得到了一个适当的响应。

我们可以在application.properties中配置我们选择的用户 ID 和密码,如下所示:

   security.user.name=user-name
   security.user.password=user-password

Spring Security 还提供了使用 LDAP 或 JDBC 或任何其他数据源进行用户凭据身份验证的选项。

集成测试

我们之前为服务编写的集成测试将因为无效的凭据而开始失败。我们现在将更新集成测试以提供基本身份验证凭据:

    private TestRestTemplate template = new TestRestTemplate();
    HttpHeaders headers = createHeaders("user-name", "user-password");

    HttpHeaders createHeaders(String username, String password) {
      return new HttpHeaders() {
       {
         String auth = username + ":" + password;
         byte[] encodedAuth = Base64.getEncoder().encode
         (auth.getBytes(Charset.forName("US-ASCII")));
         String authHeader = "Basic " + new String(encodedAuth);
         set("Authorization", authHeader);
        }
      };
     }

    @Test
    public void retrieveTodos() throws Exception {
      String expected = "["
      + "{id:1,user:Jack,desc:\"Learn Spring MVC\",done:false}" + ","
      + "{id:2,user:Jack,desc:\"Learn Struts\",done:false}" + "]";
      ResponseEntity<String> response = template.exchange(
      createUrl("/users/Jack/todos"), HttpMethod.GET,
      new HttpEntity<String>(null, headers),
      String.class);
      JSONAssert.assertEquals(expected, response.getBody(), false);
    }

需要注意的一些重要事项如下:

  • createHeaders("user-name", "user-password"):此方法创建Base64\. getEncoder().encode基本身份验证标头

  • ResponseEntity<String> response = template.exchange(createUrl("/users/Jack/todos"), ;HttpMethod.GET,new HttpEntity<String>(null, headers), String.class): 关键变化是使用HttpEntity来提供我们之前创建的标头给 REST 模板

单元测试

我们不希望在单元测试中使用安全性。以下代码片段显示了如何在单元测试中禁用安全性:

   @RunWith(SpringRunner.class)
   @WebMvcTest(value = TodoController.class, secure = false)
   public class TodoControllerTest {

关键部分是WebMvcTest注解上的secure = false参数。这将禁用单元测试的 Spring Security。

OAuth 2 认证

OAuth 是一种协议,提供了一系列流程,用于在各种网络应用程序和服务之间交换授权和认证信息。它使第三方应用程序能够从服务中获取对用户信息的受限访问权限,例如 Facebook、Twitter 或 GitHub。

在深入细节之前,回顾一下通常与 OAuth 2 认证相关的术语将会很有用。

让我们考虑一个例子。假设我们想要将 Todo API 暴露给互联网上的第三方应用程序。

以下是典型 OAuth 2 交换中的重要参与者:

  • 资源所有者:这是第三方应用程序的用户,希望使用我们的 Todo API。它决定我们的 API 中的信息可以向第三方应用程序提供多少。

  • 资源服务器:托管 Todo API,我们想要保护的资源。

  • 客户端:这是希望使用我们的 API 的第三方应用程序。

  • 授权服务器:提供 OAuth 服务的服务器。

高级流程

以下步骤展示了典型 OAuth 认证的高级流程:

  1. 应用程序请求用户授权访问 API 资源。

  2. 当用户提供访问权限时,应用程序会收到授权授予。

  3. 应用程序提供用户授权授予和自己的客户端凭据给授权服务器。

  4. 如果认证成功,授权服务器将以访问令牌回复。

  5. 应用程序调用提供认证访问令牌的 API(资源服务器)。

  6. 如果访问令牌有效,资源服务器返回资源的详细信息。

为我们的服务实现 OAuth 2 认证

Spring Security 的 OAuth 2(spring-security-oauth2)是为 Spring Security 提供 OAuth 2 支持的模块。我们将在pom.xml文件中将其添加为依赖项:

    <dependency>
      <groupId>org.springframework.security.oauth</groupId>
      <artifactId>spring-security-oauth2</artifactId>
    </dependency>

设置授权和资源服务器

spring-security-oauth2 截至 2017 年 6 月尚未更新以适应 Spring Framework 5.x 和 Spring Boot 2.x 的变化。我们将使用 Spring Boot 1.5.x 来举例说明 OAuth 2 认证。代码示例在 GitHub 存储库中;github.com/PacktPublishing/Mastering-Spring-5.0

通常,授权服务器会是一个不同的服务器,而不是 API 暴露的应用程序。为了简化,我们将使当前的 API 服务器同时充当资源服务器和授权服务器。

以下代码片段显示了如何使我们的应用程序充当资源和授权服务器:

   @EnableResourceServer
   @EnableAuthorizationServer
   @SpringBootApplication
   public class Application {

以下是一些重要的事项:

  • @EnableResourceServer:OAuth 2 资源服务器的便利注解,启用 Spring Security 过滤器,通过传入的 OAuth 2 令牌对请求进行身份验证

  • @EnableAuthorizationServer:一个便利注解,用于在当前应用程序上下文中启用授权服务器,必须是DispatcherServlet上下文,包括AuthorizationEndpointTokenEndpoint

现在我们可以在application.properties中配置访问详情,如下所示:

    security.user.name=user-name
    security.user.password=user-password
    security.oauth2.client.clientId: clientId
    security.oauth2.client.clientSecret: clientSecret
    security.oauth2.client.authorized-grant-types:     
    authorization_code,refresh_token,password
    security.oauth2.client.scope: openid

一些重要的细节如下:

  • security.user.namesecurity.user.password是资源所有者的身份验证详细信息,是第三方应用程序的最终用户

  • security.oauth2.client.clientIdsecurity.oauth2.client.clientSecret是客户端的身份验证详细信息,是第三方应用程序(服务消费者)

执行 OAuth 请求

我们需要一个两步骤的过程来访问 API:

  1. 获取访问令牌。

  2. 使用访问令牌执行请求。

获取访问令牌

要获取访问令牌,我们调用授权服务器(http://localhost:8080/oauth/token),在基本身份验证模式下提供客户端身份验证详细信息和用户凭据作为表单数据的一部分。以下截图显示了我们如何在基本身份验证中配置客户端身份验证详细信息:

以下截图显示了如何将用户身份验证详细信息配置为 POST 参数的一部分:

我们使用grant_type作为密码,表示我们正在发送用户身份验证详细信息以获取访问令牌。当我们执行请求时,我们会得到类似以下代码片段所示的响应:

    {
      "access_token": "a633dd55-102f-4f53-bcbd-a857df54b821",
      "token_type": "bearer",
      "refresh_token": "d68d89ec-0a13-4224-a29b-e9056768c7f0",
      "expires_in": 43199,
      "scope": "openid"
    }

以下是一些重要细节:

  • access_token: 客户端应用程序可以使用访问令牌来进行进一步的 API 调用身份验证。然而,访问令牌将在通常非常短的时间内过期。

  • refresh_token: 客户端应用程序可以使用refresh_token向认证服务器提交新请求,以获取新的access_token

使用访问令牌执行请求

一旦我们有了access_token,我们可以使用access_token执行请求,如下截图所示:

正如您在前面的截图中所看到的,我们在请求标头中提供了访问令牌,称为 Authorization。我们使用格式的值"Bearer {access_token}"。身份验证成功,我们得到了预期的资源详细信息。

集成测试

现在我们将更新我们的集成测试以提供 OAuth 2 凭据。以下测试突出了重要细节:

    @Test
    public void retrieveTodos() throws Exception {
      String expected = "["
      + "{id:1,user:Jack,desc:\"Learn Spring MVC\",done:false}" + ","
      +"{id:2,user:Jack,desc:\"Learn Struts\",done:false}" + "]";
      String uri = "/users/Jack/todos";
      ResourceOwnerPasswordResourceDetails resource = 
      new ResourceOwnerPasswordResourceDetails();
      resource.setUsername("user-name");
      resource.setPassword("user-password");
      resource.setAccessTokenUri(createUrl("/oauth/token"));
      resource.setClientId("clientId");
      resource.setClientSecret("clientSecret");
      resource.setGrantType("password");
      OAuth2RestTemplate oauthTemplate = new 
      OAuth2RestTemplate(resource,new 
      DefaultOAuth2ClientContext());
      ResponseEntity<String> response = 
      oauthTemplate.getForEntity(createUrl(uri), String.class);
     JSONAssert.assertEquals(expected, response.getBody(), false);
    }

一些重要的事项如下:

  • ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails(): 我们使用用户凭据和客户端凭据设置了ResourceOwnerPasswordResourceDetails

  • resource.setAccessTokenUri(createUrl("/oauth/token")): 配置认证服务器的 URL

  • OAuth2RestTemplate oauthTemplate = new OAuth2RestTemplate(resource,new DefaultOAuth2ClientContext()): OAuth2RestTemplateRestTemplate的扩展,支持 OAuth 2 协议

在本节中,我们看了如何在资源中启用 OAuth 2 身份验证。

国际化

国际化i18n)是开发应用程序和服务的过程,使它们可以为世界各地的不同语言和文化进行定制。它也被称为本地化。国际化或本地化的目标是构建可以以多种语言和格式提供内容的应用程序。

Spring Boot 内置支持国际化。

让我们构建一个简单的服务,以了解如何在我们的 API 中构建国际化。

我们需要向我们的 Spring Boot 应用程序添加LocaleResolver和消息源。以下代码片段应包含在Application.java中:

    @Bean
    public LocaleResolver localeResolver() {
      SessionLocaleResolver sessionLocaleResolver = 
      new SessionLocaleResolver();
      sessionLocaleResolver.setDefaultLocale(Locale.US);
      return sessionLocaleResolver;
    }

   @Bean
   public ResourceBundleMessageSource messageSource() {
     ResourceBundleMessageSource messageSource = 
     new ResourceBundleMessageSource();
     messageSource.setBasenames("messages");
     messageSource.setUseCodeAsDefaultMessage(true);
    return messageSource;
   }

一些重要的事项如下:

  • sessionLocaleResolver.setDefaultLocale(Locale.US): 我们设置了Locale.US的默认区域设置。

  • messageSource.setBasenames("messages"): 我们将消息源的基本名称设置为messages。如果我们处于 fr 区域设置(法国),我们将使用message_fr.properties中的消息。如果在message_fr.properties中找不到消息,则将在默认的message.properties中搜索。

  • messageSource.setUseCodeAsDefaultMessage(true): 如果未找到消息,则将代码作为默认消息返回。

让我们配置各自文件中的消息。让我们从messages属性开始。该文件中的消息将作为默认消息:

    welcome.message=Welcome in English

让我们也配置messages_fr.properties。该文件中的消息将用于区域设置。如果此处不存在消息,则将使用messages.properties中的默认消息:

   welcome.message=Welcome in French

让我们创建一个服务,根据"Accept-Language"头中指定的区域设置返回特定消息:

    @GetMapping("/welcome-internationalized")
    public String msg(@RequestHeader(value = "Accept-Language", 
    required = false) Locale locale) {
      return messageSource.getMessage("welcome.message", null, 
      locale);
    }

以下是需要注意的几点:

  • @RequestHeader(value = "Accept-Language", required = false) Locale locale:区域设置从请求头Accept-Language中获取。不是必需的。如果未指定区域设置,则使用默认区域设置。

  • messageSource.getMessage("welcome.message", null, locale): messageSource被自动装配到控制器中。我们根据给定的区域设置获取欢迎消息。

以下屏幕截图显示了在不指定默认Accept-Language时调用前面的服务时的响应:

messages.properties返回默认消息。

以下屏幕截图显示了在使用Accept-Language fr调用前面的服务时的响应:

messages_fr.properties返回本地化消息。

在前面的示例中,我们定制了服务,根据请求中的区域设置返回本地化消息。类似的方法可以用于国际化组件中的所有服务。

缓存

从服务中缓存数据在提高应用程序性能和可扩展性方面起着至关重要的作用。在本节中,我们将看一下 Spring Boot 提供的实现选项。

Spring 提供了基于注解的缓存抽象。我们将首先使用 Spring 缓存注解。稍后,我们将介绍JSR-107缓存注解,并将它们与 Spring 抽象进行比较。

Spring-boot-starter-cache

Spring Boot 为缓存提供了一个启动器项目spring-boot-starter-cache。将其添加到应用程序中会引入所有依赖项,以启用JSR-107和 Spring 缓存注解。以下代码片段显示了spring-boot-starter-cache的依赖项详细信息。让我们将其添加到我们的文件pom.xml中:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>

启用缓存

在我们开始使用缓存之前,我们需要在应用程序上启用缓存。以下代码片段显示了如何启用缓存:

    @EnableCaching
    @SpringBootApplication
    public class Application {

@EnableCaching将在 Spring Boot 应用程序中启用缓存。

Spring Boot 会自动配置适当的 CacheManager 框架,作为相关缓存的提供者。稍后我们将详细了解 Spring Boot 如何决定 CacheManager。

缓存数据

现在我们已经启用了缓存,我们可以在希望缓存数据的方法上添加@Cacheable注解。以下代码片段显示了如何在retrieveTodos上启用缓存:

    @Cacheable("todos")
    public List<Todo> retrieveTodos(String user) {

在前面的示例中,特定用户的todos被缓存。对于特定用户的方法的第一次调用,todos将从服务中检索。对于相同用户的后续调用,数据将从缓存中返回。

Spring 还提供了有条件的缓存。在以下代码片段中,仅当满足指定条件时才启用缓存:

    @Cacheable(cacheNames="todos", condition="#user.length < 10”)
    public List<Todo> retrieveTodos(String user) {

Spring 还提供了额外的注解来从缓存中清除数据并向缓存中添加一些自定义数据。一些重要的注解如下所示:

  • @CachePut:用于显式向缓存中添加数据

  • @CacheEvict:用于从缓存中删除过期数据

  • @Caching:允许在同一个方法上使用多个嵌套的@Cacheable@CachePut@CacheEvict注解

JSR-107 缓存注解

JSR-107旨在标准化缓存注解。以下是一些重要的JSR-107注解:

  • @CacheResult:类似于@Cacheable

  • @CacheRemove:类似于@CacheEvict;如果发生异常,@CacheRemove支持有条件的驱逐

  • @CacheRemoveAll:类似于@CacheEvict(allEntries=true);用于从缓存中移除所有条目

JSR-107和 Spring 的缓存注解在提供的功能方面非常相似。它们中的任何一个都是一个不错的选择。我们稍微倾向于JSR-107,因为它是一个标准。但是,请确保在同一个项目中不要同时使用两者。

自动检测顺序

启用缓存时,Spring Boot 自动配置开始寻找缓存提供程序。以下列表显示了 Spring Boot 搜索缓存提供程序的顺序。列表按优先级递减的顺序排列:

  • JCache(JSR-107)(EhCache 3、Hazelcast、Infinispan 等)

  • EhCache 2.x

  • Hazelcast

  • Infinispan

  • Couchbase

  • Redis

  • Caffeine

  • Guava

  • Simple

摘要

Spring Boot 使得开发基于 Spring 的应用变得简单。它使我们能够非常快速地创建生产就绪的应用程序。

在本章中,我们介绍了如何向我们的应用程序添加异常处理、缓存和国际化等功能。我们讨论了使用 Swagger 记录 REST 服务的最佳实践。我们了解了如何使用 Spring Security 保护我们的微服务的基础知识。

在下一章中,我们将把注意力转向 Spring Boot 中的高级功能。我们将学习如何在我们的 REST 服务之上提供监控,学习如何将微服务部署到云上,并了解如何在使用 Spring Boot 开发应用程序时变得更加高效。

第七章:高级 Spring Boot 功能

在上一章中,我们通过异常处理、HATEOAS、缓存和国际化扩展了我们的微服务。在本章中,让我们把注意力转向将我们的服务部署到生产环境。为了能够将服务部署到生产环境,我们需要能够设置和创建功能来配置、部署和监控服务。

以下是本章将回答的一些问题:

  • 如何外部化应用程序配置?

  • 如何使用配置文件来配置特定环境的值?

  • 如何将我们的应用程序部署到云端?

  • 嵌入式服务器是什么?如何使用 Tomcat、Jetty 和 Undertow?

  • Spring Boot Actuator 提供了哪些监控功能?

  • 如何通过 Spring Boot 成为更高效的开发者?

外部化配置

应用程序通常只构建一次(JAR 或 WAR),然后部署到多个环境中。下图显示了应用程序可以部署到的不同环境:

在前述的每个环境中,应用程序通常具有以下内容:

  • 连接到数据库

  • 连接到多个服务

  • 特定环境配置

将配置在不同环境之间变化的配置外部化到配置文件或数据库中是一个很好的做法。

Spring Boot 提供了一种灵活的、标准化的外部化配置方法。

在本节中,我们将看一下以下内容:

  • 如何在我们的服务中使用application.properties中的属性?

  • 如何使应用程序配置成为一件轻而易举的事情?

  • Spring Boot 为Spring Profiles提供了什么样的支持?

  • 如何在application.properties中配置属性?

在 Spring Boot 中,application.properties是默认的配置值来源文件。Spring Boot 可以从类路径的任何位置获取application.properties文件。通常,application.properties位于src\main\resources,如下图所示:

在第六章中,扩展微服务,我们看了一些使用application.properties中的配置自定义 Spring Security 的示例:

    security.basic.enabled=false
    management.security.enabled=false
    security.user.name=user-name
    security.user.password=user-password
    security.oauth2.client.clientId: clientId
    security.oauth2.client.clientSecret: clientSecret
    security.oauth2.client.authorized-grant-types:                
    authorization_code,refresh_token,password
    security.oauth2.client.scope: openid

与此类似,所有其他 Spring Boot starters、模块和框架都可以通过application.properties中的配置进行自定义。在下一节中,让我们看一下 Spring Boot 为这些框架提供的一些配置选项。

通过application.properties自定义框架

在本节中,我们将讨论一些可以通过application.properties进行配置的重要事项。

有关完整列表,请参阅docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#common-application-properties

日志

一些可以配置的事项如下:

  • 日志配置文件的位置

  • 日志文件的位置

  • 日志级别

以下代码片段显示了一些示例:

# Location of the logging configuration file.
  logging.config=
# Log file name.
  logging.file=
# Configure Logging level. 
# Example `logging.level.org.springframework=TRACE`
  logging.level.*=

嵌入式服务器配置

嵌入式服务器是 Spring Boot 最重要的特性之一。一些可以通过应用程序属性进行配置的嵌入式服务器特性包括:

  • 服务器端口

  • SSL 支持和配置

  • 访问日志配置

以下代码片段显示了一些可以通过应用程序属性进行配置的嵌入式服务器特性:

# Path of the error controller.
server.error.path=/error
# Server HTTP port.
server.port=8080
# Enable SSL support.
server.ssl.enabled=
# Path to key store with SSL certificate
server.ssl.key-store=
# Key Store Password
server.ssl.key-store-password=
# Key Store Provider
server.ssl.key-store-provider=
# Key Store Type
server.ssl.key-store-type=
# Should we enable access log of Tomcat?
server.tomcat.accesslog.enabled=false
# Maximum number of connections that server can accept
server.tomcat.max-connections=

Spring MVC

Spring MVC 可以通过application.properties进行广泛配置。以下是一些重要的配置:

# Date format to use. For instance `dd/MM/yyyy`.
 spring.mvc.date-format=
# Locale to use.
 spring.mvc.locale=
# Define how the locale should be resolved.
 spring.mvc.locale-resolver=accept-header
# Should "NoHandlerFoundException" be thrown if no Handler is found?
 spring.mvc.throw-exception-if-no-handler-found=false
# Spring MVC view prefix. Used by view resolver.
 spring.mvc.view.prefix=
# Spring MVC view suffix. Used by view resolver.
 spring.mvc.view.suffix=

Spring starter security

Spring Security 可以通过application.properties进行广泛配置。以下示例显示了与 Spring Security 相关的一些重要配置选项:

# Set true to Enable basic authentication
 security.basic.enabled=true
# Provide a Comma-separated list of uris you would want to secure
 security.basic.path=/**
# Provide a Comma-separated list of paths you don't want to secure
 security.ignored=
# Name of the default user configured by spring security
 security.user.name=user
# Password of the default user configured by spring security. 
 security.user.password=
# Roles granted to default user
 security.user.role=USER

数据源、JDBC 和 JPA

数据源、JDBC 和还可以通过application.properties进行广泛配置。以下是一些重要选项:

# Fully qualified name of the JDBC driver. 
 spring.datasource.driver-class-name=
# Populate the database using 'data.sql'.
 spring.datasource.initialize=true
# JNDI location of the datasource.
 spring.datasource.jndi-name=
# Name of the datasource.
 spring.datasource.name=testdb
# Login password of the database.
 spring.datasource.password=
# Schema (DDL) script resource references.
 spring.datasource.schema=
# Db User to use to execute DDL scripts
 spring.datasource.schema-username=
# Db password to execute DDL scripts
 spring.datasource.schema-password=
# JDBC url of the database.
 spring.datasource.url=
# JPA - Initialize the schema on startup.
 spring.jpa.generate-ddl=false
# Use Hibernate's newer IdentifierGenerator for AUTO, TABLE and SEQUENCE.
 spring.jpa.hibernate.use-new-id-generator-mappings=
# Enable logging of SQL statements.
 spring.jpa.show-sql=false

其他配置选项

通过application.properties可以配置的其他一些事项如下:

  • 配置文件

  • HTTP 消息转换器(Jackson/JSON)

  • 事务管理

  • 国际化

以下示例显示了一些配置选项:

# Comma-separated list (or list if using YAML) of active profiles.
 spring.profiles.active=
# HTTP message conversion. jackson or gson
 spring.http.converters.preferred-json-mapper=jackson
# JACKSON Date format string. Example `yyyy-MM-dd HH:mm:ss`.
 spring.jackson.date-format=
# Default transaction timeout in seconds.
 spring.transaction.default-timeout=
# Perform the rollback on commit failures.
 spring.transaction.rollback-on-commit-failure=
# Internationalisation : Comma-separated list of basenames
 spring.messages.basename=messages
# Cache expiration for resource bundles, in sec. -1 will cache for ever
 spring.messages.cache-seconds=-1

application.properties 中的自定义属性

到目前为止,我们已经看过了使用 Spring Boot 为各种框架提供的预构建属性。在本节中,我们将看看如何创建我们的应用程序特定配置,这些配置也可以在application.properties中配置。

让我们考虑一个例子。我们希望能够与外部服务进行交互。我们希望能够外部化此服务的 URL 配置。

以下示例显示了我们如何在application.properties中配置外部服务:

   somedataservice.url=http://abc.service.com/something

我们想要在我们的数据服务中使用;somedataservice.url`属性的值。以下代码片段显示了我们如何在示例数据服务中实现这一点。

    @Component
    public class SomeDataService {
      @Value("${somedataservice.url}")
      private String url;
      public String retrieveSomeData() {
        // Logic using the url and getting the data
       return "data from service";
      }
    }

需要注意的一些重要事项如下:

  • @Component public class SomeDataService:数据服务 bean 由 Spring 管理,因为有@Component注解。

  • @Value("${somedataservice.url}")somedataservice.url的值将自动装配到url变量中。url的值可以在 bean 的方法中使用。

配置属性-类型安全的配置管理

虽然;@Value注解提供了动态配置,但它也有一些缺点:

  • 如果我们想在一个服务中使用三个属性值,我们需要使用@Value三次进行自动装配。

  • @Value注解和消息的键将分布在整个应用程序中。如果我们想要查找应用程序中可配置的值列表,我们必须搜索@Value注解。

Spring Boot 通过强类型的ConfigurationProperties功能提供了更好的应用程序配置方法。这使我们能够做到以下几点:

  • 在预定义的 bean 结构中具有所有属性

  • 这个 bean 将作为所有应用程序属性的集中存储

  • 配置 bean 可以在需要应用程序配置的任何地方进行自动装配

示例配置 bean 如下所示:

    @Component
    @ConfigurationProperties("application")
    public class ApplicationConfiguration {
      private boolean enableSwitchForService1;
      private String service1Url;
      private int service1Timeout;
      public boolean isEnableSwitchForService1() {
        return enableSwitchForService1;
      }
     public void setEnableSwitchForService1
     (boolean enableSwitchForService1) {
        this.enableSwitchForService1 = enableSwitchForService1;
      }
     public String getService1Url() {
       return service1Url;
     }
     public void setService1Url(String service1Url) {
       this.service1Url = service1Url;
     }
     public int getService1Timeout() {
       return service1Timeout;
     }
     public void setService1Timeout(int service1Timeout) {
       this.service1Timeout = service1Timeout;
    }
  }

需要注意的一些重要事项如下:

  • @ConfigurationProperties("application")是外部化配置的注解。我们可以将此注解添加到任何类中,以绑定到外部属性。双引号中的值--application--在将外部配置绑定到此 bean 时用作前缀。

  • 我们正在定义 bean 中的多个可配置值。

  • 由于绑定是通过 Java bean 属性描述符进行的,因此需要 getter 和 setter。

以下代码片段显示了如何在application.properties中定义这些属性的值:

    application.enableSwitchForService1=true
    application.service1Url=http://abc-dev.service.com/somethingelse
    application.service1Timeout=250

需要注意的一些重要事项如下:

  • application:在定义配置 bean 时,前缀被定义为@ConfigurationProperties("application")

  • 通过将前缀附加到属性的名称来定义值

我们可以通过将ApplicationConfiguration自动装配到 bean 中,在其他 bean 中使用配置属性。

    @Component
    public class SomeOtherDataService {
      @Autowired
      private ApplicationConfiguration configuration;
      public String retrieveSomeData() {
        // Logic using the url and getting the data
        System.out.println(configuration.getService1Timeout());
        System.out.println(configuration.getService1Url());
        System.out.println(configuration.isEnableSwitchForService1());
        return "data from service";
      }
    }

需要注意的一些重要事项如下:

  • @Autowired private ApplicationConfiguration configurationApplicationConfiguration被自动装配到SomeOtherDataService

  • configuration.getService1Timeout(), configuration.getService1Url(), configuration.isEnableSwitchForService1():可以使用配置 bean 上的 getter 方法在 bean 方法中访问值

默认情况下,将外部配置的值绑定到配置属性 bean 的任何失败都将导致服务器启动失败。这可以防止因运行在生产环境中的配置错误的应用程序而引起的问题。

让我们使用错误的服务超时来看看会发生什么:

    application.service1Timeout=SOME_MISCONFIGURATION

应用程序将因错误而无法启动。

 ***************************
 APPLICATION FAILED TO START
 ***************************
Description:
Binding to target com.mastering.spring.springboot.configuration.ApplicationConfiguration@79d3473e failed:

Property: application.service1Timeout
Value: SOME_MISCONFIGURATION
Reason: Failed to convert property value of type 'java.lang.String' to required type 'int' for property 'service1Timeout'; nested exception is org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [int]

Action:
Update your application's configuration

配置文件

到目前为止,我们看了如何将应用程序配置外部化到属性文件application.properties。我们希望能够在不同环境中为相同的属性具有不同的值。

配置文件提供了在不同环境中提供不同配置的方法。

以下代码片段显示了如何在application.properties中配置活动配置文件:

    spring.profiles.active=dev

一旦配置了活动配置文件,您可以在application-{profile-name}.properties中定义特定于该配置文件的属性。对于dev配置文件,属性文件的名称将是application-dev.properties。以下示例显示了application-dev.properties中的配置:

    application.enableSwitchForService1=true
    application.service1Url=http://abc-dev.service.com/somethingelse
    application.service1Timeout=250

如果活动配置文件是dev,则application-dev.properties中的值将覆盖application.properties中的默认配置。

我们可以为多个环境进行配置,如下所示:

基于配置文件的 Bean 配置

配置文件还可以用于在不同环境中定义不同的 bean 或不同的 bean 配置。所有标有@Component@Configuration的类也可以标有额外的@Profile注解,以指定启用该 bean 或配置的配置文件。

让我们考虑一个例子。一个应用程序需要在不同环境中启用不同的缓存。在dev环境中,它使用非常简单的缓存。在生产环境中,我们希望使用分布式缓存。这可以使用配置文件来实现。

以下 bean 显示了在dev环境中启用的配置:

    @Profile("dev")
    @Configuration
    public class DevSpecificConfiguration {
      @Bean
      public String cache() {
        return "Dev Cache Configuration";
      }
    }

以下 bean 显示了在生产环境中启用的配置:

    @Profile("prod")
    @Configuration
    public class ProdSpecificConfiguration {
      @Bean
      public String cache() {
        return "Production Cache Configuration - Distributed Cache";
      }
   }

根据配置的活动配置文件,选择相应的配置。请注意,在此示例中,我们实际上并没有配置分布式缓存。我们返回一个简单的字符串来说明可以使用配置文件来实现这些变化。

其他选项用于应用程序配置值

到目前为止,我们采用的方法是使用application.propertiesapplication-{profile-name}.properties中的键值对来配置应用程序属性。

Spring Boot 提供了许多其他配置应用程序属性的方法。

以下是提供应用程序配置的一些重要方法:

  • 命令行参数

  • 创建一个名为SPRING_APPLICATION_JSON的系统属性,并包含 JSON 配置

  • ServletConfig 初始化参数

  • ServletContext 初始化参数

  • Java 系统属性(System.getProperties()

  • 操作系统环境变量

  • 打包在.jar之外的特定配置文件,位于应用程序的类路径中(application-{profile}.properties

  • 打包在.jar中的特定配置文件(application-{profile}.properties和 YAML 变体)

  • .jar之外的应用程序属性

  • 打包在.jar中的应用程序属性

有关更多信息,请参阅 Spring Boot 文档docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-external-config

此列表顶部的方法比列表底部的方法具有更高的优先级。例如,如果在启动应用程序时提供了一个名为spring.profiles.active的命令行参数,它将覆盖通过application.properties提供的任何配置,因为命令行参数具有更高的优先级。

这在确定如何在不同环境中配置应用程序方面提供了很大的灵活性。

YAML 配置

Spring Boot 还支持 YAML 来配置您的属性。

YAML 是“YAML Ain't Markup Language”的缩写。它是一种人类可读的结构化格式。YAML 通常用于配置文件。

要了解 YAML 的基本语法,请查看下面的示例(application.yaml)。这显示了如何在 YAML 中指定我们的应用程序配置。

spring:
   profiles:
      active: prod
security:
   basic:
      enabled: false
   user:
      name=user-name
      password=user-password
oauth2:
   client:
      clientId: clientId
      clientSecret: clientSecret
      authorized-grant-types: authorization_code,refresh_token,password
      scope: openid
application:
   enableSwitchForService1: true
   service1Url: http://abc-dev.service.com/somethingelse
   service1Timeout: 250

正如您所看到的,YAML 配置比application.properties更易读,因为它允许更好地对属性进行分组。

YAML 的另一个优点是它允许您在单个配置文件中为多个配置文件指定配置。以下代码片段显示了一个示例:

application:
  service1Url: http://service.default.com
---
spring:
  profiles: dev
  application:
    service1Url: http://service.dev.com
---
spring:
   profiles: prod
   application:
    service1Url: http://service.prod.com

在这个例子中,http://service.dev.com将在dev配置文件中使用,而http://service.prod.com将在prod配置文件中使用。在所有其他配置文件中,http://service.default.com将作为服务 URL 使用。

嵌入式服务器

Spring Boot 引入的一个重要概念是嵌入式服务器。

让我们首先了解传统 Java Web 应用程序部署与这个称为嵌入式服务器的新概念之间的区别。

传统上,使用 Java Web 应用程序,我们构建 Web 应用程序存档(WAR)或企业应用程序存档(EAR)并将它们部署到服务器上。在我们可以在服务器上部署 WAR 之前,我们需要在服务器上安装 Web 服务器或应用服务器。应用服务器将安装在服务器上的 Java 实例之上。因此,我们需要在可以部署我们的应用程序之前在机器上安装 Java 和应用程序(或 Web 服务器)。以下图显示了 Linux 中的一个示例安装:

Spring Boot 引入了嵌入式服务器的概念,其中 Web 服务器是应用程序可部署的一部分--JAR。使用嵌入式服务器部署应用程序时,只需在服务器上安装 Java 即可。以下图显示了一个示例安装:

当我们使用 Spring Boot 构建任何应用程序时,默认情况下是构建一个 JAR。使用spring-boot-starter-web,默认的嵌入式服务器是 Tomcat。

当我们使用spring-boot-starter-web时,在 Maven 依赖项部分可以看到一些与 Tomcat 相关的依赖项。这些依赖项将作为应用程序部署包的一部分包含进去:

要部署应用程序,我们需要构建一个 JAR。我们可以使用以下命令构建一个 JAR:

mvn clean install

以下屏幕截图显示了创建的 JAR 的结构。

BOOT-INF\classes包含所有与应用程序相关的类文件(来自src\main\java)以及来自src\main\resources的应用程序属性:

BOOT-INF\lib中的一些库在以下屏幕截图中显示:

BOOT-INF\lib包含应用程序的所有 JAR 依赖项。其中有三个 Tomcat 特定的 JAR 文件。这三个 JAR 文件在将应用程序作为 Java 应用程序运行时启用了嵌入式 Tomcat 服务的启动。因此,只需安装 Java 即可在服务器上部署此应用程序。

切换到 Jetty 和 Undertow

以下屏幕截图显示了切换到使用 Jetty 嵌入式服务器所需的更改:

我们所需要做的就是在spring-boot-starter-web中排除 Tomcat 启动器依赖项,并在spring-boot-starter-jetty中包含一个依赖项。

现在您可以在 Maven 依赖项部分看到许多 Jetty 依赖项。以下截图显示了一些与 Jetty 相关的依赖项:

切换到 Undertow 同样很容易。使用spring-boot-starter-undertow代替spring-boot-starter-jetty

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-undertow</artifactId>
   </dependency>

构建 WAR 文件

Spring Boot 还提供了构建传统 WAR 文件而不是使用 JAR 的选项。

首先,我们需要在pom.xml中更改我们的打包为WAR

    <packaging>war</packaging>

我们希望防止 Tomcat 服务器作为 WAR 文件中的嵌入式依赖项。我们可以通过修改嵌入式服务器(以下示例中的 Tomcat)的依赖项来将其范围设置为提供。以下代码片段显示了确切的细节:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-tomcat</artifactId>
      <scope>provided</scope>
   </dependency>

当构建 WAR 文件时,不包括 Tomcat 依赖项。我们可以使用此 WAR 文件部署到应用服务器,如 WebSphere 或 Weblogic,或 Web 服务器,如 Tomcat。

开发工具

Spring Boot 提供了可以改善开发 Spring Boot 应用程序体验的工具。其中之一是 Spring Boot 开发工具。

要使用 Spring Boot 开发工具,我们需要包含一个依赖项:

    <dependencies>
     <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-devtools</artifactId>
       <optional>true</optional>
     </dependency>
   </dependencies>

Spring Boot 开发工具默认禁用视图模板和静态文件的缓存。这使开发人员可以在进行更改后立即看到更改。

另一个重要功能是当类路径中的任何文件更改时自动重新启动。因此,在以下情况下应用程序会自动重新启动:

  • 当我们对控制器或服务类进行更改时

  • 当我们对属性文件进行更改时

Spring Boot 开发工具的优点如下:

  • 开发人员不需要每次都停止和启动应用程序。只要有变化,应用程序就会自动重新启动。

  • Spring Boot 开发工具中的重新启动功能是智能的。它只重新加载活跃开发的类。它不会重新加载第三方 JAR(使用两个不同的类加载器)。因此,当应用程序中的某些内容发生变化时,重新启动速度比冷启动应用程序要快得多。

实时重新加载

另一个有用的 Spring Boot 开发工具功能是实时重新加载。您可以从livereload.com/extensions/下载特定的浏览器插件。

您可以通过单击浏览器中的按钮来启用实时重新加载。 Safari 浏览器中的按钮如下截图所示。它位于地址栏旁边的左上角。

如果在浏览器中显示的页面或服务上进行了代码更改,它们将自动刷新为新内容。不再需要点击刷新按钮!

Spring Boot 执行器

当应用程序部署到生产环境时:

  • 我们希望立即知道某些服务是否宕机或非常缓慢

  • 我们希望立即知道任何服务器是否没有足够的可用空间或内存

这被称为应用程序监控

Spring Boot 执行器提供了许多生产就绪的监控功能。

我们将通过添加一个简单的依赖项来添加 Spring Boot 执行器:

    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
     </dependency>
   </dependencies>

一旦执行器添加到应用程序中,它就会启用许多端点。当我们启动应用程序时,我们会看到许多新添加的映射。以下截图显示了启动日志中这些新映射的摘录:

执行器公开了许多端点。执行器端点(http://localhost:8080/application)充当所有其他端点的发现。当我们从 Postman 执行请求时,以下截图显示了响应:

HAL 浏览器

许多这些端点暴露了大量数据。为了能够更好地可视化信息,我们将在我们的应用程序中添加一个HAL 浏览器

    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-rest-hal-browser</artifactId>
    </dependency>

Spring Boot Actuator 在 Spring Boot 应用程序和环境中捕获的所有数据周围暴露了 REST API。HAL 浏览器使得在 Spring Boot Actuator API 周围进行可视化表示成为可能:

当我们在浏览器中启动http://localhost:8080/application时,我们可以看到 actuator 暴露的所有 URL。

让我们通过 HAL 浏览器浏览 actuator 作为不同端点的一部分暴露的所有信息。

配置属性

configprops端点提供了关于可以通过应用程序属性进行配置的配置选项的信息。它基本上是所有@ConfigurationProperties的汇总列表。下面的屏幕截图显示了 HAL 浏览器中的 configprops:

为了举例说明,以下部分从服务响应中显示了 Spring MVC 可用的配置选项:

"spring.mvc-  org.springframework.boot.autoconfigure.web.WebMvcProperties": {
   "prefix": "spring.mvc",
   "properties": {
                   "dateFormat": null,
                   "servlet": {
                     "loadOnStartup": -1
                  },
   "staticPathPattern": "/**",
   "dispatchOptionsRequest": true,
   "dispatchTraceRequest": false,
   "locale": null,
   "ignoreDefaultModelOnRedirect": true,
   "logResolvedException": true,
   "async": {
              "requestTimeout": null
            },
   "messageCodesResolverFormat": null,
   "mediaTypes": {},
   "view": {
             "prefix": null,
             "suffix": null
           },
   "localeResolver": "ACCEPT_HEADER",
   "throwExceptionIfNoHandlerFound": false
    }
 }

为了为 Spring MVC 提供配置,我们将前缀与属性中的路径组合在一起。例如,要配置loadOnStartup,我们使用名称为spring.mvc.servlet.loadOnStartup的属性。

环境细节

环境(env)端点提供了有关操作系统、JVM 安装、类路径、系统环境变量以及各种应用程序属性文件中配置的值的信息。以下屏幕截图显示了 HAL 浏览器中的环境端点:

以下是从/application/env服务的响应中提取的内容。它显示了一些系统详细信息以及应用程序配置的详细信息:

"systemEnvironment": {
    "JAVA_MAIN_CLASS_13377": "com.mastering.spring.springboot.Application",
    "PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
    "SHELL": "/bin/bash",
    "JAVA_STARTED_ON_FIRST_THREAD_13019": "1",
    "APP_ICON_13041": "../Resources/Eclipse.icns",
    "USER": "rangaraokaranam",
    "TMPDIR": "/var/folders/y_/x4jdvdkx7w94q5qsh745gzz00000gn/T/",
    "SSH_AUTH_SOCK": "/private/tmp/com.apple.launchd.IcESePQCLV/Listeners",
    "XPC_FLAGS": "0x0",
    "JAVA_STARTED_ON_FIRST_THREAD_13041": "1",
    "APP_ICON_11624": "../Resources/Eclipse.icns",
    "LOGNAME": "rangaraokaranam",
    "XPC_SERVICE_NAME": "0",
    "HOME": "/Users/rangaraokaranam"
  },
  "applicationConfig: [classpath:/application-prod.properties]": {
    "application.service1Timeout": "250",
    "application.service1Url": "http://abc-    prod.service.com/somethingelse",
    "application.enableSwitchForService1": "false"
  },

健康

健康服务提供了磁盘空间和应用程序状态的详细信息。以下屏幕截图显示了从 HAL 浏览器执行的服务:

Mappings

Mappings 端点提供了有关从应用程序暴露的不同服务端点的信息:

  • URI

  • 请求方法

  • Bean

  • 暴露服务的控制器方法

Mappings 提供了所有@RequestMapping路径的汇总列表。以下是从/application/mappings端点的响应中提取的内容。我们可以看到在本书中之前创建的不同控制器方法的映射:

"{[/welcome-internationalized],methods=[GET]}": {
   "bean": "requestMappingHandlerMapping",
   "method": "public java.lang.String 
    com.mastering.spring.springboot.controller.
    BasicController.msg(java.uti l.Locale)"
 },
 "{[/welcome],methods=[GET]}": {
    "bean": "requestMappingHandlerMapping",
    "method": "public java.lang.String 
     com.mastering.spring.springboot.controller.
     BasicController.welcome()"
 },
 "{[/welcome-with-object],methods=[GET]}": {
     "bean": "requestMappingHandlerMapping",
     "method": "public com.mastering.spring.springboot.
      bean.WelcomeBeancom.mastering.spring.springboot.
      controller.BasicController.welcomeWithObject()"
 },
 "{[/welcome-with-parameter/name/{name}],methods=[GET]}": {
      "bean": "requestMappingHandlerMapping",
      "method": "public 
       com.mastering.spring.springboot.bean.WelcomeBean   
       com.mastering.spring.springboot.controller.
       BasicController.welcomeWithParameter(java.lang.String)"
 },
 "{[/users/{name}/todos],methods=[POST]}": {
       "bean": "requestMappingHandlerMapping",
       "method": "org.springframework.http.ResponseEntity<?>    
        com.mastering.spring.springboot.controller.
        TodoController.add(java.lang.String,com.mastering.spring.
        springboot.bean.Todo)"
  },
 "{[/users/{name}/todos],methods=[GET]}": {
        "bean": "requestMappingHandlerMapping",
        "method": "public java.util.List<com.mastering.spring.
         springboot.bean.Todo> 
         com.mastering.spring.springboot.controller.
         TodoController.retrieveTodos(java.lang.String)"
 },
 "{[/users/{name}/todos/{id}],methods=[GET]}": {
        "bean": "requestMappingHandlerMapping",
        "method": "public 
         org.springframework.hateoas.Resource<com.mastering.
         spring.springboot.bean.Todo>  
         com.mastering.spring.springboot.controller.
         TodoController.retrieveTodo(java.lang.String,int)"
 },

Beans

beans 端点提供了有关加载到 Spring 上下文中的 bean 的详细信息。这对于调试与 Spring 上下文相关的任何问题非常有用。

以下是从/application/beans端点的响应中提取的内容:

  {
     "bean": "basicController",
     "aliases": [],
     "scope": "singleton",
     "type": "com.mastering.spring.springboot.
      controller.BasicController",
     "resource": "file [/in28Minutes/Workspaces/
      SpringTutorial/mastering-spring-chapter-5-6-  
      7/target/classes/com/mastering/spring/springboot/
      controller/BasicController.class]",
      "dependencies": [
                     "messageSource"
                    ]
   },
   {
      "bean": "todoController",
      "aliases": [],
      "scope": "singleton",
      "type": "com.mastering.spring.springboot.
       controller.TodoController",
       "resource": "file [/in28Minutes/Workspaces/SpringTutorial/
       mastering-spring-chapter-5-6-
       7/target/classes/com/mastering/spring/
       springboot/controller/TodoController.class]",
       "dependencies": [
                      "todoService"
                     ]
    }

它显示了两个 bean:basicControllertodoController的详细信息。您可以看到所有 bean 的以下详细信息:

  • bean 的名称及其别名

  • bean 的范围

  • Bean 的类型

  • 创建此 bean 的类的确切位置

  • Bean 的依赖关系

指标

指标端点显示以下一些重要的指标:

  • 服务器--空闲内存、处理器、正常运行时间等

  • JVM--关于堆、线程、垃圾收集、会话等的详细信息

  • 应用程序服务提供的响应

以下是从/application/metrics端点的响应中提取的内容:

{
 "mem": 481449,
 "mem.free": 178878,
 "processors": 4,
 "instance.uptime": 1853761,
 "uptime": 1863728,
 "systemload.average": 2.3349609375,
 "heap.committed": 413696,
 "heap.init": 65536,
 "heap.used": 234817,
 "heap": 932352,
 "nonheap.committed": 69248,
 "nonheap.init": 2496,
 "nonheap.used": 67754,
 "nonheap": 0,
 "threads.peak": 23,
 "threads.daemon": 21,
 "threads.totalStarted": 30,
 "threads": 23,
 "classes": 8077,
 "classes.loaded": 8078,
 "classes.unloaded": 1,
 "gc.ps_scavenge.count": 15,
 "gc.ps_scavenge.time": 242,
 "gc.ps_marksweep.count": 3,
 "gc.ps_marksweep.time": 543,
 "httpsessions.max": -1,
 "httpsessions.active": 0,
 "gauge.response.actuator": 8,
 "gauge.response.mappings": 12,
 "gauge.response.beans": 83,
 "gauge.response.health": 14,
 "gauge.response.root": 9,
 "gauge.response.heapdump": 4694,
 "gauge.response.env": 6,
 "gauge.response.profile": 12,
 "gauge.response.browser.star-star": 10,
 "gauge.response.actuator.root": 2,
 "gauge.response.configprops": 272,
 "gauge.response.actuator.star-star": 13,
 "counter.status.200.profile": 1,
 "counter.status.200.actuator": 8,
 "counter.status.200.mappings": 1,
 "counter.status.200.root": 5,
 "counter.status.200.configprops": 1,
 "counter.status.404.actuator.star-star": 3,
 "counter.status.200.heapdump": 1,
 "counter.status.200.health": 1,
 "counter.status.304.browser.star-star": 132,
 "counter.status.302.actuator.root": 4,
 "counter.status.200.browser.star-star": 37,
 "counter.status.200.env": 2,
 "counter.status.302.root": 5,
 "counter.status.200.beans": 1,
 "counter.status.200.actuator.star-star": 210,
 "counter.status.302.actuator": 1
 }

自动配置

自动配置是 Spring Boot 的最重要特性之一。自动配置端点(/application/autoconfig)暴露了与自动配置相关的详细信息。它显示了成功或失败的特定自动配置的原因的正匹配和负匹配。

以下提取显示了响应中一些正匹配的内容:

"positiveMatches": {
  "AuditAutoConfiguration#auditListener": [
   {
     "condition": "OnBeanCondition",
     "message": "@ConditionalOnMissingBean (types:     
      org.springframework.boot.actuate.audit.
      listener.AbstractAuditListener; SearchStrategy: all) did not find 
      any beans"
   }
 ],
 "AuditAutoConfiguration#authenticationAuditListener": [
 {
   "condition": "OnClassCondition",
   "message": "@ConditionalOnClass found required class
   'org.springframework.security.authentication.
   event.AbstractAuthenticationEvent'"
 },

以下提取显示了响应中一些负匹配的内容:

"negativeMatches": {
  "CacheStatisticsAutoConfiguration.
   CaffeineCacheStatisticsProviderConfiguration": [
 {
   "condition": "OnClassCondition",
   "message": "@ConditionalOnClass did not find required class  
   'com.github.benmanes.caffeine.cache.Caffeine'"
 }
 ],
   "CacheStatisticsAutoConfiguration.
   EhCacheCacheStatisticsProviderConfiguration": [
 {
   "condition": "OnClassCondition",
   "message": "@ConditionalOnClass did not find required classes
   'net.sf.ehcache.Ehcache',   
   'net.sf.ehcache.statistics.StatisticsGateway'"
 }
 ],

所有这些细节对于调试自动配置非常有用。

调试

在调试问题时,三个执行器端点非常有用:

  • /application/heapdump:提供堆转储

  • /application/trace:提供应用程序最近几个请求的跟踪

  • /application/dump:提供线程转储

将应用程序部署到 Cloud

Spring Boot 对大多数流行的云平台即服务PaaS)提供商有很好的支持。

一些流行的云端包括:

  • Cloud Foundry

  • Heroku

  • OpenShift

  • 亚马逊网络服务AWS

在本节中,我们将专注于将我们的应用程序部署到 Cloud Foundry。

Cloud Foundry

Cloud Foundry 的 Java 构建包对 Spring Boot 有很好的支持。我们可以部署基于 JAR 的独立应用程序,也可以部署传统的 Java EE WAR 应用程序。

Cloud Foundry 提供了一个 Maven 插件来部署应用程序:

<build>
   <plugins>
      <plugin>
         <groupId>org.cloudfoundry</groupId>
         <artifactId>cf-maven-plugin</artifactId>
         <version>1.1.2</version>
      </plugin>
   </plugins>
</build>

在我们部署应用程序之前,我们需要为应用程序配置目标和空间以部署应用程序。

涉及以下步骤:

  1. 我们需要在account.run.pivotal.io/sign-up创建一个 Pivotal Cloud Foundry 账户。

  2. 一旦我们有了账户,我们可以登录到run.pivotal.io创建一个组织和空间。准备好组织和空间的详细信息,因为我们需要它们来部署应用程序。

我们可以使用orgspace的配置更新插件:

<build>
   <plugins>
      <plugin>
         <groupId>org.cloudfoundry</groupId>
         <artifactId>cf-maven-plugin</artifactId>
         <version>1.1.2</version>
         <configuration>
            <target>http://api.run.pivotal.io</target>
            <org>in28minutes</org>
            <space>development</space>
            <memory>512</memory>
            <env>
               <ENV-VAR-NAME>prod</ENV-VAR-NAME>
            </env>
         </configuration>
      </plugin>
   </plugins>
</build>

我们需要使用 Maven 插件在命令提示符或终端上登录到 Cloud Foundry:

mvn cf:login -Dcf.username=<<YOUR-USER-ID>> -Dcf.password=<<YOUR-PASSWORD>>

如果一切顺利,您将看到一条消息,如下所示:

[INFO] ------------------------------------------------------------------
 [INFO] Building Your First Spring Boot Example 0.0.1-SNAPSHOT
 [INFO] -----------------------------------------------------------------
 [INFO]
 [INFO] --- cf-maven-plugin:1.1.2:login (default-cli) @ springboot-for-beginners-example ---
 [INFO] Authentication successful
 [INFO] -----------------------------------------------------------------
 [INFO] BUILD SUCCESS
 [INFO] -----------------------------------------------------------------
 [INFO] Total time: 14.897 s
 [INFO] Finished at: 2017-02-05T16:49:52+05:30
 [INFO] Final Memory: 22M/101M
 [INFO] -----------------------------------------------------------------

一旦您能够登录,您可以将应用程序推送到 Cloud Foundry:

mvn cf:push

一旦我们执行命令,Maven 将编译,运行测试,构建应用程序的 JAR 或 WAR,然后将其部署到云端:

[INFO] Building jar: /in28Minutes/Workspaces/SpringTutorial/springboot-for-beginners-example-rest-service/target/springboot-for-beginners-example-0.0.1-SNAPSHOT.jar
 [INFO]
 [INFO] --- spring-boot-maven-plugin:1.4.0.RELEASE:repackage (default) @ springboot-for-beginners-example ---
 [INFO]
 [INFO] <<< cf-maven-plugin:1.1.2:push (default-cli) < package @ springboot-for-beginners-example <<<
 [INFO]
 [INFO] --- cf-maven-plugin:1.1.2:push (default-cli) @ springboot-for-beginners-example ---
 [INFO] Creating application 'springboot-for-beginners-example'
 [INFO] Uploading '/in28Minutes/Workspaces/SpringTutorial/springboot-for-beginners-example-rest-service/target/springboot-for-beginners-example-0.0.1-SNAPSHOT.jar'
 [INFO] Starting application
 [INFO] Checking status of application 'springboot-for-beginners-example'
 [INFO] 1 of 1 instances running (1 running)
 [INFO] Application 'springboot-for-beginners-example' is available at 'http://springboot-for-beginners-example.cfapps.io'
 [INFO] ----------------------------------------------------------------- [INFO] BUILD SUCCESS
 [INFO] ----------------------------------------------------------------- [INFO] Total time: 02:21 min
 [INFO] Finished at: 2017-02-05T16:54:55+05:30
 [INFO] Final Memory: 29M/102M
 [INFO] -----------------------------------------------------------------

一旦应用程序在云端运行起来,我们可以使用日志中的 URL 来启动应用程序:springboot-for-beginners-example.cfapps.io

您可以在docs.run.pivotal.io/buildpacks/java/build-tool-int.html#maven找到有关 Cloud Foundry 的 Java Build Pack 的更多信息。

总结

Spring Boot 使开发基于 Spring 的应用程序变得容易。它使我们能够非常快速地创建生产就绪的应用程序。

在本章中,我们了解了 Spring Boot 提供的不同外部配置选项。我们查看了嵌入式服务器,并将一个测试应用程序部署到了 PaaS 云平台--Cloud Foundry。我们探讨了如何使用 Spring Boot 执行器在生产环境中监视我们的应用程序。最后,我们看了一下使开发人员更加高效的功能--Spring Boot 开发人员工具和实时重新加载。

在下一章中,我们将把注意力转向数据。我们将涵盖 Spring Data,并看看它如何使与 JPA 集成和提供 Rest 服务更容易。

第八章:Spring Data

第七章中,高级 Spring Boot 功能,我们讨论了高级 Spring Boot 功能,如外部化配置、监控、嵌入式服务器和部署到云端。在本章中,让我们把注意力转向数据。我们存储数据的地方以及我们如何存储数据在过去的十年中发生了快速的演变。在几十年的关系数据库稳定之后,在过去的十年中,一些非结构化的非关系数据库开始占据重要地位。随着各种数据存储的出现,与这些数据存储进行通信的框架变得更加重要。虽然 JPA 使得与关系数据库进行通信变得容易,但 Spring Data 旨在引入一种通用的方法来与更广泛的数据存储进行通信--无论是关系型还是其他类型的数据存储。

在本章中,我们将回答以下一些问题:

  • 什么是 Spring Data?

  • Spring Data 的目标是什么?

  • 如何使用 Spring Data 和 Spring Data JPA 与关系数据库进行通信?

  • 如何使用 Spring Data 与 Spring Data JPA 与关系数据库进行通信?

背景-数据存储

大多数应用程序与各种数据存储进行通信。应用程序与数据存储进行通信的方式已经有了相当大的发展。Java EE 提供的最基本的 API 是JDBC(Java 数据库连接)。JDBC 用于从 Java EE 的第一个版本开始与关系数据库通信。JDBC 基于使用 SQL 查询来操作数据。以下是典型的 JDBC 代码示例:

    PreparedStatement st = null; 
    st = conn.prepareStatement(INSERT_TODO_QUERY); 
    st.setString(1, bean.getDescription()); 
    st.setBoolean(2, bean.isDone()); 
    st.execute();

典型的 JDBC 代码包含以下内容:

  • 要执行的查询(或存储过程)

  • 设置查询参数到语句对象的代码

  • 将 ResultSet(执行查询的结果)转换为 bean 的代码

典型项目涉及数千行 JDBC 代码。JDBC 代码编写和维护起来很麻烦。为了在 JDBC 之上提供额外的层,出现了两个流行的框架:

  • myBatis(之前称为 iBatis):MyBatis 消除了手动编写代码来设置参数和检索结果的需要。它提供了简单的基于 XML 或注释的配置,将 Java POJO 映射到数据库。

  • Hibernate:Hibernate 是一个ORM(对象/关系映射)框架。ORM 框架帮助您将对象映射到关系数据库中的表。Hibernate 的好处在于开发人员不需要手动编写查询。一旦对象和表之间的关系被映射,Hibernate 就会使用映射来创建查询和填充/检索数据。

Java EE 提出了一个名为JPA(Java 持久化 API)的 API,它基本上是根据当时流行的 ORM 实现--Hibernate 框架来定义的。Hibernate(自 3.4.0.GA 以来)支持/实现 JPA。

在关系数据库中,数据存储在规范化的、定义良好的表中。虽然 Java EE 试图解决与关系数据存储通信的挑战,但在过去的十年中,其他几种数据存储变得流行。随着大数据和实时数据需求的发展,新的和更无结构的数据存储形式出现了。这些类型的数据库通常被归类为 NoSQL 数据库。例如 Cassandra(列)、MongoDB(文档)和 Hadoop。

Spring Data

每种数据存储都有不同的连接和检索/更新数据的方式。Spring Data 旨在提供一种一致的模型--另一种抽象层--以访问不同类型的数据存储中的数据。

以下是一些重要的 Spring Data 功能:

  • 通过各种存储库轻松集成多个数据存储

  • 根据存储库方法名称解析和形成查询的能力

  • 提供默认的 CRUD 功能

  • 基本支持审计,例如由用户创建和最后由用户更改

  • 与 Spring 强大的集成

  • 与 Spring MVC 的出色集成,通过Spring Data Rest公开 REST 控制器

Spring Data 是一个由多个模块组成的综合项目。一些重要的 Spring Data 模块列举如下:

  • Spring Data Commons:定义了所有 Spring Data 模块的共同概念--存储库和查询方法

  • Spring Data JPA:提供与 JPA 存储库的轻松集成

  • Spring Data MongoDB:提供与 MongoDB(基于文档的数据存储)的轻松集成

  • Spring Data REST:提供将 Spring Data 存储库作为 REST 服务暴露出来的功能,代码量最小

  • Spring Data for Apache Cassandra:提供与 Cassandra 的轻松集成

  • 提供与 Hadoop 的轻松集成

在本章中,我们将深入研究 Spring Data、存储库和查询方法背后的共同概念。在最初的示例中,我们将使用 Spring Data JPA 来说明这些概念。在本章后面,我们还将看一下与 MongoDB 的示例集成。

Spring Data Commons

Spring Data Commons 提供了 Spring Data 模块背后的基本抽象。我们将使用 Spring Data JPA 作为示例来说明这些抽象。

Spring Data Commons 中的一些重要接口列举如下:

   Repository<T, ID extends Serializable>
   CrudRepository<T, ID extends Serializable> extends Repository<T, ID>
   PagingAndSortingRepository<T, ID extends Serializable> extends   
   CrudRepository<T, ID>

Repository

Repository 是 Spring Data 的核心接口。它是一个标记接口

CrudRepository 接口

CrudRepository定义了基本的CreateReadUpdateDelete方法。CrudRepository中的重要方法如下所示:

    public interface CrudRepository<T, ID extends Serializable>
      extends Repository<T, ID> {
      <S extends T> S save(S entity);
      findOne(ID primaryKey);
      Iterable<T> findAll();
      Long count();
      void delete(T entity);
      boolean exists(ID primaryKey);
      // … more functionality omitted.
    }

PagingAndSortingRepository 接口

PagingAndSortingRepository 定义了方法,提供了将 ResultSet 分成页面以及对结果进行排序的功能:

   public interface PagingAndSortingRepository<T, ID extends
     Serializable>
     extends CrudRepository<T, ID> {
       Iterable<T> findAll(Sort sort);
       Page<T> findAll(Pageable pageable);
    }

我们将在 Spring Data JPA 部分的示例中查看使用Sort类和PagePageable接口的示例。

Spring Data JPA

Spring Data JPA 实现了 Spring Data Common 接口中定义的核心功能。

JpaRepository是 JPA 特定的存储库接口。

   public interface JpaRepository<T, ID extends Serializable>
   extends PagingAndSortingRepository<T, ID>, 
   QueryByExampleExecutor<T>     {

SimpleJpaRepository是 JPA 的 CrudRepository 接口的默认实现:

   public class SimpleJpaRepository<T, ID extends Serializable>
   implements JpaRepository<T, ID>, JpaSpecificationExecutor<T>

Spring Data JPA 示例

让我们建立一个简单的项目,以了解与 Spring Data Commons 和 Spring Data JPA 相关的不同概念。

以下是涉及的步骤:

  1. 使用spring-boot-starter-data-jpa作为依赖项创建一个新项目。

  2. 添加实体。

  3. SpringBootApplication类添加到运行应用程序。

  4. 创建存储库。

使用 Starter Data JPA 创建新项目

我们将使用以下依赖项创建一个简单的 Spring Boot Maven 项目:

   <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
     <groupId>com.h2database</groupId>
     <artifactId>h2</artifactId>
     <scope>runtime</scope>
  </dependency>
  <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-test</artifactId>
     <scope>test</scope>
  </dependency>

spring-boot-starter-data-jpa是 Spring Boot 的 Spring Data JPA 启动器项目。spring-boot-starter-data-jpa引入的重要依赖包括JTA(Java 事务 API)、Hibernate Core 和 Entity Manager(默认 JPA 实现)。其他一些重要的依赖包如下截图所示:

实体

让我们定义一些实体来用在我们的示例中。我们将创建一个名为Todo的实体来管理待办事项。一个简单的示例如下所示:

   @Entity
   public class Todo {
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;
     @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "userid")
     private User user;
     private String title;
     private String description;
     private Date targetDate;
     private boolean isDone;
     public Todo() {// Make JPA Happy
    }
   }

需要注意的重要事项如下:

  • Todo有一个标题,一个描述,一个目标日期和一个完成指示器(isDone)。JPA 需要一个构造函数。

  • @Entity: 该注解指定该类是一个实体。

  • @Id: 指定 ID 是实体的主键。

  • @GeneratedValue(strategy = GenerationType.AUTO): GeneratedValue注解用于指定如何生成主键。在这个例子中,我们使用了GenerationType.AUTO的策略。这表示我们希望持久性提供者选择正确的策略。

  • @ManyToOne(fetch = FetchType.LAZY): 表示UserTodo之间的多对一关系。@ManyToOne关系用于关系的一侧。FetchType.Lazy表示数据可以懒加载。

  • @JoinColumn(name = "userid"): JoinColumn注解指定外键列的名称。

以下代码片段显示了User实体:

   @Entity
   public class User {
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Long id;
     private String userid;
     private String name;
     @OneToMany(mappedBy = "user")
     private List<Todo> todos;
     public User() {// Make JPA Happy
    }
   }

需要注意的重要事项如下:

  • 用户被定义为具有useridname属性的实体。ID 是自动生成的主键。

  • @OneToMany(mappedBy = "user")OneToMany注解用于一对多关系的多端。mappedBy属性指示关系的所有者实体的属性。

SpringBootApplication 类

让我们创建一个SpringBootApplication类,以便能够运行 Spring Boot 应用程序。以下代码片段显示了一个简单的示例:

    @SpringBootApplication
    public class SpringDataJpaFirstExampleApplication {
      public static void main(String[] args) {
        SpringApplication.run( 
        SpringDataJpaFirstExampleApplication.class, args);
      }
    }

以下代码片段显示了我们将SpringDataJpaFirstExampleApplication作为 Java 应用程序运行时生成的一些日志:

LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
org.hibernate.Version : HHH000412: Hibernate Core {5.0.11.Final}
org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
org.hibernate.tool.hbm2ddl.SchemaExport : HHH000227: Running hbm2ddl schema export
org.hibernate.tool.hbm2ddl.SchemaExport : HHH000230: Schema export complete
j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'

一些重要观察结果如下:

  • HHH000412: Hibernate Core {5.0.11.Final}:Hibernate 框架已初始化

  • HHH000400: Using dialect: org.hibernate.dialect.H2Dialect:初始化了 H2 内存数据库

  • HHH000227: Running hbm2ddl schema export:基于可用的实体(TodoUser)和它们之间的关系,创建了一个模式

在上一次执行中发生了很多魔法。让我们看一些重要的问题:

  1. 尽管我们没有在pom.xml中明确声明依赖关系,但 Hibernate 框架是如何介入的?

  2. H2 内存数据库是如何使用的?

  3. 创建的模式是什么?

现在让我们回答每个问题。

尽管我们没有在pom.xml中明确声明依赖关系,但 Hibernate 框架是如何介入的?

  • Hibernate 是 Spring Boot Starter JPA 的依赖之一。因此,它是默认使用的 JPA 实现。

H2 内存数据库是如何使用的?

  • 在我们的依赖项中,我们包含了一个运行时范围的 H2 依赖项。当 Spring Boot Data JPA 自动配置运行时,它注意到我们没有在配置中包含任何数据源(实际上,我们根本没有配置)。Spring Boot Data JPA 然后尝试自动配置一个内存数据库。它看到类路径上的 H2。因此,它初始化了一个内存中的 H2 数据库。

创建的模式是什么?

以下代码片段显示了根据我们声明的实体类和关系创建的模式。这是由 Spring Boot Data JPA 自动配置自动创建的。

    create table todo (
      id bigint generated by default as identity,
      description varchar(255),
      is_done boolean not null,
      target_date timestamp,
      title varchar(255),
      userid bigint,
      primary key (id)
     )
    create table user (
      id bigint generated by default as identity,
      name varchar(255),
      userid varchar(255),
      primary key (id)
     )
    alter table todo
    add constraint FK4wek61l9imiccm4ypjj5hfn2g
   foreign key (userid)
   references user

todo表对用户表有一个外键用户 ID。

填充一些数据

为了能够测试我们将创建的存储库,我们将在这些表中填充一些测试数据。我们需要做的就是在src\main\resources中包含名为data.sql的文件,并包含以下语句:

insert into user (id, name, userid)
 values (1, 'User Name 1', 'UserId1');
insert into user (id, name, userid)
 values (2, 'User Name 2', 'UserId2');
insert into user (id, name, userid)
 values (3, 'User Name 3', 'UserId3');
insert into user (id, name, userid)
 values (4, 'User Name 4', 'UserId4');
insert into todo (id, title, description, is_done, target_date, userid)
 values (101, 'Todo Title 1', 'Todo Desc 1', false, CURRENT_DATE(), 1);
insert into todo (id, title, description, is_done, target_date, userid)
 values (102, 'Todo Title 2', 'Todo Desc 2', false, CURRENT_DATE(), 1);
insert into todo (id, title, description, is_done, target_date, userid)
 values (103, 'Todo Title 3', 'Todo Desc 3', false, CURRENT_DATE(), 2);

这些是简单的插入语句。我们创建了四个用户 - 第一个用户有两个待办事项,第二个用户有一个待办事项,最后两个用户没有。

当您再次将SpringDataJpaFirstExampleApplication作为 Java 应用程序运行时,您将在日志中看到一些额外的语句:

ScriptUtils : Executing SQL script from URL [file:/in28Minutes/Workspaces/SpringDataJPA-Preparation/Spring-Data-JPA-Trial-Run/target/classes/data.sql]

ScriptUtils : Executed SQL script from URL [file:/in28Minutes/Workspaces/SpringDataJPA-Preparation/Spring-Data-JPA-Trial-Run/target/classes/data.sql] in 42 ms.

日志语句确认数据正在填充到 H2 内存数据库中。让我们把注意力转向创建存储库,以从 Java 代码中访问和操作数据。

一个简单的存储库

可以通过扩展存储库标记接口来创建自定义存储库。在以下示例中,我们使用两个方法扩展了存储库接口--findAllcount

    import org.springframework.data.repository.Repository;
    public interface TodoRepository extends Repository<Todo, Long> {
      Iterable<Todo> findAll();
      long count();
    }

需要注意的一些重要事项如下:

  • public interface TodoRepository extends Repository<Todo, Long>TodoRepository接口扩展了Repository接口。两个泛型类型表示正在管理的实体--Todo 和主键的类型,即Long

  • Iterable<Todo> findAll(): 用于列出所有待办事项。请注意,方法的名称应与CrudRepository中定义的名称匹配。

  • long count(): 用于查找所有待办事项的计数。

单元测试

让我们编写一个简单的单元测试,测试我们是否能够使用TodoRepository访问todo数据。以下代码片段显示了重要细节:

    @DataJpaTest
    @RunWith(SpringRunner.class)
    public class TodoRepositoryTest {
      @Autowired
      TodoRepository todoRepository;
      @Test
      public void check_todo_count() {
        assertEquals(3, todoRepository.count());
      }
    }

需要注意的一些重要事项如下:

  • @DataJpaTest: DataJpaTest注解通常与SpringRunner一起在 JPA 存储库单元测试中使用。此注解将仅启用与 JPA 相关的自动配置。测试将默认使用内存数据库。

  • @RunWith(SpringRunner.class): SpringRunnerSpringJUnit4ClassRunner的简单别名。它启动了一个 Spring 上下文。

  • @Autowired TodoRepository todoRepository: 自动装配TodoRepository以在测试中使用。

  • assertEquals(3, todoRepository.count()): 检查返回的计数是否为3。请记住,我们在data.sql中插入了三个todos

一个警告:在前面的示例中,我们正在采用一种快捷方式来编写单元测试。理想情况下,单元测试不应依赖于数据库中已创建的数据。我们将在未来的测试中解决这个问题。

Extending Repository接口帮助我们在实体上公开选定的方法。

CrudRepository 接口

我们可以扩展CrudRepository以公开实体上的所有创建、读取、更新和删除方法。以下代码片段显示了TodoRepository扩展CrudRepository

    public interface TodoRepository extends CrudRepository<Todo, Long>
     {
    }

TodoRepository可用于执行CrudRepository接口公开的所有方法。让我们编写一些单元测试来测试其中一些方法。

单元测试

findById()方法可用于使用主键查询。以下代码片段显示了一个示例:

    @Test
    public void findOne() {
      Optional<Todo> todo = todoRepository.findById(101L);
      assertEquals("Todo Desc 1", todo.get().getDescription());
    }

Optional表示一个可以为 null 的对象的容器对象。Optional中的一些重要方法如下所示:

  • isPresent(): 检查Optional是否包含非空值。

  • orElse(): 如果包含的对象为空,则使用默认值。

  • ifPresent(): 如果包含的对象不为空,则执行ifPresent中的代码。

  • get(): 检索包含的对象。

existsById()方法可用于检查具有给定 ID 的实体是否存在。以下示例显示了如何执行此操作:

    @Test
    public void exists() {
      assertFalse(todoRepository.existsById(105L));
      assertTrue(todoRepository.existsById(101L));
    }

deleteById()方法用于删除具有特定 ID 的实体。在下面的例子中,我们正在删除一个todo,将可用的todos从三个减少到两个:

    @Test
    public void delete() {
      todoRepository.deleteById(101L);
      assertEquals(2,todoRepository.count());
    }

deleteAll()方法用于删除特定存储库管理的所有实体。在这个具体的例子中,todo表中的所有todos都被删除了:

    @Test
    public void deleteAll() {
      todoRepository.deleteAll();
      assertEquals(0,todoRepository.count());
    }

save()方法可用于更新或插入实体。以下示例显示了如何更新todo的描述。以下测试使用TestEntityManager在检索数据之前刷新数据。TestEntityManager是作为@DataJpaTest注解功能的一部分自动装配的:

    @Autowired
    TestEntityManager entityManager;
    @Test
    public void save() {
      Todo todo = todoRepository.findById(101L).get();
      todo.setDescription("Todo Desc Updated");
      todoRepository.save(todo);
      entityManager.flush();
      Todo updatedTodo = todoRepository.findById(101L).get();
      assertEquals("Todo Desc Updated",updatedTodo.getDescription());
     }

PagingAndSortingRepository 接口

PagingAndSortingRepository扩展了CrudRepository,并提供了以分页和指定排序机制检索实体的方法。看看下面的例子:

    public interface UserRepository 
    extends PagingAndSortingRepository<User, Long> {
      }

需要注意的重要事项如下:

  • public interface UserRepository extends PagingAndSortingRepositoryUserRepository接口扩展了PagingAndSortingRepository接口

  • <User, Long>: 实体类型为User,具有类型为Long的 ID 字段

单元测试

让我们编写一些测试来使用UserRepository的排序和分页功能。测试的基础与TodoRepositoryTest非常相似:

    @DataJpaTest
    @RunWith(SpringRunner.class)
    public class UserRepositoryTest {
      @Autowired
      UserRepository userRepository;
      @Autowired
      TestEntityManager entityManager;
    }

让我们编写一个简单的测试来对用户进行排序并将users打印到日志中:

    @Test
    public void testing_sort_stuff() {
      Sort sort = new Sort(Sort.Direction.DESC, "name")
      .and(new Sort(Sort.Direction.ASC, "userid"));
    Iterable<User> users = userRepository.findAll(sort);
    for (User user : users) {
      System.out.println(user);
     }
   }

需要注意的一些重要事项如下:

  • new Sort(Sort.Direction.DESC, "name"): 我们希望按名称降序排序。

  • and(new Sort(Sort.Direction.ASC, "userid")): and()方法是一个连接方法,用于组合不同的排序配置。在这个例子中,我们添加了按用户 ID 升序排序的次要条件。

  • userRepository.findAll(sort): 排序条件作为参数传递给findAll()方法。

前面测试的输出如下所示。用户按名称降序排序:

User [id=4, userid=UserId4, name=User Name 4, todos=0]
User [id=3, userid=UserId3, name=User Name 3, todos=0]
User [id=2, userid=UserId2, name=User Name 2, todos=1]
User [id=1, userid=UserId1, name=User Name 1, todos=2]

分页测试如下所示:

    @Test
    public void using_pageable_stuff() {
      PageRequest pageable = new PageRequest(0, 2);
      Page<User> userPage = userRepository.findAll(pageable);
      System.out.println(userPage);
      System.out.println(userPage.getContent());
    }

测试的输出如下所示:

Page 1 of 2 containing com.in28minutes.model.User instances
[User [id=1, userid=UserId1, name=User Name 1, todos=2],
User [id=2, userid=UserId2, name=User Name 2, todos=1]]

需要注意的重要事项如下:

  • new PageRequest(0, 2): 我们请求第一页(索引 0),并设置每页的大小为 2

  • userRepository.findAll(pageable): PageRequest对象作为参数发送到findAll方法

  • Page 1 of 2:输出显示我们正在查看两个页面中的第一个页面

关于PageRequest的一些重要事项如下:

  • PageRequest对象具有next()previous()first()方法来遍历页面

  • PageRequest构造函数(public PageRequest(int page, int size, Sort sort))还接受第三个参数--Sort order

Page 及其子接口 Slice 中的重要方法如下所示:

  • int getTotalPages(): 返回结果页面的数量

  • long getTotalElements(): 返回所有页面中的元素总数

  • int getNumber(): 返回当前页面的编号

  • int getNumberOfElements(): 返回当前页面中的元素数

  • List<T> getContent(): 以列表形式获取当前片段(或页面)的内容

  • boolean hasContent(): 返回当前片段是否有任何元素

  • boolean isFirst(): 返回这是否是第一个片段

  • boolean isLast(): 返回这是否是最后一个片段

  • boolean hasNext(): 返回是否有下一个片段

  • boolean hasPrevious(): 返回是否有上一个片段

  • Pageable nextPageable(): 获取下一个片段的访问权限

  • Pageable previousPageable(): 获取上一个片段的访问权限

查询方法

在前面的部分中,我们查看了CrudRepositoryPagingAndSortingRepository接口。我们查看了它们默认提供的不同方法。Spring Data 并不止于此。它定义了一些模式,允许您定义自定义查询方法。在本节中,我们将看一些 Spring Data 提供的自定义查询方法的示例选项。

我们将从与查找特定属性值匹配的行相关的示例开始。以下示例显示了按名称搜索User的不同方法:

    public interface UserRepository 
    extends PagingAndSortingRepository<User, Long> {
      List<User> findByName(String name);
      List<User> findByName(String name, Sort sort);
      List<User> findByName(String name, Pageable pageable);
      Long countByName(String name);
      Long deleteByName(String name);
      List<User> removeByName(String name);
   }

需要注意的重要事项如下:

  • List<User> findByName(String name): 模式是findBy,后跟您想要查询的属性的名称。属性的值作为参数传递。

  • List<User> findByName(String name, Sort sort): 该方法允许您指定特定的排序顺序。

  • List<User> findByName(String name, Pageable pageable): 该方法允许使用分页。

  • 除了 find,我们还可以使用 read、query 或 get 来命名方法。例如,queryByName 代替 findByName。

  • 与 find..By 类似,我们可以使用 count..By 来查找计数,并使用 delete..By(或 remove..By)来删除记录。

以下示例显示了如何按包含元素的属性进行搜索:

    List<User> findByTodosTitle(String title);

用户包含TodosTodotitle属性。要创建一个根据 todo 的标题搜索用户的方法,我们可以在UserRepository中创建一个名为findByTodosTitle的方法。

以下示例显示了使用findBy可能的一些更多变化:

    public interface TodoRepository extends CrudRepository<Todo, Long>  
    {
      List<Todo> findByTitleAndDescription
      (String title, String description);
      List<Todo> findDistinctTodoByTitleOrDescription
      (String title,String description);
      List<Todo> findByTitleIgnoreCase(String title, String
      description);
      List<Todo> findByTitleOrderByIdDesc(String lastname);
      List<Todo> findByIsDoneTrue(String lastname);
    }

需要注意的重要事项如下:

  • findByTitleAndDescription: 可以使用多个属性来查询

  • findDistinctTodoByTitleOrDescription: 查找不同的行

  • findByTitleIgnoreCase: 说明了忽略大小写的用法

  • findByTitleOrderByIdDesc: 说明了指定特定排序顺序的示例

以下示例显示了如何使用 find 查找特定记录的子集:

    public interface UserRepository 
    extends PagingAndSortingRepository<User, Long> {
      User findFirstByName(String name);
      User findTopByName(String name);
      List<User> findTop3ByName(String name);
      List<User> findFirst3ByName(String name);
   }

需要注意的重要事项如下:

  • findFirstByName, findTopByName: 查询第一个用户

  • findTop3ByName, findFirst3ByName: 查找前三个用户

查询

Spring Data JPA 还提供了编写自定义查询的选项。以下代码片段显示了一个简单的示例:

    @Query("select u from User u where u.name = ?1")
    List<User> findUsersByNameUsingQuery(String name);

需要注意的重要事项如下:

  • @Query: 用于定义存储库方法的查询的注释

  • select u from User u where u.name = ?1:要执行的查询。?1代表第一个参数

  • findUsersByNameUsingQuery:调用此方法时,将使用指定的查询和名称作为参数执行

命名参数

我们可以使用命名参数使查询更易读。下面来自 UserRepository 的代码片段显示了一个示例:

    @Query("select u from User u where u.name = :name")
    List<User> findUsersByNameUsingNamedParameters
    (@Param("name") String name);

需要注意的重要事项如下:

  • select u from User u where u.name = :name:在查询中定义了一个命名参数"name"

  • findUsersByNameUsingNamedParameters(@Param("name") String name)@Param("name")在参数列表中定义了命名参数

命名查询

是在实体本身上定义命名查询。以下示例显示了如何在

    @Entity
    @NamedQuery(name = "User.findUsersWithNameUsingNamedQuery", 
    query = "select u from User u where u.name = ?1")
    public class User {

要在存储库中使用此查询,我们需要创建一个与命名查询同名的方法。下面的代码片段显示了 UserRepository 中对应的方法:

    List<User> findUsersWithNameUsingNamedQuery(String name);

请注意,命名查询的名称是User.findUsersWithNameUsingNamedQuery。因此,存储库中的方法名称应为findUsersWithNameUsingNamedQuery

本地查询

Spring Data JPA 还提供了执行本地查询的选项。以下示例演示了在UserRepository中执行简单本地查询:

    @Query(value = "SELECT * FROM USERS WHERE u.name = ?1", 
     nativeQuery = true)
    List<User> findUsersByNameNativeQuery(String name);

需要注意的重要事项如下:

  • SELECT * FROM USERS WHERE u.name = ?1:这是要执行的本地查询。请注意,我们没有引用 User 实体,而是在查询中使用了表名 users。

  • nativeQuery = true:此属性确保查询作为本地查询执行。

Spring Data Rest

Spring Data Rest 提供了一个非常简单的选项,可以在数据存储库周围公开 CRUD RESTful 服务。

Spring Data Rest 的一些重要特性包括以下内容:

  • 围绕 Spring Data 存储库公开 REST API

  • 支持分页和过滤

  • 了解 Spring Data 存储库中的查询方法并将其公开为搜索资源

  • 支持的框架包括 JPA、MongoDB 和 Cassandra

  • 默认情况下公开了自定义资源的选项

我们将首先在pom.xml中包含 Spring Boot Data Rest starter:

    <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-rest</artifactId>
    </dependency>

我们可以通过添加一个简单的注释使UserRepository公开 REST 服务,如下面的代码片段所示:

    @RepositoryRestResource(collectionResourceRel = "users", path =
     "users")
    public interface UserRepository 
    extends PagingAndSortingRepository<User, Long> {

需要注意的重要事项如下:

  • @RepositoryRestResource:用于使用 REST 公开存储库的注释

  • collectionResourceRel = "users":在生成的链接中要使用的collectionResourceRel

  • path = "users":要公开资源的路径

当我们将SpringDataJpaFirstExampleApplication作为 Java 应用程序启动时,日志中可以看到以下内容:

s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
o.s.b.w.servlet.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto ****
o.s.d.r.w.RepositoryRestHandlerMapping : Mapped "{[/{repository}], methods=[OPTIONS]
o.s.d.r.w.RepositoryRestHandlerMapping : Mapped "{[/{repository}], methods=[HEAD]
o.s.d.r.w.RepositoryRestHandlerMapping : Mapped "{[/{repository}], methods=[GET]
o.s.d.r.w.RepositoryRestHandlerMapping : Mapped "{[/{repository}], methods=[POST]
o.s.d.r.w.RepositoryRestHandlerMapping : Mapped "{[/{repository}/{id}], methods=[OPTIONS]
o.s.d.r.w.RepositoryRestHandlerMapping : Mapped "{[/{repository}/{id}/{property}]
o.s.d.r.w.RepositoryRestHandlerMapping : Mapped "{[/{repository}/search], methods=[GET]

前面的日志显示了 Spring MVC DispatcherServlet 已启动并准备好为不同的请求方法和 URI 提供服务。

GET 方法

当我们向http://localhost:8080/users发送GET请求时,我们会得到如下所示的响应。为了简洁起见,响应已编辑以删除UserId2UserId3UserId4的详细信息:

    {
      "_embedded" : {
      "users" : [ {
                   "userid" : "UserId1",
                   "name" : "User Name 1",
                   "_links" : {
                     "self" : {
                        "href" : "http://localhost:8080/users/1"
                        },
                     "user" : {
                        "href" : "http://localhost:8080/users/1"
                       },
                    "todos" : {
                         "href" : "http://localhost:8080/users/1/todos"
                        }
                     }
               } ]
       },
      "_links" : {

         "self" : {
                  "href" : "http://localhost:8080/users"
                  },
                "profile" : {
                      "href" : "http://localhost:8080/profile/users"
                       },
                "search" : {
                      "href" : "http://localhost:8080/users/search"
             }
     },
     "page" : {
             "size" : 20,
             "totalElements" : 4,
             "totalPages" : 1,
             "number" : 0
            }
     }

POST 方法

以下屏幕截图显示了如何发送POST请求以创建新用户:

以下代码片段显示了响应:

    {
      "userid": "UserId5",
      "name": "User Name 5",
      "_links": {
       "self": {
         "href": "http://localhost:8080/users/5"
            },
      "user": {
         "href": "http://localhost:8080/users/5"
          },
      "todos": {
         "href": "http://localhost:8080/users/5/todos"
         }
       }
    }

响应包含已创建资源的 URI--http://localhost:8080/users/5

搜索资源

Spring Data Rest 公开了存储库中其他方法的搜索资源。例如,findUsersByNameUsingNamedParameters方法在http://localhost:8080/users/search/findUsersByNameUsingNamedParameters?name=User%20Name%201处公开。下面的代码片段显示了对上述 URL 发送Get请求的响应:

    {
      "_embedded": {
          "users": [
                     {
                       "userid": "UserId1",
                       "name": "User Name 1",
                       "_links": {
                         "self": {
                                "href": "http://localhost:8080/users/1"
                                },
                          "user": {
                                "href": "http://localhost:8080/users/1"
                          },
                         "todos": {
                            "href":    
     "http://localhost:8080/users/1/todos"
                          }
                        }
                     }
                  ]
               },
     "_links": {
      "self": {
          "href":"http://localhost:8080/users/search/
      findUsersByNameUsingNamedParameters?name=User%20Name%201"
        }
     }
    }

大数据

正如我们在本章介绍中讨论的,有各种数据存储库提供了传统数据库的替代方案。在过去几年中,“大数据”这个词变得很流行。虽然对于大数据没有统一的定义,但有一些共同的特征:

  • 非结构化数据:数据没有特定的结构

  • 大容量:通常比传统数据库能够处理更多的数据量,例如日志流、Facebook 帖子、推文

  • 易于扩展:通常提供水平和垂直扩展的选项

Hadoop、Cassandra 和 MongoDB 是其中受欢迎的选项。

在本节中,我们将以 MongoDB 为例,使用 Spring Data 进行连接。

MongoDB

按照docs.mongodb.org/manual/installation/上的说明在你特定的操作系统上安装 MongoDB。

要开始连接到 MongoDB,需要在pom.xml中包含 Spring Boot MongoDB starter 的依赖项:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>

让我们创建一个新的实体类Person来存储到 MongoDB。以下代码段显示了一个带有 ID 和姓名的Person类:

    public class Person {
      @Id
      private String id;
      private String name;
      public Person() {// Make JPA Happy
      }
    public Person(String name) {
      super();
      this.name = name;
     }
   }

我们希望将Person实体存储到 MongoDB。我们需要创建一个新的存储库。以下代码段显示了一个 MongoDB 存储库:

    public interface PersonMongoDbRepository 
    extends MongoRepository<Person, String> {
      List<Person> findByName(String name);
      Long countByName(String name);
    }

重要事项如下:

  • PersonMongoDbRepository extends MongoRepositoryMongoRepository是一个特定于 MongoDB 的存储库接口

  • MongoRepository<Person, String>:我们希望存储具有 String 类型键的Person实体

  • List<Person> findByName(String name):一个简单的通过姓名查找人的方法

单元测试

我们将编写一个简单的单元测试来测试这个存储库。单元测试的代码如下所示:

    @DataMongoTest
    @RunWith(SpringRunner.class)
    public class PersonMongoDbRepositoryTest {
      @Autowired
      PersonMongoDbRepository personRepository;
      @Test
      public void simpleTest(){
        personRepository.deleteAll();
        personRepository.save(new Person( "name1"));
        personRepository.save(new Person( "name2"));
        for (Person person : personRepository.findAll()) {
          System.out.println(person);
         }
        System.out.println(personRepository.findByName("name1"));
        System.out.println(personRepository.count());
       }
     }

一些重要事项如下:

  • 确保在运行测试时 MongoDB 正在运行。

  • @DataMongoTestDataMongoTest注解与SpringRunner一起用于典型的 MongoDB 单元测试。这将除了与 MongoDB 相关的内容之外,禁用自动配置。

  • @Autowired PersonMongoDbRepository personRepository:将 MongoDB 存储库自动装配到被测试的对象。

一个重要的事项是测试中的所有代码与为 Spring Data JPA 编写的代码非常相似。这个例子展示了 Spring Data 使得连接到不同类型的数据存储变得非常简单。与非关系型大数据存储交互的代码与与关系型数据库交互的代码类似。这就是 Spring Data 的魔力。

总结

Spring Boot 使得基于 Spring 的应用程序开发变得容易。Spring Data 使得连接到不同的数据存储变得容易。

我们看到了 Spring Data 如何通过简单的概念(如存储库)使得连接到不同的数据存储变得容易。我们还了解了如何将 Spring Data 与 Spring Data JPA 结合使用来连接到内存中的关系型数据库,以及如何使用 Spring Data MongoDB 来连接和保存数据到一个大数据存储,比如 MongoDB。

在下一章中,我们将把注意力转向云端。我们将学习 Spring Cloud 以及它如何解决云端的问题。

第九章:Spring Cloud

在本章中,我们将介绍与开发云原生应用程序和使用 Spring Cloud 伞下的项目实现相关的一些重要模式。我们将介绍以下功能:

  • 使用 Spring Cloud Config Server 实现集中式微服务配置

  • 使用 Spring Cloud Bus 同步微服务实例的配置

  • 使用 Feign 创建声明性 REST 客户端

  • 使用 Ribbon 实现客户端负载均衡

  • 使用 Eureka 实现名称服务器

  • 使用 Zuul 实现 API 网关

  • 使用 Spring Cloud Sleuth 和 Zipkin 实现分布式跟踪

  • 使用 Hystrix 实现容错

介绍 Spring Cloud

在第四章中,向微服务和云原生应用的演进,我们讨论了单片应用程序的问题以及架构如何演变为微服务。然而,微服务也有自己的一系列挑战:

  • 采用微服务架构的组织还需要在不影响微服务团队创新能力的情况下,就微服务的一致性做出具有挑战性的决策。

  • 更小的应用意味着更多的构建、发布和部署。通常会使用更多的自动化来解决这个问题。

  • 微服务架构是基于大量更小、细粒度服务构建的。管理这些服务的配置和可用性存在挑战。

  • 由于应用程序的分布式特性,调试问题变得更加困难。

为了从微服务架构中获得最大的好处,微服务应该是 Cloud-Native——可以轻松部署在云上。在第四章中,向微服务和云原生应用的演进,我们讨论了十二要素应用的特征——这些模式通常被认为是云原生应用中的良好实践。

Spring Cloud 旨在提供一些在构建云上系统时常见的模式的解决方案。一些重要的特性包括以下内容:

  • 管理分布式微服务配置的解决方案

  • 使用名称服务器进行服务注册和发现

  • 在多个微服务实例之间进行负载均衡

  • 使用断路器实现更具容错性的服务

  • 用于聚合、路由和缓存的 API 网关

  • 跨微服务的分布式跟踪

重要的是要理解 Spring Cloud 不是一个单一的项目。它是一组旨在解决部署在云上的应用程序所面临问题的子项目。

一些重要的 Spring Cloud 子项目如下:

  • Spring Cloud Config:实现了在不同环境下不同微服务之间的集中外部配置。

  • Spring Cloud Netflix:Netflix 是微服务架构的早期采用者之一。在 Spring Cloud Netflix 的支持下,许多内部 Netflix 项目开源了。例如 Eureka、Hystrix 和 Zuul。

  • Spring Cloud Bus:使得与轻量级消息代理集成微服务更加容易。

  • Spring Cloud Sleuth:与 Zipkin 一起,提供了分布式跟踪解决方案。

  • Spring Cloud Data Flow:提供了构建围绕微服务应用程序的编排能力。提供 DSL、GUI 和 REST API。

  • Spring Cloud Stream:提供了一个简单的声明性框架,用于将基于 Spring(和 Spring Boot)的应用程序与诸如 Apache Kafka 或 RabbitMQ 之类的消息代理集成。

Spring Cloud 伞下的所有项目都有一些共同点:

  • 它们解决了在云上开发应用程序时的一些常见问题

  • 它们与 Spring Boot 集成得很好

  • 它们通常配置简单的注解

  • 它们广泛使用自动配置

Spring Cloud Netflix

Netflix 是第一批开始从单片到微服务架构转变的组织之一。Netflix 一直非常开放地记录这一经验。一些内部 Netflix 框架在 Spring Cloud Netflix 的支持下开源。如在 Spring Cloud Netflix 网站上所定义的(cloud.spring.io/spring-cloud-netflix/):

Spring Cloud Netflix 通过自动配置和绑定到 Spring 环境以及其他 Spring 编程模型习语,为 Spring Boot 应用程序提供了 Netflix OSS 集成。

Spring Cloud Netflix 支持的一些重要项目如下:

  • Eureka: 提供微服务的服务注册和发现功能的名称服务器。

  • Hystrix: 通过断路器构建容错微服务的能力。还提供了一个仪表板。

  • Feign: 声明式 REST 客户端,使调用使用 JAX-RS 和 Spring MVC 创建的服务变得容易。

  • Ribbon: 提供客户端负载均衡能力。

  • Zuul: 提供典型的 API 网关功能,如路由、过滤、认证和安全。它可以通过自定义规则和过滤器进行扩展。

演示微服务设置

我们将使用两个微服务来演示本章的概念:

  • 微服务 A: 一个简单的微服务,公开了两个服务--一个用于从配置文件中检索消息,另一个random service提供了一个随机数列表。

  • 服务消费者微服务: 一个简单的微服务,公开了一个称为add服务的简单计算服务。add服务从微服务 A中消费了random service并将数字相加。

以下图显示了微服务之间以及公开的服务之间的关系:

让我们快速设置这些微服务。

微服务 A

让我们使用 Spring Initializr (start.spring.io)来开始使用微服务 A。选择 GroupId、ArtifactId 和框架,如下面的截图所示:

我们将创建一个服务来公开一组随机数:

    @RestController
    public class RandomNumberController {
      private Log log =
        LogFactory.getLog(RandomNumberController.class);
      @RequestMapping("/random")
      public List<Integer> random() {
        List<Integer> numbers = new ArrayList<Integer>();
        for (int i = 1; i <= 5; i++) {
          numbers.add(generateRandomNumber());
        }
        log.warn("Returning " + numbers);
        return numbers;
      }
      private int generateRandomNumber() {
        return (int) (Math.random() * 1000);
      }
    }

需要注意的一些重要事项如下:

  • @RequestMapping("/random") public List<Integer> random(): 随机服务返回一个随机数列表

  • private int generateRandomNumber() {: 生成 0 到 1000 之间的随机数

以下片段显示了从http://localhost:8080/random服务的示例响应:

    [666,257,306,204,992]

接下来,我们希望创建一个服务,从application.properties中的应用程序配置返回一个简单的消息。

让我们定义一个简单的应用程序配置,其中包含一个属性--message

    @Component
    @ConfigurationProperties("application")
    public class ApplicationConfiguration {
      private String message;
      public String getMessage() {
        return message;
      }
      public void setMessage(String message) {
        this.message = message;
      }
    }

以下是一些重要事项需要注意:

  • @ConfigurationProperties("application"): 定义了一个定义application.properties的类。

  • private String message: 定义了一个属性--message。该值可以在application.properties中使用application.message作为键进行配置。

让我们根据下面的片段配置application.properties

    spring.application.name=microservice-a
    application.message=Default Message

需要注意的一些重要事项如下:

  • spring.application.name=microservice-a: spring.application.name用于为应用程序命名

  • application.message=Default Message: 为application.message配置了默认消息

让我们创建一个控制器来读取消息并返回它,如下面的片段所示:

    @RestController
    public class MessageController {
      @Autowired
      private ApplicationConfiguration configuration;
      @RequestMapping("/message")
      public Map<String, String> welcome() {
        Map<String, String> map = new HashMap<String, String>();
        map.put("message", configuration.getMessage());
        return map;
      }
    }

需要注意的重要事项如下:

  • @Autowired private ApplicationConfiguration configuration: 自动装配ApplicationConfiguration以启用读取配置消息值。

  • @RequestMapping("/message") public Map<String, String> welcome(): 在 URI/message上公开一个简单的服务。

  • map.put("message", configuration.getMessage()):服务返回一个具有一个条目的映射。它有一个键消息,值是从ApplicationConfiguration中获取的。

当在http://localhost:8080/message执行服务时,我们得到以下响应:

    {"message":"Default Message"}

服务消费者

让我们设置另一个简单的微服务来消费微服务 A 公开的random service。让我们使用 Spring Initializr (start.spring.io)来初始化微服务,如下面的屏幕截图所示:

让我们添加消费random service的服务:

    @RestController
    public class NumberAdderController {
      private Log log = LogFactory.getLog(
        NumberAdderController.class);
      @Value("${number.service.url}")
      private String numberServiceUrl;
      @RequestMapping("/add")
      public Long add() {
        long sum = 0;
        ResponseEntity<Integer[]> responseEntity =
          new RestTemplate()
          .getForEntity(numberServiceUrl, Integer[].class);
        Integer[] numbers = responseEntity.getBody();
        for (int number : numbers) {
          sum += number;
        }
        log.warn("Returning " + sum);
        return sum;
      }
    }

需要注意的重要事项如下:

  • @Value("${number.service.url}") private String numberServiceUrl:我们希望数字服务的 URL 在应用程序属性中可配置。

  • @RequestMapping("/add") public Long add(): 在 URI/add上公开一个服务。add方法使用RestTemplate调用数字服务,并具有对返回的数字求和的逻辑。

让我们配置application.properties,如下面的片段所示:

    spring.application.name=service-consumer
    server.port=8100
    number.service.url=http://localhost:8080/random

需要注意的重要事项如下:

  • spring.application.name=service-consumer:为 Spring Boot 应用程序配置名称

  • server.port=8100:使用8100作为服务消费者的端口

  • number.service.url=http://localhost:8080/random:配置用于 add 服务的数字服务 URL

当在 URLhttp://localhost:8100/add调用服务时,将返回以下响应:

    2890

以下是微服务 A 日志的摘录:

    c.m.s.c.c.RandomNumberController : Returning [752,
      119, 493, 871, 445]

日志显示,来自微服务 A 的random service返回了5个数字。服务消费者中的add服务将它们相加并返回结果2890

我们现在有我们的示例微服务准备好了。在接下来的步骤中,我们将为这些微服务添加云原生功能。

端口

在本章中,我们将创建六个不同的微服务应用程序和组件。为了保持简单,我们将为特定应用程序使用特定的端口。

以下表格显示了我们在本章中创建的不同应用程序所保留的端口:

微服务组件 使用的端口
微服务 A 80808081
服务消费者微服务 8100
配置服务器(Spring Cloud Config) 8888
Eureka 服务器(名称服务器) 8761
Zuul API 网关服务器 8765
Zipkin 分布式跟踪服务器 9411

我们的两个微服务已经准备好了。我们准备为我们的微服务启用云功能。

集中式微服务配置

Spring Cloud Config 提供了外部化微服务配置的解决方案。让我们首先了解外部化微服务配置的需求。

问题陈述

在微服务架构中,我们通常有许多小型微服务相互交互,而不是一组大型的单片应用程序。每个微服务通常部署在多个环境中--开发、测试、负载测试、暂存和生产。此外,不同环境中可能有多个微服务实例。例如,特定的微服务可能正在处理大量负载。在生产环境中可能有多个该微服务的实例。

应用程序的配置通常包括以下内容:

  • 数据库配置:连接到数据库所需的详细信息

  • 消息代理配置:连接到 AMQP 或类似资源所需的任何配置

  • 外部服务配置:微服务需要的其他服务

  • 微服务配置:与微服务的业务逻辑相关的典型配置

每个微服务实例都可以有自己的配置--不同的数据库,不同的外部服务等。例如,如果一个微服务在五个环境中部署,并且每个环境中有四个实例,则该微服务可以拥有总共 20 个不同的配置。

以下图显示了 Microservice A 所需的典型配置。我们正在查看开发中的两个实例,QA 中的三个实例,阶段中的一个实例以及生产中的四个实例:

解决方案

为不同的微服务单独维护配置会使运维团队难以处理。如下图所示的解决方案是创建一个集中式配置服务器

集中式配置服务器保存了所有不同微服务的配置。这有助于将配置与应用程序部署分开。

相同的可部署文件(EAR 或 WAR)可以在不同的环境中使用。但是,所有配置(在不同环境之间变化的内容)将存储在集中式配置服务器中。

需要做出的一个重要决定是决定是否为不同的环境有单独的集中配置服务器实例。通常,您希望对生产配置的访问比其他环境更受限制。至少,我们建议为生产环境使用单独的集中配置服务器。其他环境可以共享一个配置服务器实例。

选项

以下截图显示了 Spring Initializer 提供的 Cloud Config Servers 选项:

在本章中,我们将使用 Spring Cloud Config 配置 Cloud Config Server。

Spring Cloud Config

Spring Cloud Config 提供了对集中式微服务配置的支持。它是两个重要组件的组合:

  • Spring Cloud Config Server:提供支持,通过版本控制仓库(GIT 或子版本)公开集中配置

  • Spring Cloud Config Client:提供应用连接到 Spring Cloud Config Server 的支持

以下图显示了使用 Spring Cloud Config 的典型微服务架构。多个微服务的配置存储在单个GIT仓库中:

实现 Spring Cloud Config Server

以下图显示了使用 Spring Cloud Config 更新 Microservice A 和服务消费者的实现。在下图中,我们将 Microservice A 与 Spring Cloud Config 集成,以从本地 Git 仓库中检索其配置:

实现 Spring Cloud Config 需要以下内容:

  1. 设置 Spring Cloud Config 服务器。

  2. 设置本地 Git 仓库并将其连接到 Spring Cloud Config 服务器。

  3. 更新 Microservice A 以使用来自 Cloud Config Server 的配置--使用 Spring Cloud Config Client。

设置 Spring Cloud Config Server

让我们使用 Spring Initializr(start.spring.io)设置 Cloud Config Server。以下截图显示了要选择的 GroupId 和 ArtifactId。确保选择 Config Server 作为依赖项:

如果要将 Config Server 添加到现有应用程序中,请使用此处显示的依赖项:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-config-server</artifactId>
    </dependency>

项目创建后,第一步是添加EnableConfigServer注解。以下代码片段显示了将注解添加到ConfigServerApplication中:

    @EnableConfigServer
    @SpringBootApplication
    public class ConfigServerApplication {

将 Spring Cloud Config Server 连接到本地 Git 仓库

配置服务器需要连接到一个 Git 存储库。为了保持简单,让我们连接到一个本地 Git 存储库。

您可以从git-scm.com为您的特定操作系统安装 Git。

以下命令可帮助您设置一个简单的本地 Git 存储库。

安装 Git 后切换到您选择的目录。在终端或命令提示符上执行以下命令:

mkdir git-localconfig-repo
cd git-localconfig-repo
git init

git-localconfig-repo文件夹中创建一个名为microservice-a.properties的文件,内容如下:

    management.security.enabled=false
    application.message=Message From Default Local Git Repository

执行以下命令将microservice-a.properties添加并提交到本地 Git 存储库:

git add -A
git commit -m "default microservice a properties"

现在我们已经准备好了具有我们配置的本地 Git 存储库,我们需要将配置服务器连接到它。让我们按照这里所示配置config-server中的application.properties

    spring.application.name=config-server
    server.port=8888
    spring.cloud.config.server.git.uri=file:///in28Minutes
    /Books/MasteringSpring/git-localconfig-repo

一些重要的事项如下:

  • server.port=8888:配置配置服务器的端口。8888通常是配置服务器最常用的端口。

  • spring.cloud.config.server.git.uri=file:///in28Minutes/Books/MasteringSpring/git-localconfig-repo:配置到本地 Git 存储库的 URI。如果要连接到远程 Git 存储库,可以在这里配置 Git 存储库的 URI。

启动服务器。当您访问 URLhttp://localhost:8888/microservice-a/default时,您将看到以下响应:

    {  
      "name":"microservice-a",
      "profiles":[  
        "default"
       ],
       "label":null,
       "version":null,
       "state":null,
       "propertySources":[  
        {  
          "name":"file:///in28Minutes/Books/MasteringSpring
          /git-localconfig-repo/microservice-a.properties",
          "source":{  
            "application.message":"Message From Default
             Local Git Repository"
          }
        }]
    }

一些重要的事项如下:

  • http://localhost:8888/microservice-a/default:URI 格式为/{application-name}/{profile}[/{label}]。这里,application-namemicroservice-a,配置文件是default

  • 由于我们使用默认配置文件,该服务将从microservice-a.properties返回配置。您可以在propertySources>name字段的响应中看到它。

  • "source":{"application.message":"Message From Default Local Git Repository"}:响应的内容是属性文件的内容。

创建特定于环境的配置

让我们为dev环境为 Microservice A 创建一个特定的配置。

git-localconfig-repo中创建一个名为microservice-a-dev.properties的新文件,内容如下:

application.message=Message From Dev Git Repository

执行以下命令将microservice-a-dev.properties添加并提交到本地 Git 存储库:

git add -A
git commit -m "default microservice a properties" 

当您访问 URLhttp://localhost:8888/microservice-a/dev时,您将看到以下响应:

    {  
      "name":"microservice-a",
      "profiles":[  
        "dev"
      ],
      "label":null,
      "version":null,
      "state":null,
      "propertySources":[  
      {  
        "name":"file:///in28Minutes/Books/MasteringSpring
         /git-localconfig-repo/microservice-a-dev.properties",
        "source":{  
          "application.message":"Message From Dev Git Repository"
        }
      },
      {  
      "name":"file:///in28Minutes/Books/MasteringSpring
        /git-localconfig-repo/microservice-a.properties",
      "source":{  
        "application.message":"Message From Default
         Local Git Repository"
      }}]
    }

响应包含来自microservice-a-dev.propertiesdev配置。还返回了默认属性文件(microservice-a.properties)中的配置。在microservice-a-dev.properties中配置的属性(特定于环境的属性)优先级高于在microservice-a.properties中配置的默认属性。

类似于dev,可以为不同的环境创建 Microservice A 的单独配置。如果在单个环境中需要多个实例,可以使用标签进行区分。可以使用格式为http://localhost:8888/microservice-a/dev/{tag}的 URL 来根据特定标签检索配置。

下一步是将 Microservice A 连接到配置服务器。

Spring Cloud 配置客户端

我们将使用 Spring Cloud 配置客户端将Microservice A连接到配置服务器。依赖项如下所示。将以下代码添加到Microservice Apom.xml文件中:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>

Spring Cloud 的依赖项与 Spring Boot 的管理方式不同。我们将使用依赖项管理来管理依赖项。以下代码段将确保使用所有 Spring Cloud 依赖项的正确版本:

    <dependencyManagement>
       <dependencies>
          <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-dependencies</artifactId>
             <version>Dalston.RC1</version>
             <type>pom</type>
             <scope>import</scope>
          </dependency>
       </dependencies>
    </dependencyManagement>

Microservice A中的application.properties重命名为bootstrap.properties

按照这里所示进行配置:

    spring.application.name=microservice-a
    spring.cloud.config.uri=http://localhost:8888

由于我们希望微服务 A连接到Config Server,因此我们使用spring.cloud.config.uri提供Config Server的 URI。 Cloud Config Server 用于检索微服务 A 的配置。因此,配置在bootstrap.properties中提供。

Spring Cloud Context:Spring Cloud 为部署在云中的 Spring 应用程序引入了一些重要概念。引导应用程序上下文是一个重要概念。它是微服务应用程序的父上下文。它负责加载外部配置(例如,来自 Spring Cloud Config Server)和解密配置文件(外部和本地)。引导上下文使用 bootstrap.yml 或 bootstrap.properties 进行配置。我们之前必须将 application.properties 的名称更改为 Microservice A 中的 bootstrap.properties,因为我们希望 Microservice A 使用 Config Server 进行引导。

Microservice A 重新启动时日志中的提取如下所示:

    Fetching config from server at: http://localhost:8888
    Located environment: name=microservice-a, profiles=[default],
    label=null, version=null, state=null
    Located property source: CompositePropertySource 
    [name='configService', propertySources=[MapPropertySource
    [name='file:///in28Minutes/Books/MasteringSpring/git-localconfig-
    repo/microservice-a.properties']]]

微服务 A服务正在使用来自Spring Config Server的配置,地址为http://localhost:8888

当调用http://localhost:8080/message上的消息服务时,以下是响应:

    {"message":"Message From Default Local Git Repository"}

消息是从localconfig-repo/microservice-a.properties文件中提取的。

您可以将活动配置设置为dev以获取 dev 配置:

    spring.profiles.active=dev

服务消费者微服务的配置也可以存储在local-config-repo中,并使用 Spring Config Server 公开。

Spring Cloud Bus

Spring Cloud Bus 使得将微服务连接到轻量级消息代理(如 Kafka 和 RabbitMQ)变得轻松。

Spring Cloud Bus 的需求

考虑一个在微服务中进行配置更改的例子。假设在生产环境中有五个运行中的微服务 A实例。我们需要进行紧急配置更改。例如,让我们在localconfig-repo/microservice-a.properties中进行更改:

    application.message=Message From Default Local 
      Git Repository Changed

为了使微服务 A获取此配置更改,我们需要在http://localhost:8080/refresh上调用POST请求。可以在命令提示符处执行以下命令以发送POST请求:

curl -X POST http://localhost:8080/refresh

您将在http://localhost:8080/message看到配置更改的反映。以下是服务的响应:

    {"message":"Message From Default Local Git Repository Changed"}

我们有五个运行中的 Microservice A 实例。配置更改仅对执行 URL 的 Microservice A 实例反映。其他四个实例在执行刷新请求之前将不会接收配置更改。

如果有多个微服务实例,则对每个实例执行刷新 URL 变得很麻烦,因为您需要对每个配置更改执行此操作。

使用 Spring Cloud Bus 传播配置更改

解决方案是使用 Spring Cloud Bus 通过消息代理(如 RabbitMQ)向多个实例传播配置更改。

以下图显示了不同实例的微服务(实际上,它们也可以是完全不同的微服务)如何使用 Spring Cloud Bus 连接到消息代理:

每个微服务实例将在应用程序启动时向 Spring Cloud Bus 注册。

当刷新调用一个微服务实例时,Spring Cloud Bus 将向所有微服务实例传播更改事件。微服务实例在接收更改事件时将从配置服务器请求更新的配置。

实施

我们将使用 RabbitMQ 作为消息代理。在继续之前,请确保已安装并启动了 RabbitMQ。

RabbitMQ 的安装说明请参见www.rabbitmq.com/download.html

下一步是为Microservice A添加与 Spring Cloud Bus 的连接。让我们在 Microservice A 的pom.xml文件中添加以下依赖项:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>

我们可以通过将端口作为启动 VM 参数之一来在不同端口上运行Microservice A。以下屏幕截图显示了如何在 Eclipse 中将服务器端口配置为 VM 参数。配置的值为-Dserver.port=8081

我们将在端口8080(默认)和8081上运行 Microservice A。以下是在重新启动 Microservice A 时日志的摘录:

o.s.integration.channel.DirectChannel : Channel 'microservice-a.springCloudBusInput' has 1 subscriber(s).
Bean with name 'rabbitConnectionFactory' has been autodetected for JMX exposure
Bean with name 'refreshBusEndpoint' has been autodetected for JMX exposure
Created new connection: SimpleConnection@6d12ea7c [delegate=amqp://guest@127.0.0.1:5672/, localPort= 61741]
Channel 'microservice-a.springCloudBusOutput' has 1 subscriber(s).
 declaring queue for inbound: springCloudBus.anonymous.HK-dFv8oRwGrhD4BvuhkFQ, bound to: springCloudBus
Adding {message-handler:inbound.springCloudBus.default} as a subscriber to the 'bridge.springCloudBus' channel

所有Microservice A的实例都已在Spring Cloud Bus中注册,并监听 Cloud Bus 上的事件。RabbitMQ 连接的默认配置是自动配置的魔术结果。

现在让我们更新microservice-a.properties中的新消息:

    application.message=Message From Default Local
      Git Repository Changed Again

提交文件并发送请求以刷新其中一个实例的配置,比如端口8080,使用 URLhttp://localhost:8080/bus/refresh

    curl -X POST http://localhost:8080/bus/refresh

以下是运行在端口8081上的第二个Microservice A实例的日志摘录:

Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@510cb933: startup date [Mon Mar 27 21:39:37 IST 2017]; root of context hierarchy
Fetching config from server at: http://localhost:8888
Started application in 1.333 seconds (JVM running for 762.806)
Received remote refresh request. Keys refreshed [application.message]

您可以看到,即使刷新 URL 未在端口8081上调用,更新的消息仍然从配置服务器中获取。这是因为 Microservice A 的所有实例都在 Spring Cloud Bus 上监听更改事件。一旦在其中一个实例上调用刷新 URL,它就会触发更改事件,所有其他实例都会获取更改后的配置。

您将看到配置更改反映在 Microservice A 的两个实例中,分别是http://localhost:8080/messagehttp://localhost:8081/message。以下是服务的响应:

    {"message":"Message From Default Local 
      Git Repository Changed Again"}

声明式 REST 客户端 - Feign

Feign 帮助我们使用最少的配置和代码创建 REST 服务的 REST 客户端。您只需要定义一个简单的接口并使用适当的注释。

RestTemplate通常用于进行 REST 服务调用。Feign 帮助我们编写 REST 客户端,而无需RestTemplate和围绕它的逻辑。

Feign 与 Ribbon(客户端负载平衡)和 Eureka(名称服务器)很好地集成。我们将在本章后面看到这种集成。

要使用 Feign,让我们将 Feign starter 添加到服务消费者微服务的pom.xml文件中:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-feign</artifactId>
    </dependency>

我们需要将 Spring Cloud 的dependencyManagement添加到pom.xml文件中,因为这是服务消费者微服务使用的第一个 Cloud 依赖项:

    <dependencyManagement>
       <dependencies>
         <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-dependencies</artifactId>
           <version>Dalston.RC1</version>
           <type>pom</type>
           <scope>import</scope>
         </dependency>
       </dependencies>
    </dependencyManagement>

下一步是添加注释以启用对ServiceConsumerApplication中 Feign 客户端的扫描。以下代码片段显示了@EnableFeignClients注释的用法:

    @EnableFeignClients("com.mastering.spring.consumer")
    public class ServiceConsumerApplication {

我们需要定义一个简单的接口来创建一个random service的 Feign 客户端。以下代码片段显示了详细信息:

    @FeignClient(name ="microservice-a", url="localhost:8080")
    public interface RandomServiceProxy {
      @RequestMapping(value = "/random", method = RequestMethod.GET)
      public List<Integer> getRandomNumbers();
    }

需要注意的一些重要事项如下:

  • @FeignClient(name ="microservice-a", url="localhost:8080"): FeignClient注解用于声明需要创建具有给定接口的 REST 客户端。我们现在正在硬编码Microservice A的 URL。稍后,我们将看看如何将其连接到名称服务器并消除硬编码的需要。

  • @RequestMapping(value = "/random", method = RequestMethod.GET): 此特定的 GET 服务方法在 URI/random上公开。

  • public List<Integer> getRandomNumbers(): 这定义了服务方法的接口。

让我们更新NumberAdderController以使用RandomServiceProxy来调用服务。以下代码片段显示了重要细节:

    @RestController
    public class NumberAdderController {
      @Autowired
      private RandomServiceProxy randomServiceProxy;
      @RequestMapping("/add")
      public Long add() {
        long sum = 0;
        List<Integer> numbers = randomServiceProxy.getRandomNumbers();
        for (int number : numbers) {
          sum += number;
         }
          return sum;
        }
    }

需要注意的一些重要事项如下:

  • @Autowired private RandomServiceProxy randomServiceProxy: RandomServiceProxy被自动装配。

  • List<Integer> numbers = randomServiceProxy.getRandomNumbers(): 看看使用 Feign 客户端是多么简单。不再需要使用RestTemplate

当我们在服务消费者微服务中调用add服务时,您将获得以下响应:

    2103

可以通过配置来启用 Feign 请求的 GZIP 压缩,如下所示:

    feign.compression.request.enabled=true
    feign.compression.response.enabled=true

负载均衡

微服务是云原生架构中最重要的构建模块。微服务实例根据特定微服务的负载进行扩展和缩减。我们如何确保负载在不同微服务实例之间均匀分布?这就是负载均衡的魔力所在。负载均衡对于确保负载在不同微服务实例之间均匀分布至关重要。

Ribbon

如下图所示,Spring Cloud Netflix Ribbon 提供了客户端负载均衡,使用轮询执行在不同微服务实例之间。

实施

我们将在服务消费者微服务中添加 Ribbon。服务消费者微服务将在两个微服务 A实例之间分发负载。

让我们从在服务消费者微服务的pom.xml文件中添加 Ribbon 依赖开始:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-ribbon</artifactId>
    </dependency>

接下来,我们可以配置不同微服务 A实例的 URL。在服务消费者微服务的application.properties中添加以下配置:

    random-proxy.ribbon.listOfServers= 
      http://localhost:8080,http://localhost:8081

然后我们将在服务代理RandomServiceProxy上指定@RibbonClient注解。@RibbonClient注解用于指定 ribbon 客户端的声明性配置:

    @FeignClient(name ="microservice-a")
    @RibbonClient(name="microservice-a")
    public interface RandomServiceProxy {

当您重新启动服务消费者微服务并访问http://localhost:8100/add上的添加服务时,您将获得以下响应:

    2705

这个请求由运行在端口8080上的微服务 A实例处理,日志中显示了一部分内容:

    c.m.s.c.c.RandomNumberController : Returning [487,
      441, 407, 563, 807]

当我们再次在相同的 URLhttp://localhost:8100/add上访问添加服务时,我们会得到以下响应:

    3423

然而,这次请求由运行在端口8081上的微服务 A实例处理。日志中显示了一部分内容:

    c.m.s.c.c.RandomNumberController : Returning [661,
      520, 256, 988, 998]

我们现在已经成功地将负载分布在不同的微服务 A实例之间。虽然这还有待进一步改进,但这是一个很好的开始。

虽然轮询(RoundRobinRule)是 Ribbon 使用的默认算法,但还有其他选项可用:

  • AvailabilityFilteringRule将跳过宕机的服务器和具有大量并发连接的服务器。

  • WeightedResponseTimeRule将根据响应时间选择服务器。如果服务器响应时间长,它将获得更少的请求。

可以在应用程序配置中指定要使用的算法:

    microservice-a.ribbon.NFLoadBalancerRuleClassName = 
      com.netflix.loadbalancer.WeightedResponseTimeRule

microservice-a是我们在@RibbonClient(name="microservice-a")注解中指定的服务名称。

以下图显示了我们已经设置的组件的架构:

名称服务器

微服务架构涉及许多较小的微服务相互交互。除此之外,每个微服务可能有多个实例。手动维护外部服务连接和配置将会很困难,因为新的微服务实例是动态创建和销毁的。名称服务器提供了服务注册和服务发现的功能。名称服务器允许微服务注册自己,并发现它们想要与之交互的其他微服务的 URL。

硬编码微服务 URL 的限制

在前面的例子中,我们在服务消费者微服务的application.properties中添加了以下配置:

    random-proxy.ribbon.listOfServers=
      http://localhost:8080,http://localhost:8081

这个配置代表了所有微服务 A的实例。看看这些情况:

  • 创建了一个新的微服务 A实例

  • 现有的微服务 A实例不再可用

  • 微服务 A被移动到不同的服务器

在所有这些实例中,需要更新配置并刷新微服务以获取更改。

名称服务器的工作原理

名称服务器是前述情况的理想解决方案。以下图表显示了名称服务器的工作原理:

所有微服务(不同的微服务及其所有实例)将在每个微服务启动时注册到名称服务器。当服务消费者想要获取特定微服务的位置时,它会请求名称服务器。

为每个微服务分配一个唯一的微服务 ID。这将用作注册请求和查找请求中的键。

微服务可以自动注册和注销。每当服务消费者使用微服务 ID 查找名称服务器时,它将获得该特定微服务实例的列表。

选项

以下截图显示了 Spring Initializr(start.spring.io)中用于服务发现的不同选项:

我们将在示例中使用 Eureka 作为服务发现的名称服务器。

实施

我们示例中 Eureka 的实现涉及以下内容:

  1. 设置Eureka Server

  2. 更新“微服务 A”实例以注册到Eureka Server

  3. 更新服务消费者微服务以使用 Eureka Server 中注册的“微服务 A”实例。

设置 Eureka Server

我们将使用 Spring Initializr(start.spring.io)为 Eureka Server 设置一个新项目。以下截图显示了要选择的 GroupId、ArtifactId 和 Dependencies:

下一步是将EnableEurekaServer注解添加到SpringBootApplication类中。以下片段显示了详细信息:

    @SpringBootApplication
    @EnableEurekaServer
    public class EurekaServerApplication {

以下片段显示了application.properties中的配置:

    server.port = 8761
    eureka.client.registerWithEureka=false
    eureka.client.fetchRegistry=false

我们正在使用端口8761作为Eureka Naming Server。启动EurekaServerApplication

Eureka 仪表板的截图在http://localhost:8761中显示如下:

目前,没有应用程序注册到 Eureka。在下一步中,让我们注册“微服务 A”和其他服务到 Eureka。

使用 Eureka 注册微服务

要将任何微服务注册到 Eureka 名称服务器,我们需要在 Eureka Starter 项目中添加依赖项。需要将以下依赖项添加到“Microservice A”的pom.xml文件中:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>

下一步是将EnableDiscoveryClient添加到SpringBootApplication类中。这里显示了MicroserviceAApplication的示例:

    @SpringBootApplication
    @EnableDiscoveryClient
    public class MicroserviceAApplication {

Spring Cloud Commons 托管了在不同 Spring Cloud 实现中使用的公共类。一个很好的例子是@EnableDiscoveryClient注解。Spring Cloud Netflix Eureka、Spring Cloud Consul Discovery 和 Spring Cloud Zookeeper Discovery 提供了不同的实现。

我们将在应用程序配置中配置命名服务器的 URL。对于 Microservice A,应用程序配置在本地 Git 存储库文件git-localconfig-repomicroservice-a.properties中:

    eureka.client.serviceUrl.defaultZone=
      http://localhost:8761/eureka

当两个“微服务 A”的实例都重新启动时,您将在Eureka Server的日志中看到以下消息:

    Registered instance MICROSERVICE-A/192.168.1.5:microservice-a
      with status UP (replication=false)
    Registered instance MICROSERVICE-A/192.168.1.5:microservice-a:
      8081 with status UP (replication=false)

Eureka 仪表板的截图在http://localhost:8761中显示如下:

现在有两个“微服务 A”的实例已经注册到Eureka Server中。类似的更新也可以在Config Server上进行,以便将其连接到Eureka Server

在下一步中,我们希望连接服务消费者微服务,以从 Eureka 服务器中获取“微服务 A”的实例的 URL。

将服务消费者微服务连接到 Eureka

需要将 Eureka starter 项目添加为服务消费者微服务的pom.xml文件中的依赖项:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>

目前,“微服务 A”的不同实例的 URL 在服务消费者微服务中是硬编码的,如下所示,在application.properties中:

    microservice-a.ribbon.listOfServers=
      http://localhost:8080,http://localhost:8081

然而,现在我们不想硬编码微服务 A 的 URL。我们希望服务消费者微服务从Eureka Server获取 URL。我们通过在服务消费者微服务的application.properties中配置Eureka Server的 URL 来实现这一点。我们将注释掉对微服务 A URL 的硬编码:

    #microservice-a.ribbon.listOfServers=
      http://localhost:8080,http://localhost:8081
    eureka.client.serviceUrl.defaultZone=
      http://localhost:8761/eureka

接下来,我们将在ServiceConsumerApplication类上添加EnableDiscoveryClient,如下所示:

    @SpringBootApplication
    @EnableFeignClients("com.mastering.spring.consumer")
    @EnableDiscoveryClient
    public class ServiceConsumerApplication {

一旦服务消费者微服务重新启动,您将看到它会在Eureka Server中注册自己。以下是从Eureka Server日志中提取的内容:

    Registered instance SERVICE-CONSUMER/192.168.1.5:
      service-consumer:8100 with status UP (replication=false)

RandomServiceProxy中,我们已经在 Feign 客户端上为microservice-a配置了一个名称,如下所示:

    @FeignClient(name ="microservice-a")
    @RibbonClient(name="microservice-a")
    public interface RandomServiceProxy {

服务消费者微服务将使用此 ID(微服务 A)查询Eureka Server以获取实例。一旦从Eureka Service获取 URL,它将调用 Ribbon 选择的服务实例。

当在http://localhost:8100/add调用add服务时,它会返回适当的响应。

以下是涉及的不同步骤的快速回顾:

  1. 每个微服务 A 实例启动时,都会向Eureka Name Server注册。

  2. 服务消费者微服务请求Eureka Name Server获取微服务 A 的实例。

  3. 服务消费者微服务使用 Ribbon 客户端负载均衡器来决定调用微服务 A 的特定实例。

  4. 服务消费者微服务调用特定实例的微服务 A。

Eureka Service的最大优势是服务消费者微服务现在与微服务 A 解耦。每当新的微服务 A 实例启动或现有实例关闭时,服务消费者微服务无需重新配置。

API 网关

微服务有许多横切关注点:

  • 认证、授权和安全:我们如何确保微服务消费者是他们声称的人?我们如何确保消费者对微服务有正确的访问权限?

  • 速率限制:消费者可能有不同类型的 API 计划,每个计划的限制(微服务调用次数)也可能不同。我们如何对特定消费者强制执行限制?

  • 动态路由:特定情况(例如,一个微服务宕机)可能需要动态路由。

  • 服务聚合:移动设备的 UI 需求与桌面设备不同。一些微服务架构具有针对特定设备定制的服务聚合器。

  • 容错性:我们如何确保一个微服务的失败不会导致整个系统崩溃?

当微服务直接相互通信时,这些问题必须由各个微服务单独解决。这种架构可能难以维护,因为每个微服务可能以不同的方式处理这些问题。

最常见的解决方案之一是使用 API 网关。所有对微服务的服务调用都应该通过 API 网关进行。API 网关通常为微服务提供以下功能:

  • 认证和安全

  • 速率限制

  • 洞察和监控

  • 动态路由和静态响应处理

  • 负载限制

  • 聚合多个服务的响应

使用 Zuul 实现客户端负载平衡

Zuul 是 Spring Cloud Netflix 项目的一部分。它是一个 API 网关服务,提供动态路由、监控、过滤、安全等功能。

实现 Zuul 作为 API 网关涉及以下内容:

  1. 设置新的 Zuul API 网关服务器。

  2. 配置服务消费者以使用 Zuul API 网关。

设置新的 Zuul API 网关服务器

我们将使用 Spring Initializr(start.spring.io)为 Zuul API 网关设置一个新项目。以下屏幕截图显示了要选择的 GroupId、ArtifactId 和 Dependencies:

下一步是在 Spring Boot 应用程序上启用 Zuul 代理。这是通过在ZuulApiGatewayServerApplication类上添加@EnableZuulProxy注解来完成的。以下代码片段显示了详细信息:

    @EnableZuulProxy
    @EnableDiscoveryClient
    @SpringBootApplication
    public class ZuulApiGatewayServerApplication {

我们将在端口8765上运行 Zuul 代理。以下代码片段显示了application.properties中所需的配置:

    spring.application.name=zuul-api-gateway
    server.port=8765
    eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka

我们正在配置 Zuul 代理的端口,并将其连接到 Eureka Name 服务器。

Zuul 自定义过滤器

Zuul 提供了创建自定义过滤器以实现典型 API 网关功能(如身份验证、安全性和跟踪)的选项。在本例中,我们将创建一个简单的日志记录过滤器来记录每个请求。以下代码片段显示了详细信息:

    @Component
    public class SimpleLoggingFilter extends ZuulFilter {
      private static Logger log = 
        LoggerFactory.getLogger(SimpleLoggingFilter.class);
      @Override
      public String filterType() {
        return "pre";
      }
      @Override
      public int filterOrder() {
        return 1;
      }
      @Override
      public boolean shouldFilter() {
        return true;
      }
      @Override
      public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest httpRequest = context.getRequest();
        log.info(String.format("Request Method : %s n URL: %s", 
        httpRequest.getMethod(),
        httpRequest.getRequestURL().toString()));
        return null;
      }
    }

需要注意的一些重要事项如下:

  • SimpleLoggingFilter extends ZuulFilter: ZuulFilter是创建 Zuul 过滤器的基本抽象类。任何过滤器都应实现此处列出的四种方法。

  • public String filterType(): 可能的返回值是"pre"表示预路由过滤,"route"表示路由到原始位置,"post"表示后路由过滤,"error"表示错误处理。在本例中,我们希望在执行请求之前进行过滤。我们返回值"pre"

  • public int filterOrder(): 定义过滤器的优先级。

  • public boolean shouldFilter(): 如果过滤器只应在某些条件下执行,可以在此处实现逻辑。如果要求过滤器始终执行,则返回true

  • public Object run(): 实现过滤器逻辑的方法。在我们的示例中,我们正在记录请求方法和请求的 URL。

当我们通过启动ZuulApiGatewayServerApplication作为 Java 应用程序来启动 Zuul 服务器时,您将在Eureka Name Server中看到以下日志:

    Registered instance ZUUL-API-GATEWAY/192.168.1.5:zuul-api-
      gateway:8765 with status UP (replication=false)

这表明Zuul API 网关正在运行。Zuul API 网关也已注册到Eureka Server。这允许微服务消费者与名称服务器通信,以获取有关Zuul API 网关的详细信息。

以下图显示了http://localhost:8761上的 Eureka 仪表板。您可以看到Microservice Aservice consumerZuul API Gateway的实例现在已注册到Eureka Server

以下是从Zuul API 网关日志中提取的内容:

    Mapped URL path [/microservice-a/**] onto handler of type [
    class org.springframework.cloud.netflix.zuul.web.ZuulController]
    Mapped URL path [/service-consumer/**] onto handler of type [
    class org.springframework.cloud.netflix.zuul.web.ZuulController]

默认情况下,Zuul 会为 Microservice A 中的所有服务和服务消费者微服务启用反向代理。

通过 Zuul 调用微服务

现在让我们通过服务代理调用random service。随机微服务的直接 URL 是http://localhost:8080/random。这是由应用程序名称为microservice-a的 Microservice A 公开的。

通过Zuul API Gateway调用服务的 URL 结构是http://localhost:{port}/{microservice-application-name}/{service-uri}。因此,random serviceZuul API Gateway URL 是http://localhost:8765/microservice-a/random。当您通过 API Gateway 调用random service时,您会得到下面显示的响应。响应类似于直接调用 random service 时通常会得到的响应:

    [73,671,339,354,211]

以下是从Zuul Api Gateway日志中提取的内容。您可以看到我们在Zuul API Gateway中创建的SimpleLoggingFilter已被执行:

    c.m.s.z.filters.pre.SimpleLoggingFilter : Request Method : GET
    URL: http://localhost:8765/microservice-a/random

add服务由服务消费者公开,其应用程序名称为 service-consumer,服务 URI 为/add。因此,通过 API Gateway 执行add服务的 URL 是http://localhost:8765/service-consumer/add。来自服务的响应如下所示。响应类似于直接调用add服务时通常会得到的响应:

    2488

以下是从Zuul API Gateway日志中提取的内容。您可以看到初始的add服务调用是通过 API 网关进行的:

    2017-03-28 14:05:17.514 INFO 83147 --- [nio-8765-exec-1] 
    c.m.s.z.filters.pre.SimpleLoggingFilter : Request Method : GET
    URL: http://localhost:8765/service-consumer/add

add服务调用Microservice A上的random service。虽然对 add 服务的初始调用通过 API 网关进行,但从 add 服务(服务消费者微服务)到random service(Microservice A)的调用并未通过 API 网关路由。在理想情况下,我们希望所有通信都通过 API 网关进行。

在下一步中,让我们也让服务消费者微服务的请求通过 API 网关进行。

配置服务消费者以使用 Zuul API 网关

以下代码显示了RandomServiceProxy的现有配置,用于调用Microservice A上的random service@FeignClient注解中的 name 属性配置为使用 Microservice A 的应用名称。请求映射使用了/random URI:

    @FeignClient(name ="microservice-a")
    @RibbonClient(name="microservice-a")
    public interface RandomServiceProxy {
    @RequestMapping(value = "/random", method = RequestMethod.GET)
      public List<Integer> getRandomNumbers();
    }

现在,我们希望调用通过 API 网关进行。我们需要使用 API 网关的应用名称和random service的新 URI 在请求映射中。以下片段显示了更新的RandomServiceProxy类:

    @FeignClient(name="zuul-api-gateway")
    //@FeignClient(name ="microservice-a")
    @RibbonClient(name="microservice-a")
    public interface RandomServiceProxy {
      @RequestMapping(value = "/microservice-a/random", 
      method = RequestMethod.GET)
      //@RequestMapping(value = "/random", method = RequestMethod.GET)
      public List<Integer> getRandomNumbers();
    }

当我们在http://localhost:8765/service-consumer/add调用 add 服务时,我们将看到典型的响应:

    2254

然而,现在我们将在Zuul API 网关上看到更多的事情发生。以下是从Zuul API 网关日志中提取的内容。您可以看到服务消费者上的初始 add 服务调用,以及对 Microservice A 上的random service的调用,现在都通过 API 网关进行路由:

2017-03-28 14:10:16.093 INFO 83147 --- [nio-8765-exec-4] c.m.s.z.filters.pre.SimpleLoggingFilter : Request Method : GET
URL: http://localhost:8765/service-consumer/add
2017-03-28 14:10:16.685 INFO 83147 --- [nio-8765-exec-5] c.m.s.z.filters.pre.SimpleLoggingFilter : Request Method : GET
URL: http://192.168.1.5:8765/microservice-a/random

我们看到了在Zuul API Gateway上实现简单日志过滤器的基本实现。类似的方法可以用于实现其他横切关注点的过滤器。

分布式跟踪

在典型的微服务架构中,涉及许多组件。以下是其中一些:

  • 不同的微服务

  • API 网关

  • 命名服务器

  • 配置服务器

典型的调用可能涉及四五个以上的组件。这些是需要问的重要问题:

  • 我们如何调试问题?

  • 我们如何找出特定问题的根本原因?

典型的解决方案是具有仪表板的集中式日志记录。将所有微服务日志汇总到一个地方,并在其上提供仪表板。

分布式跟踪选项

以下截图显示了 Spring Initializr 网站上分布式跟踪的选项:

在这个例子中,我们将使用 Spring Cloud Sleuth 和 Zipkin Server 的组合来实现分布式跟踪。

实现 Spring Cloud Sleuth 和 Zipkin

Spring Cloud Sleuth提供了在不同微服务组件之间唯一跟踪服务调用的功能。Zipkin是一个分布式跟踪系统,用于收集微服务中需要用于排除延迟问题的数据。我们将实现 Spring Cloud Sleuth 和 Zipkin 的组合来实现分布式跟踪。

涉及的步骤如下:

  1. 将 Microservice A、API 网关和服务消费者与 Spring Cloud Sleuth 集成。

  2. 设置 Zipkin 分布式跟踪服务器。

  3. 将 Microservice A、API 网关和服务消费者与 Zipkin 集成。

将微服务组件与 Spring Cloud Sleuth 集成

当我们在服务消费者上调用 add 服务时,它将通过 API 网关调用 Microservice A。为了能够跟踪服务调用跨不同组件,我们需要为请求流程分配一个唯一的东西。

Spring Cloud Sleuth 提供了跟踪服务调用跨不同组件的选项,使用了一个称为span的概念。每个 span 都有一个唯一的 64 位 ID。唯一 ID 可用于跟踪调用跨组件的情况。

以下片段显示了spring-cloud-starter-sleuth的依赖项:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>

我们需要在以下列出的三个项目中添加 Spring Cloud Sleuth 的前置依赖:

  • Microservice A

  • 服务消费者

  • Zuul API 网关服务器

我们将从跟踪所有微服务之间的服务请求开始。为了能够跟踪所有请求,我们需要配置一个AlwaysSampler bean,如下面的代码片段所示:

    @Bean
    public AlwaysSampler defaultSampler() {
      return new AlwaysSampler();
    }

AlwaysSampler bean 需要在以下微服务应用程序类中进行配置:

  • MicroserviceAApplication

  • ServiceConsumerApplication

  • ZuulApiGatewayServerApplication

当我们在http://localhost:8765/service-consumer/add调用add服务时,我们将看到典型的响应:

    1748

然而,您将开始在日志条目中看到更多细节。这里显示了来自服务消费者微服务日志的简单条目:

2017-03-28 20:53:45.582 INFO [service-consumer,d8866b38c3a4d69c,d8866b38c3a4d69c,true] 89416 --- [l-api-gateway-5] c.netflix.loadbalancer.BaseLoadBalancer : Client:zuul-api-gateway instantiated a LoadBalancer:DynamicServerListLoadBalancer:{NFLoadBalancer:name=zuul-api-gateway,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null

[service-consumer,d8866b38c3a4d69c,d8866b38c3a4d69c,true]:第一个值service-consumer是应用程序名称。关键部分是第二个值--d8866b38c3a4d69c。这是可以用来跟踪此请求在其他微服务组件中的值。

以下是service consumer日志中的一些其他条目:

2017-03-28 20:53:45.593 INFO [service-consumer,d8866b38c3a4d69c,d8866b38c3a4d69c,true] 89416 --- [l-api-gateway-5] c.n.l.DynamicServerListLoadBalancer : Using serverListUpdater PollingServerListUpdater
 2017-03-28 20:53:45.597 INFO [service-consumer,d8866b38c3a4d69c,d8866b38c3a4d69c,true] 89416 --- [l-api-gateway-5] c.netflix.config.ChainedDynamicProperty : Flipping property: zuul-api-gateway.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2017-03-28 20:53:45.599 INFO [service-consumer,d8866b38c3a4d69c,d8866b38c3a4d69c,true] 89416 --- [l-api-gateway-5] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client zuul-api-gateway initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=zuul-api-gateway,current list of Servers=[192.168.1.5:8765],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:1; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
 [service-consumer,d8866b38c3a4d69c,d8866b38c3a4d69c,true] 89416 --- [nio-8100-exec-1] c.m.s.c.service.NumberAdderController : Returning 1748

以下是Microservice A日志的摘录:

[microservice-a,d8866b38c3a4d69c,89d03889ebb02bee,true] 89404 --- [nio-8080-exec-8] c.m.s.c.c.RandomNumberController : Returning [425, 55, 51, 751, 466]

以下是Zuul API Gateway日志的摘录:

[zuul-api-gateway,d8866b38c3a4d69c,89d03889ebb02bee,true] 89397 --- [nio-8765-exec-8] c.m.s.z.filters.pre.SimpleLoggingFilter : Request Method : GET
URL: http://192.168.1.5:8765/microservice-a/random

正如您在前面的日志摘录中所看到的,我们可以使用日志中的第二个值--称为 span ID--来跟踪跨微服务组件的服务调用。在本例中,span ID 是d8866b38c3a4d69c

然而,这需要搜索所有微服务组件的日志。一种选择是使用类似ELKElasticsearchLogstashKibana)堆栈实现集中式日志。我们将采用更简单的选择,在下一步中创建一个 Zipkin 分布式跟踪服务。

设置 Zipkin 分布式跟踪服务器

我们将使用 Spring Initializr (start.spring.io)来设置一个新项目。以下截图显示了要选择的 GroupId、ArtifactId 和 Dependencies:

依赖项包括以下内容:

  • Zipkin Stream:存在多种选项来配置 Zipkin 服务器。在本例中,我们将通过创建一个独立的服务监听事件并将信息存储在内存中来保持简单。

  • Zipkin UI:提供带有搜索功能的仪表板。

  • Stream Rabbit:用于将 Zipkin 流与 RabbitMQ 服务绑定。

在生产环境中,您可能希望拥有更健壮的基础设施。一种选择是将永久数据存储连接到 Zipkin Stream 服务器。

接下来,我们将在ZipkinDistributedTracingServerApplication类中添加@EnableZipkinServer注解,以启用 Zipkin 服务器的自动配置。以下代码片段显示了详细信息:

    @EnableZipkinServer
    @SpringBootApplication
    public class ZipkinDistributedTracingServerApplication {

我们将使用端口9411来运行跟踪服务器。以下代码片段显示了需要添加到application.properties文件中的配置:

    spring.application.name=zipkin-distributed-tracing-server
    server.port=9411

您可以在http://localhost:9411/上启动 Zipkin UI 仪表板。以下是该仪表板的截图。由于没有任何微服务连接到 Zipkin,因此没有显示任何数据:

将微服务组件与 Zipkin 集成

我们将需要连接我们想要跟踪的所有微服务组件与Zipkin 服务器。以下是我们将开始的组件列表:

  • Microservice A

  • 服务消费者

  • Zuul API 网关服务器

我们只需要在前述项目的pom.xml文件中添加对spring-cloud-sleuth-zipkinspring-cloud-starter-bus-amqp的依赖:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-sleuth-zipkin</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>

继续执行http://localhost:8100/add上的add服务。现在您可以在 Zipkin 仪表板上看到详细信息。以下截图显示了一些详细信息:

前两行显示了失败的请求。第三行显示了成功请求的详细信息。我们可以通过点击成功的行来进一步挖掘。以下截图显示了显示的详细信息:

在每个服务上都有一个花费的时间。您可以通过点击服务栏进一步了解。以下截图显示了显示的详细信息:

在本节中,我们为我们的微服务添加了分布式跟踪。现在我们将能够直观地跟踪我们的微服务中发生的一切。这将使得追踪和调试问题变得容易。

Hystrix - 容错

微服务架构是由许多微服务组件构建的。如果一个微服务出现故障会怎么样?所有依赖的微服务都会失败并使整个系统崩溃吗?还是错误会被优雅地处理,并为用户提供降级的最小功能?这些问题决定了微服务架构的成功。

微服务架构应该是有弹性的,并且能够优雅地处理服务错误。Hystrix 为微服务提供了容错能力。

实施

我们将在服务消费者微服务中添加 Hystrix,并增强 add 服务,即使 Microservice A 宕机也能返回基本响应。

我们将从向服务消费者微服务的pom.xml文件中添加 Hystrix Starter 开始。以下代码片段显示了依赖项的详细信息:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-hystrix</artifactId>
    </dependency>

接下来,我们将通过向ServiceConsumerApplication类添加@EnableHystrix注解来启用 Hystrix 自动配置。以下代码片段显示了详细信息:

    @SpringBootApplication
    @EnableFeignClients("com.mastering.spring.consumer")
    @EnableHystrix
    @EnableDiscoveryClient
    public class ServiceConsumerApplication {

NumberAdderController公开了一个请求映射为/add的服务。这使用RandomServiceProxy来获取随机数。如果这个服务失败了怎么办?Hystrix 提供了一个回退。以下代码片段显示了如何向请求映射添加一个回退方法。我们只需要向@HystrixCommand注解添加fallbackMethod属性,定义回退方法的名称--在这个例子中是getDefaultResponse

    @HystrixCommand(fallbackMethod = "getDefaultResponse")
    @RequestMapping("/add")
    public Long add() {
      //Logic of add() method 
    }

接下来,我们定义了getDefaultResponse()方法,其返回类型与add()方法相同。它返回一个默认的硬编码值:

    public Long getDefaultResponse() {
      return 10000L;
     }

让我们关闭微服务 A 并调用http://localhost:8100/add。您将得到以下响应:

    10000

Microservice A失败时,服务消费者微服务会优雅地处理它并提供降级功能。

摘要

Spring Cloud 使得向微服务添加云原生功能变得容易。在本章中,我们看了一些开发云原生应用程序中的重要模式,并使用各种 Spring Cloud 项目来实现它们。

重要的是要记住,开发云原生应用程序的领域仍处于起步阶段--在最初的几年。它需要更多的时间来成熟。预计未来几年模式和框架会有一些演变。

在下一章中,我们将把注意力转向 Spring Data Flow。云上的典型用例包括实时数据分析和数据管道。这些用例涉及多个微服务之间的数据流动。Spring Data Flow 提供了分布式流和数据管道的模式和最佳实践。

第十章:Spring Cloud Data Flow

Spring Data Flow 将微服务架构引入了典型的数据流和事件流场景。我们将在本章后面更多地讨论这些场景。基于其他 Spring 项目,如 Spring Cloud Stream、Spring Integration 和 Spring Boot,Spring Data Flow 使得使用基于消息的集成定义和扩展数据和事件流的用例变得容易。

在本章中,我们将讨论以下主题:

  • 我们为什么需要异步通信?

  • 什么是 Spring Cloud Stream?它如何构建在 Spring Integration 之上?

  • 我们为什么需要 Spring Data Flow?

  • Spring Data Flow 中的重要概念是什么?

  • Spring Data Flow 有哪些有用的用例?

我们还将实现一个简单的事件流场景,其中有三个微服务充当源(生成事件的应用程序)、处理器和汇(消费事件的应用程序)。我们将使用 Spring Cloud Stream 实现这些微服务,并使用 Spring Cloud Data Flow 在消息代理上建立它们之间的连接。

基于消息的异步通信

在集成应用程序时有两个选项:

  • 同步:服务消费者调用服务提供者并等待响应。

  • 异步:服务消费者通过将消息放在消息代理上调用服务提供者,但不等待响应。

我们在第五章,使用 Spring Boot 构建微服务中构建的服务(random服务,add服务)是同步集成的示例。这些是典型的通过 HTTP 公开的 Web 服务。服务消费者调用服务并等待响应。下一次调用只有在前一个服务调用完成后才会进行。

这种方法的一个重要缺点是期望服务提供者始终可用。如果服务提供者宕机,或者由于某种原因服务执行失败,服务消费者将需要重新执行服务。

另一种方法是使用基于消息的异步通信。服务消费者将消息放在消息代理上。服务提供者在消息代理上监听,一旦有消息可用,就会处理它。

一个优点是,即使服务提供者暂时宕机,它可以在恢复时处理消息代理上的消息。服务提供者不需要一直可用。虽然可能会有延迟,但数据最终会保持一致。

以下图显示了基于异步消息的通信的示例:

异步通信改善可靠性的两种情况:

  • 如果服务提供者宕机,那么消息将在消息代理中排队。当服务提供者恢复时,它将处理这些消息。因此,即使服务提供者宕机,消息也不会丢失。

  • 如果消息处理出现错误,服务提供者将把消息放入错误通道。当错误被分析和修复后,消息可以从错误通道移动到输入通道,并排队等待重新处理。

重要的一点是,在前面的两种情况中,服务消费者不需要担心服务提供者是否宕机或消息处理失败。服务消费者发送消息后就可以忘记它了。消息架构确保消息最终会成功处理。

基于消息的异步通信通常用于事件流和数据流:

  • 事件流:这涉及基于事件的处理逻辑。例如,新客户事件、股价变动事件或货币变动事件。下游应用程序将在消息代理上监听事件并对其做出反应。

  • 数据流:这涉及通过多个应用程序增强的数据,并最终存储到数据存储中。

在功能上,数据流架构之间交换的消息内容与事件流架构不同。但从技术上讲,它只是从一个系统发送到另一个系统的另一条消息。在本章中,我们不会区分事件和数据流。Spring Cloud 数据流可以处理所有这些流--尽管只有数据流在名称中。我们可以互换使用事件流、数据流或消息流来指示不同应用程序之间的消息流。

异步通信的复杂性

虽然前面的示例是两个应用程序之间的简单通信,但在现实世界的应用程序中,典型的流程可能要复杂得多。

下图显示了涉及消息流的三个不同应用程序的示例场景。源应用程序生成事件。处理器应用程序处理事件并生成另一条消息,将由接收应用程序处理:

另一个示例场景涉及一个事件被多个应用程序消耗。例如,当客户注册时,我们希望给他们发送电子邮件、欢迎包和邮件。该场景的简单消息架构如下图所示:

要实现上述场景,涉及许多不同的步骤:

  1. 配置消息代理。

  2. 在消息代理上创建不同的通道。

  3. 编写应用程序代码以连接到消息代理上的特定通道。

  4. 在应用程序中安装必要的绑定器以连接到消息代理。

  5. 建立应用程序与消息代理之间的连接。

  6. 构建和部署应用程序。

考虑这样一个场景,其中流程中的一些应用程序必须处理大量的消息负载。我们需要根据负载创建多个这样的应用程序实例。实现复杂性变得多方面。这些是 Spring Cloud 数据流和 Spring Cloud Stream 旨在解决的挑战。

在下一节中,我们将看看不同的 Spring 项目--Spring Cloud Stream(构建在 Spring 集成之上)和 Spring Cloud 数据流如何使我们能够进行基于消息的集成,而无需进行大量配置。

用于异步消息的 Spring 项目

在本节中,我们将看看 Spring 提供的不同项目,以实现应用程序之间基于消息的通信。我们将从 Spring 集成开始,然后转向在云上甚至能够实现基于消息的集成的项目--Spring Cloud Stream 和 Spring Cloud 数据流。

Spring 集成

Spring 集成有助于在消息代理上无缝集成微服务。它允许程序员专注于业务逻辑,并将技术基础设施的控制(使用什么消息格式?如何连接到消息代理?)交给框架。Spring 集成通过定义良好的接口和消息适配器提供了各种配置选项。Spring 集成网站(projects.spring.io/spring-integration/):

扩展 Spring 编程模型以支持众所周知的企业集成模式。Spring 集成使 Spring 应用程序内部实现轻量级消息传递,并通过声明性适配器支持与外部系统的集成。这些适配器提供了对 Spring 支持远程调用、消息传递和调度的更高级抽象。Spring 集成的主要目标是提供一个简单的模型来构建企业集成解决方案,同时保持关注点的分离,这对于生成可维护、可测试的代码至关重要。

Spring Integration 提供的功能包括以下内容:

  • 企业集成模式的简单实现

  • 聚合来自多个服务的响应

  • 从服务中过滤结果

  • 服务消息转换

  • 多协议支持--HTTP、FTP/SFTP、TCP/UDP、JMS

  • 支持不同风格的 Web 服务(SOAP 和 REST)

  • 支持多个消息代理,例如 RabbitMQ

在上一章中,我们使用了 Spring Cloud 来使我们的微服务成为云原生--部署在云中并利用云部署的所有优势。

然而,使用 Spring Integration 构建的应用程序,特别是与消息代理交互的应用程序,需要大量配置才能部署到云中。这阻止它们利用云的典型优势,例如自动扩展。

我们希望扩展 Spring Integration 提供的功能,并在云上提供这些功能。我们希望我们的微服务云实例能够自动与消息代理集成。我们希望能够自动扩展我们的微服务云实例,而无需手动配置。这就是 Spring Cloud Stream 和 Spring Cloud Data Flow 的用武之地。

Spring Cloud Stream

Spring Cloud Stream 是构建面向云的消息驱动微服务的首选框架。

Spring Cloud Stream 允许程序员专注于围绕事件处理的业务逻辑构建微服务,将这里列出的基础设施问题留给框架处理:

  • 消息代理配置和通道创建

  • 针对消息代理的特定转换

  • 创建绑定器以连接到消息代理

Spring Cloud Stream 完美地融入了微服务架构。在事件处理或数据流的用例中,可以设计具有明确关注点分离的典型微服务。单独的微服务可以处理业务逻辑,定义输入/输出通道,并将基础设施问题留给框架。

典型的流应用程序涉及事件的创建、事件的处理和存储到数据存储中。Spring Cloud Stream 提供了三种简单的应用程序类型来支持典型的流程:

  • Source:Source 是事件的创建者,例如触发股价变动事件的应用程序。

  • Processor:Processor 消耗事件,即处理消息,对其进行一些处理,并创建带有结果的事件。

  • Sink:Sink 消耗事件。它监听消息代理并将事件存储到持久数据存储中。

Spring Cloud Stream 用于在数据流中创建单独的微服务。Spring Cloud Stream 微服务定义业务逻辑和连接点,即输入和/或输出。Spring Cloud Data Flow 有助于定义流程,即连接不同的应用程序。

Spring Cloud Data Flow

Spring Cloud Data Flow 有助于在使用 Spring Cloud Stream 创建的不同类型的微服务之间建立消息流。

基于流行的开源项目,Spring XD 简化了数据管道和工作流的创建--特别是针对大数据用例。然而,Spring XD 在适应与数据管道相关的新要求(例如金丝雀部署和分布式跟踪)方面存在挑战。Spring XD 架构基于运行时依赖于多个外围设备。这使得调整集群规模成为一项具有挑战性的任务。Spring XD 现在被重新命名为 Spring Cloud Data Flow。Spring Cloud Data Flow 的架构基于可组合的微服务应用程序。

Spring Cloud Data Flow 中的重要特性如下:

  • 配置流,即数据或事件如何从一个应用程序流向另一个应用程序。Stream DSL 用于定义应用程序之间的流程。

  • 建立应用程序与消息代理之间的连接。

  • 提供围绕应用程序和流的分析。

  • 将在流中定义的应用程序部署到目标运行时。

  • 支持多个目标运行时。几乎每个流行的云平台都得到支持。

  • 在云上扩展应用程序。

  • 创建和调用任务。

有时,术语可能会有点混淆。流是流的另一种术语。重要的是要记住,Spring Cloud Stream 实际上并没有定义整个流。它只有助于创建整个流中涉及的微服务之一。正如我们将在接下来的部分中看到的,流实际上是使用 Spring Cloud Data Flow 中的 Stream DSL 来定义的。

Spring Cloud Stream

Spring Cloud Stream 用于创建涉及流的单个微服务,并定义与消息代理的连接点。

Spring Cloud Stream 是建立在两个重要的 Spring 项目之上的:

  • Spring Boot:使微服务能够创建适用于生产的微服务

  • Spring Integration:使微服务能够通过消息代理进行通信

Spring Cloud Stream 的一些重要特性如下:

  • 将微服务连接到消息代理的最低配置。

  • 支持各种消息代理--RabbitMQ、Kafka、Redis 和 GemFire。

  • 支持消息的持久性--如果服务宕机,它可以在恢复后开始处理消息。

  • 支持消费者组--在负载较重的情况下,您需要多个相同微服务的实例。您可以将所有这些微服务实例分组到一个消费者组中,以便消息只被可用实例中的一个接收。

  • 支持分区--可能存在这样的情况,您希望确保一组特定的消息由同一个实例处理。分区允许您配置标准来识别由同一分区实例处理的消息。

Spring Cloud Stream 架构

以下图显示了典型 Spring Cloud Stream 微服务的架构。源只有一个输入通道,处理器既有输入通道又有输出通道,而汇则只有一个输出通道:

应用程序声明它们想要什么样的连接--输入和/或输出。Spring Cloud Stream 将建立连接应用程序与消息代理所需的一切。

Spring Cloud Stream 将执行以下操作:

  • 将输入和/或输出通道注入到应用程序中

  • 通过特定于消息代理的绑定器建立与消息代理的连接。

绑定器为 Spring Cloud Stream 应用程序带来了可配置性。一个 Spring Cloud Stream 应用程序只声明通道。部署团队可以在运行时配置通道连接到哪个消息代理(Kafka 或 RabbitMQ)。Spring Cloud Stream 使用自动配置来检测类路径上可用的绑定器。要连接到不同的消息代理,我们只需要改变项目的依赖。另一个选项是在类路径中包含多个绑定器,并在运行时选择要使用的绑定器。

事件处理-股票交易示例

让我们想象一个场景。一位股票交易员对他/她投资的股票的重大股价变动感兴趣。以下图显示了使用 Spring Cloud Stream 构建的这样一个应用程序的简单架构:

需要注意的重要事项如下:

  • 重要股价变动微服务:每当交易所上市的任何股票的价格发生重大变动时,它会在消息代理上触发一个事件。这是应用程序。

  • 股票智能微服务:这个微服务监听股价变化事件的消息代理。当有新消息时,它会检查股票库存并将有关用户当前持仓的信息添加到消息中,并将另一条消息放在消息代理上。这是处理器应用程序。

  • 事件存储微服务:这个微服务在消息代理上监听投资股票警报的股价变化。当有新消息时,它将其存储在数据存储中。这是接收器应用程序。

前面的架构为我们提供了在不进行重大更改的情况下增强系统的灵活性:

  • 电子邮件微服务和短信微服务在消息代理上监听投资股票警报的股价变化,并发送电子邮件/短信警报。

  • 股票交易员可能希望对他们没有投资的其他股票进行重大更改。股票智能微服务可以进一步增强。

正如我们之前讨论的,Spring Cloud Stream 帮助我们构建流的基本构建模块,也就是微服务。我们将使用 Spring Cloud Stream 创建三个微服务。我们稍后将使用这三个微服务并使用 Spring Cloud Data Flow 创建一个流,也就是使用 Spring Cloud Data Flow 在应用程序之间创建一个流程。

我们将从下一节开始使用 Spring Cloud Stream 创建微服务。在开始源、处理器和接收器流应用程序之前,我们将设置一个简单的模型项目:

股票交易示例的模型

StockPriceChangeEvent类包含股票的代码、股票的旧价格和股票的新价格:

    public class StockPriceChangeEvent {
      private final String stockTicker;
      private final BigDecimal oldPrice;
      private final BigDecimal newPrice;
      //Setter, Getters and toString()
    }

StockPriceChangeEventWithHoldings类扩展了StockPriceChangeEvent。它有一个额外的属性--holdingsholdings变量用于存储交易员当前拥有的股票数量:

    public class StockPriceChangeEventWithHoldings 
    extends StockPriceChangeEvent {
      private Integer holdings;
      //Setter, Getters and toString()
    }

StockTicker枚举存储应用程序支持的股票列表:

    public enum StockTicker {
      GOOGLE, FACEBOOK, TWITTER, IBM, MICROSOFT
    }

源应用程序

源应用程序将是股价变化事件的生产者。它将定义一个输出通道并将消息放在消息代理上。

让我们使用 Spring Initializr(start.spring.io)来设置应用程序。提供这里列出的详细信息,然后点击生成项目:

  • 组:com.mastering.spring.cloud.data.flow

  • Artifact:significant-stock-change-source

  • 依赖项:Stream Rabbit

以下是pom.xml文件中的一些重要依赖项:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>

使用以下代码更新SpringBootApplication文件:

    @EnableBinding(Source.class)
    @SpringBootApplication
    public class SignificantStockChangeSourceApplication {
      private static Logger logger = LoggerFactory.getLogger 
     (SignificantStockChangeSourceApplication.class);
     // psvm - main method
     @Bean
     @InboundChannelAdapter(value = Source.OUTPUT, 
     poller = @Poller(fixedDelay = "60000", maxMessagesPerPoll = "1"))
     public MessageSource<StockPriceChangeEvent>
     stockPriceChangeEvent()     {
       StockTicker[] tickers = StockTicker.values();
       String randomStockTicker = 
       tickers[ThreadLocalRandom.current().nextInt(tickers.length)] 
      .name();
       return () - > {
        StockPriceChangeEvent event = new         
        StockPriceChangeEvent(randomStockTicker,
        new BigDecimal(getRandomNumber(10, 20)), new   
        BigDecimal(getRandomNumber(10, 20)));
        logger.info("sending " + event);
        return MessageBuilder.withPayload(event).build();
        };
      }
     private int getRandomNumber(int min, int max) {
       return ThreadLocalRandom.current().nextInt(min, max + 1);
     }
    }

需要注意的一些重要事项如下:

  • @EnableBinding(Source.class)EnableBinding注解使类与它需要的相应通道进行绑定--输入和/或输出。源类用于注册一个具有一个输出通道的 Cloud Stream。

  • @Bean @InboundChannelAdapter(value = Source.OUTPUT, poller = @Poller(fixedDelay = "60000", maxMessagesPerPoll = "1"))InboundChannelAdapter注解用于指示该方法可以创建要放在消息代理上的消息。value 属性用于指示消息要放置的通道的名称。Poller用于调度消息的生成。在这个例子中,我们使用fixedDelay每分钟生成一次消息(60 * 1000 ms)。

  • private int getRandomNumber(int min, int max):这个方法用于在传递的范围内创建一个随机数。

Source接口定义了一个输出通道,如下面的代码所示:

    public abstract interface 
    org.springframework.cloud.stream.messaging.Source {
      public static final java.lang.String OUTPUT = "output";
      @org.springframework.cloud.stream.
      annotation.Output(value="output")
      public abstract org.springframework.
      messaging.MessageChannel   output();
     }

处理器

处理器应用程序将从消息代理的输入通道接收消息。它将处理消息并将其放在消息代理的输出通道上。在这个特定的例子中,处理包括将当前持仓的位置添加到消息中。

让我们使用 Spring Initializr(start.spring.io)来设置应用程序。提供这里列出的详细信息,然后点击生成项目:

  • 组:com.mastering.spring.cloud.data.flow

  • 构件:stock-intelligence-processor

  • 依赖:Stream Rabbit

使用以下代码更新SpringBootApplication文件:

    @EnableBinding(Processor.class)@SpringBootApplication
    public class StockIntelligenceProcessorApplication {
      private static Logger logger = 
      LoggerFactory.getLogger
      (StockIntelligenceProcessorApplication.class);
      private static Map < StockTicker, Integer > holdings =
        getHoldingsFromDatabase();
        private static Map < StockTicker,
        Integer > getHoldingsFromDatabase() {
          final Map < StockTicker,
          Integer > holdings = new HashMap < >();
          holdings.put(StockTicker.FACEBOOK, 10);
          holdings.put(StockTicker.GOOGLE, 0);
          holdings.put(StockTicker.IBM, 15);
          holdings.put(StockTicker.MICROSOFT, 30);
          holdings.put(StockTicker.TWITTER, 50);
          return holdings;
        }
        @Transformer(inputChannel = Processor.INPUT,
        outputChannel = Processor.OUTPUT)
        public Object addOurInventory(StockPriceChangeEvent event) {
          logger.info("started processing event " + event);
          Integer holding =  holdings.get(
            StockTicker.valueOf(event.getStockTicker()));
          StockPriceChangeEventWithHoldings eventWithHoldings =
            new StockPriceChangeEventWithHoldings(event, holding);
          logger.info("ended processing eventWithHoldings " 
            + eventWithHoldings);
          return eventWithHoldings;
        }
        public static void main(String[] args) {
          SpringApplication.run(
            StockIntelligenceProcessorApplication.class,args);
        }
    }

需要注意的一些重要事项如下:

  • @EnableBinding(Processor.class): EnableBinding注解用于将类与其所需的相应通道绑定--输入和/或输出。Processor类用于注册一个具有一个输入通道和一个输出通道的 Cloud Stream。

  • private static Map<StockTicker, Integer> getHoldingsFromDatabase(): 这个方法处理消息,更新持有量,并返回一个新对象,该对象将作为新消息放入输出通道。

  • @Transformer(inputChannel = Processor.INPUT, outputChannel = Processor.OUTPUT): Transformer注解用于指示一个能够将一种消息格式转换/增强为另一种消息格式的方法。

如下所示,Processor类扩展了SourceSink类。因此,它定义了输出和输入通道:

   public abstract interface 
   org.springframework.cloud.stream.messaging.Processor extends 
   org.springframework.cloud.stream.messaging.Source, 
   org.springframework.cloud.stream.messaging.Sink {
  }

Sink

Sink 将从消息代理中提取消息并处理它。在这个例子中,我们将提取消息并记录它。Sink 只定义了一个输入通道。

让我们使用 Spring Initializr (start.spring.io)来设置应用程序。提供这里列出的细节,然后点击生成项目:

  • 组:com.mastering.spring.cloud.data.flow

  • 构件:event-store-sink

  • 依赖:Stream Rabbit

使用以下代码更新SpringBootApplication文件:

    @EnableBinding(Sink.class)@SpringBootApplication
    public class EventStoreSinkApplication {
      private static Logger logger = 
      LoggerFactory.getLogger(EventStoreSinkApplication.class);
      @StreamListener(Sink.INPUT)
      public void loggerSink(StockPriceChangeEventWithHoldings event) {
      logger.info("Received: " + event);
    }
    public static void main(String[] args) {
      SpringApplication.run(EventStoreSinkApplication.class, args);
    }
   }

需要注意的一些重要事项如下:

  • @EnableBinding(Sink.class): EnableBinding注解用于将类与其所需的相应通道绑定--输入和/或输出。Sink类用于注册一个具有一个输入通道的 Cloud Stream。

  • public void loggerSink(StockPriceChangeEventWithHoldings event): 这个方法通常包含将消息存储到数据存储的逻辑。在这个例子中,我们将消息打印到日志中。

  • @StreamListener(Sink.INPUT): StreamListener注解用于监听传入消息的通道。在这个例子中,StreamListener配置为监听默认输入通道。

如下代码所示,Sink接口定义了一个输入通道:

    public abstract interface   
    org.springframework.cloud.stream.messaging.Sink {
      public static final java.lang.String INPUT = "input";
      @org.springframework.cloud.stream.annotation.Input(value="input")
      public abstract org.springframework.messaging.SubscribableChannel 
      input();
    }

现在我们有了三个流应用程序准备好了,我们需要连接它们。在下一节中,我们将介绍 Spring Cloud Data Flow 如何帮助连接不同的流。

Spring Cloud Data Flow

Spring Cloud Data Flow 有助于建立使用 Spring Cloud Stream 创建的不同类型的微服务之间的消息流。通过 Spring Cloud Data Flow 服务器部署的所有微服务都应该是定义了适当通道的 Spring Boot 微服务。

Spring Cloud Data Flow 提供了接口来定义应用程序,并使用 Spring DSL 定义它们之间的流。Spring Data Flow 服务器理解 DSL 并在应用程序之间建立流。

通常,这涉及多个步骤:

  • 使用应用程序名称和应用程序的可部署单元之间的映射来从存储库下载应用程序构件。Spring Data Flow Server 支持 Maven 和 Docker 存储库。

  • 将应用程序部署到目标运行时。

  • 在消息代理上创建通道。

  • 建立应用程序和消息代理通道之间的连接。

Spring Cloud Data Flow 还提供了在需要时扩展所涉及的应用程序的选项。部署清单将应用程序映射到目标运行时。部署清单需要回答的一些问题如下:

  • 需要创建多少个应用程序实例?

  • 每个应用程序实例需要多少内存?

数据流服务器理解部署清单并按照指定的方式创建目标运行时。Spring Cloud Data Flow 支持各种运行时:

  • 云原生

  • Apache YARN

  • Kubernetes

  • Apache Mesos

  • 用于开发的本地服务器

本章中的示例将使用本地服务器。

高级架构

在前面的示例中,我们有三个需要在数据流中连接的微服务。以下图表示使用 Spring Cloud Data Flow 实现解决方案的高级架构:

在前面的图中,源、接收器和处理器是使用 Spring Cloud Stream 创建的 Spring Boot 微服务:

  • 源微服务定义了一个输出通道

  • 处理器微服务定义了输入和输出通道

  • 接收器微服务定义了一个输入通道

实施 Spring Cloud Data Flow

实施 Spring Cloud Data Flow 涉及五个步骤:

  1. 设置 Spring Cloud Data Flow 服务器。

  2. 设置 Data Flow Shell 项目。

  3. 配置应用程序。

  4. 配置流。

  5. 运行流。

设置 Spring Cloud Data Flow 服务器

让我们使用 Spring Initializr(start.spring.io)来设置应用程序。提供这里列出的详细信息,然后单击“生成项目”:

  • 组:com.mastering.spring.cloud.data.flow

  • Artifact:local-data-flow-server

  • 依赖项:本地 Data Flow 服务器

以下是pom.xml文件中一些重要的依赖项:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-dataflow-server-
      local</artifactId>
    </dependency>

更新SpringBootApplication文件,使用以下代码:

    @EnableDataFlowServer
    @SpringBootApplication
    public class LocalDataFlowServerApplication {
      public static void main(String[] args) {
        SpringApplication.run(LocalDataFlowServierApplication.class,
        args);
      }
    }

@EnableDataFlowServer注解用于激活 Spring Cloud Data Flow 服务器实现。

在运行本地 Data Flow 服务器之前,请确保消息代理 RabbitMQ 正在运行。

以下是在启动LocalDataFlowServerApplication时的启动日志中的重要摘录:

Tomcat initialized with port(s): 9393 (http)
Starting H2 Server with URL: jdbc:h2:tcp://localhost:19092/mem:dataflow
Adding dataflow schema classpath:schema-h2-common.sql for h2 database
Adding dataflow schema classpath:schema-h2-streams.sql for h2 database
Adding dataflow schema classpath:schema-h2-tasks.sql for h2 database
Adding dataflow schema classpath:schema-h2-deployment.sql for h2 database
Executed SQL script from class path resource [schema-h2-common.sql] in 37 ms.
Executed SQL script from class path resource [schema-h2-streams.sql] in 2 ms.
Executed SQL script from class path resource [schema-h2-tasks.sql] in 3 ms.
Executing SQL script from class path resource [schema-h2-deployment.sql]
Executed SQL script from class path resource [schema-h2-deployment.sql] in 3 ms.
Mapped "{[/runtime/apps/{appId}/instances]}" onto public org.springframework.hateoas.PagedResources
Mapped "{[/runtime/apps/{appId}/instances/{instanceId}]}" onto public 
Mapped "{[/streams/definitions/{name}],methods=[DELETE]}" onto public void org.springframework.cloud.dataflow.server.controller.StreamDefinitionController.delete(java.lang.String)
Mapped "{[/streams/definitions],methods=[GET]}" onto public org.springframework.hateoas.PagedResources
Mapped "{[/streams/deployments/{name}],methods=[POST]}" onto public void org.springframework.cloud.dataflow.server.controller.StreamDeploymentController.deploy(java.lang.String,java.util.Map<java.lang.String, java.lang.String>)
Mapped "{[/runtime/apps]}" onto public org.springframework.hateoas.PagedResources<org.springframework.cloud.dataflow.rest.resource.AppStatusResource> org.springframework.cloud.dataflow.server.controller.RuntimeAppsController.list(org.springframework.data.domain.Pageable,org.springframework.data.web.PagedResourcesAssembler<org.springframework.cloud.deployer.spi.app.AppStatus>) throws java.util.concurrent.ExecutionException,java.lang.InterruptedException
Mapped "{[/tasks/executions],methods=[GET]}" onto public org.springframework.hateoas.PagedResources

需要注意的一些重要事项如下:

  • Spring Cloud Data Flow 服务器的默认端口是9393。可以通过在application.properties中指定不同的端口server.port来更改这一点。

  • Spring Cloud Data Flow 服务器使用内部模式存储所有应用程序、任务和流的配置。在本例中,我们尚未配置任何数据库。因此,默认情况下使用H2内存数据库。Spring Cloud Data Flow 服务器支持各种数据库,包括 MySQL 和 Oracle,用于存储配置。

  • 由于使用了H2内存数据库,您可以看到在启动期间设置了不同的模式,并且还执行了不同的 SQL 脚本来设置数据。

  • Spring Cloud Data Flow 服务器公开了许多围绕其配置、应用程序、任务和流的 API。我们将在后面的部分更多地讨论这些 API。

以下屏幕截图显示了 Spring Cloud Data Flow 的启动屏幕,网址为http://localhost:9393/dashboard

有不同的选项卡可用于查看和修改应用程序、流和任务。在下一步中,我们将使用命令行界面--Data Flow Shell 来设置应用程序和流。

设置 Data Flow Shell 项目

Data Flow Shell 提供了使用命令配置 Spring Data Flow 服务器中的流和其他内容的选项。

让我们使用 Spring Initializr(start.spring.io)来设置应用程序。提供这里列出的详细信息,然后单击“生成项目”:

  • 组:com.mastering.spring.cloud.data.flow

  • Artifact:data-flow-shell

  • 依赖项:Data Flow Shell

以下是pom.xml文件中一些重要的依赖项:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dataflow-shell</artifactId>
    </dependency>

更新SpringBootApplication文件,使用以下代码:

    @EnableDataFlowShell
    @SpringBootApplication
    public class DataFlowShellApplication {
      public static void main(String[] args) {
      SpringApplication.run(DataFlowShellApplication.class, args);
     }
    }

@EnableDataFlowShell注解用于激活 Spring Cloud Data Flow shell。

以下屏幕截图显示了启动 Data Flow Shell 应用程序时显示的消息。我们可以在命令提示符中输入命令:

您可以尝试help命令以获取支持的命令列表。以下屏幕截图显示了执行help命令时打印的一些命令:

当您执行以下任何命令时,您会发现打印出空列表,因为我们尚未配置这些:

  • app list

  • stream list

  • task list

  • runtime apps

配置应用程序

在开始配置流之前,我们需要注册构成流的应用程序。我们有三个应用程序要注册--源、处理器和接收器。

要在 Spring Cloud Data Flow 中注册应用程序,您需要访问应用程序可部署。Spring Cloud Data Flow 提供了从 Maven 存储库中获取应用程序可部署的选项。为了简化,我们将从本地 Maven 存储库中获取应用程序。

在使用 Spring Cloud Stream 创建的三个应用程序上运行mvn clean install

  • significant-stock-change-source

  • stock-intelligence-processor

  • event-store-sink

这将确保所有这些应用程序都构建并存储在您的本地 Maven 存储库中。

从 Maven 存储库注册应用的命令语法如下所示:

app register —-name {{NAME_THAT_YOU_WANT_TO_GIVE_TO_APP}} --type source --uri maven://{{GROUP_ID}}:{{ARTIFACT_ID}}:jar:{{VERSION}}

三个应用程序的 Maven URI 如下所示:

maven://com.mastering.spring.cloud.data.flow:significant-stock-change-source:jar:0.0.1-SNAPSHOT
maven://com.mastering.spring.cloud.data.flow:stock-intelligence-processor:jar:0.0.1-SNAPSHOT
maven://com.mastering.spring.cloud.data.flow:event-store-sink:jar:0.0.1-SNAPSHOT

创建应用程序的命令在此处列出。这些命令可以在 Data Flow Shell 应用程序上执行:

app register --name significant-stock-change-source --type source --uri maven://com.mastering.spring.cloud.data.flow:significant-stock-change-source:jar:0.0.1-SNAPSHOT

app register --name stock-intelligence-processor --type processor --uri maven://com.mastering.spring.cloud.data.flow:stock-intelligence-processor:jar:0.0.1-SNAPSHOT

app register --name event-store-sink --type sink --uri maven://com.mastering.spring.cloud.data.flow:event-store-sink:jar:0.0.1-SNAPSHOT

当成功注册应用程序时,您将看到此处显示的消息:

Successfully registered application 'source:significant-stock-change-source'

Successfully registered application 'processor:stock-intelligence-processor'

Successfully registered application 'sink:event-store-sink'

您还可以在 Spring Cloud Data Flow 仪表板上查看已注册的应用程序,如下图所示:http://localhost:9393/dashboard

我们还可以使用仪表板注册应用程序,如下图所示:

配置流

Stream DSL 可用于配置流--这里显示了一个简单的示例,用于连接app1app2。由app1放在输出通道上的消息将在app2的输入通道上接收:

app1 | app2

我们希望连接这三个应用程序。以下代码片段显示了用于连接前述应用程序的 DSL 的示例:

#source | processor | sink

significant-stock-change-source|stock-intelligence-processor|event-store-sink

这表示以下内容:

  • 源的输出通道应链接到处理器的输入通道

  • 处理器的输出通道应链接到接收器的输入通道

创建流的完整命令如下所示:

stream create --name process-stock-change-events --definition significant-stock-change-source|stock-intelligence-processor|event-store-sink

如果成功创建流,则应看到以下输出:

Created new stream 'process-stock-change-events'

您还可以在 Spring Cloud Data Flow 仪表板的 Streams 选项卡上查看已注册的流,如下图所示:http://localhost:9393/dashboard

部署流

要部署流,可以在 Data Flow Shell 上执行以下命令:

stream deploy --name process-stock-change-events

当发送请求创建流时,您将看到此处显示的消息:

Deployment request has been sent for stream 'process-stock-change-events'

以下摘录显示了本地数据流服务器日志中的一部分:

o.s.c.d.spi.local.LocalAppDeployer : deploying app process-stock-change-events.event-store-sink instance 0

Logs will be in /var/folders/y_/x4jdvdkx7w94q5qsh745gzz00000gn/T/spring-cloud-dataflow-3084432375250471462/process-stock-change-events-1492100265496/process-stock-change-events.event-store-sink

o.s.c.d.spi.local.LocalAppDeployer : deploying app process-stock-change-events.stock-intelligence-processor instance 0

Logs will be in /var/folders/y_/x4jdvdkx7w94q5qsh745gzz00000gn/T/spring-cloud-dataflow-3084432375250471462/process-stock-change-events-1492100266448/process-stock-change-events.stock-intelligence-processor

o.s.c.d.spi.local.LocalAppDeployer : deploying app process-stock-change-events.significant-stock-change-source instance 0

Logs will be in /var/folders/y_/x4jdvdkx7w94q5qsh745gzz00000gn/T/spring-cloud-dataflow-3084432375250471462/process-stock-change-events-1492100267242/process-stock-change-events.significant-stock-change-source

以下是一些需要注意的重要事项:

  • 当部署流时,Spring Cloud Data Flow 将部署流中的所有应用程序,并通过消息代理设置应用程序之间的连接。应用程序代码独立于消息代理。Kafka 与 RabbitMQ 相比具有不同的消息代理设置。Spring Cloud Data Flow 会处理它。如果要从 RabbitMQ 切换到 Kafka,则应用程序代码无需更改。

  • 本地数据流服务器日志包含所有应用程序的日志路径--源、处理器和接收器。

日志消息 - 设置与消息工厂的连接

以下代码片段显示了与从SourceTransformerSink应用程序设置消息代理相关的摘录:

#Source Log
CachingConnectionFactory : Created new connection: SimpleConnection@725b3815 [delegate=amqp://guest@127.0.0.1:5672/, localPort= 58373]

#Transformer Log
o.s.i.endpoint.EventDrivenConsumer : Adding {transformer:stockIntelligenceProcessorApplication.addOurInventory.transformer} as a subscriber to the 'input' channel

o.s.integration.channel.DirectChannel : Channel 'application:0.input' has 1 subscriber(s).

o.s.i.endpoint.EventDrivenConsumer : started stockIntelligenceProcessorApplication.addOurInventory.transformer

o.s.i.endpoint.EventDrivenConsumer : Adding {message-handler:inbound.process-stock-change-events.significant-stock-change-source.process-stock-change-events} as a subscriber to the 'bridge.process-stock-change-events.significant-stock-change-source' channel

o.s.i.endpoint.EventDrivenConsumer : started inbound.process-stock-change-events.significant-stock-change-source.process-stock-change-events

#Sink Log

c.s.b.r.p.RabbitExchangeQueueProvisioner : declaring queue for inbound: process-stock-change-events.stock-intelligence-processor.process-stock-change-events, bound to: process-stock-change-events.stock-intelligence-processor

o.s.a.r.c.CachingConnectionFactory : Created new connection: SimpleConnection@3de6223a [delegate=amqp://guest@127.0.0.1:5672/, localPort= 58372]

以下是一些需要注意的事项:

  • 创建新连接:SimpleConnection@725b3815 [delegate=amqp://guest@127.0.0.1:5672/, localPort= 58373]:由于我们将spring-cloud-starter-stream-rabbit添加到了三个应用程序的类路径中,所以使用的消息代理是 RabbitMQ。

  • 将{transformer:stockIntelligenceProcessorApplication.addOurInventory.transformer}添加为“input”通道的订阅者:类似于此,每个应用程序的输入和/或输出通道在消息代理上设置。源和处理器应用程序在通道上监听传入消息。

日志消息-事件流程

有关处理消息的提取如下所示:

#Source Log
SignificantStockChangeSourceApplication : sending StockPriceChangeEvent [stockTicker=MICROSOFT, oldPrice=15, newPrice=12]

#Transformer Log
.f.StockIntelligenceProcessorApplication : started processing event StockPriceChangeEvent [stockTicker=MICROSOFT, oldPrice=18, newPrice=20]

.f.StockIntelligenceProcessorApplication : ended processing eventWithHoldings StockPriceChangeEventWithHoldings [holdings=30, toString()=StockPriceChangeEvent [stockTicker=MICROSOFT, oldPrice=18, newPrice=20]]

#Sink Log
c.m.s.c.d.f.EventStoreSinkApplication : Received: StockPriceChangeEventWithHoldings [holdings=30, toString()=StockPriceChangeEvent [stockTicker=MICROSOFT, oldPrice=18, newPrice=20]]

源应用程序发送StockPriceChangeEventTransformer应用程序接收事件,将持有添加到消息中,并创建新的StockPriceChangeEventWithHoldings事件。接收器应用程序接收并记录此消息。

Spring Cloud Data Flow REST API

Spring Cloud Data Flow 提供了围绕应用程序、流、任务、作业和指标的 RESTful API。可以通过向http://localhost:9393/发送GET请求来获取完整列表。

以下屏幕截图显示了GET请求的响应:

所有 API 都是不言自明的。让我们看一个向http://localhost:9393/streams/definitions发送GET请求的示例:

{  
  "_embedded":{  
  "streamDefinitionResourceList":[  
         {  
            "name":"process-stock-change-events"
            "dslText":"significant-stock-change-source|stock-
            intelligence-processor|event-store-sink",
            "status":"deployed",
            "statusDescription":"All apps have been successfully
             deployed",
            "_links":{  
               "self":{  
                  "href":"http://localhost:9393/streams/definitions/
                   process-stock-change-events"
               }
            }
         }
      ]
   },
   "_links":{  
      "self":{  
         "href":"http://localhost:9393/streams/definitions"
      }
   },
   "page":{
      "size":20,
      "totalElements":1,
      "totalPages":1,
      "number":0
   }
}

需要注意的重要事项如下:

  • API 是 RESTful 的。_embedded元素包含请求的数据。_links元素包含 HATEOAS 链接。页面元素包含分页信息。

  • _embedded.streamDefinitionResourceList.dslText包含流的定义"significant-stock-change-source|stock-intelligence-processor|event-store-sink"

  • _embedded.streamDefinitionResourceList.status

Spring Cloud Task

Spring Cloud Data Flow 还可以用于创建和调度批处理应用程序。在过去的十年中,Spring Batch 一直是开发批处理应用程序的首选框架。Spring Cloud Task 扩展了这一点,并使批处理程序可以在云上执行。

让我们使用 Spring Initializr (start.spring.io)来设置应用程序。提供此处列出的详细信息,然后单击“生成项目”:

  • 组:com.mastering.spring.cloud.data.flow

  • 构件:simple-logging-task

  • 依赖项:Cloud Task

使用以下代码更新SimpleLoggingTaskApplication类:

@SpringBootApplication
@EnableTask

public class SimpleLoggingTaskApplication {

@Bean
public CommandLineRunner commandLineRunner() {
  return strings -> System.out.println(
  "Task execution :" + new SimpleDateFormat().format(new Date()));
  }
public static void main(String[] args) {
  SpringApplication.run(SimpleLoggingTaskApplication.class, args);
  }
}

此代码只是将当前时间戳与 sysout 放在一起。@EnableTask注解在 Spring Boot 应用程序中启用任务功能。

我们可以使用以下命令在数据流 shell 上注册任务:

app register --name simple-logging-task --type task --uri maven://com.mastering.spring.cloud.data.flow:simple-logging-task:jar:0.0.1-SNAPSHOT
task create --name simple-logging-task-definition --definition "simple-logging-task"

这些命令与用于注册我们之前创建的流应用程序的命令非常相似。我们正在添加一个任务定义,以便能够执行该任务。

可以使用以下命令启动任务:

task launch simple-logging-task-definition

任务执行也可以在 Spring Cloud Flow 仪表板上触发和监视。

摘要

Spring Cloud Data Flow 为数据流和事件流带来了云原生功能。它使得在云上创建和部署流变得容易。在本章中,我们介绍了如何使用 Spring Cloud Stream 设置事件驱动流中的单个应用程序。我们以 1000 英尺的视角来创建具有 Spring Cloud Task 的任务。我们使用 Spring Cloud Data Flow 来设置流,还执行简单任务。

在下一章中,我们将开始了解构建 Web 应用程序的新方法--响应式风格。我们将了解为什么非阻塞应用程序备受推崇,以及如何使用 Spring Reactive 构建响应式应用程序。

第十一章:响应式编程

在前一章中,我们讨论了使用 Spring Cloud Data Flow 在微服务中实现典型的数据流使用案例。

函数式编程标志着从传统的命令式编程转向更声明式的编程风格。响应式编程建立在函数式编程之上,提供了一种替代的风格。

在本章中,我们将讨论响应式编程的基础知识。

微服务架构促进基于消息的通信。响应式编程的一个重要原则是围绕事件(或消息)构建应用程序。我们需要回答一些重要的问题,包括以下内容:

  • 什么是响应式编程?

  • 典型的使用案例是什么?

  • Java 为响应式编程提供了什么样的支持?

  • Spring WebFlux 中的响应式特性是什么?

响应式宣言

几年前的大多数应用程序都有以下的奢侈条件:

  • 多秒级的响应时间

  • 多个小时的离线维护

  • 较小的数据量

时代已经改变。新设备(手机、平板等)和新的方法(基于云的)已经出现。在今天的世界中,我们正在谈论:

  • 亚秒级的响应时间

  • 100%的可用性

  • 数据量呈指数增长

在过去几年中出现了不同的方法来应对这些新兴挑战。虽然响应式编程并不是一个真正新的现象,但它是成功应对这些挑战的方法之一。

响应式宣言(www.reactivemanifesto.org)旨在捕捉共同的主题。

我们相信需要一个连贯的系统架构方法,并且我们相信所有必要的方面已经被单独认可:我们希望系统具有响应性、弹性、弹性和消息驱动。我们称这些为响应式系统。

构建为响应式系统的系统更加灵活、松散耦合和可扩展。这使得它们更容易开发和适应变化。它们对故障更具有容忍性,当故障发生时,它们以优雅的方式而不是灾难性地应对。响应式系统具有高度的响应性,为用户提供有效的交互反馈。

虽然响应式宣言清楚地阐述了响应式系统的特性,但对于响应式系统的构建方式并不是很清晰。

响应式系统的特点

以下图显示了响应式系统的重要特点:

重要特点如下:

  • 响应性:系统对用户做出及时的响应。设置了明确的响应时间要求,并且系统在所有情况下都满足这些要求。

  • 弹性:分布式系统是使用多个组件构建的。任何一个组件都可能发生故障。响应式系统应该被设计成在局部空间内包含故障,例如在每个组件内。这可以防止整个系统在局部故障的情况下崩溃。

  • 弹性:响应式系统在不同负载下保持响应。在高负载下,这些系统可以添加额外的资源,而在负载减少时释放资源。弹性是通过使用通用硬件和软件实现的。

  • 消息驱动:响应式系统由消息(或事件)驱动。这确保了组件之间的低耦合。这保证了系统的不同组件可以独立扩展。使用非阻塞通信确保线程的生存时间更短。

响应式系统对不同类型的刺激做出响应。一些例子如下:

  • 对事件做出反应:基于消息传递构建,响应式系统对事件做出快速响应。

  • 对负载做出反应:响应式系统在不同负载下保持响应。在高负载下使用更多资源,在较低负载下释放资源。

  • 对故障做出反应:反应式系统可以优雅地处理故障。反应式系统的组件被构建为局部化故障。外部组件用于监视组件的可用性,并在需要时复制组件。

  • 对用户做出反应:反应式系统对用户做出响应。当消费者未订阅特定事件时,它们不会浪费时间执行额外的处理。

反应式用例 - 股票价格页面

虽然反应式宣言帮助我们理解反应式系统的特性,但它并不能真正帮助我们理解反应式系统是如何构建的。为了理解这一点,我们将考虑构建一个简单用例的传统方法,并将其与反应式方法进行比较。

我们要构建的用例是一个显示特定股票价格的股票价格页面。只要页面保持打开状态,我们希望在页面上更新股票的最新价格。

传统方法

传统方法使用轮询来检查股票价格是否发生变化。以下的序列图展示了构建这样一个用例的传统方法:

页面渲染后,会定期向股票价格服务发送获取最新价格的 AJAX 请求。这些调用必须进行,无论股票价格是否发生变化,因为网页不知道股票价格的变化。

反应式方法

反应式方法涉及连接不同的组件,以便能够对事件做出反应。

当股票价格网页加载时,网页会注册股票价格服务的事件。当股票价格变化事件发生时,会触发一个事件。最新的股票价格会更新在网页上。以下的序列图展示了构建股票价格页面的反应式方法:

反应式方法通常包括三个步骤:

  1. 订阅事件。

  2. 事件的发生。

  3. 注销。

当股票价格网页最初加载时,它会订阅股票价格变化事件。订阅的方式根据使用的反应式框架和/或消息代理(如果有)而有所不同。

当特定股票的股票价格变化事件发生时,会为所有订阅者触发一个新的事件。监听器确保网页上显示最新的股票价格。

一旦网页关闭(或刷新),订阅者会发送注销请求。

传统方法和反应式方法之间的比较

传统方法非常简单。反应式方法需要实现反应式订阅和事件链。如果事件链涉及消息代理,它会变得更加复杂。

在传统方法中,我们轮询变化。这意味着每分钟(或指定的间隔)都会触发整个序列,无论股票价格是否发生变化。在反应式方法中,一旦我们注册了事件,只有当股票价格发生变化时才会触发序列。

传统方法中线程的生命周期更长。线程使用的所有资源会被锁定更长时间。考虑到服务器同时为多个请求提供服务的整体情况,线程和它们的资源会有更多的竞争。在反应式方法中,线程的生命周期较短,因此资源的竞争较少。

传统方法中的扩展涉及扩展数据库并创建更多的 Web 服务器。由于线程的寿命很短,反应式方法可以处理更多用户。虽然反应式方法具有传统方法的所有扩展选项,但它提供了更多的分布式选项。例如,股价变动事件的触发可以通过消息代理与应用程序通信,如下图所示:

这意味着 Web 应用程序和股价变动触发的应用程序可以独立扩展。这在需要时提供了更多的扩展选项。

Java 中的反应式编程

Java 8 没有内置对反应式编程的支持。许多框架提供了反应式功能。我们将在后续章节中讨论反应式流、Reactor 和 Spring WebFlux。

反应式流

反应式流是一项旨在提供异步流处理和非阻塞背压标准的倡议。这包括针对运行时环境(JVM 和 JavaScript)以及网络协议的努力。

需要注意的一些重要事项如下:

  • 反应式流旨在定义一组最小的接口、方法和协议,以实现反应式编程

  • 反应式流旨在成为一种与语言无关的方法,实现在 Java(基于 JVM)和 JavaScript 语言中

  • 支持多个传输流(TCP、UDP、HTTP 和 WebSockets)

反应式流的 Maven 依赖关系如下所示:

    <dependency>
      <groupId>org.reactivestreams</groupId>
      <artifactId>reactive-streams</artifactId>
      <version>1.0.0</version>
    </dependency>

    <dependency>
      <groupId>org.reactivestreams</groupId>
      <artifactId>reactive-streams-tck</artifactId>
      <version>1.0.0</version>
      <scope>test</scope>
    </dependency>

在 Reactive Streams 中定义的一些重要接口如下所示:

    public interface Subscriber<T> {
      public void onSubscribe(Subscription s);
      public void onNext(T t);
      public void onError(Throwable t);
      public void onComplete();
    }
   public interface Publisher<T> {
     public void subscribe(Subscriber<? super T> s);
   }
   public interface Subscription {
     public void request(long n);
     public void cancel();
  }

需要注意的一些重要事项如下:

  • 接口发布者Publisher根据其订阅者的需求提供元素流。一个发布者可以为任意数量的订阅者提供服务。订阅者数量可能会随时间变化。

  • 接口订阅者Subscriber注册以监听事件流。订阅是一个两步过程。第一步是调用 Publisher.subscribe(Subscriber)。第二步涉及调用 Subscription.request(long)。完成这些步骤后,订阅者可以使用onNext(T t)方法开始处理通知。onComplete()方法表示通知的结束。每当Subscriber实例能够处理更多时,可以通过 Subscription.request(long)发出需求信号。

  • 接口订阅Subscription表示Subscriber和其Publisher之间的链接。订阅者可以使用request(long n)请求更多数据。它可以使用cancel()方法取消通知的订阅。

Reactor

Reactor 是 Spring Pivotal 团队的一个反应式框架。它建立在 Reactive Streams 之上。正如我们将在本章后面讨论的那样,Spring Framework 5.0 使用 Reactor 框架来实现反应式 Web 功能。

Reactor 的依赖关系如下所示:

    <dependency>
      <groupId>io.projectreactor</groupId>
      <artifactId>reactor-core</artifactId>
      <version>3.0.6.RELEASE</version>
   </dependency>
   <dependency>
     <groupId>io.projectreactor.addons</groupId>
     <artifactId>reactor-test</artifactId>
     <version>3.0.6.RELEASE</version>
  </dependency>

Reactor 在SubscriberConsumerSubscriptions术语的基础上增加了一些重要的内容。

  • Flux:Flux 表示发出 0 到n个元素的反应式流

  • Mono:Mono 表示发出零个或一个元素的反应式流

在后续的示例中,我们将创建存根 Mono 和 Flux 对象,这些对象将预先配置为在特定时间间隔内发出元素。我们将创建消费者(或观察者)来监听这些事件并对其做出反应。

Mono

创建 Mono 非常简单。以下 Mono 在 5 秒延迟后发出一个元素。

   Mono<String> stubMonoWithADelay = 
   Mono.just("Ranga").delayElement(Duration.ofSeconds(5));

我们希望从 Mono 中监听事件并将其记录到控制台。我们可以使用此处指定的语句来实现:

    stubMonoWithADelay.subscribe(System.out::println);

但是,如果您在以下代码中以Test注释运行程序,并运行前面两个语句,您会发现控制台上没有打印任何内容:

    @Test
    public void monoExample() throws InterruptedException {
      Mono<String> stubMonoWithADelay =   
      Mono.just("Ranga").delayElement(Duration.ofSeconds(5));
      stubMonoWithADelay.subscribe(System.out::println);
     }

由于Test执行在 Mono 在 5 秒后发出元素之前结束,因此不会打印任何内容到控制台。为了防止这种情况,让我们使用Thread.sleep延迟Test的执行:

    @Test
    public void monoExample() throws InterruptedException {
      Mono<String> stubMonoWithADelay = 
      Mono.just("Ranga").delayElement(Duration.ofSeconds(5));
      stubMonoWithADelay.subscribe(System.out::println);
      Thread.sleep(10000);
    }

当我们使用stubMonoWithADelay.subscribe(System.out::println)创建一个订阅者时,我们使用了 Java 8 引入的函数式编程特性。System.out::println是一个方法定义。我们将方法定义作为参数传递给一个方法。

这是因为有一个特定的函数接口叫做Consumer。函数接口是只有一个方法的接口。Consumer函数接口用于定义接受单个输入参数并返回无结果的操作。Consumer接口的概要显示在以下代码片段中:

     @FunctionalInterface
     public interface Consumer<T> {
       void accept(T t); 
     }

我们可以明确定义Consumer,而不是使用 lambda 表达式。以下代码片段显示了重要细节:

    class SystemOutConsumer implements Consumer<String> {
      @Override
      public void accept(String t) {
        System.out.println("Received " + t + " at " + new Date());
      }
    }
    @Test
    public void monoExample() throws InterruptedException {
      Mono<String> stubMonoWithADelay = 
      Mono.just("Ranga").delayElement(Duration.ofSeconds(5));
      stubMonoWithADelay.subscribe(new SystemOutConsumer());
      Thread.sleep(10000);
     }

重要事项如下:

  • class SystemOutConsumer implements Consumer<String>:我们创建了一个实现函数接口ConsumerSystemOutConsumer类。输入类型为String

  • public void accept(String t):我们定义 accept 方法来将字符串的内容打印到控制台。

  • stubMonoWithADelay.subscribe(new SystemOutConsumer()):我们创建了一个SystemOutConsumer的实例来订阅事件。

输出显示在以下截图中:

我们可以有多个订阅者监听来自 Mono 或 Flux 的事件。以下代码片段显示了如何创建额外的订阅者:

    class WelcomeConsumer implements Consumer<String> {
      @Override
      public void accept(String t) {
        System.out.println("Welcome " + t);
      } 
    }
    @Test
    public void monoExample() throws InterruptedException {
      Mono<String> stubMonoWithADelay = 
      Mono.just("Ranga").delayElement(Duration.ofSeconds(5));
      stubMonoWithADelay.subscribe(new SystemOutConsumer());
      stubMonoWithADelay.subscribe(new WelcomeConsumer());
      Thread.sleep(10000);
    }

重要事项如下:

  • class WelcomeConsumer implements Consumer<String>:我们正在创建另一个 Consumer 类,WelcomeConsumer

  • stubMonoWithADelay.subscribe(new WelcomeConsumer()):我们将WelcomeConsumer的一个实例添加为 Mono 事件的订阅者

输出显示在以下截图中:

Flux

Flux 代表一个发出 0 到n个元素的响应流。以下代码片段显示了一个简单的 Flux 示例:

    @Test
    public void simpleFluxStream() {
      Flux<String> stubFluxStream = Flux.just("Jane", "Joe");
      stubFluxStream.subscribe(new SystemOutConsumer());  
    }

重要事项如下:

  • Flux<String> stubFluxStream = Flux.just("Jane", "Joe"):我们使用Flux.just方法创建了一个 Flux。它可以创建包含硬编码元素的简单流。

  • stubFluxStream.subscribe(new SystemOutConsumer()):我们在 Flux 上注册了一个SystemOutConsumer的实例作为订阅者。

输出显示在以下截图中:

以下代码片段显示了一个具有两个订阅者的 Flux 的更复杂的示例:

    private static List<String> streamOfNames = 
    Arrays.asList("Ranga", "Adam", "Joe", "Doe", "Jane");
    @Test
    public void fluxStreamWithDelay() throws InterruptedException {
      Flux<String> stubFluxWithNames = 
      Flux.fromIterable(streamOfNames)
     .delayElements(Duration.ofMillis(1000));
      stubFluxWithNames.subscribe(new SystemOutConsumer());
      stubFluxWithNames.subscribe(new WelcomeConsumer());
      Thread.sleep(10000);
    }

重要事项如下:

  • Flux.fromIterable(streamOfNames).delayElements(Duration.ofMillis(1000)):从指定的字符串列表创建一个 Flux。元素在指定的 1000 毫秒延迟后发出。

  • stubFluxWithNames.subscribe(new SystemOutConsumer())stubFluxWithNames.subscribe(new WelcomeConsumer()):我们在 Flux 上注册了两个订阅者。

  • Thread.sleep(10000):与第一个 Mono 示例类似,我们引入了 sleep 来使程序等待直到 Flux 发出的所有元素都被发出。

输出显示在以下截图中:

Spring Web Reactive

Spring Web Reactive是 Spring Framework 5 中的一个重要新功能。它为 Web 应用程序带来了响应式能力。

Spring Web Reactive 基于与 Spring MVC 相同的基本编程模型。以下表格提供了两个框架的快速比较:

. Spring MVC Spring Web Reactive
用途 传统的 Web 应用程序 响应式 Web 应用程序
编程模型 @Controller with @RequestMapping 与 Spring MVC 相同
基本 API Servlet API 响应式 HTTP
运行在 Servlet 容器 Servlet 容器(>3.1)、Netty 和 Undertow

在随后的步骤中,我们希望为 Spring Web Reactive 实现一个简单的用例。

以下是涉及的重要步骤:

  • 使用 Spring Initializr 创建项目

  • 创建返回事件流(Flux)的反应式控制器

  • 创建 HTML 视图

使用 Spring Initializr 创建项目

让我们从使用 Spring Initializr(start.spring.io/)创建一个新项目开始。以下屏幕截图显示了详细信息:

需要注意的几点如下:

  • 组:com.mastering.spring.reactive

  • Artifact:spring-reactive-example

  • 依赖项:ReactiveWeb(用于构建反应式 Web 应用程序)和DevTools(用于在应用程序代码更改时进行自动重新加载)

下载项目并将其作为 Maven 项目导入到您的 IDE 中。

pom.xml文件中的重要依赖项如下所示:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
   </dependency>

   <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-webflux</artifactId>
   </dependency>

   <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-test</artifactId>
     <scope>test</scope>
   </dependency>

spring-boot-starter-webflux依赖项是 Spring Web Reactive 的最重要的依赖项。快速查看spring-boot-starter-webfluxpom.xml文件,可以看到 Spring Reactive 的构建块--spring-webfluxspring-webspring-boot-starter-reactor-netty

Netty是默认的嵌入式反应式服务器。以下代码段显示了依赖项:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-reactor-netty</artifactId>
    </dependency>

    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
    </dependency>

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webflux</artifactId>
    </dependency>

创建一个反应式控制器

创建 Spring Reactive Controller 与创建 Spring MVC Controller 非常相似。基本结构相同:@RestController和不同的@RequestMapping注解。以下代码段显示了一个名为StockPriceEventController的简单反应式控制器:

    @RestController
    public class StockPriceEventController {
      @GetMapping("/stocks/price/{stockCode}")
      Flux<String> retrieveStockPriceHardcoded
      (@PathVariable("stockCode") String stockCode) {
        return Flux.interval(Duration.ofSeconds(5))
        .map(l -> getCurrentDate() + " : " 
        + getRandomNumber(100, 125))
        .log();
      }
     private String getCurrentDate() {
       return (new Date()).toString();
     }
     private int getRandomNumber(int min, int max) {
       return ThreadLocalRandom.current().nextInt(min, max + 1);
     }
    }

需要注意的几点如下:

  • @RestController@GetMapping("/stocks/price/{stockCode}"):基本结构与 Spring MVC 相同。我们正在创建一个映射到指定 URI 的映射。

  • Flux<String> retrieveStockPriceHardcoded(@PathVariable("stockCode") String stockCode):Flux 表示 0 到n个元素的流。返回类型Flux<String>表示该方法返回表示股票当前价格的值的流。

  • Flux.interval().map(l -> getCurrentDate() + " : " + getRandomNumber(100, 125)):我们正在创建一个硬编码的 Flux,返回一系列随机数。

  • Duration.ofSeconds(5): 每 5 秒返回一次流元素。

  • Flux.<<****>>.log(): 在 Flux 上调用log()方法有助于观察所有 Reactive Streams 信号并使用 Logger 支持对其进行跟踪。

  • private String getCurrentDate():将当前时间作为字符串返回。

  • private int getRandomNumber(int min, int max):返回minmax之间的随机数。

创建 HTML 视图

在上一步中,我们将 Flux 流映射到"/stocks/price/{stockCode}" URL。在这一步中,让我们创建一个视图来在屏幕上显示股票的当前价值。

我们将创建一个简单的静态 HTML 页面(resources/static/stock-price.html),其中包含一个按钮,用于开始从流中检索。以下代码段显示了 HTML:

    <p>
      <button id="subscribe-button">Get Latest IBM Price</button>
      <ul id="display"></ul>
    </p>

我们想要创建一个 JavaScript 方法来注册到流中,并将新元素附加到特定的 div。以下代码段显示了 JavaScript 方法:

    function registerEventSourceAndAddResponseTo(uri, elementId) {
      var stringEvents = document.getElementById(elementId); 
      var stringEventSource = new (uri);
      stringEventSource.onmessage = function(e) {
        var newElement = document.createElement("li");
        newElement.innerHTML = e.data;
        stringEvents.appendChild(newElement);
      }
    }

EventSource接口用于接收服务器发送的事件。它通过 HTTP 连接到服务器,并以 text/event-stream 格式接收事件。当它接收到一个元素时,将调用onmessage方法。

以下代码段显示了注册获取最新 IBM 价格按钮的 onclick 事件的代码:

    addEvent("click", document.getElementById('subscribe-button'), 
    function() {
            registerEventSourceAndAddResponseTo("/stocks/price/IBM", 
            "display"); 
          }
     );
     function addEvent(evnt, elem, func) {
       if (typeof(EventSource) !== "undefined") {
         elem.addEventListener(evnt,func,false);
       }
       else { // No much to do
         elem[evnt] = func;
       }
    }

启动 SpringReactiveExampleApplication

将应用类SpringReactiveExampleApplication作为 Java 应用程序启动。在启动日志中,您将看到的最后一条消息之一是Netty started on port(s): 8080。Netty 是 Spring Reactive 的默认嵌入式服务器。

当您导航到localhost:8080/static/stock-price.html URL 时,以下屏幕截图显示了浏览器:

当点击“获取最新的 IBM 价格”按钮时,EventSource开始注册从"/stocks/price/IBM"接收事件。一旦接收到元素,它就会显示在屏幕上。

下一个截图显示了在接收到一些事件后屏幕上的情况。您可以观察到每隔 5 秒接收到一个事件:

下一个截图显示了在关闭浏览器窗口后日志中的一部分内容:

您可以观察到一系列onNext方法调用,这些调用会在元素可用时触发。当关闭浏览器窗口时,将调用cancel()方法来终止流。

在这个例子中,我们创建了一个控制器返回一个事件流(作为Flux),并且一个网页使用EventSource注册到事件流。在下一个例子中,让我们看看如何将事件流的范围扩展到数据库。

响应式数据库

所有普通数据库操作都是阻塞的;也就是说,线程会等待直到从数据库接收到响应。

为了充分利用响应式编程,端到端的通信必须是响应式的,也就是基于事件流的。

ReactiveMongo旨在是响应式的,避免阻塞操作。所有操作,包括选择、更新或删除,都会立即返回。数据可以使用事件流流入和流出数据库。

在本节中,我们将使用 Spring Boot 响应式 MongoDB 启动器创建一个简单的示例,连接到 ReactiveMongo。

涉及以下步骤:

  1. 集成 Spring Boot 响应式 MongoDB 启动器。

  2. 创建股票文档的模型对象。

  3. 创建reactiveCrudRepository

  4. 使用命令行运行器初始化股票数据。

  5. 在 Rest Controller 中创建响应式方法。

  6. 更新视图以订阅事件流。

集成 Spring Boot 响应式 MongoDB 启动器

为了连接到 ReactiveMongo 数据库,Spring Boot 提供了一个启动项目--Spring Boot 响应式 MongoDB 启动器。让我们将其添加到我们的pom.xml文件中:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-mongodb-
        reactive</artifactId>
    </dependency>

spring-boot-starter-data-mongodb-reactive启动器引入了spring-data-mongodbmongodb-driver-asyncmongodb-driver-reactivestreams依赖项。以下代码片段显示了spring-boot-starter-data-mongodb-reactive启动器中的重要依赖项:

    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-mongodb</artifactId>
      <exclusions>
       <exclusion>
         <groupId>org.mongodb</groupId>
         <artifactId>mongo-java-driver</artifactId>
       </exclusion>
      <exclusion>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
      </exclusion>
     </exclusions>
    </dependency>
    <dependency>
     <groupId>org.mongodb</groupId>
     <artifactId>mongodb-driver</artifactId>
    </dependency>
    <dependency>
     <groupId>org.mongodb</groupId>
     <artifactId>mongodb-driver-async</artifactId>
    </dependency>
    <dependency>
     <groupId>org.mongodb</groupId>
     <artifactId>mongodb-driver-reactivestreams</artifactId>
    </dependency>
    <dependency>
     <groupId>io.projectreactor</groupId>
     <artifactId>reactor-core</artifactId>
    </dependency>

EnableReactiveMongoRepositories注解启用了 ReactiveMongo 的功能。以下代码片段显示了它被添加到SpringReactiveExampleApplication类中:

    @SpringBootApplication
    @EnableReactiveMongoRepositories
    public class SpringReactiveExampleApplication {

创建一个模型对象 - 一个股票文档

我们将创建Stock文档类,如下所示。它包含三个成员变量--codenamedescription

    @Document
    public class Stock {
      private String code;
      private String name;
      private String description;
        //Getters, Setters and Constructor  
    }

创建一个 ReactiveCrudRepository

传统的 Spring Data 存储库是阻塞的。Spring Data 引入了一个新的存储库用于与响应式数据库交互。以下代码显示了ReactiveCrudRepository接口中声明的一些重要方法:

    @NoRepositoryBean
    public interface ReactiveCrudRepository<T, ID extends Serializable> 
    extends Repository<T, ID> {
      <S extends T> Mono<S> save(S entity);
      Mono<T> findById(ID id);
      Mono<T> findById(Mono<ID> id);
      Mono<Boolean> existsById(ID id);
      Flux<T> findAll();
      Mono<Long> count();
      Mono<Void> deleteById(ID id);
      Mono<Void> deleteAll();  
     }

在前面的接口中的所有方法都是非阻塞的。它们返回的是 Mono 或 Flux,可以在触发事件时用来检索元素。

我们想要为股票文档对象创建一个存储库。以下代码片段显示了StockMongoReactiveCrudRepository的定义。我们使用Stock作为被管理的文档,并且键的类型为String来扩展ReactiveCrudRepository

    public interface StockMongoReactiveCrudRepository 
    extends ReactiveCrudRepository<Stock, String> { 
     }

使用命令行运行器初始化股票数据

让我们使用命令行运行器向 ReactiveMongo 插入一些数据。以下代码片段显示了添加到SpringReactiveExampleApplication的详细信息:

    @Bean
    CommandLineRunner initData(
    StockMongoReactiveCrudRepository mongoRepository) {
      return (p) -> {
      mongoRepository.deleteAll().block();
      mongoRepository.save(
      new Stock("IBM", "IBM Corporation", "Desc")).block();
      mongoRepository.save(
      new Stock("GGL", "Google", "Desc")).block();
      mongoRepository.save(
      new Stock("MST", "Microsoft", "Desc")).block();
     };
    }

mongoRepository.save()方法用于将Stock文档保存到 ReactiveMongo。block()方法确保在执行下一条语句之前保存操作已完成。

在 Rest Controller 中创建响应式方法

现在我们可以添加控制器方法来使用StockMongoReactiveCrudRepository检索详细信息:

    @RestController
    public class StockPriceEventController {
      private final StockMongoReactiveCrudRepository repository;
      public StockPriceEventController(
      StockMongoReactiveCrudRepository repository) {
        this.repository = repository;
     }

   @GetMapping("/stocks")
   Flux<Stock> list() {
     return this.repository.findAll().log();
   }

   @GetMapping("/stocks/{code}")
   Mono<Stock> findById(@PathVariable("code") String code) {
     return this.repository.findById(code).log();
   }
  }

以下是一些重要事项需要注意:

  • private final StockMongoReactiveCrudRepository repositoryStockMongoReactiveCrudRepository通过构造函数注入。

  • @GetMapping("/stocks") Flux<Stock> list():公开一个GET方法来检索股票列表。返回一个 Flux,表示这将是一个股票流。

  • @GetMapping("/stocks/{code}") Mono<Stock> findById(@PathVariable("code") String code)findById返回一个 Mono,表示它将返回 0 或 1 个股票元素。

更新视图以订阅事件流

我们希望更新视图,添加新按钮来触发事件以列出所有股票并显示特定股票的详细信息。以下代码显示了要添加到resources\static\stock-price.html的代码:

    <button id="list-stocks-button">List All Stocks</button>
    <button id="ibm-stock-details-button">Show IBM Details</button>

以下代码片段启用了新按钮的点击事件,触发与它们各自事件的连接:

    <script type="application/javascript">
    addEvent("click", 
    document.getElementById('list-stocks-button'), 
    function() {
      registerEventSourceAndAddResponseTo("/stocks","display"); 
     }
    );
    addEvent("click", 
    document.getElementById('ibm-stock-details-button'), 
    function() {
      registerEventSourceAndAddResponseTo("/stocks/IBM","display"); 
    }
    );
    </script>

启动 SpringReactiveExampleApplication

启动 MongoDB 和SpringReactiveExampleApplication类。以下截图显示了在http://localhost:8080/static/stock-price.html加载页面时的屏幕:

以下截图显示了单击股票列表时的屏幕:

以下截图显示了单击显示 IBM 详细信息按钮时的屏幕:

总结

在本章中,我们快速了解了响应式编程的世界。我们讨论了 Java 响应式世界中的重要框架--Reactive Streams、Reactor 和 Spring Web Flux。我们使用事件流实现了一个简单的网页。

响应式编程并非万能之策。虽然它可能并非所有用例的正确选择,但它是您应该评估的可能选择。它的语言、框架支持和响应式编程的使用处于初期阶段。

在下一章中,我们将继续讨论使用 Spring Framework 开发应用程序的最佳实践。

第十二章:Spring 最佳实践

在前几章中,我们讨论了一些 Spring 项目--Spring MVC、Spring Boot、Spring Cloud、Spring Cloud Data Flow 和 Spring Reactive。企业应用程序开发的挑战并不仅仅是选择正确的框架。最大的挑战之一是正确使用这些框架。

在本章中,我们将讨论使用 Spring 框架进行企业应用程序开发的最佳实践。我们将讨论以下相关的最佳实践:

  • 企业应用程序的结构

  • Spring 配置

  • 管理依赖版本

  • 异常处理

  • 单元测试

  • 集成测试

  • 会话管理

  • 缓存

  • 日志记录

Maven 标准目录布局

Maven 为所有项目定义了标准目录布局。一旦所有项目采用了这种布局,开发人员就可以轻松地在项目之间切换。

以下截图显示了一个 Web 项目的示例目录布局:

以下是一些重要的标准目录:

  • src/main/java:所有与应用程序相关的源代码

  • src/main/resources:所有与应用程序相关的资源--Spring 上下文文件、属性文件、日志配置等

  • src/main/webapp:与 Web 应用程序相关的所有资源--视图文件(JSP、视图模板、静态内容等)

  • src/test/java:所有单元测试代码

  • src/test/resources:所有与单元测试相关的资源

分层架构

关注点分离(SoC)是核心设计目标之一。无论应用程序或微服务的大小如何,创建分层架构都是一种良好的实践。

分层架构中的每一层都有一个关注点,并且应该很好地实现它。分层应用程序还有助于简化单元测试。每个层中的代码可以通过模拟以下层来完全进行单元测试。以下图显示了典型微服务/ Web 应用程序中一些重要的层:

前面图表中显示的层如下:

  • 呈现层:在微服务中,呈现层是 Rest 控制器所在的地方。在典型的 Web 应用程序中,该层还包含与视图相关的内容--JSP、模板和静态内容。呈现层与服务层交互。

  • 服务层:这充当业务层的外观。不同的视图--移动、Web 和平板电脑,可能需要不同类型的数据。服务层了解它们的需求,并根据呈现层提供正确的数据。

  • 业务层:这是所有业务逻辑的地方。另一个最佳实践是将大部分业务逻辑放入领域模型中。业务层与数据层交互以获取数据,并在其上添加业务逻辑。

  • 持久层:负责从数据库中检索和存储数据。该层通常包含 JPA 映射或 JDBC 代码。

推荐实践

建议为每个层使用不同的 Spring 上下文。这有助于分离每个层的关注点。这也有助于针对特定层的单元测试代码。

应用程序context.xml可用于从所有层导入上下文。这可以是在应用程序运行时加载的上下文。以下是一些可能的 Spring 上下文名称:

  • application-context.xml

  • presentation-context.xml

  • services-context.xml

  • business-context.xml

  • persistence-context.xml

重要层的 API 和实现分离

确保应用程序层之间松耦合的另一个最佳实践是在每个层中拥有单独的 API 和实现模块。以下截图显示了具有两个子模块--API 和 impl 的数据层:

数据pom.xml定义了两个子模块:

    <modules>
      <module>api</module>
      <module>impl</module>
    </modules>

api模块用于定义数据层提供的接口。impl模块用于创建实现。

业务层应该使用数据层的 API 进行构建。业务层不应该依赖于数据层的实现(impl模块)。这有助于在两个层之间创建清晰的分离。数据层的实现可以更改而不影响业务层。

以下片段显示了业务层pom.xml文件中的一部分内容:

    <dependency>
      <groupId>com.in28minutes.example.layering</groupId>
      <artifactId>data-api</artifactId>
    </dependency>

    <dependency>
      <groupId>com.in28minutes.example.layering</groupId>
      <artifactId>data-impl</artifactId>
      <scope>runtime</scope>
    </dependency>

虽然data-api依赖项具有默认范围--compile--,但data-impl依赖项具有运行时范围。这确保了在编译业务层时data-impl模块不可用。

虽然可以为所有层实现单独的APIimpl,但建议至少在业务层中使用。

异常处理

有两种类型的异常:

  • 已检查的异常:当服务方法抛出此异常时,所有使用者方法应该处理或抛出异常

  • 未经检查的异常:使用者方法不需要处理或抛出服务方法抛出的异常

RuntimeException及其所有子类都是未经检查的异常。所有其他异常都是已检查的异常。

已检查的异常会使您的代码难以阅读。请看以下示例:

    PreparedStatement st = null;
    try {
        st = conn.prepareStatement(INSERT_TODO_QUERY);
        st.setString(1, bean.getDescription());
        st.setBoolean(2, bean.isDone());
        st.execute();
        } catch (SQLException e) {
          logger.error("Failed : " + INSERT_TODO_QUERY, e);
          } finally {
            if (st != null) {
              try {
                st.close();
                } catch (SQLException e) {
                // Ignore - nothing to do..
                }
          }
      }

PreparedStatement类中 execute 方法的声明如下所示:

    boolean execute() throws SQLException

SQLException是一个已检查的异常。因此,调用execute()方法的任何方法都应该处理异常或抛出异常。在前面的示例中,我们使用try-catch块处理异常。

Spring 对异常处理的方法

Spring 对这个问题采取了不同的方法。它使大多数异常变成了未经检查的。代码变得简单:

    jdbcTemplate.update(INSERT_TODO_QUERY, 
    bean.getDescription(),bean.isDone());

JDBCTemplate中的 update 方法不声明抛出任何异常。

推荐的方法

我们建议采用与 Spring 框架类似的方法。在决定从方法中抛出什么异常时,始终要考虑方法的使用者。

方法的使用者是否能对异常做些什么?

在前面的示例中,如果查询执行失败,consumer方法将无法做任何事情,除了向用户显示错误页面。在这种情况下,我们不应该复杂化事情并强制使用者处理异常。

我们建议在应用程序中采用以下异常处理方法:

  • 考虑使用者。如果方法的使用者除了记录日志或显示错误页面外无法做任何有用的事情,就将其设置为未经检查的异常。

  • 在最顶层,通常是表示层,要有catch all异常处理来显示错误页面或向使用者发送错误响应。有关实现catch all异常处理的更多详细信息,请参阅第三章中的使用 Spring MVC 构建 Web 应用程序中的@ControllerAdvice

保持 Spring 配置的轻量级

Spring 在注解之前的一个问题是应用程序上下文 XML 文件的大小。应用程序上下文 XML 文件有时会有数百行(有时甚至有数千行)。然而,使用注解后,就不再需要这样长的应用程序上下文 XML 文件了。

我们建议您使用组件扫描来定位和自动装配 bean,而不是在 XML 文件中手动装配 bean。保持应用程序上下文 XML 文件非常小。我们建议您在需要一些与框架相关的配置时使用 Java @Configuration

在 ComponentScan 中使用 basePackageClasses 属性

在使用组件扫描时,建议使用basePackageClasses属性。以下片段显示了一个示例:

    @ComponentScan(basePackageClasses = ApplicationController.class) 
    public class SomeApplication {

basePackageClasses属性是basePackages()的类型安全替代,用于指定要扫描注释组件的包。将扫描每个指定类的包。

这将确保即使包被重命名或移动,组件扫描也能正常工作。

在模式引用中不使用版本号

Spring 可以从依赖项中识别出正确的模式版本。因此,在模式引用中不再需要使用版本号。类片段显示了一个例子:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans 

      xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context/
      http://www.springframework.org/schema/context/spring-
      context.xsd">
      <!-- Other bean definitions-->
    </beans>

优先使用构造函数注入而不是 setter 注入进行强制依赖项

bean 有两种依赖项:

  • 强制依赖项:这些是您希望对 bean 可用的依赖项。如果依赖项不可用,您希望上下文加载失败。

  • 可选依赖项:这些是可选的依赖项。它们并不总是可用。即使这些依赖项不可用,加载上下文也是可以的。

我们建议您使用构造函数注入而不是 setter 注入来连接强制依赖项。这将确保如果缺少强制依赖项,则上下文将无法加载。以下片段显示了一个例子:

    public class SomeClass {
      private MandatoryDependency mandatoryDependency
      private OptionalDependency optionalDependency;
      public SomeClass(MandatoryDependency mandatoryDependency) {
      this.mandatoryDependency = mandatoryDependency;
    }
    public void setOptionalDependency(
    OptionalDependency optionalDependency) {
      this.optionalDependency = optionalDependency;
    }
    //All other logic
   }

Spring 文档的摘录(docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#beans-constructor-injection)如下所示:

Spring 团队通常倡导构造函数注入,因为它使我们能够将应用程序组件实现为不可变对象,并确保所需的依赖项不为空。此外,构造函数注入的组件始终以完全初始化的状态返回给客户端(调用)代码。另外,大量的构造函数参数是糟糕的代码味道,意味着该类可能具有太多的责任,应该进行重构以更好地处理关注点的分离。主要应该仅将 setter 注入用于可以在类内分配合理默认值的可选依赖项。否则,代码使用依赖项的地方必须执行非空检查。setter 注入的一个好处是 setter 方法使该类的对象能够在以后重新配置或重新注入。因此,通过JMX MBeans进行管理是 setter 注入的一个引人注目的用例。

为 Spring 项目管理依赖项版本

如果您正在使用 Spring Boot,则管理依赖项版本的最简单选项是将spring-boot-starter-parent用作父 POM。这是我们在本书中所有项目示例中使用的选项:

    <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>${spring-boot.version}</version>
      <relativePath /> <!-- lookup parent from repository -->
    </parent>

spring-boot-starter-parent管理了 200 多个依赖项的版本。在 Spring Boot 发布之前,确保这些依赖项的所有版本能够很好地协同工作。以下是一些受管依赖项的版本:

<activemq.version>5.14.3</activemq.version>
 <ehcache.version>2.10.3</ehcache.version>
 <elasticsearch.version>2.4.4</elasticsearch.version>
 <h2.version>1.4.193</h2.version>
 <jackson.version>2.8.7</jackson.version>
 <jersey.version>2.25.1</jersey.version>
 <junit.version>4.12</junit.version>
 <mockito.version>1.10.19</mockito.version>
 <mongodb.version>3.4.2</mongodb.version>
 <mysql.version>5.1.41</mysql.version>
 <reactor.version>2.0.8.RELEASE</reactor.version>
 <reactor-spring.version>2.0.7.RELEASE</reactor-spring.version>
 <selenium.version>2.53.1</selenium.version>
 <spring.version>4.3.7.RELEASE</spring.version>
 <spring-amqp.version>1.7.1.RELEASE</spring-amqp.version>
 <spring-cloud-connectors.version>1.2.3.RELEASE</spring-cloud-connectors.version>
 <spring-batch.version>3.0.7.RELEASE</spring-batch.version>
 <spring-hateoas.version>0.23.0.RELEASE</spring-hateoas.version>
 <spring-kafka.version>1.1.3.RELEASE</spring-kafka.version>
 <spring-restdocs.version>1.1.2.RELEASE</spring-restdocs.version>
 <spring-security.version>4.2.2.RELEASE</spring-security.version>
<thymeleaf.version>2.1.5.RELEASE</thymeleaf.version>

建议您不要覆盖项目 POM 文件中受管依赖项的任何版本。这样可以确保当我们升级 Spring Boot 版本时,我们将获得所有依赖项的最新版本升级。

有时,您必须使用自定义公司 POM 作为父 POM。以下片段显示了如何在这种情况下管理依赖项版本:

    <dependencyManagement>
      <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-dependencies</artifactId>
          <version>${spring-boot.version}</version>
          <type>pom</type>
          <scope>import</scope>
        </dependency>
      </dependencies>
    </dependencyManagement>

如果您没有使用 Spring Boot,则可以使用 Spring BOM 管理所有基本的 Spring 依赖项:

    <dependencyManagement>
      <dependencies>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-framework-bom</artifactId>
          <version>${org.springframework-version}</version>
          <type>pom</type>
          <scope>import</scope>
        </dependency>
      </dependencies>
    </dependencyManagement>

单元测试

虽然单元测试的基本目的是查找缺陷,但各层编写单元测试的方法是不同的。在本节中,我们将快速查看各层的单元测试示例和最佳实践。

业务层

在为业务层编写测试时,我们建议您避免在单元测试中使用 Spring 框架。这将确保您的测试是框架无关的,并且运行速度更快。

以下是一个在不使用 Spring 框架的情况下编写的单元测试的示例:

    @RunWith(MockitoJUnitRunner.class)
    public class BusinessServiceMockitoTest {
      private static final User DUMMY_USER = new User("dummy");
      @Mock
      private DataService dataService;
      @InjectMocks
      private BusinessService service = new BusinessServiceImpl();
      @Test
      public void testCalculateSum() {
        BDDMockito.given(dataService.retrieveData(
        Matchers.any(User.class)))
        .willReturn(Arrays.asList(
        new Data(10), new Data(15), new Data(25)));
        long sum = service.calculateSum(DUMMY_USER);
        assertEquals(10 + 15 + 25, sum);
       }
     }

Spring 框架用于在运行应用程序中连接依赖关系。然而,在您的单元测试中,使用@InjectMocks Mockito 注解与@Mock结合使用是最佳选择。

Web 层

Web 层的单元测试涉及测试控制器--REST 和其他。

我们建议以下操作:

  • 在构建在 Spring MVC 上的 Web 层中使用 Mock MVC

  • Jersey 测试框架是使用 Jersey 和 JAX-RS 构建的 REST 服务的不错选择

设置 Mock MVC 框架的一个快速示例如下所示:

    @RunWith(SpringRunner.class)
    @WebMvcTest(TodoController.class)
    public class TodoControllerTest {
      @Autowired
      private MockMvc mvc;
      @MockBean
      private TodoService service;
      //Tests
    }

使用@WebMvcTest将允许我们使用自动装配MockMvc并执行 Web 请求。@WebMVCTest的一个很棒的特性是它只实例化控制器组件。所有其他 Spring 组件都预期被模拟,并可以使用@MockBean进行自动装配。

数据层

Spring Boot 为数据层单元测试提供了一个简单的注解@DataJpaTest。一个简单的示例如下所示:

    @DataJpaTest
    @RunWith(SpringRunner.class)
    public class UserRepositoryTest {
      @Autowired
      UserRepository userRepository;
      @Autowired
      TestEntityManager entityManager;
     //Test Methods
    }

@DataJpaTest也可能注入一个TestEntityManager bean,它提供了一个专门为测试设计的替代标准 JPA entityManager

如果您想在@DataJpaTest之外使用TestEntityManager,您也可以使用@AutoConfigureTestEntityManager注解。

数据 JPA 测试默认针对嵌入式数据库运行。这确保了测试可以运行多次而不影响数据库。

其他最佳实践

我们建议您遵循测试驱动开发(TDD)的方法来开发代码。在编写代码之前编写测试可以清楚地了解正在编写的代码单元的复杂性和依赖关系。根据我的经验,这会导致更好的设计和更好的代码。

我参与的最好的项目认识到单元测试比源代码更重要。应用程序会不断发展。几年前的架构今天已经是遗留的。通过拥有出色的单元测试,我们可以不断重构和改进我们的项目。

一些指导方针列如下:

  • 单元测试应该易读。其他开发人员应该能够在不到 15 秒的时间内理解测试。力求编写作为代码文档的测试。

  • 单元测试只有在生产代码中存在缺陷时才应该失败。这似乎很简单。然而,如果单元测试使用外部数据,它们可能会在外部数据更改时失败。随着时间的推移,开发人员对单元测试失去信心。

  • 单元测试应该运行得很快。慢测试很少运行,失去了单元测试的所有好处。

  • 单元测试应该作为持续集成的一部分运行。一旦在版本控制中提交,构建(包括单元测试)应该运行并在失败时通知开发人员。

集成测试

虽然单元测试测试特定层,但集成测试用于测试多个层中的代码。为了保持测试的可重复性,我们建议您在集成测试中使用嵌入式数据库而不是真实数据库。

我们建议您为使用嵌入式数据库的集成测试创建一个单独的配置文件。这样可以确保每个开发人员都有自己的数据库来运行测试。让我们看几个简单的例子。

application.properties文件:

    app.profiles.active: production

application-production.properties文件:

    app.jpa.database: MYSQL
    app.datasource.url: <<VALUE>>
    app.datasource.username: <<VALUE>>
    app.datasource.password: <<VALUE>>

application-integration-test.properties文件:

    app.jpa.database: H2
    app.datasource.url=jdbc:h2:mem:mydb
    app.datasource.username=sa
    app.datasource.pool-size=30

我们需要在测试范围内包含 H2 驱动程序依赖项,如下面的代码片段所示:

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
   </dependency>

   <dependency>
     <groupId>com.h2database</groupId>
     <artifactId>h2</artifactId>
     <scope>test</scope>
   </dependency>

使用@ActiveProfiles("integration-test")的集成测试示例如下所示。集成测试现在将使用嵌入式数据库运行:

    @ActiveProfiles("integration-test")
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = Application.class, webEnvironment =    
    SpringBootTest.WebEnvironment.RANDOM_PORT)
    public class TodoControllerIT {
      @LocalServerPort
      private int port;
      private TestRestTemplate template = new TestRestTemplate();
      //Tests
    }

集成测试对于能够持续交付可工作软件至关重要。Spring Boot 提供的功能使得实现集成测试变得容易。

Spring Session

管理会话状态是分发和扩展 Web 应用程序中的重要挑战之一。HTTP 是一种无状态协议。用户与 Web 应用程序的交互状态通常在 HttpSession 中管理。

在会话中尽可能少地保存数据是很重要的。专注于识别和删除会话中不需要的数据。

考虑一个具有三个实例的分布式应用程序,如下所示。每个实例都有自己的本地会话副本:

想象一下,用户当前正在从App Instance 1提供服务。假设App Instance 1关闭,负载均衡器将用户发送到App Instance 2App Instance 2不知道App Instance 1中可用的会话状态。用户必须重新登录并重新开始。这不是一个良好的用户体验。

Spring Session 提供了将会话存储外部化的功能。Spring Session 提供了将会话状态存储到不同数据存储的替代方法,而不是使用本地 HttpSession:

Spring Session 还提供了明确的关注点分离。无论使用哪种会话数据存储,应用程序代码都保持不变。我们可以通过配置在会话数据存储之间切换。

示例

在此示例中,我们将连接 Spring Session 以使用 Redis 会话存储。虽然将数据放入会话的代码保持不变,但数据将存储到 Redis 而不是 HTTP 会话中。

涉及三个简单的步骤:

  1. 添加 Spring Session 的依赖项。

  2. 配置过滤器以用 Spring Session 替换 HttpSession。

  3. 通过扩展AbstractHttpSessionApplicationInitializer启用 Tomcat 的过滤。

添加 Spring Session 的依赖项

连接到 Redis 存储的 Spring Session 所需的依赖项是spring-session-data-redislettuce-core

    <dependency>
      <groupId>org.springframework.session</groupId>
      <artifactId>spring-session-data-redis</artifactId>
      <type>pom</type>
    </dependency>

   <dependency>
     <groupId>io.lettuce</groupId>
     <artifactId>lettuce-core</artifactId>
   </dependency>

配置过滤器以用 Spring Session 替换 HttpSession

以下配置创建了一个 Servlet 过滤器,用 Spring Session 中的会话实现替换HTTPSession--在此示例中为 Redis 数据存储:

    @EnableRedisHttpSession 
    public class ApplicationConfiguration {
      @Bean 
      public LettuceConnectionFactory connectionFactory() {
        return new LettuceConnectionFactory(); 
      } 
   }

通过扩展 AbstractHttpSessionApplicationInitializer 启用 Tomcat 的过滤

在上一步中,需要在每个请求到 Servlet 容器(Tomcat)上启用 Servlet 过滤器。以下代码段显示了涉及的代码:

    public class Initializer 
    extends AbstractHttpSessionApplicationInitializer {
      public Initializer() {
        super(ApplicationConfiguration.class); 
      }
    }

这就是您需要的所有配置。Spring Session 的好处在于,您的应用程序代码与HTTPSession通信不会改变!您可以继续使用 HttpSession 接口,但在后台,Spring Session 确保会话数据存储到外部数据存储--在此示例中为 Redis:

    req.getSession().setAttribute(name, value);

Spring Session 提供了连接到外部会话存储的简单选项。在外部会话存储上备份会话可以确保用户即使在一个应用程序实例关闭时也能故障转移。

缓存

缓存是构建高性能应用程序的必要条件。您不希望一直访问外部服务或数据库。不经常更改的数据可以被缓存。

Spring 提供了透明的机制来连接和使用缓存。启用应用程序缓存涉及以下步骤:

  1. 添加 Spring Boot Starter Cache 依赖项。

  2. 添加缓存注释。

让我们详细讨论这些。

添加 Spring Boot Starter Cache 依赖项

以下代码段显示了spring-boot-starter-cache依赖项。它引入了配置缓存所需的所有依赖项和自动配置:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>

添加缓存注释

下一步是添加缓存注释,指示何时需要向缓存中添加或删除内容。以下代码段显示了一个示例:

    @Component
    public class ExampleRepository implements Repository {
      @Override
      @Cacheable("something-cache-key")
      public Something getSomething(String id) {
          //Other code
      }
    }

支持的一些注释如下:

  • 可缓存:用于缓存方法调用的结果。默认实现根据传递给方法的参数构造键。如果在缓存中找到值,则不会调用该方法。

  • CachePut:类似于 @Cacheable。一个重要的区别是该方法总是被调用,并且结果被放入缓存中。

  • CacheEvict:触发从缓存中清除特定元素。通常在元素被删除或更新时执行。

关于 Spring 缓存的另外一些重要事项如下:

  • 默认使用的缓存是 ConcurrentHashMap

  • Spring 缓存抽象符合 JSR-107 标准

  • 可以自动配置的其他缓存包括 EhCache、Redis 和 Hazelcast

日志记录

Spring 和 Spring Boot 依赖于 Commons Logging API。它们不依赖于任何其他日志记录框架。Spring Boot 提供了 starter 来简化特定日志记录框架的配置。

Logback

Starter spring-boot-starter-logging 是使用 Logback 框架所需的全部内容。这个依赖是大多数 starter 中包含的默认日志记录。包括 spring-boot-starter-web。依赖关系如下所示:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>

以下片段显示了 spring-boot-starter-logging 中包含的 logback 和相关依赖项:

    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
    </dependency>

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
    </dependency>

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jul-to-slf4j</artifactId>
    </dependency>

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>log4j-over-slf4j</artifactId>
    </dependency>

Log4j2

要使用 Log4j2,我们需要使用 starter spring-boot-starter-log4j2。当我们使用 spring-boot-starter-web 等 starter 时,我们需要确保在 spring-boot-starter-logging 中排除该依赖项。以下片段显示了详细信息:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
      <exclusions>
        <exclusion>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
       </exclusions>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>

以下片段显示了 spring-boot-starter-log4j2 starter 中使用的依赖项:

    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-slf4j-impl</artifactId>
    </dependency>

   <dependency>
     <groupId>org.apache.logging.log4j</groupId>
     <artifactId>log4j-api</artifactId>
   </dependency>

   <dependency>
     <groupId>org.apache.logging.log4j</groupId>
     <artifactId>log4j-core</artifactId>
   </dependency>

  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
  </dependency>

框架独立配置

无论使用哪种日志记录框架,Spring Boot 都允许在应用程序属性中进行一些基本配置选项。一些示例如下所示:

   logging.level.org.springframework.web=DEBUG
   logging.level.org.hibernate=ERROR 
   logging.file=<<PATH_TO_LOG_FILE>>

在微服务时代,无论您使用哪种框架进行日志记录,我们建议您将日志记录到控制台(而不是文件),并使用集中式日志存储工具来捕获所有微服务实例的日志。

摘要

在本章中,我们介绍了开发基于 Spring 的应用程序的一些最佳实践。我们涵盖了在项目结构化方面的最佳实践--分层、遵循 Maven 标准目录布局,并使用api和 implementation 模块。我们还讨论了如何将 Spring 配置保持最小化的最佳实践。我们还讨论了与日志记录、缓存、会话管理和异常处理相关的最佳实践。

第十三章:在 Spring 中使用 Kotlin

Kotlin 是一种静态类型的 JVM 语言,可以编写富有表现力、简短和可读的代码。Spring Framework 5.0 对 Kotlin 有很好的支持。

在本章中,我们将探讨 Kotlin 的一些重要特性,并学习如何使用 Kotlin 和 Spring Boot 创建基本的 REST 服务。

通过本章,您将了解以下内容:

  • Kotlin 是什么?

  • 它与 Java 相比如何?

  • 如何在 Eclipse 中创建 Kotlin 项目?

  • 如何在 Spring Boot 中创建一个 Kotlin 项目?

  • 如何使用 Kotlin 实现和单元测试一个简单的 Spring Boot REST 服务?

Kotlin

Kotlin 是一种开源的静态类型语言,可用于构建在 JVM、Android 和 JavaScript 平台上运行的应用程序。Kotlin 由 JetBrains 在 Apache 2.0 许可下开发,源代码可在 GitHub 上获得(github.com/jetbrains/kotlin)。

以下是 Kotlin 的首席语言设计师 Andrey Breslav 的一些引用。这些引用有助于我们了解 Kotlin 背后的思维过程:

Project Kotlin 的主要目的是为开发人员创建一种通用语言,可以作为一种安全、简洁、灵活且 100%兼容 Java 的有用工具。

Kotlin 旨在成为一种工业级的面向对象语言,并且比 Java 更好,但仍然可以完全与 Java 代码互操作,允许公司逐步从 Java 迁移到 Kotlin。

Kotlin 是 Android 支持的官方语言之一。官方的 Android 开发者页面为 Kotlin(developer.android.com/kotlin/index.html)强调了 Kotlin 为何迅速受到开发人员欢迎的重要原因:

Kotlin 是一种富有表现力、简洁、可扩展、强大且令人愉悦的阅读和编写的语言。它在空值和不可变性方面具有出色的安全功能,这与我们的投资相一致,使 Android 应用默认情况下健康且性能良好。最重要的是,它与我们现有的 Android 语言和运行时是可互操作的。

Kotlin 的一些重要内容包括以下内容:

  • 与 Java 完全兼容。您可以从 Kotlin 调用 Java 代码,反之亦然。

  • 简洁且易读的语言。Kotlin FAQ(kotlinlang.org/docs/reference/faq.html)估计代码行数减少了 40%。

  • 支持函数式和面向对象编程。

  • IntelliJ IDEA、Android Studio、Eclipse 和 NetBeans 是支持 Kotlin 的 IDE。虽然支持程度不及 Java,但每天都在改进。

  • 所有主要的构建工具——Gradle、Maven 和 Ant——都支持构建 Kotlin 项目。

Kotlin 与 Java 的比较

Java 是由 Sun Microsystems 的 James Gosling 开发并于 1995 年发布的。至今已经保持了 20 多年的流行。

Java 受欢迎的一个重要原因是 Java 平台,包括 Java 虚拟机(JVM)。Java 平台为 Java 语言提供了安全性和可移植性。在过去几年中出现了许多旨在利用 Java 平台优势的语言。它们编译为字节码,可以在 JVM 上运行。这些语言包括以下框架:

  • Clojure

  • Groovy

  • Scala

  • JRuby

  • Jython

Kotlin 旨在解决 Java 语言中的一些重要问题,并提供简洁的替代方案。与 Java 语言的一些重要区别如下。

变量和类型推断

Kotlin 从赋给它的值推断变量的类型。在以下示例中,intVariable被赋予了Int类型:

    //Type Inference
    var intVariable = 10

由于 Kotlin 是类型安全的,如果取消注释以下代码片段,将导致编译错误:

    //intVariable = "String" 
    //If uncommented -> Type mismatch: 
    //inferred type is String but Int was expected

变量和不可变性

通常,像其他所有编程语言一样,变量的值可以更改。以下代码片段显示了一个例子:

    var variable = 5
    variable = 6 //You can change value

但是,如果使用val(而不是var)来定义变量,那么变量是不可变的。变量的值不能被改变。这类似于 Java 中的final变量。考虑以下代码:

    val immutable = 6
    //immutable = 7 //Val cannot be reassigned

类型系统

在 Kotlin 中,一切都是对象。没有原始变量。

以下是重要的数字类型:

  • 双精度--64 位

  • 浮点型--32 位

  • 长整型--64 位

  • 整型--32 位

  • 短整型--16 位

  • 字节--8 位

与 Java 不同,Kotlin 不将字符视为数字类型。对字符的任何数字操作都将导致编译错误。考虑以下代码:

    var char = 'c'
    //Operator '==' cannot be applied to 'Char' and 'Int'
    //if(char==1) print (char);
    Null safety

Java 程序员非常熟悉java.lang.NullPointerException。对空对象变量执行的任何操作都会抛出NullPointerException

Kotlin 的类型系统旨在消除空指针异常。普通变量不能持有 null。如果取消注释,以下代码片段将无法编译:

    var string: String = "abc"
    //string = null //Compilation Error

为了能够在变量中存储 null,需要使用特殊声明。即,类型后跟一个?。例如,考虑以下String?

    var nullableString: String? = "abc"
    nullableString = null

一旦变量声明为可空,只允许安全的(?)或非空断言(!!.)调用。直接引用将导致编译错误

    //Compilation Error
    //print(nullableString.length)
    if (nullableString != null) {
      print(nullableString.length)
     }
    print(nullableString?.length)

函数

在 Kotlin 中,使用fun关键字声明函数。以下代码片段显示了一个例子:

    fun helloBasic(name: String): String {
      return "Hello, $name!"
    }

函数参数在函数名后的括号中指定。nameString类型的参数。返回类型在参数后指定。函数的返回类型是String

以下代码行显示了对helloBasic函数的调用:

    println(helloBasic("foo")) // => Hello, foo!

Kotlin 还允许 n。以下代码行显示了一个例子:

    println(helloBasic(name = "bar"))

函数参数可以选择具有默认值

    fun helloWithDefaultValue(name: String = "World"): String {
      return "Hello, $name!"
    }

以下代码行显示了对helloWithDefaultValue函数的调用,而不指定任何参数。使用了 name 参数的默认值:

    println(helloWithDefaultValue()) //Hello, World

如果一个函数只有一个表达式,那么它可以在一行上定义。helloWithOneExpression函数是helloWithDefaultValue函数的简化版本。返回类型从值中推断出来

    fun helloWithOneExpression(name: String = "world") 
    = "Hello, $name!"

返回 void 并且只有一个表达式的函数也可以在一行上定义。以下代码片段显示了一个例子:

    fun printHello(name: String = "world") 
    = println("Hello, $name!")

数组

在 Kotlin 中,数组由Array类表示。以下代码片段显示了Array类中的一些重要属性和方法:

    class Array<T> private constructor() {
      val size: Int
      operator fun get(index: Int): T
      operator fun set(index: Int, value: T): Unit
      operator fun iterator(): Iterator<T>
      // ...
     }

可以使用intArrayOf函数创建数组

    val intArray = intArrayOf(1, 2, 10)

以下代码片段显示了可以在数组上执行的一些重要操作:

    println(intArray[0])//1
    println(intArray.get(0))//1
    println(intArray.all { it > 5 }) //false
    println(intArray.any { it > 5 }) //true
    println(intArray.asList())//[1, 2, 10]
    println(intArray.max())//10
    println(intArray.min())//1

集合

Kotlin 有简单的函数来初始化集合。以下代码行显示了初始化列表的示例:

    val countries = listOf("India", "China", "USA")

以下代码片段显示了可以在列表上执行的一些重要操作:

    println(countries.size)//3
    println(countries.first())//India
    println(countries.last())//USA
    println(countries[2])//USA

在 Kotlin 中,使用listOf创建的列表是不可变的。要能够更改列表的内容,需要使用mutableListOf函数

    //countries.add("China") //Not allowed
    val mutableContries = mutableListOf("India", "China", "USA")
    mutableContries.add("China")

mapOf函数用于初始化地图,如下面的代码片段所示:

    val characterOccurances = 
    mapOf("a" to 1, "h" to 1, "p" to 2, "y" to 1)//happy
    println(characterOccurances)//{a=1, h=1, p=2, y=1}

以下代码行显示了检索特定键的值:

    println(characterOccurances["p"])//2

地图可以在循环中解构为其键值组成部分。以下代码行显示了详细信息:

    for ((key, value) in characterOccurances) {
      println("$key -> $value")
    }

没有 c

在 Java 中,必须处理或重新抛出已检查的异常。这导致了许多不必要的代码。以下示例显示了如何处理try catch块抛出的new FileReader("pathToFile") - throws FileNotFoundExceptionreader.read() - throws IOException的已检查异常:

    public void openSomeFileInJava(){
      try {
            FileReader reader = new FileReader("pathToFile");
            int i=0;
            while(i != -1){
              i = reader.read();
              //Do something with what was read
            }
      reader.close();
      } catch (FileNotFoundException e) {
           //Exception handling code
        } catch (IOException e) {
        //Exception handling code
      }
    }

Kotlin 没有任何已检查的异常。由客户端代码决定是否要处理异常。客户端不强制进行异常处理。

数据类

通常,我们会创建许多 bean 类来保存数据。Kotlin 引入了数据类的概念。以下代码块显示了数据类的声明:

    data class Address(val line1: String,
    val line2: String,
    val zipCode: Int,
    val state: String,
    val country: String)

Kotlin 提供了主构造函数、equals()hashcode()和一些其他用于数据类的实用方法。以下代码显示了使用构造函数创建对象:

    val myAddress = Address("234, Some Apartments", 
    "River Valley Street", 54123, "NJ", "USA")

Kotlin 还提供了toString

    println(myAddress)
    //Address(line1=234, Some Apartments, line2=River Valley 
    //Street, zipCode=54123, state=NJ, country=USA)

copy函数可以用来复制(克隆)现有的数据类对象。以下代码片段显示了细节:

    val myFriendsAddress = myAddress.copy(line1 = "245, Some Apartments")
    println(myFriendsAddress)
    //Address(line1=245, Some Apartments, line2=River Valley 
    //Street, zipCode=54123, state=NJ, country=USA)

数据类的对象可以很容易地被解构。以下代码显示了细节。println使用字符串模板来打印值:

    val (line1, line2, zipCode, state, country) = myAddress;
tln("$line1 $line2 $zipCode $state $country"); 
    //234, Some Apartments River Valley Street 54123 NJ USA

在 Eclipse 中创建一个 Kotlin 项目

在 Eclipse 中使用 Kotlin 之前,我们需要在 Eclipse 中安装 Kotlin 插件。

Kotlin 插件

Kotlin 插件可以从marketplace.eclipse.org/content/kotlin-plugin-eclipse安装。点击以下截图中的安装按钮:

选择 Kotlin 插件并点击确认按钮,如下截图所示:

接受后续步骤中的默认设置来安装插件。安装需要一些时间。安装插件完成后重新启动 Eclipse。

创建一个 Kotlin 项目

现在让我们创建一个新的 Kotlin 项目。在 Eclipse 中,点击文件 | 新建 | 项目...,如下截图所示:

从列表中选择 Kotlin 项目。

Kotlin-Hello-World作为项目名称,接受所有默认设置,然后点击完成。Eclipse 将创建一个新的 Kotlin 项目。

以下截图显示了典型 Kotlin 项目的结构。项目中都有Kotlin Runtime LibraryJRE System Library

创建一个 Kotlin 类

要创建一个新的 Kotlin 类,右键单击文件夹,然后选择新建 | 其他,如下截图所示:

选择类,如下截图所示:

给你的新 Kotlin 类起一个名字(HelloWorld)和一个包(com.mastering.spring.kotlin.first)。点击完成。

创建一个 main 函数,如下代码所示:

    fun main(args: Array<String>) {
      println("Hello, world!")
    }

运行 Kotlin 类

右键单击HelloWorld.kt文件,然后点击运行为 | tlin,如下截图所示:

Hello, World在控制台上打印出来,如下所示:

使用 Kotlin 创建 Spring Boot 项目

我们将使用 Spring Initializr(start.spring.io)来初始化一个 Kotlin 项目。以下截图显示了要选择的 Group 和 ArtifactId:

以下是一些重要的事项:

  • 选择 Web 作为依赖

  • 选择 Kotlin 作为语言(截图顶部的第二个下拉菜单)

  • 点击生成项目并将下载的项目导入 Eclipse 作为 Maven 项目

以下截图显示了生成项目的结构:

以下是一些重要的事项:

  • src/main/kotlin:这是所有 Kotlin 源代码的文件夹。这类似于 Java 项目中的src/main/java

  • src/test/kotlin:这是所有 Kotlin 测试代码的文件夹。这类似于 Java 项目中的src/test/java

  • 资源文件夹与典型的 Java 项目相同--src/main/resourcessrc/test/resources

  • Kotlin 运行库用作执行环境,而不是 JRE。

依赖和插件

除了 Java Spring Boot 项目中的常规依赖项外,pom.xml中还有两个额外的依赖项。

    <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-stdlib-jre8</artifactId>
      <version>${kotlin.version}</version>
    </dependency>

    <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-reflect</artifactId>
      <version>${kotlin.version}</version>
    </dependency>

以下是一些重要事项需要注意:

  • kotlin-stdlib-jre8是支持 Java 8 中添加的新 JDK API 的标准库。

  • kotlin-reflect是在 Java 平台上使用反射功能的运行时组件

除了spring-boot-maven-pluginkotlin-maven-plugin也作为pom.xml中的插件添加。kotlin-maven-plugin编译 Kotlin 源代码和模块。该插件配置为在compiletest-compile阶段使用。以下代码显示了详细信息:

    <plugin>
     <artifactId>kotlin-maven-plugin</artifactId>
     <groupId>org.jetbrains.kotlin</groupId>
     <version>${kotlin.version}</version>
     <configuration>
       <compilerPlugins>
         <plugin>spring</plugin>
       </compilerPlugins>
       <jvmTarget>1.8</jvmTarget>
     </configuration>
    <executions>
    <execution>
      <id>compile</id>
      <phase>compile</phase>
      <goals>
        <goal>compile</goal>
      </goals>
    </execution>
    <execution>
      <id>test-compile</id>
      <phase>test-compile</phase>
      <goals>
        <goal>test-compile</goal>
      </goals>
     </execution>
    </executions>
    <dependencies>
      <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-maven-allopen</artifactId>
        <version>${kotlin.version}</version>
       </dependency>
    </dependencies>
   </plugin>

Spring Boot 应用程序类

以下代码块显示了生成的SpringBootApplicationFirstWebServiceWithKotlinApplication。我们将该类设置为开放以使 Spring Boot 能够覆盖它:

    @SpringBootApplication
    open class FirstWebServiceWithKotlinApplication
    fun main(args: Array<String>) {
      SpringApplication
      .run(
         FirstWebServiceWithKotlinApplication::class.java,
         *args)
    }

以下是一些重要事项需要注意:

  • 包、导入和注解与 Java 类相同。

  • 在 Java 中,主函数的声明是public static void main(String[] args)。在上面的示例中,我们使用了 Kotlin 函数语法。Kotlin 没有静态方法。在类外声明的任何函数都可以在不需要类引用的情况下调用。

  • 在 Java 中启动SpringApplication是使用SpringApplication.run(FirstWebServiceWithKotlinApplication.class, args)完成的。

  • ::用于获取 Kotlin 类的运行时引用。因此,FirstWebServiceWithKotlinApplication::class给我们提供了对 Kotlin 类的运行时引用。要获取 Java 类引用,我们需要在引用上使用.java属性。因此,在 Kotlin 中,语法是FirstWebServiceWithKotlinApplication::class.java

  • 在 Kotlin 中,*被称为扩展操作符。当将数组传递给接受可变参数的函数时使用。因此,我们将使用*args将数组传递给run方法。

该应用程序可以通过将FirstWebServiceWithKotlinApplication作为 Kotlin 应用程序运行来启动。

Spring Boot 应用程序测试类

以下代码片段显示了生成的SpringBootApplicationTestFirstWebServiceWithKotlinApplicationTests

    @RunWith(SpringRunner::class)
    @SpringBootTest
    class FirstWebServiceWithKotlinApplicationTests {
      @Test
      fun contextLoads() {
      }
    }

以下是一些重要事项需要注意:

  • 包、导入和注解与 Java 类相同。

  • ::用于获取 Kotlin 类的运行时引用。与 Java 中的@RunWith(SpringRunner.class)相比,Kotlin 代码使用@RunWith(SpringRunner::class)

  • 测试类的声明使用了 Kotlin 函数语法。

使用 Kotlin 实现 REST 服务

我们将首先创建一个返回硬编码字符串的服务。之后,我们将讨论返回适当的 JSON 响应的示例。我们还将看一个传递路径参数的示例。

返回字符串的简单方法

让我们从创建一个简单的 REST 服务返回welcome消息开始:

    @RestController
    class BasicController {
      @GetMapping("/welcome")
      fun welcome() = "Hello World"
    }

以下是一个可比较的 Java 方法。一个主要的区别是我们如何能够在 Kotlin 中一行定义一个函数--fun welcome() = "Hello World"

    @GetMapping("/welcome")
    public String welcome() {
      return "Hello World";
    }

如果我们将FirstWebServiceWithKotlinApplication.kt作为 Kotlin 应用程序运行,它将启动嵌入式 Tomcat 容器。我们可以在浏览器中启动 URL(http://localhost:8080/welcome),如下图所示:

单元测试

让我们快速编写一个单元测试来测试前面的控制器方法:

    @RunWith(SpringRunner::class)
    @WebMvcTest(BasicController::class)
    class BasicControllerTest {
      @Autowired
      lateinit var mvc: MockMvc;
      @Test
      fun `GET welcome returns "Hello World"`() {
        mvc.perform(
           MockMvcRequestBuilders.get("/welcome").accept(
           MediaType.APPLICATION_JSON))
           .andExpect(status().isOk())
           .andExpect(content().string(equalTo("Hello World")));
       } 
     }

在上述单元测试中,我们将使用BasicController启动一个 Mock MVC 实例。以下是一些需要注意的快速事项:

  • 注解@RunWith(SpringRunner.class)@WebMvcTest(BasicController::class)与 Java 类似,只是类引用不同。

  • @Autowired lateinit var mvc: MockMvc: 这样自动装配了MockMvc bean,可以用于发出请求。声明为非空的属性必须在构造函数中初始化。对于通过依赖注入自动装配的属性,我们可以通过在变量声明中添加lateinit来避免空值检查。

  • fun GET welcome returns "Hello World"(): 这是 Kotlin 的一个独特特性。我们不是给测试方法命名,而是给测试添加一个描述。这很棒,因为理想情况下,测试方法不会被其他方法调用。

  • mvc.perform(MockMvcRequestBuilders.get("/welcome").accept(MediaType.APPLICATION_JSON)): 这执行了一个带有 Accept 头值application/json/welcome请求,这与 Java 代码类似。

  • andExpect(status().isOk()): 这期望响应的状态是200(成功)。

  • andExpect(content().string(equalTo("Hello World"))): 这期望响应的内容等于"Hello World"

集成测试

当我们进行集成测试时,我们希望启动嵌入式服务器,并配置所有的控制器和 bean。以下代码块展示了我们如何创建一个简单的集成测试:

    @RunWith(SpringRunner::class)
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    class BasicControllerIT {
      @Autowired
      lateinit var restTemplate: TestRestTemplate
      @Test
      fun `GET welcome returns "Hello World"`() {
        // When
        val body = restTemplate.getForObject("/welcome", 
        String::class.java)
        // Then
        assertThat(body).isEqualTo("Hello World")
      }
    }

以下是一些重要事项需要注意:

  • @RunWith(SpringRunner::class), @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT): SpringBootTest在 Spring TestContext的基础上提供了额外的功能。它支持配置完全运行容器和 TestRestTemplate(用于执行请求)的端口。这与 Java 代码类似,只是类引用不同。

  • @Autowired lateinit var restTemplate: TestRestTemplate: TestRestTemplate通常用于集成测试。它在RestTemplate的基础上提供了额外的功能,特别适用于测试上下文的集成。它不会遵循重定向,这样我们就可以断言响应位置。lateinit允许我们避免对自动装配变量进行空值检查。

返回对象的简单 REST 方法

我们将创建一个简单的 POJO WelcomeBean,其中包含一个名为 message 的成员字段和一个参数构造函数,如下面的代码行所示:

    data class WelcomeBean(val message: String = "")

相应的 Java 类列在下面:

    public class WelcomeBean {
      private String message;
      public WelcomeBean(String message) {
        super();
        this.message = message;
      }
      public String getMessage() {
      return message;
     }
   }

Kotlin 会自动为数据类添加构造函数和其他实用方法。

在之前的方法中,我们返回了一个字符串。让我们创建一个返回正确的 JSON 响应的方法。看一下下面的方法:

    @GetMapping("/welcome-with-object")
    fun welcomeWithObject() = WelcomeBean("Hello World")

该方法返回一个简单的WelcomeBean,其中包含一个"Hello World"的消息。

执行请求

让我们发送一个测试请求,看看我们得到什么响应。以下截图显示了输出:

http://localhost:8080/welcome-with-object URL 的响应如下所示:

    {"message":"Hello World"}

单元测试

让我们快速编写一个单元测试,检查 JSON 响应,然后将测试添加到BasicControllerTest中:

    @Test
    fun `GET welcome-with-object returns "Hello World"`() {
      mvc.perform(
      MockMvcRequestBuilders.get("/welcome-with-object")
      .accept(MediaType.APPLICATION_JSON))
      .andExpect(status().isOk())
      .andExpect(content().string(
      containsString("Hello World")));
    }

这个测试与之前的单元测试非常相似,不同之处在于我们使用containsString来检查内容是否包含"Hello World"子字符串。

集成测试

让我们把注意力转移到编写一个集成测试,然后在BasicControllerIT中添加一个方法,如下面的代码片段所示:

    @Test
    fun `GET welcome-with-object returns "Hello World"`() {
      // When
      val body = restTemplate.getForObject("/welcome-with-object",
      WelcomeBean::class.java)
      // Then
     assertThat(body.message, containsString("Hello World"));
   }

这个方法类似于之前的集成测试,不同之处在于我们在assertThat方法中断言一个子字符串。

带有路径变量的 GET 方法

让我们把注意力转移到路径变量。路径变量用于将 URI 中的值绑定到控制器方法上的变量。在下面的例子中,我们想要对名称进行参数化,以便我们可以使用名称定制欢迎消息:

    @GetMapping("/welcome-with-parameter/name/{name}")
    fun welcomeWithParameter(@PathVariable name: String) = 
    WelcomeBean("Hello World, $name")

以下是一些重要事项需要注意:

  • @GetMapping("/welcome-with-parameter/name/{name}"): {name}表示这个值将是变量。我们可以在 URI 中有多个变量模板。

  • welcomeWithParameter(@PathVariable String name): @PathVariable 确保来自 URI 的变量值绑定到变量名。

  • fun welcomeWithParameter(@PathVariable name: String) = WelcomeBean("Hello World, $name"): 我们使用 Kotlin 的单表达式函数声明直接返回创建的 WelcomeBean"Hello World, $name" 使用了 Kotlin 字符串模板。$name 将被路径变量 name 的值替换。

执行请求

让我们发送一个测试请求,看看我们得到什么响应。以下截图显示了响应:

http://localhost:8080/welcome-with-parameter/name/Buddy URL 的响应如下:

    {"message":"Hello World, Buddy!"}

正如预期的那样,URI 中的名称用于形成响应中的消息。

单元测试

让我们快速为上述方法编写一个单元测试。我们将要在 URI 的一部分中传递一个名称,并检查响应是否包含该名称。以下代码显示了我们如何做到这一点:

    @Test
    fun `GET welcome-with-parameter returns "Hello World, Buddy"`() {
      mvc.perform(
      MockMvcRequestBuilders.get(
      "/welcome-with-parameter/name/Buddy")
     .accept(MediaType.APPLICATION_JSON))
     .andExpect(status().isOk())
     .andExpect(content().string(
     containsString("Hello World, Buddy")));
    }

需要注意的几个重要事项如下:

  • MockMvcRequestBuilders.get("/welcome-with-parameter/name/Buddy"): 这与 URI 中的变量模板匹配。我们将传入名称 Buddy

  • .andExpect(content().string(containsString("Hello World, Buddy"))): 我们期望响应包含带有名称的消息。

集成测试

上述方法的集成测试非常简单。看一下以下的 test 方法:

   @Test
   fun `GET welcome-with-parameter returns "Hello World"`() {
     // When
     val body = restTemplate.getForObject(
     "/welcome-with-parameter/name/Buddy", 
     WelcomeBean::class.java)
     // Then
    assertThat(body.message, 
    containsString("Hello World, Buddy"));
   }

需要注意的几个重要事项如下:

  • restTemplate.getForObject("/welcome-with-parameter/name/Buddy", WelcomeBean::class.java): 这与 URI 中的变量模板匹配。我们传入名称 Buddy

  • assertThat(response.getBody(), containsString("Hello World, Buddy")): 我们期望响应包含带有名称的消息。

在本节中,我们了解了使用 Spring Boot 创建简单 REST 服务的基础知识。我们还确保了我们有良好的单元测试和集成测试。

总结

Kotlin 帮助开发人员编写简洁、可读的代码。它与 Spring Boot 的理念完美契合,使应用程序开发更加简单快速。

在本章中,我们从理解 Kotlin 及其与 Java 的比较开始。我们使用 Spring Boot 和 Kotlin 构建了一些简单的 REST 服务。我们看到了 Kotlin 用于服务和单元测试的代码是简洁的示例。

Kotlin 在过去几年取得了巨大进步--成为 Android 官方支持的语言是一个很好的第一步。Spring Framework 5.0 对 Kotlin 的支持是锦上添花。Kotlin 的未来取决于它在更大的 Java 开发社区中的成功程度。它有潜力成为你工具库中的重要工具。

posted @ 2025-09-12 13:57  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报