Spring5-示例-全-
Spring5 示例(全)
原文:
zh.annas-archive.org/md5/35e67558679e034a33e79aeca83447f0译者:飞龙
前言
随着需求的增长,组织正在寻找健壮且可扩展的系统。因此,Spring 框架已成为 Java 开发中最受欢迎的框架。它不仅简化了软件开发,还提高了开发者的生产力。本书涵盖了使用 Spring 在 Java 中开发健壮应用程序的有效方法。
本书面向的对象
对于刚开始使用 Spring 的开发者来说,本书将介绍 Spring 5.0 的新框架概念,并随后讲解其在 Java 和 Kotlin 中的实现。本书还将帮助经验丰富的 Spring 开发者深入了解 Spring 5.0 中新增的功能。
本书涵盖的内容
第一章,《春之之旅》,将引导你了解 Spring 框架的主要概念。在这里,我们将学习通过安装 OpenJDK、Maven、IntelliJ IDEA 和 Docker 来设置环境。最后,我们将创建我们的第一个 Spring 应用程序。
第二章,《春之世界起步——CMS 应用程序》,将首先通过使用 Spring Initializr 来为我们的 CMS 应用程序创建配置来动手实践。然后我们将学习如何创建 REST 资源,添加服务层,并最终与 AngularJS 集成。
第三章,《使用 Spring Data 和响应式模式进行持久化》,将在上一章创建的 CMS 应用程序的基础上进行构建。在这里,我们将通过了解 Spring Data Reactive MongoDB 和 PostgreSQL 来学习如何在真实数据库上持久化数据。我们还将了解 Project Reactor,它将帮助你创建 JVM 生态系统中的非阻塞应用程序。
第四章,《Kotlin 基础和 Spring Data Redis》,将为你提供一个 Kotlin 的基本介绍,同时展示该语言的优势。然后我们将学习如何使用 Redis,它将作为消息代理使用发布/订阅功能。
第五章,《响应式 Web 客户端》,将教你如何使用 Spring Reactive Web 客户端并以响应式的方式执行 HTTP 调用。我们还将介绍 RabbitMQ 和 Spring Actuator。
第六章,《玩转服务器端事件》,将帮助你开发一个能够通过文本内容过滤推文的程序。我们将通过使用服务器端事件(这是一种从服务器向客户端发送数据流的标准方式)来消费推特流来实现这一点。
第七章,《航空票务系统》,将教你如何使用 Spring Messaging、WebFlux 和 Spring Data 组件来构建航空票务系统。在本章中,你还将了解断路器和 OAuth。最后,我们将创建一个包含许多微服务以确保可扩展性的系统。
第八章,电路断路器和安全,将帮助您了解如何为我们的业务微服务应用服务发现功能,同时了解电路断路器模式如何帮助我们为应用程序带来弹性。
第九章,整合一切,将使整本书的视角更加清晰,同时向您介绍 Turbine 服务器。我们还将查看 Hystrix 仪表板来监控我们的不同微服务,以确保应用程序的可维护性和最佳性能。
为了充分利用本书
阅读本书的读者应具备基本的 Java 知识。了解分布式系统将是一个额外的优势。
要执行本书中的代码文件,您需要以下软件/依赖项:
-
IntelliJ IDEA 社区版
-
Docker CE
-
pgAdmin
-
Docker Compose
您将通过本书获得安装过程等方面的帮助。
下载示例代码文件
您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在www.packtpub.com登录或注册。
-
选择“支持”选项卡。
-
点击“代码下载与勘误”。
-
在搜索框中输入书名,并遵循屏幕上的说明。
文件下载完成后,请确保使用最新版本解压或提取文件夹:
-
Windows 系统下的 WinRAR/7-Zip
-
Mac 系统下的 Zipeg/iZip/UnRarX
-
Linux 系统下的 7-Zip/PeaZip
本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Spring-5.0-By-Example。我们还有其他丰富的图书和视频代码包,可在github.com/PacktPublishing/找到。请查看它们!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/Spring50ByExample_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“它包括在默认配置文件application.yaml中配置的基础设施连接。”
代码块设置如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
management:
endpoints:
web:
expose: "*"
任何命令行输入或输出都应如下所示:
docker-compose up -d
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“下一个屏幕将显示,我们可以配置环境变量:”
警告或重要提示看起来是这样的。
小贴士和技巧看起来是这样的。
联系我们
我们始终欢迎读者的反馈。
一般反馈:请通过 feedback@packtpub.com 发送电子邮件,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过 questions@packtpub.com 发送电子邮件给我们。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packtpub.com 联系我们,并附上材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为何不在购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
如需了解 Packt 的更多信息,请访问 packtpub.com。
第一章:春之之旅
Spring 是 JVM 平台的开源模块化框架。框架是一组库的集合,其主要目标是解决常见的软件开发问题。框架应以通用形式解决这些问题。
Rod Johnson 于 2002 年与他的书籍出版一起创建了 Spring 框架,该书籍被称为《专家一对一 J2EE 设计与开发》。该框架背后的想法是解决 Java 企业版的复杂性。
当时,这种以解决方案为导向的方法非常关注基础设施的细节,使用该解决方案的开发者会花费大量时间编写代码来解决基础设施问题。自从其创建以来,Rod Johnson 的主要关注点之一就是提高开发者的生产力。
该框架最初被视为 Java 运行时环境的轻量级容器,并在社区中变得流行,尤其是在依赖注入功能方面。该框架使依赖注入变得极其简单。开发者以前从未见过这样的功能,因此,全世界的人们都采用了这个项目。年复一年,它在软件开发世界中的知名度一直在增加。
在早期版本中,该框架必须与 XML 文件一起工作以配置容器。当时,这比 J2EE 应用程序好得多,在 J2EE 应用程序中,有必要创建许多 Ant 文件来创建样板类和接口。
该框架一直被视为 Java 平台的高级技术,但在 2014 年,Spring 团队推出了 Spring Boot 平台。这个平台在 Java 企业生态系统中取得了巨大成功,并改变了开发者构建 Java 企业应用程序的方式。
今天,Spring 是 Java 开发的事实上的框架,全球各地的公司都在其系统中使用它。社区充满活力,并以不同的方式为开发做出贡献,例如在世界上最重要的 Java 大会上提出问题、添加代码和讨论框架。让我们来看看并玩转这个著名的 Java 开发者框架。
在本章中,我们将涵盖以下主题:
-
Spring 框架的主要模块
-
每个模块的 Spring 注解
-
设置开发环境
-
Docker 和 Docker 命令
Spring 模块化
自从成立以来,该框架就特别关注模块化。这是一个重要的框架特性,因为它使框架成为不同架构风格和应用程序不同部分的绝佳选择。
这意味着该框架不是一个具有偏见的全栈框架,它规定了使一切工作的规则。我们可以根据需要使用该框架,并将其与广泛的规范和第三方库集成。
例如,对于门户 Web 应用程序,Spring MVC 支持模板引擎和 REST 端点,并将它们与流行的 JavaScript 框架 AngularJS 集成。
此外,如果应用程序需要分布式系统的支持,框架可以提供名为 Spring Cloud 的惊人模块,该模块为分布式环境提供了一些基本功能,例如服务注册和发现、断路器、智能路由和客户端负载均衡。
Spring 通过不同的语言,如 Java、Kotlin 和 Groovy(您可以选择风味并使开发任务变得有趣)使 Java 运行时应用程序的开发变得容易。
它被分为各种模块。主要模块如下:
-
Spring 核心框架
-
Spring 数据
-
Spring 安全
-
Spring Cloud
-
Spring Web-MVC
在这本书中,我们将涵盖 Java 企业应用程序中最常见的解决方案,包括令人惊叹的 Spring Cloud 项目。我们还可以找到一些有趣的项目,如 Spring Batch 和 Spring Integration,但这些项目针对特定需求。
Spring 核心框架
此模块是框架的基础,包含对依赖注入、Spring MVC(模型-视图-控制器)支持的 Web 功能以及相对较新的 WebFlux 框架和面向方面的编程的基本支持。此外,此模块支持 JDBC、JMS、JPA 和声明式事务管理的基础。我们将探索它并了解此模块的主要项目。所以,让我们开始吧!
核心容器
核心容器是整个 Spring 生态系统的基石,包括四个组件——核心、bean、上下文和表达式语言。
核心和 bean 负责提供框架的基本功能和依赖注入。这些模块负责管理 IoC 容器,主要功能包括在 Spring 容器中对象的实例化、配置和销毁。
Spring 上下文也称为 Spring IoC 容器,负责通过从 XML、Java 注解和/或配置文件中的 Java 代码读取配置元数据来实例化、配置和组装 bean。
这些模块内部有两个关键接口——BeanFactory和ApplicationContext。BeanFactory负责管理 bean 的生命周期,包括实例化、配置、管理和销毁,而ApplicationContext帮助开发者以通用方式处理文件资源,能够向注册的监听器发布事件。此外,ApplicationContext支持国际化,并能够处理不同 Locale 的消息。
这些模块帮助上下文组件提供一种访问容器内对象的方式。上下文组件具有ApplicationContext接口,这是容器的基本类。
一些常见的注解有@Service、@Component、@Bean和@Configuration。
Spring Messaging
Spring 框架支持广泛的 messaging 系统。Java 平台被认为是提供出色的消息应用程序支持的平台,Spring 框架遵循此方法并提供各种项目,以帮助开发者以更高的生产力和更少的底层代码行数编写强大的应用程序。这些项目的基本思想是提供一些具有方便方法的模板类,以便与消息系统交互。
此外,该项目还提供了一些监听器注解,以提供从代理监听消息的支持。框架维护不同项目的标准。一般来说,注解的前缀是消息系统的名称,例如@KafkaListener。
框架提供许多抽象,以通用方式创建消息应用程序。这是有趣的事情,因为应用程序需求在应用程序生命周期中会发生变化,消息代理解决方案也可能发生变化。然后,通过少量更改,使用 Spring 消息模块构建的应用程序可以在不同的代理上运行。这是目标。
Spring AMQP
此子项目支持 Spring 框架中的 AMQP 协议。它提供了一个与消息代理交互的模板。模板类似于一个超级高级 API,支持send和receive操作。
此集合中有两个项目:spring-amqp,可用于 ActiveMQ 等,以及spring-rabbit,它增加了对 RabbitMQ 代理的支持。此项目通过 API 声明队列、绑定和交换来实现代理管理。
这些项目鼓励广泛使用核心容器提供的依赖注入,因为它们使配置更加声明性和易于理解。
现在,RabbitMQ 代理是消息应用程序的流行选择,Spring 提供了对客户端交互以及管理任务的全支持。
一些常见的注解有@Exchange和@QueueBinding。
Spring for Apache Kafka
Spring for Apache Kafka 支持基于代理的 Apache Kafka 应用程序。它提供了一个高级 API 来与 Apache Kafka 交互。内部,这些项目使用 Kafka Java API。
此模块支持注解编程模型。基本思想是,通过几个注解和一些 POJO 模型,我们可以启动应用程序并开始监听和发送消息。
KafkaTemplate是这个项目的核心类。它使我们能够使用高级 API 将消息发送到 Apache Kafka。同时支持异步编程。
此模块通过注解提供对事务的支持。此功能通过在基于 Spring 的应用程序中使用的标准事务注解启用,例如@Transactional。
我们还学习了 Spring AMQP。该项目添加了基于此代理创建应用的概念。也支持依赖注入功能。
一些常见的注解是@EnableKafka和@KafkaListener。
Spring JMS
该项目的理念是将 Spring 框架项目的思想与 JMS 集成,并提供一个高级 API 来与代理交互。JMS 规范最糟糕的部分是它有很多样板代码来管理和关闭连接。
JmsTemplate是这个模块的核心类,它使我们能够向代理发送消息。JMS 规范有很多内在行为来处理资源的创建和释放,例如,JmsTemplate类会自动为开发者执行这些任务。
该模块还支持事务性需求。JmsTransactionManager类处理 Spring JMS 模块的事务行为。
Spring 通过几个注解来移除样板代码。该框架提高了代码的可读性,并使代码更加直观。
一些常见的注解是@JmsListener和@EnableJms。
Spring Web MVC
该模块是 Spring 团队构建的第一个支持 Spring 框架中 Web 应用的模块。该模块以 Servlet API 为基础,因此这些 Web 应用必须遵循 Servlet 规范并部署到 servlet 容器中。在 5.0 版本中,Spring 团队创建了一个响应式 Web 框架,这将在本书的后续部分进行介绍。
Spring Web MVC 模块是使用前端控制器模式开发的。当框架创建时,这种模式是许多框架(如 Struts 和 JSF 等)的共同选择。在底层,Spring 中有一个主要的 servlet 称为DispatcherServlet。这个 servlet 将通过算法重定向以执行所需的工作。
它使开发者能够在 Java 平台上创建令人惊叹的 Web 应用。该框架的这一部分提供了全面的支持来开发此类应用。为此目的,有一些有趣的功能,例如支持国际化和支持处理 cookies。此外,多部分请求是当应用需要处理上传文件和支持路由请求时的一个令人兴奋的功能。
这些特性对于大多数 Web 应用都是常见的,该框架对这些特性提供了出色的支持。这种支持使该框架成为此类应用的理想选择。在第二章“从 Spring 世界开始”——“CMS 应用”中,我们将使用此模块创建一个应用,并将深入探讨其主要功能。
该模块从声明 HTTP 端点直到将请求属性包装在 HTTP 请求中,对注解编程提供了全面的支持。这使得应用程序在没有获取请求参数等样板代码的情况下,具有极高的可读性。
在 Web 应用程序方面,它使开发者能够与健壮的模板引擎,如 Thymeleaf 和 Freemarker 一起工作。它与路由功能和 Bean 验证完全集成。
此外,该框架允许开发者使用此模块构建 REST API。鉴于所有这些支持,该模块已成为 Spring 生态系统中的宠儿。开发者已经开始使用这个堆栈创建 API,一些重要公司也开始使用它,尤其是在框架提供了一个轻松导航注解的简单方法的情况下。因此,Spring 团队在 4.0 版本中添加了新的注解@RestController。
我们将与这个模块进行大量工作。我们将逐章学习关于这个框架部分的有趣事物。
一些常见的注解包括@RequestMapping、@Controller、@Model、@RestController和@RequestBody。
Spring WebFlux
Spring 5.0 中引入的新模块 Spring WebFlux,可以用来实现使用响应式流构建的 Web 应用程序。这些系统具有非阻塞特性,并且部署在基于 Netty 构建的服务器上,例如 Undertow 和支持+ 3.1 的 servlet 容器。
Netty 是一个开源框架,它帮助开发者创建网络应用程序——即使用异步、事件驱动模式的客户端和服务器。Netty 提供了一些有趣的优势,例如更低的延迟、更高的吞吐量和更少的资源消耗。你可以在netty.io找到更多信息。
该模块支持基于 Spring MVC 模块的注解,如@GetMapping、@PostMapping等。这是一个重要的特性,使我们能够迁移到这个新版本。当然,一些调整是必要的,例如添加 Reactor 类(Mono 或 Flux)。
这个模块满足了现代 Web 处理大量并发通道的需求,其中线程-per-请求模型不是一种选择。
我们将在第三章“使用 Spring Data 添加持久性并将其转换为响应式模式”中学习这个模块,并基于响应式流实现一个完全响应式的应用程序。
一些常见的注解包括@RequestMapping、@RestController和@RequestBody。
Spring Data
Spring Data 是一个有趣的模块,它提供了使用基于 Spring 的编程来管理应用程序数据的最简单方式。该项目是一个母项目,包含子项目以支持不同的数据库技术,包括关系型和非关系型数据库。Spring 团队支持一些数据库技术,例如 Apache Cassandra、Apache Solr、Redis 和 JPA 规范,而社区维护着其他令人兴奋的项目,如 ElasticSearch、Aerospike、DynamoDb 和 Couchbase。项目的完整列表可以在 projects.spring.io/spring-data 找到。
目标是从持久化代码中移除样板代码。一般来说,数据访问层相当相似,即使在不同的项目中,也只是在项目模型上有所不同,而 Spring Data 提供了一种强大的方式来映射领域模型和仓库抽象。
存在一些核心接口;它们是一种标记,指示框架选择正确的实现。在底层,Spring 将创建一个代理并将正确的实现委托给它。这里令人惊讶的是,开发者不必编写任何持久化代码并关心这些代码;他们只需选择所需的技术,Spring 就会处理其余部分。
核心接口是 CrudRepository 和 PagingAndSortingRepository,它们的名称具有自解释性。CrudRepository 实现了 CRUD 行为,如 create、retrieval、update 和 delete。PagingAndSortingRepository 是 CrudRepository 的扩展,并添加了一些功能,如分页和排序。通常,我们会找到这些接口的派生版本,如 MongoRepository,它与 MongoDB 数据库技术交互。
一些常见的注解有 @Query、@Id 和 @EnableJpaRepositories。
Spring Security
对于 Java 应用程序的安全问题,开发者一直感到头疼,尤其是在 Java 企业版中。在应用服务器中查找对象需要大量的样板代码,而且安全层通常需要为应用程序进行大量定制。
在那种混乱的场景中,Spring 团队决定创建一个 Spring Security 项目,以帮助开发者处理 Java 应用程序的安全层。
在项目初期,它广泛支持 Java 企业版,并与 EJB 3 安全注解集成。如今,该项目支持许多不同的方式来处理 Java 应用程序的授权和认证。
Spring Security 为 Java 应用程序提供了一套全面的模型来添加授权和认证。该框架可以通过几个注解进行配置,这使得添加安全层的任务变得极其简单。其他重要特性涉及框架如何扩展。有一些接口允许开发者自定义默认框架行为,使得框架可以根据不同的应用需求进行定制。
它是一个综合性的项目,并细分为以下模块:
-
spring-security-core -
spring-security-remoting -
spring-security-web -
spring-security-config -
spring-security-ldap -
spring-security-acl -
spring-security-cas -
spring-security-openid -
spring-security-test
这些是主要模块,还有许多其他项目支持广泛的认证类型。该模块涵盖了以下认证和授权类型:
-
LDAP
-
HTTP 基本认证
-
OAuth
-
OAuth2
-
OpenID
-
云服务平台(CAAS)
-
Java 认证和授权服务(JAAS)
该模块还提供了一种领域特定语言(DSL)来简化配置。让我们看一个简单的例子:
http
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.and()
.authorizeRequests()
.antMatchers("/signup","/about").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
示例是从 spring.io 博客中提取的。更多详情,请访问 spring.io/blog/2013/07/11/spring-security-java-config-preview-readability/。
如我们所见,领域特定语言(DSL)使得配置任务变得极其简单且易于理解。
Spring Security 的主要特性如下:
-
会话管理
-
防御攻击(CSRF、会话固定等)
-
Servlet API 集成
-
认证和授权
我们将在第八章“断路器和安全”中了解更多关于 Spring Security 的内容,第八章。我们还将将其付诸实践。
@EnableWebSecurity 是一个常见的注解。
Spring Cloud
Spring Cloud 是另一个综合性的项目。该项目的主要目标是帮助开发者创建分布式系统。分布式系统需要解决一些常见问题,并且当然,有一套模式可以帮助我们,例如服务发现、断路器、配置管理、智能路由系统和分布式会话。Spring Cloud 工具提供了所有这些实现和详细文档的项目。
主要项目如下:
-
Spring Cloud Netflix
-
Spring Cloud Config
-
Spring Cloud Consul
-
Spring Cloud Security
-
Spring Cloud Bus
-
Spring Cloud Stream
Spring Cloud Netflix
Spring Cloud Netflix 可能是当今最受欢迎的 Spring 模块。这个出色的项目允许我们通过 Spring Boot 自动配置功能将 Spring 生态系统与 Netflix OSS 集成。支持的 Netflix OSS 库包括用于服务发现的 Eureka、用于启用客户端负载均衡的 Ribbon、通过 Hystrix 实现的断路器来保护我们的应用程序免受外部故障的影响并使系统具有弹性、Zuul 组件提供智能路由并可以作为边缘服务。最后,Feign 组件可以帮助开发者通过几个注解创建用于 REST API 的 HTTP 客户端。
让我们来看看这些内容:
- Spring Cloud Netflix Eureka:这个项目的重点是提供符合 Netflix 标准的应用程序服务发现。服务发现是一个重要的功能,使我们能够移除硬编码的配置来提供主机名和端口;在云环境中更为重要,因为机器是短暂的,因此很难维护名称和 IP 地址。该功能相当简单,Eureka 服务器提供服务注册,Eureka 客户端会自行联系其注册信息。
一些常见的注解是 @EnableEurekaServer 和 @EnableEurekaClient。
- Spring Cloud Feign:Netflix 团队创建了 Feign 项目。这是一个非常好的项目,使得配置 HTTP 客户端用于 REST 应用程序比以前容易得多。这些实现基于注解。该项目为 HTTP 路径、HTTP 头等提供了一些注解,当然,Spring Cloud Feign 通过注解和自动配置与 Spring Cloud 生态系统集成。此外,Spring Cloud Feign 可以与 Eureka 服务器结合使用。
一些常见的注解是 @EnableFeignClients 和 @FeignClient。
- Spring Cloud Ribbon:Ribbon 是一个客户端负载均衡器。配置应主要提供一个特定客户端的服务器列表。它必须有名称。在 Ribbon 的术语中,它被称为命名客户端。该项目还提供了一系列负载均衡规则,如轮询和可用性过滤等。当然,框架允许开发者创建自定义规则。Ribbon 有一个与 Eureka 服务器集成的 API,用于启用服务发现,这包含在框架中。此外,由于 API 可以在运行时识别运行中的服务器,因此还支持诸如容错等基本功能。
一些常见的注解是 @RibbonClient 和 @LoadBalanced。
- Spring Cloud Hystrix:一个备受赞誉的 Netflix 项目,该项目提供了一个断路器模式实现。其概念类似于电路断路器。框架将监视标记有
@HystrixCommand的方法,并监视失败的调用。如果失败的调用次数超过配置中允许的数字,断路器将打开。当电路打开时,将调用回退方法,直到电路关闭并正常操作。它将为我们的系统提供弹性和容错特性。Spring 生态系统完全集成了 Hystrix,但它仅在@Component和@Service豆上工作。
一些常见的注解包括@EnableCircuitBreaker和@HystrixCommand。
Spring Cloud Config
这个令人兴奋的项目提供了一个轻松管理分布式系统系统配置的方法,在云环境中这是一个关键问题,因为文件系统是短暂的。它还帮助我们维护部署管道的不同阶段。Spring 配置文件完全集成到这个模块中。
我们需要一个应用程序来为其他应用程序提供配置。我们可以通过思考服务器和客户端的概念来理解其工作原理,服务器将通过 HTTP 提供一些配置,而客户端将在服务器上查找配置。此外,还可以对属性值进行加密和解密。
有一些存储实现来提供这些属性文件,默认实现是 Git。它使我们能够将属性文件存储在 Git 中,或者我们也可以使用文件系统。这里重要的是源代码不重要。
Git是一个分布式版本控制。这个工具通常用于开发目的,特别是在开源社区中。与 SVN 等市场参与者相比,主要优势在于分布式架构。
Spring Cloud Bus与该模块之间有一个有趣的集成。如果它们集成在一起,就可以在集群上广播配置更改。如果应用程序配置频繁更改,这是一个重要功能。有两个注解告诉 Spring 在运行时应用更改:@RefreshScope和@ConfigurationProperties。
在第七章,航空票务系统中,我们将实现一个令人兴奋的服务,使用此模块为我们的微服务提供外部配置。将更详细地解释服务器概念。客户端的详细信息也将被展示。
@EnableConfigServer 是一个常见的注解。
Spring Cloud Consul
Spring Cloud Consul 提供了与 Hashicorp 的 Consul 的集成。这个工具以与服务发现、分布式配置和控制总线相同的方式解决问题。此模块允许我们通过基于 Spring 的编程模型使用少量注解来配置 Spring 应用程序和 Consul。还支持自动配置。这里令人惊讶的是,此模块可以通过 Spring Cloud Zuul 和 Spring Cloud Ribbon 分别与一些 Netflix OSS 库(例如)集成(例如)。
@EnableDiscoveryClient是一个常见的注解。
Spring Cloud Security
这个模块类似于 Spring Security 的扩展。然而,分布式系统对安全性的要求不同。通常,它们有集中的身份管理,或者 REST API 的情况下,认证在客户端。在分布式系统中,我们通常有微服务,这些服务在运行时环境中可能有多个实例,其特性使得认证模块与单体应用略有不同。该模块可以与 Spring Boot 应用程序一起使用,并通过几个注解和一些配置使 OAuth2 实现变得非常简单。此外,还支持一些常见模式,如单点登录、令牌中继和令牌交换。
对于基于 Spring Cloud Netflix 的微服务应用程序,这尤其有趣,因为它使得下游认证能够与 Zuul 代理一起工作,并为 Feign 客户端提供支持。使用拦截器来获取令牌。
一些常见的注解包括@EnableOAuth2Sso和@EnableResourceServer。
Spring Cloud Bus
本项目的核心目标是提供一个简单的方法来广播整个集群中的变化。应用程序可以通过消息代理连接分布式系统节点。
它为开发者提供了一个简单的方法,使用 Spring 容器提供的ApplicationContext来创建发布和订阅机制。它使得使用 Spring 生态系统的事件驱动架构风格创建应用程序成为可能。
要创建自定义事件,我们需要从RemoteApplicationEvent创建一个子类,并通过@RemoteApplicationEventScan标记该类以进行扫描。
这些项目支持三个消息代理作为传输层:
-
AMQP
-
Apache Kafka
-
Redis
@RemoteApplicationEventScan是一个常见的注解。
Spring Cloud Stream
该模块背后的理念是提供一个简单的方法来构建消息驱动的微服务。该模块具有一种有偏见的配置方式。这意味着我们需要遵循一些规则来创建这些配置。一般来说,应用程序是通过yaml|properties文件进行配置的。
该模块也支持注解。这意味着只需要几个注解就可以创建消费者、生产者和绑定;它解耦了应用程序,使其易于理解。它围绕消息代理和通道提供了一些抽象,从而使开发者的生活更加舒适和高效。
Spring Cloud Stream 为 RabbitMQ 和 Kafka 提供了绑定实现。
一些常见的注解是@EnableBinding、@Input和@Output。
Spring Integration
此模块支持许多企业应用程序模式,并将 Spring 编程模型引入此主题。Spring 编程模型提供了广泛的依赖注入支持,并且以注解编程为中心。注解指导我们如何配置框架,并定义框架行为。
建议使用 POJO 模型,因为它简单且在 Java 开发世界中广为人知。
该项目与其他模块有一些交集。一些其他项目使用这些模块概念来完成他们的工作。例如,有一个名为 Spring Cloud Stream 的项目。
企业集成模式基于广泛的通信通道、协议和模式。本项目支持其中的一些。
模块支持各种特性和通道,如下所示:
-
聚合器
-
过滤器
-
转换器
-
JMS
-
RabbitMQ
-
TCP/UDP
-
Web 服务
-
Twitter
-
邮件
-
以及更多
企业应用程序集成的三个主要概念是:
-
消息
-
消息通道
-
消息端点
最后,Spring Integration 模块提供了一种创建应用程序集成的方法,并使开发者能够利用出色的支持来完成这项工作。
一些常见的注解是@EnableIntegration、@IntegrationComponentScan和@EnablePublisher。
Spring Boot
Spring Boot 于 2014 年发布。该项目背后的想法是提供一种在 Apache Tomcat、Jetty 等任何容器之外部署 Web 应用程序的方法。这种部署方式的好处是独立于任何外部服务。它允许我们使用一个 JAR 文件运行 Web 应用程序。如今,这是一种极佳的方法,因为它形成了采用 DevOps 文化的最自然方式。
Spring Boot 提供了嵌入式的 servlet 容器,如 Apache Tomcat、Jetty 和 Undertow。当测试我们的 Web 应用程序时,它使开发过程更加高效和舒适。此外,配置期间允许通过配置文件或提供一些 bean 进行自定义。
采用 Spring Boot 框架有一些优势。该框架不需要任何 XML 进行配置。这是一件了不起的事情,因为我们将在 Java 文件中找到所有依赖项。这有助于 IDEs 协助开发者,并提高了代码的可追溯性。另一个重要的优势是,项目试图尽可能地将配置自动化。一些注解使得魔法发生。有趣的是,Spring 会注入在运行时生成的任何代码的实现。
Spring Boot 框架还提供了有趣的功能来帮助开发者和运维人员,例如健康检查、度量、安全和配置。这对于现代应用程序至关重要,其中模块在微服务架构中被分解。
还有一些其他有趣的功能可以帮助开发者从 DevOps 的角度出发。我们可以使用application-{profile}.properties或application.yaml文件来配置不同的运行时配置文件,例如开发、测试和生产。这是一个真正有用的 Spring Boot 特性。
此外,该项目对测试提供了全面的支持,从网络层到存储库层。
该框架提供了一个高级 API 来处理单元和集成测试。此外,该框架为开发者提供了许多注解和辅助类。
Spring Boot 项目是一个生产就绪的框架,为 Web 服务器、指标和监控功能提供了默认优化的配置,以帮助开发团队交付高质量的软件。
我们可以通过 Groovy 和 Java 语言进行编码来开发应用程序。两者都是 JVM 语言。在 5.0 版本中,Spring 团队宣布了对 Kotlin 语言的全支持,这是 JVM 的新语言。它使我们能够编写一致且可读的代码。我们将在第七章航空票务系统中深入探讨这一特性。
微服务与 Spring Boot
微服务架构风格通常来说是分布式的,必须是松散耦合的,并且需要很好地定义。当你想要一个微服务架构时,必须遵循这些特性。
Spring Boot 的大部分功能旨在通过使常见概念,如 RESTful HTTP 和嵌入式 Web 应用程序运行时,易于连接和使用,来提高开发者的生产力。在许多方面,它还旨在作为一个微-框架,通过允许开发者选择和选择他们需要的框架部分,而不会被庞大的或不必要的运行时依赖所淹没。这也使得 Boot 应用程序可以打包成小的部署单元,并且框架能够使用构建系统生成可运行的 Java 归档。
微服务的主要特性是:
-
小粒度组件
-
领域责任(订单、购物车)
-
编程语言无关性
-
数据库无关性
Spring Boot 使我们能够在嵌入式 Web 服务器(如 Tomcat、Jetty 和 Undertow)上运行应用程序。这使得部署我们的组件变得极其容易,因为我们可以在一个 JAR 文件中公开我们的 HTTP API。
Spring 团队甚至从开发者生产力的角度考虑,他们提供了一些名为starters的项目。这些项目是一组具有某些兼容性的依赖项。这些出色的项目还与约定优于配置一起工作。基本上,它们是开发者在每个单独的项目上都需要进行的常见配置。我们可以在我们的application.properties或application.yaml文件中更改这些设置。
微服务架构的另一个关键点是监控。假设我们正在开发一个电子商务解决方案。我们有两个组件,购物车和支付。购物车可能需要多个实例,而支付可能需要较少的实例。我们如何检查这些多个实例?我们如何检查这些服务的健康状况?当这些实例出现问题时,我们需要触发警报。这是所有服务的一个常见实现。Spring 框架提供了一个名为 Spring Boot Actuator 的模块,它为我们应用程序、数据库等提供了内置的健康检查。
设置我们的开发环境
在我们开始之前,我们需要设置我们的开发环境。我们的开发环境包括以下四个工具:
-
JDK
-
构建工具
-
IDE
-
Docker
我们将安装 JDK 版本 8.0。这个版本在 Spring Framework 5 中得到完全支持。我们将展示安装 Maven 3.3.9 的步骤,这是 Java 开发中最著名的构建工具,在最后一部分,我们将向您展示如何安装 IntelliJ IDEA Community Edition 的详细说明。我们将使用 Ubuntu 16.04,但您可以使用您喜欢的操作系统。安装步骤很简单。
安装 OpenJDK
OpenJDK 是一个稳定、免费且开源的 Java 开发工具包。此包将用于与代码编译和运行环境相关的所有内容。
此外,您还可以使用 Oracle JDK,但您应该注意许可证和协议。
要安装 OpenJDK,我们将打开一个终端并运行以下命令:
sudo apt-get install openjdk-8-jdk -y
我们可以在 OpenJDK 页面的安装部分(openjdk.java.net/install/)找到有关如何安装 Java 8 JDK 的更多信息。
使用以下命令检查安装:
java -version
您应该看到显示如下所示的 OpenJDK 版本及其相关详细信息:

现在我们已经安装了 Java 开发工具包,我们准备进行下一步。在现实世界中,我们必须有一个构建工具来帮助开发者编译、打包和测试 Java 应用程序。
在下一节中,我们将安装 Maven。
安装 Maven
Maven 是 Java 开发中流行的构建工具。一些重要的开源项目都是使用这个工具构建的。它具有简化构建过程、标准化项目结构和提供最佳实践开发指南的功能。
我们将安装 Maven,但安装步骤应在 OpenJDK 安装之后执行。
打开终端并执行以下操作:
sudo apt-get install maven -y
使用以下命令检查安装:
mvn -version
您应该看到以下输出,尽管版本可能因您而异:

做得很好。现在我们已经安装了 Maven。Maven 拥有一个充满活力的社区,它为开发者提供了许多插件来帮助完成重要任务。有插件可以执行单元测试,还有插件可以准备项目的发布活动,这些插件可以与源代码管理(SCM)软件集成。
我们将使用 spring boot maven 插件和 docker maven 插件。第一个插件将我们的应用程序转换为 JAR 文件,第二个插件使我们能够与 Docker 引擎集成,以创建镜像、运行容器等等。在接下来的几章中,我们将学习如何配置和与这些插件交互。
安装 IDE
集成开发环境是帮助开发者的一个重要工具。在这本书中,我们将使用 IntelliJ IDEA 作为开发项目的官方工具。对于其他 IDE 没有限制,因为项目将使用 Maven 作为构建工具进行开发。
集成开发环境(IDE)是开发者的个人选择,通常涉及热情;有些人喜欢的东西,其他开发者可能讨厌。请随意使用您喜欢的。
IntelliJ IDEA
IntelliJ IDEA 是 JetBrains 的产品。我们将使用社区版,这是一个开源的、用于编码 Java 和 Kotlin 的出色工具。该工具提供了出色的自动完成功能,并且完全支持 Java 8 特性。
访问 www.jetbrains.com/idea/download/#section=linux 并下载社区版。我们可以提取 tar.gz 文件并执行它。
Spring Tools Suite
Spring Tools Suite 是基于 Eclipse IDE 的,当然,由 Eclipse 基金会提供。目标是提供对 Spring 生态系统的支持,并使开发者的生活更加轻松。这个工具支持诸如 Beans 探索器等有趣的功能。
在以下链接下载工具:
安装 Docker
Docker 是一个开源项目,帮助人们运行和管理容器。对于开发者来说,Docker 在开发生命周期的不同阶段都提供了帮助。
在开发阶段,Docker 允许开发者无需在当前系统操作层安装,即可启动不同的基础设施服务,如数据库和服务发现(如 Consul)。这有助于开发者,因为开发者不需要在操作系统层安装这些系统。通常,这项任务在安装过程中可能会与库发生冲突,并消耗大量时间。
有时,开发者需要安装确切的版本。在这种情况下,有必要在期望的版本上重新安装整个应用程序。这不是一件好事,因为在此期间开发者的机器会变慢。原因很简单,在软件开发过程中使用了大量应用程序。
Docker 在这个阶段帮助开发者。运行带有 MongoDB 的容器非常简单。无需安装,它允许开发者通过一行命令启动数据库。Docker 支持镜像标签。这个特性有助于处理不同版本的软件;这对于每次都需要更改软件版本的开发者来说非常棒。
另一个优点是,当开发者需要为测试或生产目的交付工件时,Docker 可以通过 Docker 镜像实现这些任务。
Docker 帮助人们采用 DevOps 文化,并为提高整个流程的性能提供了惊人的功能。
让我们安装 Docker。
安装 Docker 最简单的方法是下载位于get.docker.com的脚本:
curl -fsSL get.docker.com -o get-docker.sh
下载完成后,我们将按照以下方式执行脚本:
sh get-docker.sh
等待脚本执行完毕,然后使用以下命令检查 Docker 安装:
docker -v
输出需要看起来像以下这样:

有时,Docker 的版本可以增加,版本至少应该是17.10.0-ce。
最后,我们将当前用户添加到 Docker 组中,这样我们就可以使用 Docker 命令行而不需要sudo关键字。输入以下命令:
sudo usermod -aG docker $USER
我们需要注销才能生效这些更改。通过输入以下命令来确认命令是否按预期工作。确保没有出现sudo关键字:
docker ps
输出应该如下所示:

介绍 Docker 概念
现在,我们将介绍一些 Docker 概念。这本书不是关于 Docker 的,但为了在接下来的几章中与我们的容器交互,一些基本的 Docker 使用说明是必要的。Docker 是一个事实上的工具,用于管理容器。
Docker 镜像
Docker 镜像类似于 Docker 容器的模板。它包含启动 Docker 容器所需的一组文件夹和文件。我们永远不会有一个正在执行模式的镜像。镜像为 Docker Engine 启动容器提供了一个模板。我们可以通过类比面向对象来更好地理解这个过程。镜像就像一个提供实例化一些对象的基础设施的基础设施的类,而实例就像一个容器。
此外,我们还有一个 Docker 仓库来存储我们的镜像。这些仓库可以是公开的或私有的。一些云服务提供商提供这些私有仓库。最著名的是 Docker Hub。它可以是免费的,但如果你选择这个选项,镜像应该是公开的。当然,Docker Hub 支持私有镜像,但在这种情况下,你必须为服务付费。
容器
Docker 容器是一种轻量级的虚拟化。轻量级意味着 Docker 使用 SO 功能来限制系统进程和管理内存、处理器和文件夹。这与使用 VM 的虚拟化不同,因为在这种模式下,技术需要模拟整个 SO、驱动器和存储。这项任务消耗了大量的计算能力,有时可能效率不高。
Docker 网络
Docker 网络是一个为容器提供运行时隔离的层。它是一种沙盒,可以在其中运行与其他容器隔离的容器。当安装 Docker 时,默认情况下会创建三个网络,这些网络不应该被删除。这三个网络如下:
-
bridge -
none -
host
此外,Docker 还提供了一个用户创建网络的简单方法。为此,Docker 提供了两个驱动器——bridge和overlay。
桥接可用于本地环境,这意味着这种网络允许在单个主机上使用。这对我们的应用程序将很有用,因为它促进了容器之间的隔离,特别是在安全性方面。这是一个好的做法。连接到这种网络的容器的名称可以用作容器的DNS。内部,Docker 会将容器名称与容器 IP 关联起来。
Overlay 网络提供了连接不同机器上容器的功能。这种网络由 Docker Swarm 用于在集群环境中管理容器。在新版本中,Docker Compose 工具原生支持 Docker Swarm。
Docker 卷
Docker 卷是建议在容器外部持久化数据的方式。这些卷完全由 Docker Engine 管理,当使用 Docker 命令行时,这些卷可以根据配置进行读写。这些卷的数据持久化在主机上的目录路径上。
有一个命令行工具可以与卷交互。这个工具的基础是docker volume命令;末尾的--help参数显示了帮助说明。
Docker 命令
现在我们将查看 Docker 命令。这些命令主要用于开发生命周期,如启动容器、停止容器、删除和检查等命令。
Docker run
docker run是最常见的 Docker 命令。这个命令应该用来启动容器。命令的基本结构如下:
docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
选项参数允许对容器进行一些配置,例如,--name参数允许您为容器配置一个名称。当容器在桥接网络中运行时,这对于 DNS 来说很重要。
网络设置也可以在run命令中进行配置,参数是--net。这使我们能够配置容器将要连接的网络。
另一个重要的选项是detached。它表示容器是否将在后台运行。-d参数指示 Docker 在后台运行容器。
Docker container
docker container命令允许您管理容器。有许多命令,如下列列表所示:
-
docker container attach -
docker container commit -
docker container cp -
docker container create -
docker container diff -
docker container exec -
docker container export -
docker container inspect -
docker container kill -
docker container logs -
docker container ls -
docker container pause -
docker container port -
docker container prune -
`docker container rename` -
docker container restart -
docker container rm -
docker container run -
docker container start -
docker container stats -
docker container stop -
docker container top -
docker container unpause -
docker container update -
docker container wait
这里有一些重要的命令。docker container exec命令允许您在运行的容器上运行命令。这是一个重要的任务,用于调试或查看容器文件。docker container prune删除已停止的容器。它在开发周期中很有帮助。有一些已知的命令,如docker container rm、docker container start、docker container stop和docker container restart。这些命令是自我解释的,并且具有类似的行为。
Docker network
docker network命令允许您通过命令行管理 Docker 网络。有六个基本命令,命令是自我解释的:
-
docker network create -
docker network connect -
docker network ls -
docker network rm -
docker network disconnect -
docker network inspect
docker network create、docker network ls和docker network rm是主要的命令。可以将它们与 Linux 命令进行比较,其中rm命令用于删除东西,而ls命令通常用于列出文件夹等东西。create命令应用于创建网络。
docker network connect和docker network disconnect命令允许您将正在运行的容器连接到所需的网络。在某些场景中可能很有用。
最后,docker network inspect命令提供了请求网络的相关详细信息。
Docker 卷
docker volume命令允许您通过命令行界面管理 Docker 卷。这里有五个命令:
-
docker volume create -
docker volume inspect -
docker volume ls -
docker volume prune -
docker volume rm
docker volume create、docker volume rm和docker volume ls命令通过 Docker Engine 有效地用于管理docker volume。这些行为与网络的行为非常相似,但针对的是卷。create命令将创建一个新的卷,并允许一些选项。ls命令列出所有卷,而rm命令将删除请求的卷。
摘要
在本章中,我们探讨了 Spring 框架的主要概念。我们了解了框架的主要模块以及这些模块如何帮助开发者以不同的架构构建应用程序,例如消息应用程序、REST API 和 Web 门户。
我们也花了一些时间准备我们的开发环境,安装了一些必要的工具,例如 Java JDK、Maven 和 IDE。这是我们在继续下一章之前必须采取的关键步骤。
我们使用 Docker 帮助我们设置开发环境,例如为数据库和我们的应用程序在 Docker 镜像中设置容器。我们安装了 Docker 并查看管理容器、网络和卷的主要命令。
在下一章中,我们将创建我们的第一个 Spring 应用程序并将其付诸实践!
第二章:进入 Spring 世界 - CMS 应用程序
现在,我们将创建我们的第一个应用程序;在这个阶段,我们已经学习了 Spring 的概念,并且我们准备好将它们付诸实践。在本章的开始,我们将介绍 Spring 依赖项以创建一个 Web 应用程序,我们还知道 Spring Initializr 是一个出色的项目,它允许开发者创建具有所需依赖项的 Spring 骨架项目。在本章中,我们将学习如何在 IDE 和命令行上搭建我们的第一个 Spring 应用程序,公开我们的第一个端点,了解其内部工作原理,并了解 Spring REST 支持的主要注解。我们将弄清楚如何为CMS(内容管理系统)应用程序创建服务层,并理解依赖注入在 Spring 容器中的工作方式。我们将遇到 Spring 的典型用法并实现我们的第一个 Spring Bean。在本章的结尾,我们将解释如何创建视图层并将其与 AngularJS 集成。
在本章中,将涵盖以下主题:
-
创建项目结构
-
运行第一个 Spring 应用程序
-
介绍 REST 支持
-
理解 Spring 中的依赖注入
创建 CMS 应用程序结构
现在,我们将使用 Spring 框架创建我们的第一个应用程序;我们将使用 Spring Initializr 为 CMS 应用程序创建一个基本结构。这个页面帮助我们启动应用程序,它是一种指南,允许我们在 Maven 或 Gradle 上配置依赖项。我们还可以选择 Spring Boot 的语言和版本。
页面看起来像这样:

在项目元数据部分,我们可以为 Maven 项目添加坐标;有一个group字段,它引用了groupId标签,我们还有artifacts字段,它引用了artifactId。这些都是 Maven 坐标。
依赖关系部分允许配置 Spring 依赖项,该字段具有自动完成功能,并帮助开发者输入正确的依赖项。
CMS 项目
在我们开始编码和学习令人惊叹的事情之前,让我们了解一下 CMS 项目,该项目的主要目的是帮助公司管理不同主题的 CMS 内容。在这个项目中,有三个主要实体:
-
News类是最重要的,它将存储新闻的内容。 -
它有一个类别,这使得搜索更容易,我们还可以按类别对新闻进行分组,当然,我们也可以按创建新闻的用户进行分组。新闻应由其他用户批准,以确保其遵循公司规则。
-
新闻也有一些标签,正如我们所见,应用程序相当标准,业务规则也很简单;这是故意的,因为我们保持对我们将要学习的新内容的关注。
现在我们知道了 Spring Initializr (start.spring.io)是如何工作的以及我们需要遵循的业务规则,我们准备好创建项目了。让我们现在就动手吧。
项目元数据部分
在“分组”字段中插入spring-five,在“工件”字段中插入cms。如果您想自定义它,没问题,这是一种信息性项目配置:

依赖项部分
在“搜索依赖项”字段中输入MVC一词。Web 模块将作为选项出现,Web 模块包含嵌入式 Tomcat 和 Spring MVC 的全栈 Web 开发,选择它。我们还需要在这个模块中放置Thymeleaf依赖项。它是一个模板引擎,将在本章末尾的视图功能中很有用。输入Thymeleaf,它包括 Thymeleaf 模板引擎,并包含与 Spring 的集成。模块将出现,然后选择它。现在我们可以在“已选依赖项”面板中看到 Web 和 Thymeleaf:

生成项目
在我们完成项目定义并选择项目依赖项后,我们准备好下载项目。可以通过点击“生成项目”按钮来完成,点击它。项目将被下载。在这个阶段,项目已准备好开始我们的工作:

压缩文件将以cms.zip(工件字段输入信息)命名,下载文件的位置取决于浏览器配置。
在打开项目之前,我们必须将Spring Initializr生成的工件解压缩到目标位置。命令应该是:
unzip -d <目标位置> /<文件路径>/cms.zip。请参考以下示例:unzip -d /home/john /home/john/Downloads/cms.zip.
现在,我们可以在我们的 IDE 中打开项目。让我们打开它并查看项目的基本结构。
运行应用程序
在我们运行应用程序之前,让我们了解一下我们的项目结构。
使用 IntelliJ IDEA 的导入项目或打开选项(两者类似)打开项目,以下页面将显示:

然后,我们可以打开或导入pom.xml文件。
应该显示以下项目结构:

打开pom.xml,我们有三个依赖项,spring-boot-starter-thymeleaf、spring-boot-starter-web、spring-boot-starter-test,以及一个有趣的插件spring-boot-maven-plugin。
这些starter依赖项是开发者的快捷方式,因为它们为模块提供了完整的依赖项。例如,在spring-boot-starter-web中,有web-mvc、jackson-databind、hibernate-validator-web以及其他一些依赖项;这些依赖项必须在类路径上才能运行 Web 应用程序,而 starters 使这项任务变得容易得多。
让我们分析我们的 pom.xml 文件,该文件应该看起来像这样:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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>spring-five</groupId>
<artifactId>cms</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>cms</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<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.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
此外,我们有一个 spring-boot-maven-plugin,这个出色的插件为 Maven 提供了 Spring Boot 支持。它允许你将应用程序打包成 Fat-JAR,该插件支持运行、启动和停止目标,以及与我们的应用程序交互。
Fat-JAR:一个包含所有项目类文件和资源,以及所有依赖项打包在一起的 JAR 文件。
现在,关于 Maven 配置的内容就到这里;让我们看看 Java 文件。
Spring Initializr 为我们创建了一个类,通常,这个类的名字是构件名称加上 Application,在我们的例子中是 CmsApplication,这个类应该看起来像这样:
package springfive.cms;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CmsApplication {
public static void main(String[] args) {
SpringApplication.run(CmsApplication.class, args);
}
}
查看内部结构
在这里有一些有趣的事情,让我们来理解它们。@SpringBootApplication 是 Spring Boot 应用程序的基本注解;它是 @Configuration、@EnableAutoConfiguration 和 @Component 注解的一种别名。让我们深入探讨:
-
第一个注解
@Configuration表示该类可以为 Spring 容器生成 bean 定义。这是一个与外部依赖项(如DataSources)一起工作的有趣注解;这是此注解最常见的使用场景。 -
第二个注解
@EnableAutoConfiguration表示,在 SpringApplicationContext容器中,它将尝试帮助我们为特定上下文配置默认的 bean。例如,当我们使用 Spring Boot 创建 Web MVC 应用程序时,我们可能需要一个 Web 服务器容器来运行它。在默认配置中,Spring 容器,连同@EnableAutoConfiguration,将为我们配置一个 Tomcat 内嵌容器 bean。这个注解对开发者非常有帮助。 -
@Component是一种模式,容器理解哪个类被认为是自动检测的,并需要实例化它。
SpringApplication 类负责从主方法启动 Spring 应用程序,它将创建一个 ApplicationContext 实例,处理由配置文件提供的配置,最后,它将加载由注解定义的单例 bean。
模式注解表示架构层中的一个概念性划分。它们帮助开发者理解类的目的和 beans 所代表的层,例如,@Repository 表示数据访问层。
运行应用程序
我们将在 IntelliJ IDEA 和命令行中运行应用程序。学习这项任务非常重要,因为我们工作在不同的开发环境中;有时应用程序的配置稍微有些复杂,我们无法使用 IDE 运行它,或者有时公司有不同的 IDE 作为标准,因此我们将学习两种不同的方法。
IntelliJ IDEA
通常,IntelliJ IDEA 会识别带有@SpringBootApplication注解的主类,并为我们创建一个运行配置,但这取决于工具的版本,让我们来做这件事。
命令行
命令行是一个更通用的运行项目工具。也多亏了 Spring Boot Maven 插件,这个任务变得简单。有两种运行方式,我们将介绍两种。
通过 Maven goal 执行命令行
第一个是一个 Spring Boot Maven 插件的 goal,它很简单;打开终端,然后转到根项目文件夹,注意,这个文件夹与我们的pom.xml文件在同一个文件夹中,执行以下命令:
mvn clean install spring-boot:run
Maven 现在将编译项目并运行主类CmsApplication,我们应该看到以下输出:

通过 JAR 文件执行命令行
要通过 Java 文件运行它,我们需要编译和打包它,然后我们可以使用 Java 命令行运行项目。要编译和打包它,我们可以使用相当标准的 Maven 命令,如下所示:
mvn clean install
在项目编译和打包为 Fat-JAR 之后,我们可以执行 JAR 文件,转到目标文件夹并检查此文件夹中的文件,可能的结果如下所示:

在我们的目标文件夹中有两个主要文件,cms-0.0.1-SNAPSHOT.jar和cms-0.0.1-SNAPSHOT.jar.original,带有.original扩展名的文件是不可执行的。它是编译产生的原始工件,另一个是我们的可执行文件。这是我们正在寻找的,让我们执行它,输入以下命令:
java -jar cms-0.0.1-SNAPSHOT.jar
结果应该如显示的那样。应用程序正在运行:

这部分就到这里,在下一节中,我们将创建第一个REST(表示状态传输)资源,并了解 REST 端点是如何工作的。
创建 REST 资源
现在,在这一节中,我们已经有一个应用程序正在运行,我们将添加一些 REST 端点并为 CMS 应用程序建模一些初始类,这些 REST 端点将有助于 AngularJS 集成。
对于 APIs 的一个必需特性是文档,而帮助我们完成这些任务的流行工具之一是 Swagger。Spring 框架支持 Swagger,我们可以通过几个注解来实现它。项目的 Spring Fox 是完成这项任务的正确工具,我们将在本章中查看这个工具。
让我们这样做。
模型
在我们开始创建我们的类之前,我们将在项目中添加Lombok依赖。这是一个非常棒的库,它在编译时提供了一些有趣的功能,如GET/SET,Val关键字使变量成为 final,@Data使一个类具有一些默认方法,如 getters/setters,equals和hashCode。
添加 Lombok 依赖
在pom.xml文件中添加以下依赖项:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
<scope>provided</scope>
</dependency>
provided 范围指示 Maven 不要将此依赖项包含在 JAR 文件中,因为我们需要在编译时使用它。我们不需要在运行时使用它。等待 Maven 下载依赖项,这就是现在所有的事情。
此外,我们还可以使用 IntelliJ IDEA 提供的“重新导入所有 Maven 项目”,它位于 Maven 项目选项卡中,如下所示:

创建模型
现在,我们将创建我们的模型,这些模型是带有 @Data 注解的 Java 类。
标签
这个类代表我们系统中的一个标签。不一定有它的存储库,因为它将与我们的 News 实体一起持久化:
package springfive.cms.domain.models;
import lombok.Data;
@Data
public class Tag {
String value;
}
类别
我们 CMS 应用程序的一个类别模型可以用来分组新闻。另外,另一个重要的事情是,这使得我们的新闻被分类,使得搜索任务变得容易。看看下面的代码:
package springfive.cms.domain.models;
import lombok.Data;
@Data
public class Category {
String id;
String name;
}
用户
它代表我们领域模型中的一个用户。我们有两个不同的配置文件,一个是作为新闻作者的作者,另一个是必须审核在门户网站上注册的新闻的审稿人。看看下面的例子:
package springfive.cms.domain.models;
import lombok.Data;
@Data
public class User {
String id;
String identity;
String name;
Role role;
}
新闻
这个类代表我们领域中的新闻,目前它没有任何行为。只暴露了属性和获取/设置方法;在未来,我们将添加一些行为:
package springfive.cms.domain.models;
import java.util.Set;
import lombok.Data;
@Data
public class News {
String id;
String title;
String content;
User author;
Set<User> mandatoryReviewers;
Set<Review> reviewers;
Set<Category> categories;
Set<Tag> tags;
}
Review 类可以在 GitHub 上找到:(github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter02/src/main/java/springfive/cms/domain/models).
如我们所见,它们是简单的 Java 类,代表我们的 CMS 应用程序领域。它是我们应用程序的核心,所有的领域逻辑都将驻留在这些类中。这是一个重要的特征。
嗨,这是 REST 资源
我们已经创建了模型,现在我们可以开始考虑我们的 REST 资源。我们将创建三个主要资源:
-
CategoryResource将负责Category类。 -
第二个是
UserResource。它将负责管理User类和 REST API 之间的交互。 -
最后一个,同样重要的是,将是
NewsResource,它将负责管理新闻实体,如评论。
创建 CategoryResource 类
我们将创建我们的第一个 REST 资源,让我们从负责管理我们的Category类的CategoryResource类开始。这个实体的实现将很简单,我们将创建创建、检索、更新和删除等 CRUD 端点。当我们创建 API 时,我们必须牢记两件重要的事情。第一件是正确的 HTTP 动词,如POST、GET、PUT和DELETE。对于 REST API 来说,拥有正确的 HTTP 动词是至关重要的,因为它为我们提供了关于 API 的内在知识。这是一个与我们的 API 交互的任何事物的模式。另一件事是状态码,它与第一件事相同,我们必须遵循这个模式,这是开发者容易识别的模式。《理查森成熟度模型》可以帮助我们创建出色的 REST API,这个模型引入了一些级别来衡量 REST API,它就像一个温度计。
首先,我们将为我们的 API 创建一个骨架。想想你的应用程序需要哪些功能。在下一节中,我们将解释如何在 REST API 中添加服务层。现在,让我们构建一个CategoryResource类,我们的实现可能看起来像这样:
package springfive.cms.domain.resources;
import java.util.Arrays;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import springfive.cms.domain.models.Category;
import springfive.cms.domain.vo.CategoryRequest;
@RestController
@RequestMapping("/api/category")
public class CategoryResource {
@GetMapping(value = "/{id}")
public ResponseEntity<Category> findOne(@PathVariable("id") String id){
return ResponseEntity.ok(new Category());
}
@GetMapping
public ResponseEntity<List<Category>> findAll(){
return ResponseEntity.ok(Arrays.asList(new Category(),new Category()));
}
@PostMapping
public ResponseEntity<Category> newCategory(CategoryRequest category){
return new ResponseEntity<>(new Category(), HttpStatus.CREATED);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void removeCategory(@PathVariable("id") String id){
}
@PutMapping("/{id}")
public ResponseEntity<Category> updateCategory(@PathVariable("id") String id,CategoryRequest category){
return new ResponseEntity<>(new Category(), HttpStatus.OK);
}
}
CategoryRequest可以在 GitHub 上找到(github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter02/src/main/java/springfive/cms/domain/vo)。
在这里有一些重要的概念。第一个是@RestController。它指示 Spring 框架,CategoryResource类将通过 Web-MVC 模块公开 REST 端点。这个注解将在框架中配置一些事情,例如HttpMessageConverters来处理 HTTP 请求和响应,如 XML 或 JSON。当然,我们需要在类路径上添加正确的库来处理 JSON 和 XML。还需要添加一些请求头,如Accept和Content-Type。这个注解是在 4.0 版本中引入的。它是一种语法糖注解,因为它被@Controller和@ResponseBody注解。
第二个是@RequestMapping注解,这个重要的注解负责我们类中的 HTTP 请求和响应。在这个代码中,当我们使用它在类级别时,它将传播到所有方法,并且方法使用它作为相对路径。@RequestMapping注解有不同的使用场景。它允许我们配置 HTTP 动词、参数和头信息。
最后,我们有@GetMapping、@PostMapping、@DeleteMapping和@PutMapping,这些注解是配置@RequestMapping与正确 HTTP 动词的一种快捷方式;一个优点是这些注解使代码更易于阅读。
除了removeCategory方法外,所有方法都返回ResponseEntity类,这使得我们能够在下一节中处理正确的 HTTP 状态码。
用户资源
UserResource类与CategoryResource类相同,只是它使用User类。我们可以在 GitHub 上找到整个代码(github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter02)。
NewsResource
NewsResource类是必不可少的,这个端点允许用户审查之前注册的新闻,并且它还提供了一个端点来返回更新的新闻。这是一个重要的功能,因为我们只对相关的新闻感兴趣。不相关的新闻不能在门户上显示。资源类应如下所示:
package springfive.cms.domain.resources;
import java.util.Arrays;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import springfive.cms.domain.models.News;
import springfive.cms.domain.models.Review;
import springfive.cms.domain.vo.NewsRequest;
@RestController
@RequestMapping("/api/news")
public class NewsResource {
@GetMapping(value = "/{id}")
public ResponseEntity<News> findOne(@PathVariable("id") String id){
return ResponseEntity.ok(new News());
}
@GetMapping
public ResponseEntity<List<News>> findAll(){
return ResponseEntity.ok(Arrays.asList(new News(),new News()));
}
@PostMapping
public ResponseEntity<News> newNews(NewsRequest news){
return new ResponseEntity<>(new News(), HttpStatus.CREATED);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void removeNews(@PathVariable("id") String id){
}
@PutMapping("/{id}")
public ResponseEntity<News> updateNews(@PathVariable("id") String id,NewsRequest news){
return new ResponseEntity<>(new News(), HttpStatus.OK);
}
@GetMapping(value = "/{id}/review/{userId}")
public ResponseEntity<Review> review(@PathVariable("id") String id,@PathVariable("userId") String userId){
return ResponseEntity.ok(new Review());
}
@GetMapping(value = "/revised")
public ResponseEntity<List<News>> revisedNews(){
return ResponseEntity.ok(Arrays.asList(new News(),new News()));
}
}
NewsRequest类可以在 GitHub 上找到(github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter02/src/main/java/springfive/cms/domain/vo)。
注意 HTTP 动词和 HTTP 状态码,因为我们需要遵循正确的语义。
添加服务层
现在,我们已经准备好了 REST 层的框架,在本节中,我们将开始为我们的应用程序创建服务层。我们将展示依赖注入是如何在底层工作的,学习 Spring 框架上的 stereotypes 注解,并开始考虑我们的持久化存储,这将在下一节中介绍。
模型中的变化
我们需要对我们的模型进行一些更改,特别是对News类。在我们的业务规则中,我们需要保护我们的信息安全,然后我们需要审查所有新闻。我们将添加一些方法来添加用户完成的新审查,并且我们还将添加一个方法来检查新闻是否被所有强制性的审查员审查过。
添加新的审查
对于这个功能,我们需要在我们的News类中创建一个方法,该方法将返回一个Review,其外观应如下所示:
public Review review(String userId,String status){
final Review review = new Review(userId, status);
this.reviewers.add(review);
return review;
}
我们不需要检查执行审查动作的用户是否是强制性的审查员。
安全地保存新闻
此外,我们还需要检查新闻是否被所有强制性的审查员完全修订。这相当简单,我们使用 Java 8,它提供了惊人的Stream接口,这使得集合交互比以前更容易。让我们这样做:
public Boolean revised() {
return this.mandatoryReviewers.stream().allMatch(reviewer -> this.reviewers.stream()
.anyMatch(review -> reviewer.id.equals(review.userId) && "approved".equals(review.status)));
}
感谢,Java 8,我们很感激。
在开始服务层之前
我们的应用程序需要一个持久化存储,以便即使在应用程序关闭的情况下,我们的记录也可以被加载。我们将为我们的存储库创建一个模拟实现。在第三章“使用 Spring Data 和响应式时尚进行持久化”中,我们将介绍 Spring Data 项目,这些项目帮助开发者使用出色的 DSL 创建惊人的存储库。现在,我们将创建一些 Spring bean 来在内存中存储我们的元素,让我们来做这件事。
CategoryService
让我们从最简单的服务开始,即CategoryService类,这个类预期的行为是 CRUD 操作。然后,我们需要表示我们的持久化存储或仓库实现,目前,我们使用临时存储和ArrayList来存储我们的类别。在下一章中,我们将为我们的 CMS 应用程序添加真正的持久化。
让我们创建我们的第一个 Spring 服务。实现如下所示:
package springfive.cms.domain.service;
import java.util.List;
import org.springframework.stereotype.Service;
import springfive.cms.domain.models.Category;
import springfive.cms.domain.repository.CategoryRepository;
@Service
public class CategoryService {
private final CategoryRepository categoryRepository;
public CategoryService(CategoryRepository categoryRepository) {
this.categoryRepository = categoryRepository;
}
public Category update(Category category){
return this.categoryRepository.save(category);
}
public Category create(Category category){
return this.categoryRepository.save(category);
}
public void delete(String id){
final Category category = this.categoryRepository.findOne(id);
this.categoryRepository.delete(category);
}
public List<Category> findAll(){
return this.categoryRepository.findAll();
}
public Category findOne(String id){
return this.categoryRepository.findOne(id);
}
}
这里有一些新内容。这个类将由 Spring 容器检测并实例化,因为它有一个@Service注解。正如我们所见,这个类中没有什么特别之处。它不一定扩展任何类或实现任何接口。我们在构造函数中收到了CategoryRepository,这个类将由 Spring 容器提供,因为我们指示容器生成这个类,但在 Spring 5 中,在构造函数中不再需要使用@Autowired。它之所以能工作,是因为这个类中只有一个构造函数,Spring 会检测到它。此外,我们还有一些表示 CRUD 行为的方法,这些方法很容易理解。
UserService
UserService类与CategoryService非常相似,但规则是关于User实体的,对于这个实体我们没有特别之处。我们有@Service注解,并且我们也收到了UserRepository构造函数。它相当简单且易于理解。我们将展示UserService实现,它必须是这样的:
package springfive.cms.domain.service;
import java.util.List;
import java.util.UUID;
import org.springframework.stereotype.Service;
import springfive.cms.domain.models.User;
import springfive.cms.domain.repository.UserRepository;
import springfive.cms.domain.vo.UserRequest;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User update(String id,UserRequest userRequest){
final User user = this.userRepository.findOne(id);
user.setIdentity(userRequest.getIdentity());
user.setName(userRequest.getName());
user.setRole(userRequest.getRole());
return this.userRepository.save(user);
}
public User create(UserRequest userRequest){
User user = new User();
user.setId(UUID.randomUUID().toString());
user.setIdentity(userRequest.getIdentity());
user.setName(userRequest.getName());
user.setRole(userRequest.getRole());
return this.userRepository.save(user);
}
public void delete(String id){
final User user = this.userRepository.findOne(id);
this.userRepository.delete(user);
}
public List<User> findAll(){
return this.userRepository.findAll();
}
public User findOne(String id){
return this.userRepository.findOne(id);
}
}
注意带有@Service注解的类声明。这在 Spring 生态系统中是一个非常常见的实现。我们还可以找到@Component、@Repository注解。@Service和@Component在服务层中很常见,它们的行为没有区别。@Repository会稍微改变一些行为,因为框架会在数据访问层转换一些异常。
NewsService
这是一个有趣的服务,它将负责管理我们的新闻状态。它将像胶水一样与领域模型交互,在这种情况下,是News实体。这个服务与其他服务非常相似。我们收到了NewsRepository类,一个依赖项,并保留仓库以维护状态,让我们这样做。
@Service注解再次出现。这对于 Spring 应用程序来说几乎是标准的。我们还可以将其更改为@Component注解,但这对我们应用程序没有任何影响。
为我们的 API 配置 Swagger
Swagger 是文档化 Web API 的事实上工具,该工具允许开发者对 API 进行建模,以交互式的方式与 API 进行交互,并提供了一种简单的方法来生成各种语言的客户端实现。
API 文档是吸引开发者使用我们的 API 的绝佳方式。
将依赖项添加到 pom.xml
在开始配置之前,我们需要添加所需的依赖项。这些依赖项包括我们的项目中的 Spring Fox,并提供了许多注解来正确配置 Swagger。让我们添加这些依赖项。
新的依赖项位于 pom.xml 文件中:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
第一个依赖项是 Swagger 的核心,包括注解和相关内容。Spring Fox Swagger UI 依赖项提供了一个丰富的 HTML 接口,允许开发者与 API 交互。
配置 Swagger
依赖项已添加,现在我们可以配置 Swagger 的基础设施。配置相当简单。我们将创建一个带有 @Configuration 的类来为 Spring 容器生成 Swagger 配置。让我们来做这件事。
查看以下 Swagger 配置:
package springfive.cms.infra.swagger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfiguration {
@Bean
public Docket documentation() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.paths(PathSelectors.any())
.build();
}
}
@Configuration 指示 Spring 为 Swagger 生成一个 bean 定义。注解 @EnableSwagger2 添加了对 Swagger 的支持。@EnableSwagger2 应与 @Configuration 一起使用,这是强制性的。
Docket 类是一个用于创建 API 定义构建器,它为 Spring Swagger MVC 框架的配置提供了合理的默认值和便捷方法。
方法 .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class)) 的调用指示框架处理带有 @RestController 注解的类。
有许多方法可以自定义 API 文档,例如,有一个方法可以添加认证头。
这就是 Swagger 配置,在下一节中,我们将创建第一个文档化的 API。
第一个文档化的 API
我们将从 CategoryResource 类开始,因为它容易理解,并且我们需要保持对技术内容的关注。我们将添加一些注解,然后魔法就会发生,让我们来做魔法。
CategoryResource 类应该看起来像这样:
package springfive.cms.domain.resources;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import springfive.cms.domain.models.Category;
import springfive.cms.domain.service.CategoryService;
import springfive.cms.domain.vo.CategoryRequest;
@RestController
@RequestMapping("/api/category")
@Api(tags = "category", description = "Category API")
public class CategoryResource {
private final CategoryService categoryService;
public CategoryResource(CategoryService categoryService) {
this.categoryService = categoryService;
}
@GetMapping(value = "/{id}")
@ApiOperation(value = "Find category",notes = "Find the Category by ID")
@ApiResponses(value = {
@ApiResponse(code = 200,message = "Category found"),
@ApiResponse(code = 404,message = "Category not found"),
})
public ResponseEntity<Category> findOne(@PathVariable("id") String id){
return ResponseEntity.ok(new Category());
}
@GetMapping
@ApiOperation(value = "List categories",notes = "List all categories")
@ApiResponses(value = {
@ApiResponse(code = 200,message = "Categories found"),
@ApiResponse(code = 404,message = "Category not found")
})
public ResponseEntity<List<Category>> findAll(){
return ResponseEntity.ok(this.categoryService.findAll());
}
@PostMapping
@ApiOperation(value = "Create category",notes = "It permits to create a new category")
@ApiResponses(value = {
@ApiResponse(code = 201,message = "Category created successfully"),
@ApiResponse(code = 400,message = "Invalid request")
})
public ResponseEntity<Category> newCategory(@RequestBody CategoryRequest category){
return new ResponseEntity<>(this.categoryService.create(category), HttpStatus.CREATED);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@ApiOperation(value = "Remove category",notes = "It permits to remove a category")
@ApiResponses(value = {
@ApiResponse(code = 200,message = "Category removed successfully"),
@ApiResponse(code = 404,message = "Category not found")
})
public void removeCategory(@PathVariable("id") String id){
}
@PutMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@ApiOperation(value = "Update category",notes = "It permits to update a category")
@ApiResponses(value = {
@ApiResponse(code = 200,message = "Category update successfully"),
@ApiResponse(code = 404,message = "Category not found"),
@ApiResponse(code = 400,message = "Invalid request")
})
public ResponseEntity<Category> updateCategory(@PathVariable("id") String id,CategoryRequest category){
return new ResponseEntity<>(new Category(), HttpStatus.OK);
}
}
有很多新的注解需要理解。@Api 是根注解,它将此类配置为 Swagger 资源。有许多配置,但我们将使用标签和描述,因为它们已经足够了。
@ApiOperation 描述了我们的 API 中的操作,通常是对请求路径的操作。value 属性被视为 Swagger 上的摘要字段,它是操作的简述,而 notes 是操作的描述(更详细的内容)。
最后一个是 @ApiResponse,它允许开发者描述操作的响应。通常,他们想要配置状态码和消息来描述操作的结果。
在运行应用程序之前,我们应该编译源代码。这可以通过使用 Maven 命令行执行 mvn clean install 或通过 IDE 使用运行应用程序来完成。
现在,我们已经配置了 Swagger 集成,我们可以在网页浏览器中检查 API 文档。为此,我们需要导航到 http://localhost:8080/swagger-ui.html 并显示此页面:

我们可以看到配置在我们 CMS 应用程序中的 API 端点。现在,我们将查看我们之前配置的分类,点击“显示/隐藏”链接。输出应该是:

如我们所见,在我们的分类 API 中有五个操作,每个操作都有一个路径和摘要来帮助理解其目的。我们可以点击请求的操作并查看关于该操作的详细信息。让我们来做吧,点击“列出分类”来查看详细文档。页面看起来像这样:

干得漂亮。现在我们有一个出色的 API,拥有优秀的文档。做得好。
让我们继续创建我们的 CMS 应用程序。
集成到 AngularJS
几年来,AngularJS 框架已经成为一种趋势,社区非常活跃,该项目由 Google 创建。
框架的主要思想是帮助开发者处理前端层的复杂性,尤其是在 HTML 部分。HTML 标记语言是静态的。它是一个创建静态文档的伟大工具,但今天它不再是现代网络应用程序的要求。这些应用程序需要是动态的。世界各地的 UX 团队努力创造令人惊叹的应用程序,具有不同的效果,这些人试图让应用程序对用户更加舒适。
AngularJS 增加了扩展 HTML 的可能性,添加一些额外的属性和标签。在本节中,我们将在前端应用程序中添加一些有趣的行为。让我们来做。
AngularJS 概念
在我们的 CMS 应用程序中,我们将使用一些 Angular 组件。我们将使用 Controllers,它们将与我们的 HTML 交互并处理一些页面的行为,例如显示错误消息的页面。Services 负责处理基础设施代码,例如与我们的 CMS API 交互。本书的目的不是成为 AngularJS 指南。然而,我们将探讨一些有趣的概念来开发我们的应用程序。
AngularJS 的常用标签包括:
-
ng-app -
ng-controller -
ng-click -
ng-hide -
ng-show
这些标签包含在 AngularJS 框架中。社区还创建了和维护了许多其他标签。例如,有一个用于处理 HTML 表单的库,我们将使用它来在我们的 CMS 门户中添加动态行为。
控制器
控制器是框架的一部分,用于处理应用程序的业务逻辑。它们应该用于控制应用程序中的数据流。控制器通过 ng-controller 指令附加到 DOM。
要向我们的视图添加一些操作,我们需要在控制器上创建函数,方法是创建函数并将它们添加到 $scope 对象中。
控制器不能用来执行 DOM 操作、格式化数据和过滤数据,在 AngularJS 世界中被认为是最佳实践。
通常,控制器会注入服务对象以委托处理业务逻辑。我们将在下一节中了解服务。
服务
服务是我们应用中处理业务逻辑的对象。在某些情况下,它们可以用来处理状态。服务对象是单例的,这意味着我们整个应用程序中只有一个实例。
在我们的应用程序中,服务负责与基于 Spring Boot 构建的 CMS API 交互。让我们来做这件事。
创建应用程序入口点
Spring Boot 框架允许我们提供静态文件。这些文件应位于以下这些文件夹中的 classpath 中,/static、/public、/resources或/META-INF/resources。
我们将使用/static文件夹,在这个文件夹中,我们将放置我们的 AngularJS 应用程序。对于模块化 AngularJS 应用程序文件夹结构有一些标准,这取决于应用程序的大小和需求。我们将使用最简单的风格以保持对 Spring 集成的关注。看看项目结构:

有一些资产可以启动和运行 AngularJS 应用程序。我们将使用内容分发网络(CDN)来加载 AngularJS 框架、Angular UI-Router,它帮助我们处理 Web 应用程序的路由,以及 Bootstrap 框架,它帮助我们开发页面。
内容分发网络是全球分布式的代理服务器。它使内容具有更高的可用性并提高性能,因为它将更靠近终端用户托管。详细的解释可以在 CloudFare 页面找到(www.cloudflare.com/learning/cdn/what-is-a-cdn/)。
然后,我们可以开始配置我们的 AngularJS 应用程序。让我们从我们的入口点index.html开始:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spring Boot Security</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body ng-app="cms">
<!-- Header -->
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar"
aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">CMS</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Home</a></li>
<li><a href="#users">Users</a></li>
<li><a href="#categories">Categories</a></li>
<li><a href="#news">News</a></li>
</ul>
</div>
</div>
</nav>
<!-- Body -->
<div class="container">
<div ui-view></div>
</div>
<script src="img/angular.min.js"></script>
<script src="img/angular-ui-router.js"></script>
<script type="text/javascript" src="img/app.js"></script>
<script type="text/javascript" src="img/controllers.js"></script>
<script type="text/javascript" src="img/services.js"></script>
<script type="text/javascript" src="img/category-controller.js"></script>
<script type="text/javascript" src="img/category-service.js"></script>
<script type="text/javascript" src="img/news-controller.js"></script>
<script type="text/javascript" src="img/news-service.js"></script>
<script type="text/javascript" src="img/user-controller.js"></script>
<script type="text/javascript" src="img/user-service.js"></script>
</body>
</html>
这里有一些重要的事情。让我们来理解它们。
ng-app标签是一个指令,用于引导 AngularJS 应用程序。这个标签是应用程序的根元素,通常放置在<body>或<html>标签上。
ui-view标签指示 Angular UI-Router 关于 HTML 文档的哪个部分将由应用程序状态处理,换句话说,指定的部分具有动态行为,其变化取决于路由系统。看看下面的代码片段:
<!-- Body -->
<div class="container">
<div ui-view></div>
</div>
这部分代码可以在index.hml文件中找到。
在ui-view之后,我们有我们的 JavaScript 文件,第一个是 AngularJS 框架,在这个版本中文件被压缩。看看我们的 JavaScript 文件,这些文件是在/static/app/components文件夹中创建的。看看这里的图片:

第二个是 UI-Router,它帮助我们管理我们的路由。最后,我们有我们的 JavaScript 文件,这些文件配置了 AngularJS 应用程序、我们的控制器以及与我们的 CMS API 交互的服务。
此外,我们还有一些引导类,用于对齐字段并使设计更容易。
创建分类控制器
现在,我们需要创建我们的控制器。我们将从最简单的开始,使示例更容易理解。《CategoryController》负责控制Category实体的数据。有两个控制器,一个使我们能够创建一个分类,另一个列出存储在数据库中的所有分类。
category-controller.js 应该是这样的:
(function (angular) {
'use strict';
// Controllers
angular.module('cms.modules.category.controllers', []).
controller('CategoryCreateController',
['$scope', 'CategoryService','$state',
function ($scope, CategoryService,$state) {
$scope.resetForm = function () {
$scope.category = null;
};
$scope.create = function (category) {
CategoryService.create(category).then(
function (data) {
console.log("Success on create Category!!!")
$state.go('categories')
}, function (err) {
console.log("Error on create Category!!!")
});
};
}]).
controller('CategoryListController',
['$scope', 'CategoryService',
function ($scope, CategoryService) {
CategoryService.find().then(function (data) {
$scope.categories = data.data;
}, function (err) {
console.log(err);
});
}]);
})(angular);
我们创建了一个 AngularJS 模块。它帮助我们保持函数的有序。它充当我们的一种命名空间。.controller 函数是一个构造函数,用于创建我们控制器的实例。我们收到了一些参数,AngularJS 框架将为我们注入这些对象。
创建分类服务
CategoryService 对象是一个单例对象,因为它是一个 AngularJS 服务。该服务将与我们的由 Spring Boot 应用程序提供的 CMS API 进行交互。
我们将使用 $http 服务。它使 HTTP 通信更容易。
让我们编写CategoryService:
(function (angular) {
'use strict';
/* Services */
</span> angular.module('cms.modules.category.services', []).
service('CategoryService', ['$http',
function ($http) {
var serviceAddress = 'http://localhost:8080';
var urlCollections = serviceAddress + '/api/category';
var urlBase = serviceAddress + '/api/category/';
this.find = function () {
return $http.get(urlCollections);
};
this.findOne = function (id) {
return $http.get(urlBase + id);
};
this.create = function (data) {
return $http.post(urlBase, data);
};
this.update = function (data) {
return $http.put(urlBase + '/id/' + data._id, data);
};
this.remove = function (data) {
return $http.delete(urlBase + '/id/' + data._id, data);
};
}
]);
})(angular);
做得好,现在我们已经实现了 CategoryService。
.service 函数是一个构造函数,用于创建服务实例,angular 在幕后工作。构造函数上有注入,对于服务,我们需要一个 $http 服务来对我们的 API 进行 HTTP 调用。这里有几个 HTTP 方法。请注意正确的方法以保持 HTTP 语义。
摘要
在本章中,我们创建了我们的第一个 Spring 应用程序。我们看到了 Spring Initializr,这是一个帮助开发者创建应用程序骨架的神奇工具。
我们探讨了 Spring 在幕后是如何工作的,以及框架是如何通过几个注解进行配置的。现在,我们对 Spring 引导函数有了基本的了解,我们可以理解框架中存在的依赖注入和组件扫描功能。
这项知识是下一章的基础,现在我们准备开始使用更高级的功能,例如持久性。让我们开始吧。下一章再见。
第三章:使用 Spring Data 和反应式方式实现持久化
在上一章中,我们创建了我们的内容管理系统(CMS)应用程序。我们还介绍了 Spring 中的 REST(表示状态传输)支持,这使得我们能够开发一个简单的 Web 应用程序。此外,我们还学习了 Spring 框架中依赖注入的工作原理,这可能是框架最著名的特性。
在本章中,我们将为我们的应用程序添加更多功能。现实世界中的系统需要在真实数据库上持久化其数据;这对于一个生产就绪的应用程序是一个基本特性。此外,根据我们的模型,我们需要选择正确的数据结构以实现性能并避免阻抗不匹配。
在本章的第一部分,我们将使用传统的 SQL 数据库作为我们应用程序的存储。我们将深入研究 Spring Data JPA(Java 持久化 API)以实现 CMS 应用程序的持久化。我们将了解如何使用这个令人惊叹的 Spring 模块启用事务。
之后,我们将转向一种更现代的数据库类型,称为 NoSQL 技术。在这个领域,我们将使用著名的数据库文档模型,即 MongoDB,然后我们将为我们的 CMS 应用程序创建最终的解决方案。
MongoDB 为我们的应用程序提供了一个出色的解决方案,因为它支持文档存储模型,并允许我们将对象以 JSON 的形式存储,这使得我们的数据更易于阅读。此外,MongoDB 是无模式的,这是一个非常出色的特性,因为一个集合可以存储不同的文档。这意味着记录可以有不同的字段、内容和大小。MongoDB 的另一个重要特性是查询模型。它提供了一个易于理解的基于文档的查询,并且基于 JSON 标记,我们的查询将比任何其他数据库都更容易阅读。
最后,我们将添加 Spring 5.0 中最重要的特性:对反应式流的支持。我们的应用程序将转变为一个具有一些重要要求的现代 Web 应用程序。
下面是本章你将学习的内容概述:
-
实现 Spring Data JPA
-
使用 Spring Data Reactive MongoDB 创建仓库
-
学习反应式 Spring
-
理解 Project Reactor
学习 Docker 的基础知识
在《第一章》中,我们学习了 Docker 概念,Spring 世界的旅程。现在,是时候测试我们的知识并将其付诸实践了。在本章的第一部分,我们将启动 MongoDB 和 Postgres 实例,作为我们应用程序的数据库。我们将在应用程序中配置连接设置。
在本章的最后部分,我们将介绍 Maven 插件,它提供了一个通过 pom.xml 创建 Docker 镜像的简单方法,只需在文件中进行一些配置。最后,我们将在 Docker 容器中运行我们的应用程序。
准备 MongoDB
让我们创建我们的 MongoDB 容器。我们将使用 Docker Hub 提供的官方镜像。
首先,我们需要拉取镜像:
docker pull mongo:3.4.10
然后,我们将看到 Docker 引擎正在下载镜像内容。
为了从我们的容器中创建隔离,我们将为我们的应用程序和数据库创建一个单独的网络。该网络应使用桥接驱动程序以允许容器通信。
让我们创建一个 docker network:
docker network create cms-application
命令输出应该是一个创建的网络 ID。您的 ID 可能与我的不同:

要检查网络是否成功创建,可以使用 docker network ls 命令来帮助我们。
我们将启动我们的 MongoDB。该网络应该是 cms-application,但我们将数据库端口映射到主机端口。出于调试目的,我们将连接一个客户端到正在运行的数据库,但请不要在非开发环境中这样做。
在主机上暴露端口不是最佳实践。因此,我们使用 Docker 容器,这是其主要优势之一是进程隔离。在这种情况下,我们将无法控制网络。否则,我们可能会引起一些端口冲突。
首先,输入以下命令:
docker run -d --name mongodb --net cms-application -p 27017:27017 mongo:3.4.10
此外,我们可以使用 docker stop mongodb 命令停止 Docker MongoDB 容器,然后通过以下命令重新启动我们的容器:docker start mongodb。
输出将是一个哈希值,代表容器的 ID。
参数说明如下:
-
-d:这指示 Docker 以后台模式运行容器 -
--name:容器名称;它将在我们的网络中充当主机名 -
--net:容器将要连接的网络 -
-p:主机端口和容器端口,它们将被映射到主机接口上的一个容器
现在,我们在机器上运行了一个相当标准的 MongoDB 实例,我们可以开始在我们的 CMS 应用程序中添加持久性。我们很快就会这样做。
准备 PostgreSQL 数据库
与 MongoDB 类似,我们将为我们的 CMS 应用程序准备一个 PostgreSQL 实例。我们将更改我们的持久层以展示 Spring Data 如何为开发者抽象化它。然后,我们需要为它准备一个 Docker Postgres 实例。
我们将使用 Postgres 的 9.6.6 版本并使用 alpine 标签,因为它比其他 Postgres 镜像更小。让我们拉取我们的镜像。命令应该是这样的:
docker pull postgres:9.6.6-alpine
然后,等待下载完成。
在前面的部分中,我们创建了一个名为 cms-application 的 Docker 网络。现在,我们将像为 MongoDB 所做的那样,在该网络上启动我们的 Postgres 实例。启动 Postgres 的命令应该是以下这样的:
docker run -d --name postgres --net cms-application -p 5432:5432 -e POSTGRES_PASSWORD=cms@springfive
postgres:9.6.6-alpine
参数列表与我们为 MongoDB 传递的相同。我们希望以后台模式运行它并将其连接到我们的自定义网络。正如我们所见,docker run 命令中还有一个新的参数。让我们来理解它:
-e:这使我们能够为容器传递环境变量。在这种情况下,我们想更改密码值。
干得好。我们已经完成了我们的基础设施需求。现在让我们立即了解持久化的细节。
Spring Data 项目
Spring Data 项目是一个伞形项目,它提供了一种熟悉的方式来在广泛的数据库技术上创建我们的数据访问层。这意味着有高级抽象来与不同类型的数据结构交互,例如文档模型、列族、键值和图。此外,Spring Data JPA 项目完全支持 JPA 规范。
这些模块为我们领域模型提供了强大的对象映射抽象。
支持不同类型的数据结构和数据库。有一组子模块来保持框架的模块化。此外,这些子模块分为两大类:第一类是 Spring 框架团队支持的项目子集,第二类是社区提供的子模块子集。
Spring 团队支持的项目包括:
-
Spring Data Commons
-
Spring Data JPA
-
Spring Data MongoDB
-
Spring Data Redis
-
Spring Data for Apache Cassandra
社区支持的项目包括:
-
Spring Data Aerospike
-
Spring Data ElasticSearch
-
Spring Data DynamoDB
-
Spring Data Neo4J
仓库接口链的基础是Repository接口。这是一个标记接口,其一般目的是存储类型信息。这个类型将被用于扩展它的其他接口。
还有一个CrudRepository接口。这是最重要的,名字本身就说明了它的作用;它提供了一些执行 CRUD 操作的方法,并提供了一些实用方法,如count()、exists()和deleteAll()。这些是仓库实现最重要的基本接口。
Spring Data JPA
Spring Data JPA 提供了一个简单的方式来使用 Java EE 中的 JPA 规范实现数据访问层。通常,这些实现有很多样板代码和重复代码,维护数据库代码中的更改也很困难。Spring Data JPA 试图解决这些问题,并提供了一种无需样板代码和重复代码的直观方式来做到这一点。
JPA 规范提供了一个抽象层,用于与已经实现的不同数据库供应商交互。Spring 在高级模式上添加了一个抽象层。这意味着 Spring Data JPA 将创建一个仓库实现,并封装整个 JPA 实现细节。我们可以用对 JPA 规范的了解很少来构建我们的持久化层。
JPA 规范是由JCP(Java 社区进程)创建的,旨在帮助开发者在 Java 类和关系数据库之间持久化、访问和管理数据。一些供应商实现了这个规范。最著名的实现是 Hibernate (hibernate.org/orm/),默认情况下,Spring Data JPA 使用 Hibernate 作为 JPA 实现。
再见,DAO(数据访问对象)模式和实现。Spring Data JPA 旨在通过经过良好测试的框架和一些生产就绪功能来解决此问题。
现在,我们已经了解了 Spring Data JPA 是什么。让我们将其付诸实践。
配置 pom.xml 以使用 Spring Data JPA
现在,我们需要添加正确的依赖项以与 Spring Data JPA 一起工作。在我们的 pom.xml 文件中需要配置几个依赖项。
第一个是一个 Spring Data JPA Starter,它提供了许多自动配置类,使我们能够快速启动应用程序。最后一个依赖项是 PostgreSQL JDBC 驱动程序,它是必需的,因为它包含了连接 PostgreSQL 数据库的 JDBC 实现类。
新的依赖项包括:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.1.4</version>
</dependency>
简单且相当容易。
配置 Postgres 连接
为了将我们的应用程序与最近创建的数据库连接起来,我们需要在 application.yaml 文件中配置几行。再次感谢 Spring Data Starter,我们的连接将自动配置。
我们也可以使用 @Bean 注解来生成连接对象,但有许多对象需要配置。我们将继续使用配置文件。它更简单、更易于理解。
为了配置数据库连接,我们需要向 Spring 框架提供一些属性,例如数据库 URL、数据库用户名、密码,以及一个驱动类名,以指导 JPA 框架关于 JDBC 类的完整路径。
application.yaml 文件应该是这样的:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/postgres
username: postgres
password: cms@springfive
driver-class-name: org.postgresql.Driver
jpa:
show-sql: true
generate-ddl: true
在 datasource 部分中,我们已配置了数据库凭据连接以及数据库主机。
application.yaml 中的 JPA 部分可以用来配置 JPA 框架。在这一部分,我们配置了在控制台记录 SQL 指令。这有助于调试和进行故障排除。此外,我们还配置了 JPA 框架,以便在应用程序启动过程中在数据库中创建我们的表。
太棒了,JPA 基础设施已配置。做得好!现在,我们可以以 JPA 风格映射我们的模型。让我们在下一节中这样做。
映射模型
我们已成功配置数据库连接。现在,我们准备使用 JPA 注解来映射我们的模型。让我们从我们的 Category 模型开始。这是一个相当简单的类,这很好,因为我们对 Spring Data JPA 的内容感兴趣。
我们 Category 模型的第一个版本应该是这样的:
package springfive.cms.domain.models;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
@Data
@Entity
@Table(name = "category")
public class Category {
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name = "system-uuid", strategy = "uuid2")
String id;
String name;
}
我们需要更改一些模型类以适应 JPA 规范。我们可以在 GitHub 上找到模型类:github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter03/cms-postgres/src/main/java/springfive/cms/domain/models。
这里有一些新内容。@Entity 注解指示 JPA 框架,被注解的类是一个实体,在我们的例子中,是 Category 类,然后框架将与之关联一个数据库表。@Table 注解用于在数据库中命名表。这些注解是在类级别插入的,这意味着在类声明之上。
@Id 注解指示 JPA 哪个注解字段是数据库表的键。为实体生成顺序 ID 不是一种好做法,尤其是如果你正在创建 API。这有助于黑客理解 ID 的逻辑,使得攻击更容易。因此,我们将生成 UUID(通用唯一标识符)而不是简单的顺序 ID。@GenericGenerator 注解指示 Hibernate(一个 JPA 规范实现供应商)生成随机 UUID。
在 CMS 应用程序中添加 JPA 存储库
一旦整个基础设施和 JPA 映射完成,我们就可以将我们的存储库添加到项目中。在 Spring Data 项目中,有一些抽象,例如 Repository、CrudRepository 和 JpaRepository。我们将使用 JpaRepository,因为它支持分页和排序功能。
我们的存储库将会相当简单。有几个标准方法,例如 save()、update() 和 delete(),我们还将查看一些 DSL 查询方法,这些方法允许开发者根据属性名称创建自定义查询。我们创建了一个 AbstractRepository 来帮助我们存储对象在内存中。这已经不再必要了。我们可以将其删除。
让我们创建我们的第一个 JPA 存储库:
package springfive.cms.domain.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import springfive.cms.domain.models.Category;
public interface CategoryRepository extends JpaRepository<Category, String> {
List<Category> findByName(String name);
List<Category> findByNameIgnoreCaseStartingWith(String name);
}
正如我们所见,JpaRepository 接口使用所需的实体类型以及实体的 ID 类型进行类型化。这部分没有秘密。这个惊人的事情发生是为了支持基于属性名称的自定义查询。在 Category 模型中,有一个名为 name 的属性。我们可以使用 By 指令在我们的 CategoryRepository 中创建自定义方法,使用 Category 模型属性。正如我们所见,在 findByName(String name) 上,Spring Data 框架将创建正确的查询来按名称查找类别。这太棒了。
自定义查询方法支持许多关键字:
| 逻辑关键字 | 逻辑表达式 |
|---|---|
AND |
And |
OR |
Or |
AFTER |
After, IsAfter |
BEFORE |
Before, IsBefore |
CONTAINING |
Containing, IsContaining, Contains |
BETWEEN |
Between, IsBetween |
ENDING_WITH |
EndingWith, IsEndingWith, EndsWith |
EXISTS |
Exists |
FALSE |
False, IsFalse |
GREATER_THAN |
GreaterThan, IsGreaterThan |
GREATHER_THAN_EQUALS |
GreaterThanEqual, IsGreaterThanEqual |
IN |
In, IsIn |
IS |
Is, Equals, (or no keyword) |
IS_EMPTY |
IsEmpty, Empty |
IS_NOT_EMPTY |
IsNotEmpty, NotEmpty |
IS_NOT_NULL |
NotNull, IsNotNull |
IS_NULL |
Null, IsNull |
LESS_THAN |
LessThan, IsLessThan |
LESS_THAN_EQUAL |
LessThanEqual, IsLessThanEqual |
LIKE |
Like, IsLike |
NEAR |
Near, IsNear |
NOT |
Not, IsNot |
NOT_IN |
NotIn, IsNotIn |
NOT_LIKE |
NotLike, IsNotLike |
REGEX |
Regex, MatchesRegex, Matches |
STARTING_WITH |
StartingWith, IsStartingWith, StartsWith |
TRUE |
True, IsTrue |
WITHIN |
Within, IsWithin |
基于属性名称创建查询有许多方法。我们可以使用关键字组合关键字,例如 findByNameAndId。Spring Data JPA 提供了一种创建查询的一致方法。
配置事务
当我们使用 JPA 规范时,大多数应用程序还需要支持事务。Spring 在其他模块中也有出色的交易支持。这种支持与 Spring Data JPA 集成,我们可以利用它。在 Spring 中配置事务非常简单;我们只需在需要时插入 @Transactional 注解即可。有一些不同的用例可以使用它。我们将在服务层使用 @Transactional,然后我们将注解放在我们的服务类中。让我们看看我们的 CategoryService 类:
package springfive.cms.domain.service;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import springfive.cms.domain.exceptions.CategoryNotFoundException;
import springfive.cms.domain.models.Category;
import springfive.cms.domain.repository.CategoryRepository;
import springfive.cms.domain.vo.CategoryRequest;
@Service
@Transactional(readOnly = true)
public class CategoryService {
private final CategoryRepository categoryRepository;
public CategoryService(CategoryRepository categoryRepository) {
this.categoryRepository = categoryRepository;
}
@Transactional
public Category update(Category category) {
return this.categoryRepository.save(category);
}
@Transactional
public Category create(CategoryRequest request) {
Category category = new Category();
category.setName(request.getName());
return this.categoryRepository.save(category);
}
@Transactional
public void delete(String id) {
final Optional<Category> category = this.categoryRepository.findById(id);
category.ifPresent(this.categoryRepository::delete);
}
public List<Category> findAll() {
return this.categoryRepository.findAll();
}
public List<Category> findByName(String name) {
return this.categoryRepository.findByName(name);
}
public List<Category> findByNameStartingWith(String name) {
return this.categoryRepository.findByNameIgnoreCaseStartingWith(name);
}
public Category findOne(String id) {
final Optional<Category> category = this.categoryRepository.findById(id);
if (category.isPresent()) {
return category.get();
} else {
throw new CategoryNotFoundException(id);
}
}
}
在 CategoryService 类中存在许多 @Transactional 注解。类级别的第一个注解指示框架为那些类中所有存在的方法配置 readOnly,除了配置了 @Transactional 的方法。在这种情况下,类级别的注解将被覆盖为 readOnly=false。当省略值时,这是默认配置。
安装和配置 pgAdmin3
要连接到我们的 PostgreSQL 实例,我们将使用 pgAdmin 3,这是 Postgres 团队提供的免费工具。
要安装 pgAdmin 3,我们可以使用以下命令:
sudo apt-get install pgadmin3 -y
这将在我们的机器上安装 pgAdmin 3。
安装完成后,打开 pgAdmin 3,然后点击添加服务器连接。按钮看起来像这样:

然后,填写以下截图所示的信息:

密码应该是:cms@springfive.。
太棒了,我们的 pgAdmin 3 工具已配置完成。
检查数据库结构中的数据
整个应用程序结构已准备就绪。现在,我们可以检查数据库以获取我们的持久化数据。有许多开源的 Postgres 客户端。我们将使用之前配置的 pgAdmin 3。
第一次打开应用程序时,您将被要求输入凭据和主机。我们必须输入与我们在 application.yaml 文件中配置的信息相同。然后,我们就可以在数据库中发出指令了。
在检查数据库之前,我们可以使用 Swagger 在我们的 CMS 系统中创建一些分类。我们可以参考第二章,从 Spring 世界开始 – CMS 应用程序中提供的说明来创建一些数据。
然后,我们可以在数据库中执行以下 SQL 指令:
select * from category;
结果应该是 Swagger 调用中创建的分类。在我的情况下,我创建了两个分类,sports 和 movies。结果将类似于以下截图所示:

了不起的工作,伙计们。应用程序已完全运行。
现在,我们将为存储库创建我们的最终解决方案。我们已经学习了 Spring Data 项目的基础知识,在下一节中,我们将把持久层改为现代数据库。
创建最终的数据访问层
我们已经玩过 Spring Data JPA 项目,并看到了它有多简单。我们学习了如何配置数据库连接,以便在 Postgres 数据库上持久化真实数据。现在,我们将为我们的应用程序创建数据访问层的最终解决方案。最终解决方案将使用 MongoDB 作为数据库,并使用提供 MongoDB 存储库支持的 Spring Data MongoDB 项目。
我们将看到与 Spring Data JPA 项目的一些相似之处。这很令人惊讶,因为我们可以在实践中证明 Spring Data 抽象的力量。通过一些更改,我们可以迁移到另一个数据库模型。
让我们在以下章节中了解新项目并将其付诸实践。
Spring Data MongoDB
Spring Data MongoDB 为我们的领域对象和 MongoDB 文档提供了集成。通过几个注解,我们的实体类就准备好在数据库中持久化了。映射基于 POJO (普通的 Java 对象)模式,这是所有 Java 开发者都熟知的。
该模块提供了两个抽象级别。第一个是高级抽象。它提高了开发者的生产力。这个级别提供了一些注解,以指导框架将领域对象转换为 MongoDB 文档,反之亦然。开发者不需要编写任何关于持久性的代码;它将由 Spring Data MongoDB 框架管理。在这个级别上还有更多令人兴奋的事情,例如 Spring Conversion Service 提供的丰富映射配置。Spring Data 项目提供了一个丰富的 DSL,使开发者能够根据属性名称创建查询。
抽象的第二层是低级抽象。在这一层,行为不是由框架自动管理的。开发者需要稍微了解一些 Spring 和 MongoDB 文档模型。框架提供了一些接口,以使开发者能够控制读写指令。这在高级抽象不适合的场景中可能很有用。在这种情况下,实体的映射控制应该更加细致。
再次强调,Spring 为开发者提供了选择权。高级抽象提高了开发者的性能,而低级抽象允许开发者有更多的控制权。
现在,我们将为我们的模型添加映射注解。让我们开始吧。
移除 PostgreSQL 和 Spring Data JPA 依赖项
我们将把我们的项目转换为使用全新的 Spring Data Reactive MongoDB 存储库。在那之后,我们将不再使用 Spring Data JPA 和 PostgreSQL 驱动程序。让我们从我们的pom.xml中移除这些依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.1.4</version>
</dependency>
然后,我们可以添加以下依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
pom.xml的最终版本可以在 GitHub 上找到:github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter03/cms-mongo-non-reactive/pom.xml。
映射领域模型
我们将在我们的领域模型上添加映射注解。Spring Data MongoDB 将使用这些注解来将我们的对象持久化到 MongoDB 集合中。我们将从Category实体开始,它应该像这样:
package springfive.cms.domain.models;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@Document(collection = "category")
public class Category {
@Id
String id;
String name;
}
我们在Category类中添加了两个新的注解。来自 Spring Data MongoDB 的@Document注解使我们能够配置集合名称。MongoDB 中的集合类似于 SQL 数据库中的表。
@Id注解来自 Spring Data Commons 项目。它很有趣,因为我们可以看到,它并不是特定于 MongoDB 映射的。带有此注解的字段注解将在 MongoDB 集合的_id字段上转换。
通过这些注解,Category类被配置为在 MongoDB 上持久化。在下一节中,我们将创建我们的存储库类。
我们需要对其他实体执行相同的任务。User和News需要以与Category类相同的方式进行配置。完整的源代码可以在 GitHub 上找到:github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter03/cms-mongo-non-reactive/src/main/java/springfive/cms/domain/models。
配置数据库连接
在我们创建我们的存储库之前,我们将配置 MongoDB 连接。存储库层抽象了驱动实现,但正确配置驱动是必要的。
在资源目录中,我们将更改之前为 Spring Data JPA 配置的 application.yaml 文件。Spring 框架支持通过 YAML 文件进行配置。这种文件对人类来说更易于阅读,并且具有某种层次结构。这些特性是选择这种扩展的原因。
application.yaml 文件应该像以下示例所示:
spring:
data:
mongodb:
database: cms
host: localhost
port: 27017
MongoDB 的 application.yaml 文件可以在 GitHub 上找到 (github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter03/cms-mongo-non-reactive/src/main/resources/application.yaml).
目前这个文件很简单。有一个 database 标签用于配置数据库名称。host 和 port 标签是关于 MongoDB 实例运行地址的。
我们还可以使用几个对象以编程方式配置连接,但这需要我们编写大量的样板代码。Spring Boot 为我们提供了现成的解决方案。让我们享受它吧。
太好了,连接配置成功。基础设施需求已解决。让我们继续实现我们的仓库。
Spring Boot 框架支持在 application.properties 或 application.yaml 中配置配置文件。这意味着如果应用程序以属性文件样式配置,我们可以使用 application-<profile>.properties。然后,这些属性将应用于所需的配置文件。在 YAML 风格中,我们可以使用一个包含多个配置文件的单一文件。
添加仓库层
一旦实体被映射,并且建立了连接,就是时候创建我们的仓库了。Spring Data 框架提供了一些接口,可以在不同的用例中使用。我们将使用 MongoDB 数据库的特化,即 MongoRepository。它扩展了 PagingAndSortingRepository 和 QueryByExampleExecutor。前者关于分页和排序功能,后者关于示例查询。
在某些情况下,数据库查询结果集可能非常大。这可能会引起一些应用程序性能问题,因为我们将会获取大量的数据库记录。我们可以限制从数据库中获取的记录数,并为此配置限制。这种技术称为 分页。我们可以在 Spring Data Commons 文档 中找到完整的文档 (docs.spring.io/spring-data/commons/docs/current/reference/html/)。
这个接口提供了许多内置方法以方便使用。有几个方法可以插入一个或多个实例,有列出请求实体所有实例的方法,有删除一个或多个实例的方法,以及许多其他功能,如排序和分页。
它使开发者能够无需代码甚至无需深入了解 MongoDB 就能创建仓库。然而,为了排除各种错误,对 MongoDB 的一些了解是必要的。
我们将首先创建CategoryRepository。将CategoryRepository的类型从类改为接口。这个接口中的代码不是必要的。Spring 容器将在应用程序启动时注入正确的实现。
让我们创建我们的第一个具体仓库,这意味着这个仓库将持久化我们在之前配置的 MongoDB 上的数据。CategoryRepository需要是这样的:
package springfive.cms.domain.repository;
import org.springframework.data.mongodb.repository.MongoRepository;
import springfive.cms.domain.models.Category;
public interface CategoryRepository extends MongoRepository<Category,String> {}
类型是interface。仓库不再有任何 stereotypes。Spring 容器可以通过它扩展的MongoRepository接口来识别实现。
MongoRepository接口应该被参数化。第一个参数是它所表示的模型类型。在我们的例子中,它表示Category类的仓库。第二个参数是关于模型 ID 的类型。我们将使用字符串类型。
现在,我们需要对其他实体User和News做同样的事情。代码与前面的代码非常相似。你可以在 GitHub 上找到完整的源代码:github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter03/cms-mongo-non-reactive/src/main/java/springfive/cms/domain/repository。
在下一节中,我们将检查数据库以断言行是否正确持久化。
检查持久性
现在,我们可以测试应用程序的持久性和所有层。我们将提供相应的 API 文档。让我们打开 Swagger 文档,并在我们的 CMS 应用程序中创建一些记录。
在 Swagger 上创建示例类别:

填写类别 JSON,如前面的截图所示,然后点击“Try it out!”。它将调用类别 API 并将类别持久化到数据库中。现在,我们可以检查它。
要连接到 MongoDB 实例并检查集合,我们将使用mongo-express工具。这是一个基于 NodeJS 编写的 Web 工具,用于与我们的数据库实例交互。
工具可以安装,但我们将在一个 Docker 容器上运行工具。Docker 工具将帮助我们完成这部分。让我们启动容器:
docker run -d --link mongodb:mongo--net cms-application -p 8081:8081 mongo-express
它指示 Docker 启动一个带有mongo-express工具的容器并连接到所需的实例。--link参数指示 Docker 为我们的 MongoDB 实例创建一个类似hostname的东西。记住我们的实例名称是mongodb;我们在之前的运行命令中已经这样做了。
干得好。前往http://localhost:8081,我们将看到这个页面:

有几个数据库。我们感兴趣的是 CMS 数据库。点击旁边的“查看”按钮旁边的 cms。然后,工具将展示所选数据库的集合;在我们的案例中,是 CMS 数据库。视图应该如下所示:

类别以集合的形式呈现。我们可以查看、导出,并以 JSON 格式导出,但就目前而言,我们感兴趣的是检查我们的 CMS 应用程序是否正确地持久化了数据。因此,点击“查看”按钮。我们将使用 MongoDB 集合数据如下:

如我们所见,数据已按预期存储在 MongoDB 中。数据库中有两个类别——体育和旅行。有一个 _class 字段,它帮助 Spring Data 将域类进行转换。
干得好,CMS 应用程序已启动并运行,并且正在 MongoDB 中持久化数据。现在,我们的应用程序几乎已准备好投入生产,数据已在外部的出色文档数据存储中持久化。
在下一节中,我们将创建我们的 Docker 镜像,然后我们将使用 Docker 命令运行 CMS 应用程序。这将很有趣。
为 CMS 创建 Docker 镜像
我们正在做一件了不起的工作。我们使用 Spring Boot 框架创建了一个应用程序。该应用程序已经使用了 Spring REST、Spring Data 和 Spring DI。
现在,我们将更进一步,创建我们的 Docker 镜像。这将有助于我们交付我们的应用程序到生产环境中。有一些优势,我们可以在本地或任何云服务提供商上运行应用程序,因为 Docker 抽象了操作系统层。我们不需要在应用程序主机上安装 Java,它还允许我们在主机上使用不同的 Java 版本。采用 Docker 进行交付涉及许多优势。
我们使用 Maven 作为构建工具。Maven 有一个出色的插件可以帮助我们创建 Docker 镜像。在下一节中,我们将了解 Maven 如何帮助我们。
配置 docker-maven-plugin
fabric8 提供了一个出色的 Maven 插件(github.com/fabric8io/docker-maven-plugin)。它遵循 Apache-2.0 许可协议,这意味着我们可以无忧无虑地使用它。
我们将配置我们的项目以使用它,并在镜像创建后,将其推送到 Docker Hub。这是一个公共 Docker 仓库。
步骤如下:
-
配置插件
-
推送 Docker 镜像
-
配置 Docker Spring 配置文件
然后,是时候展示成果了。让我们开始吧。
在 pom.xml 中添加插件
让我们配置 Maven 插件。在 pom.xml 上的插件部分添加一个插件并添加一些配置是必要的。插件应该配置如下:
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.21.0</version>
<configuration>
<images>
<image>
<name>springfivebyexample/${project.build.finalName}</name>
<build>
<from>openjdk:latest</from>
<entryPoint>java -Dspring.profiles.active=container -jar /application/${project.build.finalName}.jar</entryPoint>
<assembly>
<basedir>/application</basedir>
<descriptorRef>artifact</descriptorRef>
<inline>
<id>assembly</id>
<files>
<file>
<source>target/${project.build.finalName}.jar</source>
</file>
</files>
</inline>
</assembly>
<tags>
<tag>latest</tag>
</tags>
<ports>
<port>8080</port>
</ports>
</build>
<run>
<namingStrategy>alias</namingStrategy>
</run>
<alias>${project.build.finalName}</alias>
</image>
</images>
</configuration>
</plugin>
这里有一些新的配置。让我们从<name>标签开始——它配置了要推送到 Docker Hub 的仓库和 Docker 镜像名称。对于这本书,我们将使用springfivebyexample作为 Docker ID。我们可以看到,仓库和镜像名称之间使用*斜杠*作为分隔符。对于我们来说,镜像名称将是最终的项目名称。然后,我们需要进行配置。
Docker ID 是免费使用的,可以用来访问一些 Docker 服务,如 Docker Store、Docker Cloud 和 Docker Hub。我们可以在 Docker 页面找到更多信息(docs.docker.com/docker-id/)。
此配置应与以下代码片段中显示的相同:
<build>
<finalName>cms</finalName>
....
</build>
另一个重要的标签是<entrypoint>。这是当我们使用docker run命令时的一个 exec 系统调用指令。在我们的情况下,我们期望应用程序在容器引导时运行。我们将执行java -jar,将容器作为 Spring 的活跃配置文件传递。
我们需要传递 Java 构件的完整路径。此路径将在<assembly>标签的<basedir>参数中配置。可以是任何文件夹名称。此外,还有一个针对 Java 构件路径的配置。通常,这是编译的结果文件夹,可以在<source>标签中进行配置。
最后,我们有<port>配置。应用程序的端口将通过此标签暴露。
现在,我们将使用以下指令创建一个 Docker 镜像:
mvn clean install docker:build
应在项目的根目录下执行。docker:build命令的目标是为我们的项目构建一个 Docker 镜像。构建完成后,我们可以检查 Docker 镜像是否已成功创建。
然后,输入以下命令:
docker images
如以下截图所示,应该存在springfivebyexample/cms镜像:

好的。镜像已准备好。让我们将其推送到 Docker Hub。
推送镜像到 Docker Hub
Docker Hub 是一个公共仓库,用于存储 Docker 镜像。它是免费的,我们将为此书使用它。现在,我们将把我们的镜像推送到 Docker Hub 注册表。
该命令相当简单。输入:
docker push springfivebyexample/cms:latest
我使用了我自己创建的springfivebyexample用户。你可以在 Docker Hub 上用自己的用户测试docker push命令,并在docker push命令中更改用户。你可以在 Docker Hub 上创建你的 Docker ID(cloud.docker.com/)。
然后,镜像将被发送到注册表。就是这样。
我们可以在 Docker Hub 上找到该镜像(store.docker.com/community/images/springfivebyexample/cms)。如果你使用了你自己的用户,链接可能会改变。
配置 Docker Spring 配置文件
在我们运行 Docker 容器中的应用程序之前,我们需要创建一个 YAML 文件来配置容器配置文件。新的 YAML 文件应命名为 application-container.yaml,因为我们将会使用容器配置文件来运行它。记住,我们在上一节中在 pom.xml 上配置了 entrypoint。
让我们创建我们的新文件。该文件应与以下片段中描述的内容相同:
spring:
data:
mongodb:
database: cms
host: mongodb
port: 27017
主机必须更改以用于 MongoDB。我们在“准备 MongoDB”部分中已经使用此名称运行了 MongoDB 容器。这是一个重要的配置,我们需要在此点注意。我们不能再使用 localhost,因为应用程序现在正在 Docker 容器中运行。在这个上下文中,localhost 意味着它在同一个容器中,而 CMS 应用程序容器中没有 MongoDB。我们需要每个容器一个应用程序,避免一个容器承担多个职责。
完成。在下一节中,我们将运行我们的第一个 Docker 容器中的应用程序。这将非常棒。让我们行动起来。
运行 Docker 化的 CMS
在上一节中,我们已经创建了文件来正确配置容器配置文件。现在,是时候运行我们的容器了。命令相当简单,但我们需要注意参数。
我们运行的指令应该与以下代码相同:
docker run -d --name cms --link mongodb:mongodb --net cms-application -p 8080:8080 springfivebyexample/cms:latest
我们已经设置了 MongoDB 容器的链接。记住,我们在 YAML 文件中的 host 属性中进行了此配置。在引导阶段,应用程序将寻找名为 mongodb 的 MongoDB 实例。我们通过使用链接命令解决了这个问题。它将完美工作。
我们可以使用 docker ps 命令来检查我们的应用程序是否健康。输出应该如下所示:

在第一行,我们有我们的应用程序容器。它已经启动并运行。
了不起的工作。我们的应用程序已经完全容器化,随时可以部署到我们想要的地方。
以响应式的方式实现
我们一直在使用 Spring Boot 创建一个了不起的应用程序。该应用程序是在 Spring 框架上存在的传统 Web 栈上构建的。这意味着应用程序使用基于 Servlet API 的 Web 服务器。
Servlet 规范是使用阻塞语义或每线程一个请求的模型构建的。有时,我们需要因为非功能性需求而更改应用程序架构。例如,如果应用程序被一家大公司收购,并且该公司想要为全球推出应用程序,请求量可能会大幅增加。因此,我们需要更改架构以适应云环境中的应用程序结构。
通常,在云环境中,机器的规模比传统数据中心要小。在这种情况下,人们更倾向于使用许多小机器,并尝试水平扩展应用程序。在这种情况下,可以将 servlet 规范切换到基于 Reactive Streams 的架构。这种架构比 servlet 更适合云环境。
Spring 框架一直在创建 Spring WebFlux,以帮助开发者创建反应式 Web 应用程序。让我们改变我们的应用程序架构,转向反应式,并学习 Spring WebFlux 组件的新颖之处。
Reactive Spring
Reactive Stream Spec 是提供流处理异步编程标准的规范。如今,它在编程界越来越受欢迎,Spring 在框架中引入了它。
这种编程风格在资源使用效率上更高,并且与多核的新一代机器非常契合。
Spring reactive 使用 Project Reactor 作为 Reactive Streams 的实现。Project Reactor 由 Pivotal 支持,对 Reactive Streams Spec 的实现非常好。
现在,我们将深入探讨 Spring Boot 的反应式模块,创建一个令人惊叹的反应式 API,并尝试 Spring 框架的新风格。
Project Reactor
Project Reactor 是由 Spring 和 Pivotal 团队创建的。这个项目是 JVM 的 Reactive Streams 实现。它是一个完全非阻塞的基础,帮助开发者创建 JVM 生态系统中的非阻塞应用程序。
在我们的应用程序中使用 Reactor 有一定的限制。项目运行在 Java 8 及以上版本。这很重要,因为我们的示例和项目中将使用许多 lambda 表达式。
Spring 框架内部使用 Project Reactor 作为 Reactive Streams 的实现。
组件
让我们来看看 Project Reactor 的不同组件:
-
发布者:发布者负责将数据元素推送到流中。它通知订阅者,有新的数据即将进入流。
发布者接口定义在以下代码片段中:
/************************************************************************
* Licensed under Public Domain (CC0) *
* *
* To the extent possible under law, the person who associated CC0 with *
* this code has waived all copyright and related or neighboring *
* rights to this code. *
* *
* You should have received a copy of the CC0 legalcode along with this *
* work. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.*
************************************************************************/
package org.reactivestreams;
/**
* A {@link Publisher} is a provider of a potentially unbounded number of sequenced elements, publishing them according to
* the demand received from its {@link Subscriber}(s).
* <p>
* A {@link Publisher} can serve multiple {@link Subscriber}s subscribed {@link #subscribe(Subscriber)} dynamically
* at various points in time.
*
* @param <T> the type of element signaled.
*/
public interface Publisher<T> {
public void subscribe(Subscriber<? super T> s);
}
-
订阅者:订阅者负责使数据在流中流动。当发布者开始在数据流中发送数据块时,数据块将通过
onNext(T instance)方法被收集,这是一个参数化接口。订阅者接口定义在以下代码片段中:
/************************************************************************
* Licensed under Public Domain (CC0) *
* *
* To the extent possible under law, the person who associated CC0 with *
* this code has waived all copyright and related or neighboring *
* rights to this code. *
* *
* You should have received a copy of the CC0 legalcode along with this *
* work. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.*
************************************************************************/
package org.reactivestreams;
/**
* Will receive call to {@link #onSubscribe(Subscription)} once after passing an instance of {@link Subscriber} to {@link Publisher#subscribe(Subscriber)}.
* <p>
* No further notifications will be received until {@link Subscription#request(long)} is called.
* <p>
* After signaling demand:
* <ul>
* <li>One or more invocations of {@link #onNext(Object)} up to the maximum number defined by {@link Subscription#request(long)}</li>
* <li>Single invocation of {@link #onError(Throwable)} or {@link Subscriber#onComplete()} which signals a terminal state after which no further events will be sent.
* </ul>
* <p>
* Demand can be signaled via {@link Subscription#request(long)} whenever the {@link Subscriber} instance is capable of handling more.
*
* @param <T> the type of element signaled.
*/
public interface Subscriber<T> {
public void onSubscribe(Subscription s);
public void onNext(T t);
public void onComplete();
}
热和冷
存在两种类型的反应式序列——热和冷。这些函数直接影响实现的使用。因此,我们需要了解它们:
-
冷:冷发布者只有在接收到新的订阅时才开始生成数据。如果没有订阅,数据永远不会进入流程。
-
热:热发布者不需要任何订阅者来生成数据流。当新订阅者注册时,订阅者将只获取新发射的数据元素。
反应式类型
有两种反应式类型代表反应式序列。Mono对象代表单个值或空 0|1。Flux对象代表 0|N 个项目的序列。
我们在代码中会找到许多引用。Spring Data reactive 仓库在其方法中使用这些抽象。findOne()方法返回Mono<T>对象,而findAll()返回一个Flux<T>。在我们的 REST 资源中也会找到相同的行为。
让我们来玩一玩 Reactor。
为了更好地理解,让我们玩一玩 Reactor。我们将实际实现并理解热发布者和冷发布者之间的区别。
冷发布者在新的订阅到达之前不会产生任何数据。在下面的代码中,我们将创建一个冷发布者,并且System.out:println永远不会被执行,因为它没有任何订阅者。让我们测试这个行为:
@Test
public void coldBehavior(){
Category sports = new Category();
sports.setName("sports");
Category music = new Category();
sports.setName("music");
Flux.just(sports,music)
.doOnNext(System.out::println);
}
正如我们所见,subscribe()方法并不在这个片段中。当我们执行代码时,我们将在标准输出上看到任何数据。
我们可以在 IDE 中执行这个方法。我们将能够看到这个测试的输出。输出应该是这样的:

流程已经完成,测试通过,我们将无法看到打印输出。这就是冷发布者的行为。
现在,我们将订阅发布者,数据将通过数据流发送。让我们试试这个。
我们将在doOnNext()之后插入subscribe指令。让我们更改我们的代码:
@Test
public void coldBehaviorWithSubscribe(){
Category sports = new Category();
sports.setId(UUID.randomUUID().toString());
sports.setName("sports");
Category music = new Category();
music.setId(UUID.randomUUID().toString());
music.setName("music");
Flux.just(sports,music)
.doOnNext(System.out::println)
.subscribe();
}
输出应该是这样的:

在前面的截图上,我们可以看到发布者在流被订阅后推数据到流上。这是订阅后的冷发布者行为。
热发布者不依赖于任何订阅者。即使没有订阅者接收数据,热发布者也会发布数据。让我们看看一个例子:
@Test
public void testHotPublisher(){
UnicastProcessor<String> hotSource = UnicastProcessor.create();
Flux<Category> hotPublisher = hotSource.publish()
.autoConnect().map((String t) -> Category.builder().name(t).build());
hotPublisher.subscribe(category -> System.out.println("Subscriber 1: "+ category.getName()));
hotSource.onNext("sports");
hotSource.onNext("cars");
hotPublisher.subscribe(category -> System.out.println("Subscriber 2: "+category.getName()));
hotSource.onNext("games");
hotSource.onNext("electronics");
hotSource.onComplete();
}
让我们了解这里发生了什么。UnicastProcessor是一个只允许一个Subscriber的处理程序。当订阅者请求时,处理程序会重放通知。它将在流上发射一些数据。第一个订阅将捕获所有类别,正如我们将看到的,因为它是在事件发射之前注册的。第二个订阅将只捕获最后的事件,因为它是在最后两个发射之前注册的。
上述代码的输出应该是:

太棒了。这是热发布者的行为。
Spring WebFlux
传统的 Java 企业级 Web 应用程序基于 servlet 规范。3.1 之前的 servlet 规范是同步的,这意味着它是用阻塞语义创建的。在当时,由于计算机体积大,拥有强大的 CPU 和数百 GB 的内存,这种模型是好的。当时的应用程序通常配置有大量的线程池,因为计算机是为这种用途设计的。当时的首要部署模型是副本。有一些机器配置和应用程序部署相同。
开发者已经多年在创建这类应用程序。
现在,大多数应用程序都部署在云服务提供商上。再也没有大机器了,因为价格要高得多。而不是大机器,有许多小机器。这要便宜得多,这些机器有合理的 CPU 功率和内存。
在这个新场景中,拥有巨大线程池的应用程序不再有效,因为机器体积小,没有足够的处理所有这些线程的能力。
Spring 团队在框架中增加了对反应式流的支持。这种编程模型改变了应用程序的部署方式和构建应用程序的方式。
与线程-per-请求模型不同,应用程序使用事件循环模型创建。这种模型需要较少的线程,并且在资源使用方面更有效。
事件循环模型
由 NodeJS 语言推广的这种模型基于事件驱动编程。有两个核心概念:将在队列上入队的事件,以及跟踪和处理这些事件的处理器。
采用这种模型有一些优势。第一个是排序。事件按事件到达的顺序入队和分发。在某些用例中,这是一个重要的要求。
另一个是同步。事件循环必须在单个线程上执行。这使得状态易于处理,并避免了共享状态问题。
这里有一条重要的建议。处理器不能是同步的。否则,应用程序将被阻塞,直到处理器完成其工作负载。
Spring Data 反应式扩展
Spring Data 项目有一些扩展,可以与反应式基础一起工作。该项目提供了一些基于异步编程的实现。这意味着整个堆栈都是异步的,因为数据库驱动程序也是异步的。
Spring 反应式仓库支持 Cassandra、MongoDB 和 Redis 作为数据库存储。仓库实现提供了与非反应式实现相同的行为。有一个DSL(领域特定语言)用于创建特定领域的查询方法。
该模块使用 Project Reactor 作为反应式基础实现,但也可能将其实现更改为 RxJava。这两个库都是生产就绪的,并且被社区采用。需要注意的一点是,如果我们更改为 RxJava,我们需要确保我们的方法返回Observable和Single。
Spring Data 反应式
Spring Data 项目支持反应式数据访问。到目前为止,Spring 支持 MongoDB、Apache Cassandra 和 Redis,它们都提供了反应式驱动程序。
在我们的 CMS 应用程序中,我们将使用 MongoDB 反应式驱动程序来为我们的存储库提供反应式特性。我们将使用 Spring Data 反应式提供的新反应式接口。此外,我们还需要稍微修改一下代码。在本章中,我们将一步步进行。让我们开始吧。
实践中的反应式存储库
在我们开始之前,我们可以在 GitHub 上查看完整的源代码,或者我们可以执行以下步骤。
现在,我们已经准备好构建我们的新反应式存储库。我们需要做的第一件事是将 Maven 依赖项添加到我们的项目中。这可以通过pom.xml来完成。
让我们配置我们的新依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
我们的项目现在可以使用反应式 MongoDB 存储库了。
创建第一个反应式存储库
在我们的 CMS 项目中,我们有一些存储库。现在,我们需要将这些存储库转换为反应式存储库。我们将要做的第一件事是从CrudRepository中移除扩展,因为现在不再需要它了。现在,我们想要那个反应式的版本。
我们将更新ReactiveMongoRepository接口。接口的参数与之前插入的相同。接口应该是这样的:
package springfive.cms.domain.repository;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import springfive.cms.domain.models.Category;
public interface CategoryRepository extends ReactiveMongoRepository<Category,String> {
}
这与我们之前创建的相当相似。我们需要扩展新的ReactiveMongoRepository接口,该接口包含 CRUD 操作和其他许多方法。该接口返回Mono<Category>或Flux<Category>。方法不再返回实体。当采用反应式流时,这是一种常见的编程方式。
我们还需要修改其他存储库。您可以在 GitHub 上找到完整的源代码:github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter03/cms-mongodb/src/main/java/springfive/cms/domain/repository。
现在,我们需要修改服务层。让我们来做这件事。
修复服务层
我们需要将服务层修改为采用新的反应式编程风格。我们已经修改了存储库层,因此现在我们需要修复由于这种更改而产生的编译问题。应用程序需要是反应式的。应用程序的任何一点都可能因为我们在使用事件循环模型而被阻塞。如果我们不这样做,应用程序将会被阻塞。
修改 CategoryService
现在,我们将修复 CategoryService 类。我们将更改几个方法的返回类型。之前,我们可以返回模型类,但现在我们需要改为返回 Mono 或 Flux,类似于我们在存储库层所做的那样。
新的 CategoryService 应该像以下代码片段中所示的实现一样:
package springfive.cms.domain.service;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import springfive.cms.domain.models.Category;
import springfive.cms.domain.repository.CategoryRepository;
import springfive.cms.domain.vo.CategoryRequest;
@Service
public class CategoryService {
private final CategoryRepository categoryRepository;
public CategoryService(CategoryRepository categoryRepository) {
this.categoryRepository = categoryRepository;
}
public Mono<Category> update(String id,CategoryRequest category){
return this.categoryRepository.findById(id).flatMap(categoryDatabase -> {
categoryDatabase.setName(category.getName());
return this.categoryRepository.save(categoryDatabase);
});
}
public Mono<Category> create(CategoryRequest request){
Category category = new Category();
category.setName(request.getName());
return this.categoryRepository.save(category);
}
public void delete(String id){
this.categoryRepository.deleteById(id);
}
public Flux<Category> findAll(){
return this.categoryRepository.findAll();
}
public Mono<Category> findOne(String id){
return this.categoryRepository.findById(id);
}
}
如我们所见,方法的返回类型已经更改。
这里重要的是我们需要遵循反应式原则。当方法只返回一个实例时,我们需要使用 Mono<Category>。当方法返回一个或多个实例时,我们应该使用 Flux<Category>。这是非常重要的,因为这样开发者以及 Spring 容器才能正确地解释代码。
update() 方法有一个有趣的调用:flatMap()。项目 Reactor 允许我们使用一种 DSL 来组合调用。这非常有趣,也非常有用。它帮助开发者创建比以前更容易理解的代码。flatMap() 方法通常用于转换 Mono 或 Flux 发出的数据。在这个上下文中,我们需要将数据库检索到的分类的新名称设置为分类的新名称。
更改 REST 层
我们还将对 REST 层进行一些修复。我们更改了服务层,这导致我们的资源类中出现了一些编译问题。
我们需要添加新的依赖项 spring-web-reactive。这个依赖支持 @Controller 或 @RestController 注解用于反应式非阻塞引擎。Spring MVC 不支持反应式扩展,这个模块使得开发者能够使用与之前相同的反应式范式。
spring-web-reactive 将更改 Spring MVC 基础设施上的许多合约,例如 HandlerMapping 和 HandlerAdapter,以在这些组件上启用反应式基础。
以下图像可以帮助我们更好地理解 Spring HTTP 层:

如我们所见,@Controller 和 @RequestMapping 可以在 Spring MVC 传统应用程序中使用不同的方法,或者通过使用 Spring WebReactive 模块。
在我们开始更改我们的 REST 层之前,我们需要从我们的项目中移除 Spring Fox 依赖项和注解。目前,Spring Fox 还不支持反应式应用程序。
需要移除的依赖项包括:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
之后,我们需要从 Swagger 包中移除注释,例如 @Api 和 @ApiOperation。
现在,让我们调整我们的 REST 层。
添加 Spring WebFlux 依赖项
在我们开始更改我们的 REST 层之前,我们需要将新的依赖项添加到我们的 pom.xml 文件中。
首先,我们将移除 Spring MVC 传统依赖项。为此,我们需要移除以下依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
我们不再需要这个依赖项了。我们的应用程序现在将是反应式的。然后,我们需要添加以下代码片段中描述的新依赖项:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
spring-boot-starter-webflux 是一种依赖项的语法糖。它包含了 spring-boot-starter-reactor-netty 依赖项,即 Reactor Netty,它是内嵌在响应式 HTTP 服务器中的。
太棒了,我们的项目已经准备好将 REST 层转换为响应式应用。让我们将我们的应用程序转换为一个完全响应式的应用程序。
修改分类资源
我们将修改 CategoryResource 类。这个想法很简单。我们将使用 Mono 或 Flux 将我们的 ResponseEntity(使用模型类参数化)转换为 ResponseEntity。
新版本的 CategoryResource 应该是这样的:
package springfive.cms.domain.resources;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import springfive.cms.domain.models.Category;
import springfive.cms.domain.service.CategoryService;
import springfive.cms.domain.vo.CategoryRequest;
@RestController
@RequestMapping("/api/category")
public class CategoryResource {
private final CategoryService categoryService;
public CategoryResource(CategoryService categoryService) {
this.categoryService = categoryService;
}
@GetMapping(value = "/{id}")
public ResponseEntity<Mono<Category>> findOne(@PathVariable("id") String id){
return ResponseEntity.ok(this.categoryService.findOne(id));
}
@GetMapping
public ResponseEntity<Flux<Category>> findAll(){
return ResponseEntity.ok(this.categoryService.findAll());
}
@PostMapping
public ResponseEntity<Mono<Category>> newCategory(@RequestBody CategoryRequest category){
return new ResponseEntity<>(this.categoryService.create(category), HttpStatus.CREATED);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void removeCategory(@PathVariable("id") String id){
this.categoryService.delete(id);
}
@PutMapping("/{id}")
public ResponseEntity<Mono<Category>> updateCategory(@PathVariable("id") String id,CategoryRequest category){
return new ResponseEntity<>(this.categoryService.update(id,category), HttpStatus.OK);
}
}
代码与我们之前所做的是相当相似的。我们在方法参数中使用了 @RequestBody 注解;否则,JSON 转换器将无法工作。
这里另一个重要的特性是 return 方法。它返回 Mono 或 Flux,这是 ResponseEntity 的参数化类型。
我们可以通过使用命令行来测试响应式实现。它将在 MongoDB 上持久化 Category 对象。在终端中输入以下命令:
curl -H "Content-Type: application/json" -X POST -d '{"name":"reactive"}' http://localhost:8080/api/category
然后,我们可以使用以下命令来检查数据库。使用浏览器,访问 http://localhost:8080/api/category。以下结果应该会显示:

太棒了,我们的响应式实现按预期工作。做得好!!!
摘要
在本章中,我们学习了大量的 Spring 概念。我们向您介绍了 Spring Data 项目,这些项目帮助开发者创建我们以前从未见过的数据访问层。我们看到了使用这个项目创建仓库是多么容易。
此外,我们还介绍了一些相对较新的项目,例如 Spring WebFlux,它允许开发者创建现代网络应用程序,在项目中应用响应式流和响应式编程风格。
我们已经完成了我们的 CMS 应用程序。该应用程序具有生产就绪应用程序的特征,如数据库连接,以及具有单一职责的良好设计的服务。我们还引入了 docker-maven-plugin,它提供了一个使用 pom.xml 配置创建镜像的合理方式。
在下一章中,我们将创建一个新的应用程序,基于基于消息驱动的应用程序的 Reactive Manifesto。在那里见。
第四章:Kotlin 基础和 Spring Data Redis
Spring Boot 允许开发者创建不同风格的应用程序。在第二章“从 Spring 世界开始——CMS 应用程序”和第三章“使用 Spring Data 和响应式风格的持久化”中,我们已经创建了一个门户应用程序,现在我们将创建一个基于消息驱动架构的应用程序。它展示了 Spring 框架如何很好地适应各种应用程序架构。
在本章中,我们将开始创建一个应用程序,该应用程序将跟踪的标签存储在 Redis 数据库中。该应用程序将获取标签并将它们放入几个队列中,供我们的其他项目使用,并适当地消费和处理它们。
正如我们在以前的项目中所做的那样,我们将继续使用 Reactive Foundation 为应用程序提供可伸缩的特性。
在本章结束时,我们将:
-
学习 Kotlin 基础
-
创建了项目结构
-
创建了 Reactive Redis 存储库
-
使用 Reactive Redis 客户端应用了一些响应式编程技术
现在就让我们开始吧。
学习 Kotlin 基础
Kotlin 语言于 2016 年 2 月正式发布。JetBrains 创建了它,并从那时起一直在开发这门语言。该公司是 IntelliJ IDEA IDE 的所有者。
2012 年 2 月,JetBrains 在 Apache v2 许可证下将语言开源;该许可证允许开发者创建应用程序。
语言是 JVM(Java 虚拟机)语言之一,如 Clojure 和 Scala,这意味着该语言可以为 JVM 编译字节码。正如我们将看到的,Kotlin 与 Scala 有很多相似之处。Kotlin 以 Scala 语言为参考,但 JetBrains 团队认为 Scala 在编译时间上存在问题。
Kotlin 正在成为 Android 世界中广泛采用的语言,因此,在 2017 年的 Google I/O 上,谷歌团队宣布了对 Android 生态系统的官方支持。从那时起,这门语言每年都在增长,并且越来越受欢迎。
Kotlin 的主要特性
Kotlin 语言被设计成与 Java 代码保持互操作性。这意味着我们可以在 Kotlin 文件中使用 Java 代码风格开始编码。
语言是静态类型的,这是一个非常好的属性,因为它可以帮助我们在编译时找到一些问题。此外,静态类型语言比动态语言要快得多。IDEs(集成开发环境)在帮助开发者方面也比动态语言做得更好。
语法
语法与 Java 语法不同。乍一看,这可能是个问题,但经过几个小时对 Kotlin 的实践,这根本不是问题。
有两个有趣的保留词可以帮助理解其用法和概念:
-
var:这是一个变量声明。它表示变量是可变的,可以根据开发者的需要重新分配。 -
val:这是一个变量声明,表示该变量是不可变的,不能再重新分配。这种定义类似于 Java 语言中的 final 声明。
变量声明有一个名称,在所需的数据类型之后,中间需要用冒号作为分隔符。如果变量被初始化,则不需要类型,因为编译器可以推断正确的数据类型。让我们试一试,以便更好地理解。
这里有一个指定了数据类型的变量:
var bookName: String
在这种情况下,我们需要保留数据类型,因为变量没有被初始化,编译器无法推断类型。由于var修饰符,变量bookName可以被重新分配。
这里是一个没有指定数据类型的变量:
val book = "Spring 5.0 by Example"
声明数据类型不是必需的,因为我们已经用值Spring 5.0 by Example初始化了变量。由于val修饰符,变量不能被重新分配。如果我们尝试重新分配指令,将会得到编译错误。
Kotlin 中分号是可选的,编译器可以检测语句终止符。这是 Kotlin 与 Java 编程语言不同的另一个点:
val book = "Spring 5.0 by Example"
var bookName: String
println("Hello, world!")
没有提供分号,指令被编译。
在 Kotlin 语言中推荐使用不可变编程。它在多核环境中性能更优。同时,它使开发者更容易调试和排查问题场景。
语义
在 Kotlin 中,有类和函数。然而,不再有方法。应该使用fun关键字来声明函数。
Kotlin 借鉴了一些 Scala 语言的概念,并带来了一些特殊的类,如数据类和对象类(我们很快就会学习)。在那之前,我们将了解如何在 Kotlin 中声明函数。让我们来做这件事!
Kotlin 中声明函数
函数声明有很多变体。我们将创建一些声明来理解与 Java 方法之间的细微差别。
带参数和返回类型的简单函数
这个简单的函数有两个参数,返回类型为 String。看看参数声明并观察其顺序、名称和数据类型。
fun greetings(name:String,greeting:String):String{
return greeting + name
}
如我们所见,变量名后面的参数类型与变量声明中的类型相同。返回类型在分号分隔的参数列表之后。以下方式可以在 Java 中声明相同的函数:
public String greetings(String name,String greeting){
return greeting + name;
}
这里有一些差异。首先,Java 代码中有分号,我们可以看到方法和函数声明的顺序。
无返回值的简单函数
让我们了解如何构造无返回值的函数,以下函数将不会返回任何值:
fun printGreetings(name:String,greeting:String):Unit{
println(greeting + name)
}
有一个区别,在这种情况下,引入了 Unit;这种类型的对象对应于 Java 语言中的 void。然后,在前面的代码中,我们有一个没有返回值的函数。如果你想编译器理解函数没有返回值,可以移除 Unit 对象。
单表达式函数
当函数只有一个表达式时,我们可以移除大括号,就像在 Scala 中一样,函数体应该在 = 符号之后指定。让我们重构我们的第一个函数,如下所示:
fun greetings(name:String,greeting:String) = greeting + name
我们也可以移除 return 关键字。我们的函数现在非常简洁。我们移除了 return 和返回类型。正如我们所看到的,代码现在更易读。如果你想,也可以声明返回类型。
重写函数
要在 Kotlin 中重写一个函数,需要在函数声明上放置一个 override 关键字,并且基函数也需要有 open 关键字。
让我们来看一个例子:
open class Greetings {
open fun greeting() {}
}
class SuperGreeting() : Greetings() {
override fun greeting() {
// my super greeting
}
}
这种方式比 Java 更明确,它也增加了代码的可读性。
数据类
当我们想在系统层之间持有和传输数据时,数据类是正确的解决方案。就像在 Scala 中一样,这些类提供了一些内置功能,如 getters/setters、equals 和 hashCode、toString 方法以及 copy 函数。
让我们为这个例子创建一个示例:
data class Book(val author:String,val name:String,val description:String,val new:Boolean = false)
我们在代码中有一些有趣的事情。我们首先注意到所有的属性都是不可变的。这意味着它们都没有设置器。第二是,在类声明中,我们可以看到一个属性列表。在这种情况下,Kotlin 将创建一个包含这个类中所有属性的构造函数,因为它们是 val,这意味着它们是最终属性。
在这种情况下,不再有默认构造函数。
Kotlin 另一个有趣的功能是它允许开发者为构造函数提供默认值,在我们的例子中,如果省略了 new 属性,它将假设 false 值。我们也可以在函数的参数列表中得到相同的行为。
最后,有一个复制对象的绝佳方法。copy 方法允许开发者使用命名参数来复制对象。这意味着我们可以根据需要只更改属性。让我们来看一个例子:
fun main(args : Array<String>) {
val springFiveOld = Book("Claudio E. de Oliveira","Spring 5.0 by Example","Amazing example of Spring Boot Apps",false)
val springFiveNew = springFiveOld.copy(new = true)
println(springFiveOld)
println(springFiveNew)
}
在第一个对象中,我们使用 false 为 new 属性创建了一个书实例,然后我们使用 true 为 new 属性复制了一个新对象,其他属性没有改变。告别复杂的克隆逻辑,欢迎新的复制对象方式。
这段代码的输出应该如下所示:

如我们所见,只有 new 属性被更改,并且 toString 函数也被良好地生成。
数据类有一些限制。它们不能是抽象的、开放的、密封的或内部的。
对象
单例模式在应用程序中常用,Kotlin 提供了一种简单的方法来做这件事,而不需要太多的样板代码。
我们可以指示 Kotlin 使用 object 关键字创建一个单例对象。再次强调,Kotlin 使用 Scala 作为参考,因为在 Scala 语言中也有相同的功能。
让我们试试:
object BookNameFormatter{
fun format(book: Book):String = "The book name is" + book.name
}
我们已经创建了一个格式化器,用于返回带有书名的消息。然后,我们尝试使用这个函数:
val springFiveOld = Book("Claudio E. de Oliveira","Spring 5.0 by Example","Amazing example of Spring Boot Apps",false)
BookNameFormatter.format(springFiveOld)
函数格式可以在静态上下文中调用。因为没有实例来调用函数,因为它是一个单例对象。
伴生对象
companion object 是一个对所有该类实例都通用的对象。这意味着有很多书籍的实例,但它们的伴生对象只有一个实例。通常,开发者使用伴生对象作为工厂方法。让我们创建我们的第一个 companion object:
data class Book(val author:String,val name:String,val description:String,val new:Boolean = false{
companion object {
fun create(name:String,description: String,author: String):Book{
return Book(author,name,description)
}
}
}
如果省略了 companion object 的名称,函数可以通过单例方式调用,无需实例,如下所示:
val myBookWithFactory = Book.create("Claudio E. de Oliveira","Spring 5.0 by Example","Amazing example of Spring Boot Apps")
它就像 object 的行为。我们可以在静态上下文中调用它。
Kotlin 习惯用法
Kotlin 习惯用法是 Java 程序员的一种语法糖。它是一组代码片段,帮助开发者以 Kotlin 语言创建简洁的代码。让我们看看常见的 Kotlin 习惯用法。
字符串插值
Kotlin 支持字符串插值,在 Java 语言中做这个稍微复杂一些,但对于 Kotlin 来说不是问题。我们不需要很多代码来完成这个任务,因为 Kotlin 本地支持它。这使得代码更容易阅读和理解。让我们创建一个示例:
val bookName = "Spring 5.0"
val phrase = "The name of the book is $bookName"
如我们所见,在 Kotlin 中插值字符串是一件轻而易举的事情。再见 String.format() 和它的许多参数。我们可以使用 $bookName 来替换 bookName 变量的值。此外,我们还可以访问对象中存在的函数,但为此我们需要使用花括号。查看以下代码:
val springFiveOld = Book("Claudio E. de Oliveira","Spring 5.0 by Example","Amazing example of Spring Boot Apps",false)
val phrase = "The name of the book is ${springFiveOld.name}"
感谢,Kotlin,我们感谢这个特性。
智能转换
Kotlin 支持一个名为智能转换的功能,它允许开发者自动使用类型转换操作符。在 Java 中,在检查变量类型后,类型转换操作符必须是显式的。让我们来看看:
fun returnValue(instance: Any): String {
if (instance is String) {
return instance
}
throw IllegalArgumentException("Instance is not String")
}
如我们所见,类型转换操作符已经不再存在。在检查类型后,Kotlin 可以推断出期望的类型。让我们检查一下相同代码的 Java 版本:
public String returnValue(Object instance) {
if (instance instanceof String) {
String value = (String) instance;
return value;
}
throw IllegalArgumentException("Instance is not String");
}
它使类型转换更安全,因为我们不需要检查和应用类型转换操作符。
范围表达式
范围表达式允许开发者在使用 for 循环和 if 比较时处理范围。在 Kotlin 中处理范围有很多方法。我们在这里将查看其中大部分的常见方法。
简单案例
让我们看看一个简单的案例:
for ( i in 1..5){
println(i)
}
它将迭代从 1 到 5(包括 1 和 5),因为我们使用了 in 关键字。
until 情况
我们还可以在 for 循环中使用 until 关键字,在这种情况下,结束元素将被排除在交互之外。让我们看看一个示例:
for (i in 1 until 5) {
println(i)
}
在这种情况下,5 的值不会在控制台上打印出来,因为交互不包括最后一个元素。
downTo 情况
downTo 关键字使开发者能够以相反的顺序与数字进行交互。指令也是不言自明的。让我们看看实际应用:
for (i in 5 downTo 1) {
println(i)
}
这也很容易。交互将以相反的顺序发生,在这种情况下,值 1 将被包含。正如我们所看到的,代码非常容易理解。
步骤情况
有时候我们需要以任意步骤而不是逐个与值交互,例如。然后我们可以使用 step 指令。让我们来练习一下:
for (i in 1..6 step 2) {
print(i)
}
在这里,我们将看到以下输出:135,因为交互将从 1 值开始,并增加两个点。
极佳。Kotlin 的范围可以增加我们源代码的可读性,并有助于提高代码质量。
空安全
Kotlin 有处理空引用的惊人功能。空引用对 Java 开发者来说是一个噩梦。Java 8 有一个 Optional 对象,它帮助开发者处理可空对象,但不像 Kotlin 那样简洁。
现在,我们将探讨 Kotlin 如何帮助开发者避免 NullPointerException。让我们来理解一下。
Kotlin 的类型系统在可以持有空引用和不能持有空引用的引用之间做出区分。因此,代码更加简洁和易读,因为它为开发者提供了一种建议。
当引用不允许为空时,声明应该是这样的:
var myNonNullString:String = "my non null string"
前面的变量不能分配给空引用,如果我们这样做,我们会得到编译错误。看看代码是多么容易理解。
有时候,我们需要允许变量有空引用,在这些情况下,我们可以使用 ? 作为操作符,例如以下所示:
var allowNull:String? = "permits null references"
简单。注意 ? 操作符上的变量声明,它使变量能够接受空引用。
有两种不同的方法可以避免 Kotlin 中的 NullPointerReference。第一种可以称为 安全调用,另一种可以称为 Elvis 操作符。让我们来看看这些。
安全调用
安全调用可以使用 .? 来编写。当引用持有非空值时可以调用,如果值持有空引用,则返回空值:
val hash:TrackedHashTag? = TrackedHashTag(hashTag="java",queue="java")
val queueString = hash?.queue
当 hash? 持有空值时,空值将被分配给 queueString 属性。如果 hash? 有有效的引用,队列属性将被分配给 queueString 属性。
Elvis 操作符
它可以在开发者期望在引用为空时返回默认值时使用:
val hash:TrackedHashTag? = TrackedHashTag(hashTag="java",queue="java")
val queueString = hash?.queue ?: "unrecognized-queue"
当值持有空时,将返回默认值。
是时候在现实世界中使用 Kotlin 了。让我们开始吧。
总结
现在,我们可以使用 Kotlin 语言的基础知识。我们看到了一些示例并实践了一下。
我们研究了 Kotlin 的主要概念。我们学习了数据类如何帮助开发者在应用程序层之间传输数据。我们还了解了单例和伴随对象。现在我们可以尝试使用 Spring 框架的新支持创建一个真实的项目。
在接下来的章节中,我们将使用 Kotlin 语言创建一个项目,目前我们可以暂时忘记 Java 语言。
创建项目
现在,我们已经有了一个很好的想法,了解我们如何使用 Kotlin 语言进行编程。在本节中,我们将为我们的新项目创建基本结构,其中主要功能是消费 Twitter 流。让我们来做这件事。
项目用例
在我们开始编码之前,我们需要跟踪应用程序需求。该应用程序是消息驱动的,我们将使用代理来提供消息基础设施。我们选择 RabbitMQ 代理,因为它提供可靠性、高可用性和集群选项。此外,RabbitMQ 是现代消息驱动应用程序的流行选择。
该软件由 Pivotal 公司提供支持,该公司维护 Spring 框架。有一个庞大的社区支持该项目。
我们将拥有三个项目。这三个项目将收集 Twitter 流并将其发送给接收者,以便以格式化的方式向最终用户展示推文。
第一个,在本章中创建的,将负责在 Redis 缓存中保持跟踪的标签。
当新标签注册时,它将向第二个项目发送消息,该项目将开始消费 Twitter 流并将其重定向到所需的队列。这个队列将被其他项目消费,该项目将格式化推文,并最终将它们展示给最终用户。
我们将拥有三个微服务。让我们创建这些服务。
使用 Spring Initializr 创建项目
我们已经学习了如何使用 Spring Initializr 页面。我们将访问该页面,然后选择以下模块:
-
响应式 Web -
响应式 Redis
页面内容应该看起来像这样:

我们可以选择组和工件。使用不同的名称没有问题。然后,我们可以点击生成项目并等待下载完成。
为 Kotlin 添加 Jackson
我们需要为 Maven 项目添加 Jackson for Kotlin 依赖项。实际上,我们需要在 pom.xml 中有一个 Kotlin 标准库。此外,我们需要添加 jackson-module-kotlin,它允许我们在 Kotlin 中处理 JSON,在这些部分与 Java 有一些不同。
这部分相当简单,我们将在 pom.xml 的依赖项部分添加以下依赖项。依赖项如下:
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
<version>${jackson.version}</version>
</dependency>
现在,我们已经配置了依赖项,我们可以设置插件来编译 Kotlin 源代码。在下一节中,我们将这样做。
查找 Kotlin 的 Maven 插件
项目已成功配置 Kotlin。现在,我们将查看 pom.xml 中的 Maven 插件。配置是必要的,以指导 Maven 如何编译 Kotlin 源代码并添加到工件中。
我们将在插件部分添加以下插件:
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<configuration>
<jvmTarget>1.8</jvmTarget>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>process-test-sources</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
还有件事要做。看看 Maven 是如何配置我们的 Kotlin 代码路径的。这很简单。看看下面的:
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin<
/sourceDirectory<testSourceDirectory>${project.basedir}/src/
test/kotlin</testSourceDirectory>
.....
</build>
我们已经在源路径中添加了我们的 Kotlin 文件夹。
太棒了,项目结构已经准备好了,我们可以开始编码了!
为我们的应用程序创建 Docker 网络
要为我们的应用程序创建隔离,我们将创建一个自定义的 Docker 网络。这个网络是使用 bridge 驱动程序创建的。让我们使用以下命令来做这件事:
docker network create twitter
好的,现在我们可以通过输入以下命令来检查网络列表:
docker network list
Twitter 网络应该像下面这样在列表中:

最后一个是我们的 Twitter 网络。让我们从 Docker Hub 拉取 Redis 镜像。看看下一节。
从 Docker Hub 拉取 Redis 镜像
我们需要做的第一件事是从 Docker Hub 下载 Redis 镜像。为此,必须执行以下命令:
docker pull redis:4.0.6-alpine
我们使用了 Redis 的 Alpine 版本,因为它比其他版本更小,并且安全性合理。当镜像下载时,我们可以看到下载状态进度。
我们可以使用以下命令来检查结果:
docker images
结果应该看起来像下面这样:

查看下载的镜像。Redis 必须在列表中。
太棒了,现在我们将启动 Redis 实例。
运行 Redis 实例
镜像已下载,然后我们将为我们的应用程序启动 Redis 实例。命令可以是:
docker run -d --name redis --net twitter -p 6379:6379 redis:4.0.6-alpine
我们在这里有一些有趣的属性。我们用 redis 命名了我们的 Redis 实例,它将在下一章中运行容器化应用程序时很有用。此外,我们将 Redis 容器的端口暴露给了主机机器,用于此的命令参数是 -p。最后,我们将容器连接到了我们的 Twitter 网络中。
好的,Redis 实例已经准备好使用了。让我们检查一下 Spring Data Reactive Redis 相关的内容。
配置 redis-cli 工具
有一个很好的工具可以连接到 Redis 实例,它被称为 redis-cli。为此有一些 Docker 镜像,但我们将在我们 Linux 机器上安装它。
要安装它,我们可以执行以下命令:
sudo apt-get install redis-tools -y
太棒了,现在我们可以连接并交互我们的 Redis 容器。该工具可以执行读写指令,然后我们需要小心避免意外执行指令。
让我们连接。默认配置对我们来说足够了,因为我们已经在 run 指令中导出了端口 6379。在终端中输入以下命令:
redis-cli
然后我们将连接到我们的运行实例。命令行应显示 Redis 的主机和端口,如下面的截图所示:

太棒了,客户端已配置并测试。
现在,我们将在我们的容器上执行一些 Redis 命令。
理解 Redis
Redis 是一个开源的内存数据结构。Redis 非常适合作为数据库缓存,虽然不常见,但可以使用发布/订阅功能作为消息代理,这对于解耦应用程序非常有用。
Redis 支持一些有趣的功能,如事务、原子操作和对生存时间键的支持。生存时间对于为键设置时间非常有用,驱逐策略总是很难实现,而 Redis 为我们提供了一个内置的解决方案。
数据类型
Redis 支持很多数据类型。最常见的是字符串、散列、列表和有序集合。我们将稍微了解每个数据类型,因为这对我们选择正确的数据类型来满足我们的用例非常重要。
字符串
字符串是 Redis 更基本的数据类型。字符串值最大长度为 512 MB。我们可以将其存储为键的值中的 JSON,或者也可以作为图像存储,因为 Redis 是二进制安全的。
主要命令
让我们看看我们需要的一些重要命令:
SET:它设置键并保持值。这是 Redis 的一个简单且基本的命令。以下是一个示例:
SET "user:id:10" "joe"
命令的返回值应该是OK。这表示指令已成功执行。
GET:此命令获取请求键的值。记住GET只能用于字符串数据类型:
GET "user:id:10"
如我们所见,该命令的返回值应该是joe。
INCR:INCR命令通过一个原子操作将键值增加一。在分布式系统中处理顺序数字时非常有用。数值增加将作为命令输出返回:
SET "users" "0"
INCR "users"
GET "users"
如我们所见,INCR命令返回了命令输出1,然后我们可以使用GET来检查这个值。
DECR:DECR命令是INCR的反操作,它将以原子方式减少值:
GET "users"
DECR "users"
GET "users"
users键的值减少了一个,然后转换为0。
INCRBY:它将根据参数增加键的值。新的增加值将作为命令输出返回:
GET "users"
INCRBY "users" 2
GET "users"
新值作为命令输出返回。
列表
列表是简单的字符串列表。它们按插入顺序排序。Redis 还提供了在列表头部或尾部添加新元素的指令。
列表可以用于存储事物组,例如按categories键分组的事物组。
主要命令
LPUSH:在键的头部插入新元素。该命令也支持多个参数,在这种情况下,值将按我们传递的参数的相反顺序存储。
这里有一些命令示例:
LPUSH "categories" "sports"
LPUSH "categories" "movies"
LRANGE "categories" 0 -1
看一下LRANGE输出,正如我们所见,movie的值是列表中的第一个,因为LPUSH在头部插入了新元素。
RPUSH: 在键的尾部插入新元素。该命令也支持多个参数,在这种情况下,值将按照相应的顺序排列。
这里有一些命令示例:
RPUSH "categories" "kitchen"
RPUSH "categories" "room"
LRANGE "categories" 0 -1
正如我们所见,在LRANGE输出中,新值被插入到值的尾部。这是RPUSH命令的行为。
LSET: 它设置在请求索引上的元素。
这里有一些命令示例:
LSET "categories" 0 "series""
LRANGE "categories" 0 -1
零索引的新值是series。这是LSET命令为我们做的。
LRANGE: 它返回键的指定元素。命令参数是键、起始索引和最终停止元素。停止参数上的-1将返回整个列表:
LRANGE "categories" 0 2
LRANGE "categories" 0 -1
正如我们所见,第一个命令将返回三个元素,因为零索引将被分组。
集合
集合是一组字符串。它们有一个不允许重复值的属性。这意味着如果我们向集合中添加预存在的值,它将导致相同的元素,在这种情况下,优势是不必要验证元素是否存在于集合中。另一个重要特征是集合是无序的。这种行为与 Redis 列表不同。它在不同的用例中可能很有用,例如统计唯一访客、跟踪唯一 IP 等。
主要命令
以下是列出其主要命令及其用法的以下内容:
SADD: 它在请求键中添加元素。此外,此命令的返回值是添加到集合中的元素数量:
SADD "unique-visitors" "joe"
SADD "unique-visitors" "mary"
正如我们所见,命令返回一个,因为我们每次都添加了一个用户。
SMEMBERS: 它返回请求键的所有成员:
SMEMBERS "unique-visitors"
该命令将返回joe和mary,因为这些值存储在unique-visitors键中。
SCARD: 它返回请求键的元素数量:
SCARD "unique-visitors"
该命令将返回请求键中存储的元素数量,在这种情况下,输出将是2。
Spring Data Reactive Redis
Spring Data Redis 为从 Spring Boot 应用与 Redis 服务器交互提供了一个简单的方法。该项目是 Spring Data 家族的一部分,并为开发人员提供了高级和低级抽象。
Jedis 和 Lettuce 连接器作为此项目的驱动程序得到支持。
该项目提供了许多功能和便利来与 Redis 交互。Repository接口也得到了支持。有一个类似于其他实现(例如 Spring Data JPA)的CrudRepository用于 Redis。
此项目的核心类是RedisTemplate,它提供了一个高级 API 来执行 Redis 操作和序列化支持。我们将使用此类与 Redis 上的集合数据结构交互。
该项目的支持是反应式实现,对我们来说,这些是重要的特性,因为我们正在寻找反应式实现。
配置 ReactiveRedisConnectionFactory
要配置 ReactiveRedisConnectionFactory,我们可以使用 application.yaml 文件,因为它更容易维护和集中我们的配置。
原则与其他 Spring Data 项目相同,我们应该在 application.yaml 文件中提供主机和端口配置,如下所示:
spring:
redis:
host: localhost
port: 6379
在前面的配置文件中,我们将 Redis 配置指向了 localhost,正如我们所看到的。配置相当简单且易于理解。
完成。连接工厂已配置。下一步是提供一个 RedisTemplate 来与我们的 Redis 实例交互。请看下一节。
提供一个 ReactiveRedisTemplate
Spring Data Redis 的主要类是 ReactiveRedisTemplate,然后我们需要为 Spring 容器配置并提供一个实例。
我们需要提供一个实例并配置正确的序列化器以用于所需的 ReactiveRedisTemplate。Serializers 是 Spring Data Redis 用于将对象从存储在 Redis 的原始字节序列化和反序列化的方式。
我们将只使用 StringRedisSerializer,因为我们的 Key 和 Value 都是简单的字符串,Spring Data Redis 已经为我们准备好了这个序列化器。
让我们生成我们的 ReactiveRedisTemplate。实现应该看起来像以下这样:
package springfive.twittertracked.infra.redis
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory
import org.springframework.data.redis.core.ReactiveRedisTemplate
import org.springframework.data.redis.serializer.RedisSerializationContext
@Configuration
open class RedisConfiguration {
@Bean
open fun reactiveRedisTemplate(connectionFactory:ReactiveRedisConnectionFactory):
ReactiveRedisTemplate<String, String> {
return ReactiveRedisTemplate(connectionFactory, RedisSerializationContext.string())
}
}
太棒了。这是我们使用 Kotlin 在 Spring 框架中的第一个代码。关键字 open 是 Java 的 final 关键字的相反。这意味着这个函数可以从这个类继承。默认情况下,Kotlin 中的所有类都是 final 的。Spring 框架要求在 @Configuration 类的 @Bean 上使用非 final 函数,然后我们需要插入 open。
我们作为参数接收了 ReactiveRedisConnectionFactory。Spring 知道我们在 application.yaml 文件中使用了哪些配置来生成 Redis。然后容器可以注入这个工厂。
最后,我们声明 ReactiveRedisTemplate<String, String> 作为我们函数的返回值。
有趣的工作,我们准备好使用我们的 Redis 模板了。现在,我们将实现我们的第一个 Redis 存储库。下一节再见。
创建跟踪标签存储库
我们已经创建了 ReactiveRedisTemplate,然后我们可以在我们的存储库实现中使用这个对象。我们将创建一个简单的存储库来与 Redis 交互,记住存储库应该是反应式的,这是我们应用程序的一个重要特性。然后我们需要返回 Mono 或 Flux 来使存储库反应式。让我们看看我们的存储库实现:
package springfive.twittertracked.domain.repository
import org.springframework.data.redis.core.ReactiveRedisTemplate
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import springfive.twitterconsumer.domain.TrackedHashTag
@Service
class TrackedHashTagRepository(private val redisTemplate: ReactiveRedisTemplate<String, String>){
fun save(trackedHashTag: TrackedHashTag): Mono<TrackedHashTag>? {
return this.redisTemplate
.opsForSet().add("hash-tags", "${trackedHashTag.hashTag}:${trackedHashTag.queue}")
.flatMap { Mono.just(trackedHashTag) }
}
fun findAll(): Flux<TrackedHashTag> {
return this.redisTemplate.opsForSet().members("hash-tags").flatMap { el ->
val data = el.split(":")
Flux.just(TrackedHashTag(hashTag = data[0],queue = data[1]))
}
}
}
我们在我们的类中作为注入接收了 ReactiveRedisTemplate<String, String>,Spring 框架可以检测构造函数并注入正确的实现。
目前,我们需要这两个函数。第一个函数负责将我们的实体,TrackedHashTag,插入到 Redis 的集合结构中。我们在 Redis 上添加hash-tags键的值。这个函数返回一个包含TrackedHashTag值的Mono。请注意save函数。我们已经为我们的值创建了一个模式,该模式遵循hashtag,queue,其中标签是收集推文的值,而队列是我们将在下一节中用于发送到 RabbitMQ 队列的队列。
第二个函数返回hash-tags键的所有值,这意味着我们系统跟踪的所有标签。此外,我们还需要进行一些逻辑操作来创建我们的模型,TrackedHashTag。
存储库已完成,现在我们可以创建我们的服务层来封装存储库。让我们在下一节中这样做。
创建服务层
我们的存储库已经准备好使用,现在我们可以创建我们的服务层。这一层负责编排我们的存储库调用。在我们的案例中,这相当简单,但在一些复杂场景中,它可以帮助我们封装存储库调用。
我们的服务将被命名为TrackedHashTagService,它将负责与我们之前创建的存储库交互。实现应该看起来像以下这样:
package springfive.twittertracked.domain.service
import org.springframework.stereotype.Service
import springfive.twitterconsumer.domain.TrackedHashTag
import springfive.twitterconsumer.domain.repository.TrackedHashTagRepository
@Service
class TrackedHashTagService(private val repository: TrackedHashTagRepository) {
fun save(hashTag:TrackedHashTag) = this.repository.save(hashTag)
fun all() = this.repository.findAll()
}
干得好。在这里,有一些基本的东西。我们有一个注入我们的存储库以与 Redis 交互的构造函数。这里有趣的是函数声明。没有函数体和返回类型,因为 Kotlin 编译器可以推断返回类型,这有助于开发者避免编写样板代码。
公开 REST 资源
现在,我们已经创建了存储库和服务层,我们准备通过 HTTP 端点公开我们的服务:
package springfive.twittertracked.domain.resource
import org.springframework.web.bind.annotation.*
import springfive.twitterconsumer.domain.TrackedHashTag
import springfive.twitterconsumer.domain.service.TrackedHashTagService
@RestController
@RequestMapping("/api/tracked-hash-tag")
class TrackedHashTagResource(private val service:TrackedHashTagService) {
@GetMapping
fun all() = this.service.all()
@PostMapping
fun save(@RequestBody hashTag:TrackedHashTag) = this.service.save(hashTag)
}
代码相当简洁简单。看看这段代码有多简洁。前面的代码是 Kotlin 如何帮助开发者创建可读代码的一个例子。谢谢,Kotlin。
创建 Twitter 应用程序
对于这个项目,我们需要在 Twitter 平台上配置一个应用程序。这是必要的,因为我们将会使用 Twitter 的 API 来搜索推文,例如,而 Twitter 账户是这一需求的前提。我们不会解释如何创建 Twitter 账户。互联网上有很多关于这个的文章。
在创建 Twitter 账户后,我们需要前往apps.twitter.com/并创建一个新的应用程序。页面与以下截图非常相似:

我们将点击创建新应用程序按钮以开始创建过程。当我们点击该按钮时,将显示以下页面。我们需要填写所需的字段并接受 Twitter 协议:

我们可以选择应用程序名称,填写描述和网站。这些细节由你决定。
然后,我们需要接受协议并点击创建你的 Twitter 应用程序:

干得好。我们的 Twitter 应用程序几乎准备就绪可以使用。
现在,我们只需要配置应用程序以供使用。
我们需要检查我们的密钥和访问令牌是否正确配置。让我们点击“密钥和访问令牌”标签并检查值,如下所示:

如我们所见,在前面的截图中有一些重要的配置。消费者密钥和消费者密钥是认证 Twitter API 所必需的。这里的一个重要点是访问级别;确保它配置为只读,如前一个截图所示,我们不会在 Twitter 上执行写操作。
让我们将其 Docker 化。
太棒了。我们有一个系统,它将跟踪的标签存储在 Redis 实例上。该应用程序是完全响应式的,没有阻塞线程。
现在,我们将配置 Maven 插件以生成 Docker 镜像。配置与我们在第三章中做的配置相当相似,使用 Spring Data 和响应式模式进行持久化。然而,现在我们将创建一个容器,我们将使用 Kotlin 语言运行它。让我们来做这件事。
配置 pom.xml
现在,我们将配置我们的pom.xml文件,以便能够生成我们的 Docker 镜像。首先我们需要更改的是我们的最终名称工件,因为 Docker 镜像不允许使用-字符,然后我们需要正确配置。
配置相当简单,将<finalName>标签放在<build>节点上。让我们来做这件事:
<build>
<finalName>tracked_hashtag</finalName>
....
</build>
好的。我们已经正确配置了最终名称以正确生成 Docker 镜像。现在,我们将配置 Maven Docker 插件,通过 Maven 目标生成 Docker 镜像。
在构建节点内的插件部分,我们应该放入以下插件配置:
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.21.0</version>
<configuration>
<images>
<image>
<name>springfivebyexample/${project.build.finalName}</name>
<build>
<from>openjdk:latest</from>
<entryPoint>java -Dspring.profiles.active=container -jar
/application/${project.build.finalName}.jar</entryPoint>
<assembly>
<basedir>/application</basedir>
<descriptorRef>artifact</descriptorRef>
<inline>
<id>assembly</id>
<files>
<file>
<source>target/${project.build.finalName}.jar</source>
</file>
</files>
</inline>
</assembly>
<tags>
<tag>latest</tag>
</tags>
<ports>
<port>9090</port>
</ports>
</build>
<run>
<namingStrategy>alias</namingStrategy>
</run>
<alias>${project.build.finalName}</alias>
</image>
</images>
</configuration>
</plugin>
配置相当简单。我们之前已经这样做过了。在配置部分,我们配置了从镜像开始,在我们的例子中是openjdk:latest,Docker 入口点和暴露的端口。
让我们在下一节创建我们的 Docker 镜像。
创建镜像
我们的项目之前已经配置了 Maven Docker 插件。我们可以使用 Maven Docker 插件通过docker:build目标生成 Docker 镜像。然后,是时候生成我们的 Docker 镜像了。
要生成 Docker 镜像,请输入以下命令:
mvn clean install docker:build
现在,我们必须等待 Maven 构建并检查 Docker 镜像是否成功生成。
检查 Docker 镜像,我们应该看到新生成的镜像。为此,我们可以使用docker images命令:
docker images
对了,我们应该在镜像列表中看到springfivebyexample/tracked_hashtag:latest,如下面的截图所示:

太棒了,我们的 Docker 镜像已经准备好运行我们的第一个使用 Kotlin 语言的 Spring Boot 应用程序了。让我们现在运行它。
运行容器
让我们运行我们的容器。在此之前,我们需要记住一些事情。容器应该运行在 Twitter 网络上,以便能够连接到同样运行在 Twitter 网络上的我们的 Redis 实例。记住,当在容器基础设施中运行时,Redis 的localhost地址不再有效。
要运行我们的容器,我们可以执行以下命令:
docker run -d --name hashtag-tracker --net twitter -p 9090:9090 springfivebyexample/tracked_hashtag
恭喜,我们的应用程序正在 Docker 容器中运行,并且连接到了我们的 Redis 实例。让我们创建并测试我们的 API 以检查期望的行为。
测试 API
我们的应用容器正在运行。现在,我们可以尝试调用 API 来检查行为。在这一部分,我们将使用curl命令行。curl允许我们在 Linux 上通过命令行调用 API。此外,我们还将使用jq使命令行上的 JSON 可读,如果您没有这些工具,请查看提示框以安装这些工具。
让我们调用我们的创建 API,记住创建时我们可以在 API 的基本路径中使用POST方法。然后输入以下命令:
curl -H "Content-Type: application/json" -X POST -d '{"hashTag":"java","queue":"java"}' \
http://localhost:9090/api/tracked-hash-tag
这里有一些有趣的事情。-H参数指示curl将其放入请求头中,-d表示请求体。此外,最后我们有服务器地址。
我们已经创建了新的tracked-hash-tag。让我们检查我们的GET API 以获取这些数据:
curl 'http://localhost:9090/api/tracked-hash-tag' | jq '.'
太棒了,我们调用了curl工具,并使用jq工具打印了 JSON 值。命令输出应该看起来像以下截图:

要在 Ubuntu 上安装curl,我们可以使用sudo apt-get install curl -y。此外,要安装jq,我们可以使用sudo apt-get install jq -y。
摘要
在本章中,我们介绍了 Kotlin 语言,这是 JVM 上最突出的语言,因为它有一个超级快的编译器,如果我们以 Scala 为例,它也带来了代码的简洁性和可读性,帮助开发者创建更简洁和可读的代码。
我们还使用 Kotlin 作为语言的基本概念,在 Spring 框架中创建了我们的第一个应用程序,并看到了 Kotlin 如何以实际的方式帮助开发者。
我们介绍了 Redis 作为缓存和 Spring Data Reactive Redis,它支持以响应式范式使用 Redis。
在本章的最后部分,我们学习了如何创建 Twitter 应用程序,这要求我们创建下一个应用程序,并开始使用响应式编程和 Reactive Rest Client 来消费 Twitter API。
让我们跳到下一章,了解更多关于 Spring Reactive 的内容。
第五章:反应式 Web 客户端
到目前为止,我们已经创建了整个项目基础设施以消费 Twitter 流。我们已经创建了一个存储跟踪标签的应用程序。
在本章中,我们将学习如何使用 Spring Reactive Web Client,并使用反应式范式进行 HTTP 调用,这是 Spring 5.0 最期待的功能之一。我们将异步调用 Twitter REST API,并使用 Project Reactor 提供优雅的流式处理方式。
我们将介绍 Spring Messaging for RabbitMQ。我们将使用 Spring Messaging API 与 RabbitMQ 代理进行交互,并了解 Spring 如何帮助开发者使用高级抽象。
在本章结束时,我们将封装应用程序并创建一个 Docker 镜像。
在本章中,我们将学习以下内容:
-
反应式 Web 客户端
-
Spring Messaging for RabbitMQ
-
RabbitMQ Docker 使用
-
Spring Actuator
创建 Twitter Gathering 项目
我们学习了如何使用惊人的 Spring Initializr 创建 Spring Boot 项目。在本章中,我们将以不同的方式创建项目,以向您展示创建 Spring Boot 项目的另一种方法。
在任何目录中创建tweet-gathering文件夹。我们可以使用以下命令:
mkdir tweet-gathering
然后,我们可以访问之前创建的文件夹,并复制位于 GitHub 上的pom.xml文件:github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter05/tweet-gathering/pom.xml.
在 IDE 中打开pom.xml文件。
这里有一些有趣的依赖项。jackson-module-kotlin帮助我们在 Kotlin 语言中处理 JSON。另一个有趣的依赖项是kotlin-stdlib,它为我们提供了类路径中的 Kotlin 标准库。
在插件部分,最重要的插件是kotlin-maven-plugin,它允许并配置我们的 Kotlin 代码的构建。
在下一节中,我们将创建文件夹结构以开始编写代码。
让我们开始吧。
项目结构
项目结构遵循 maven 建议的模式。我们将使用 Kotlin 语言编写项目代码,然后我们将创建一个kotlin文件夹来存储我们的代码。
我们在之前创建的pom.xml文件上进行了配置,所以它将正常工作。让我们看看项目的正确文件夹结构:

如我们所见,基本包是springfive.twittergathering包。然后,我们将尽快在这个包中创建子包。
让我们为微服务创建基础设施。
完整的源代码可以在 GitHub 上找到:github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter05/tweet-gathering
使用 Docker 启动 RabbitMQ 服务器
我们可以使用 Docker 启动 RabbitMQ 服务器。我们不想在我们的开发机器上安装服务器,因为它可能会创建库冲突和大量的文件。让我们了解如何在 Docker 容器中启动 RabbitMQ。
让我们在下一节中这样做。
从 Docker Hub 拉取 RabbitMQ 镜像
我们需要从 Docker Hub 拉取 RabbitMQ 镜像。我们将使用官方存储库中的镜像,因为它更安全、更可靠。
要获取图像,我们需要使用以下命令:
docker pull rabbitmq:3.7.0-management-alpine
等待下载完成,然后我们就可以继续到下一部分。在下一部分,我们将学习如何设置 RabbitMQ 服务器。
启动 RabbitMQ 服务器
要启动 RabbitMQ 服务器,我们将运行 Docker 命令。有一些考虑事项我们需要注意;我们将在这个之前创建的 Twitter Docker 网络上运行这个容器,但我们将在主机上暴露一些端口,因为这使与代理交互变得更容易。
此外,我们还将使用管理镜像,因为它提供了一个页面,使我们能够管理并查看类似控制面板上的 RabbitMQ 信息。
让我们运行:
docker run -d --name rabbitmq --net twitter -p 5672:5672 -p 15672:15672 rabbitmq:3.7.0-management-alpine
等待几秒钟,以便 RabbitMQ 建立连接,然后我们可以连接到管理页面。为此,请访问 http://localhost:15672 并登录系统。默认用户是 guest,密码也是 guest。控制面板看起来像这样:

在面板上有很多有趣的信息,但到目前为止,我们将探索通道和一些有趣的部件。
太棒了。我们的 RabbitMQ 服务器已经启动并运行。我们很快就会使用这个基础设施。
Spring Messaging AMQP
这个项目支持基于 AMQP 的消息解决方案。有一个高级 API 可以与所需的代理进行交互。这些交互可以从代理发送和接收消息。
就像在其他 Spring 项目中一样,这些功能是由 模板 类提供的,它们公开了由代理提供并由 Spring 模块实现的核心理念。
这个项目有两个部分:spring-amqp 是基础抽象,而 spring-rabbit 是 RabbitMQ 的实现。我们将使用 spring-rabbit,因为我们正在使用 RabbitMQ 代理。
在我们的 pom.xml 中添加 Spring AMQP
让我们把 spring-amqp jar 包添加到我们的项目中。spring-amqp 有一个启动依赖,它会为我们配置一些常见的东西,比如 ConnectionFactory 和 RabbitTemplate,所以我们会使用它。为了添加这个依赖,我们将按照以下方式配置我们的 pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
下一步是配置连接;我们将使用 application.yaml 文件,因为我们正在使用启动器。在下一节中,我们将进行配置。
集成 Spring 应用程序和 RabbitMQ
我们已经在我们的项目中配置了spring-amqp依赖项。现在,是时候正确配置 RabbitMQ 连接了。我们将使用RabbitMQTemplate向代理发送消息;这有一些转换器,帮助我们将我们的领域模型转换为 JSON,反之亦然。
让我们配置我们的 RabbitMQ 连接。配置应该在application.yaml文件中,并且应该看起来像这样:
spring:
rabbitmq:
host: localhost
username: guest
password: guest
port: 5672
如我们所见,一些 Spring 配置与其他配置相当相似,相同的风格,yaml中的节点是技术的名称后面跟着几个属性。
我们正在使用 RabbitMQ 的默认凭据。主机和端口与 RabbitMQ 代理地址相关。配置很简单,但为我们做了很多事情,例如ConnectionFactory。
理解 RabbitMQ 交换、队列和绑定
我们正在使用 RabbitMQ 做一些有趣的事情。我们已经成功配置了连接。还有一些其他的事情我们还没有做,比如配置交换、队列和绑定,但在我们做这些之前,让我们更深入地了解一下这些术语。
交换
交换是 RabbitMQ 实体,消息被发送到那里。我们可以将其比作一条河流,水流就是消息的流程。在接下来的几节中,我们将了解四种不同类型的交换。
直接交换
直接交换允许基于路由键发送路由消息。这个名字本身就说明了问题,它允许将消息直接发送到指定的客户,即正在监听交换的客户。记住,它使用路由键作为参数来将消息路由到客户。
广播交换
广播交换将消息路由到所有独立绑定的队列,而不考虑路由键。所有绑定的队列都将接收到发送到广播交换的消息。它们可以用作具有主题行为或分布式列表。
主题交换
主题交换与直接交换类似,但主题交换使我们能够使用模式匹配,而直接交换只能允许精确的路由键。我们将在我们的项目中使用这种交换。
头部交换
头部交换是自解释的,其行为类似于主题交换,但不是使用路由键,而是使用头部属性来匹配正确的队列。
队列
队列是交换将消息写入的地方,它根据路由键来路由消息。队列是消费者获取已发布到交换的消息的地方。消息根据交换类型被路由到队列。
绑定
绑定可以被视为交换和队列之间的链接。我们可以将其视为一种交通警察,根据配置指导消息应该被重定向到何处,在这种情况下,链接。
在 Spring AMQP 上配置交换、队列和绑定
Spring AMQP 项目为之前列出的所有 RabbitMQ 实体提供了抽象,我们需要配置它以与代理交互。正如我们在其他项目中做的那样,我们需要一个 @Configuration 类,它将为 Spring 容器声明豆。
在 yaml 中声明交换、队列和绑定
我们需要配置实体名称以指导框架连接到代理实体。我们将使用 application.yaml 文件来存储这些名称,因为它更容易维护,并且是存储应用程序基础设施数据的正确方式。
实体名称的部分应如下所示:
queue:
twitter: twitter-stream
exchange:
twitter: twitter-exchange
routing_key:
track: track.*
属性是自解释的,exchange 节点具有交换机的名称,queue 节点具有队列名称,最后,routing_key 节点具有路由参数。
太棒了。属性已配置,现在我们将创建我们的 @Configuration 类。让我们在下一节中这样做。我们几乎准备好与 RabbitMQ 代理交互了。
声明 RabbitMQ 的 Spring 豆
现在,让我们创建我们的配置类。这个类相当简单,正如我们将通过 Spring 抽象看到的那样,它们也容易理解,特别是因为类名暗示了 RabbitMQ 实体。
让我们创建我们的类:
package springfive.twittergathering.infra.rabbitmq
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import org.springframework.amqp.core.Binding
import org.springframework.amqp.core.BindingBuilder
import org.springframework.amqp.core.Queue
import org.springframework.amqp.core.TopicExchange
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
open class RabbitMQConfiguration(@Value("\${queue.twitter}") private val queue:String,
@Value("\${exchange.twitter}") private val
exchange:String,
@Value("\${routing_key.track}") private val routingKey:String){
@Bean
open fun queue():Queue{
return Queue(this.queue,false)
}
@Bean
open fun exchange():TopicExchange{
return TopicExchange(this.exchange)
}
@Bean
open fun binding(queue: Queue, exchange: TopicExchange): Binding {
return BindingBuilder.bind(queue).to(exchange).with(this.routingKey)
}
@Bean
open fun converter(): Jackson2JsonMessageConverter {
return Jackson2JsonMessageConverter(ObjectMapper().registerModule(KotlinModule()))
}
}
这里有一些有趣的事情需要注意。在 RabbitMQConfiguration 构造函数中,我们注入了在 application.yaml 文件中配置的值来命名实体。之后,我们开始为容器配置 Spring 豆,以便它可以将其注入到 Spring 管理的类中。这里的关键点是,如果它们在 RabbitMQ 代理中不存在,Spring 将创建它们。谢谢,Spring,我们感激并喜欢它的帮助。
我们可以看到声明 Binding 的 DSL,它使开发者的生活变得更轻松,并防止代码中的错误。
在类的最后部分,我们声明了 Jackson2JsonMessageConverter。这些转换器用于将域模型转换为 JSON 以及相反。它使我们能够在监听器上接收域对象而不是字节数组或字符串。同样的行为也可以用在 Producers 中,我们能够发送域对象而不是 JSON。
我们需要向 Jackson2JsonMessageConverter 提供一个 ObjectMapper,我们使用了 Kotlin 模块,因为 Kotlin 处理数据类的方式,这些数据类没有无参构造函数。
干得好!我们的基础设施已完全配置。现在让我们编写生产者和消费者代码!
使用 Spring 消息传递消费消息
Spring AMQP 提供了 @RabbitListener 注解;它将为所需的队列配置订阅者,它移除了很多基础设施代码,例如连接到 RabbitListenerConnectionFactory,并程序化地创建消费者。这使得创建队列消费者变得非常简单。
spring-boot-starter-amqp 为我们提供了一些自动配置。当我们使用这个模块时,Spring 将自动为我们创建一个 RabbitListenerConnectionFactory 并配置 Spring 转换器以自动将 JSON 转换为领域类。
非常简单。Spring AMQP 真的为开发者提供了一个超级高级的抽象。
让我们看看即将在我们的应用程序中使用的示例:
@RabbitListener(queues = ["twitter-track-hashtag"])
fun receive(hashTag:TrackedHashTag) {
...
}
完整的源代码可以在 GitHub 上找到:github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter05/tweet-gathering/src/main/kotlin/springfive/twittergathering/domain/service/TwitterGatherRunner.kt
小菜一碟。代码非常容易理解,它使得我们只需关注业务规则。基础设施并不是一件好事,因为它并不为业务带来真正的价值,它只是一项技术。Spring 尝试抽象整个基础设施代码,以帮助开发者编写业务代码。这是 Spring 框架提供的一项真正有价值的资产。
感谢,Spring 团队。
使用 Spring 消息传递产生消息
spring-amqp 模块提供了一个 RabbitTemplate 类,它抽象了高级 RabbitMQ 驱动类。它提高了开发者的性能,并使应用程序无 bug,因为 Spring 模块是一组经过充分测试的代码。我们将使用 convertAndSend() 函数,该函数允许传递交换机、路由键和消息对象作为参数。请记住,这个函数使用 Spring 转换器将我们的模型类转换为 JSON 字符串。
convertAndSend() 方法有很多重载版本,根据具体的使用场景,其他版本可能更合适。我们将使用之前看到的那种简单版本。
让我们看看发送消息到代理的消息代码片段:
this.rabbitTemplate.convertAndSend("twitter-exchange","track.${hashTag.queue}",it)
好的。第一个参数是 Exchange 名称,第二个是 RoutingKey。最后,我们有消息对象,它将被转换为 JSON 字符串。
我们很快就会看到代码的实际应用。
在我们的应用程序中启用 Twitter
在本节中,我们将启用 Twitter Gathering 应用程序上 Twitter API 的使用。这个应用程序应该根据用户指定的查询获取推文。这个查询是在上一章中我们创建的先前微服务上注册的。
当用户调用 API 注册 TrackedHashTag 时,微服务将 TrackedHashTag 存储在 Redis 数据库中,并通过 RabbitMQ 发送消息。然后,这个项目将开始根据这些信息收集推文。这是数据流。在下一章中,我们将进行反应式流,并通过我们的反应式 API 分发推文。这将非常令人兴奋。
然而,目前,我们需要配置 Twitter 凭证;我们将使用 Spring bean 来完成这项工作——让我们来实现它。
生成 Twitter 凭证
我们将使用@Configuration类来提供我们的 Twitter 配置对象。@Configuration类非常适合提供基础设施 bean,如果我们没有所需模块的启动项目。
此外,我们还将使用application.yaml文件来存储 Twitter 凭证。这种类型的配置不应该保存在源代码仓库中,因为它包含敏感数据,不应该与他人共享。然后,Spring 框架使我们能够在yaml文件中声明属性,并在运行时配置环境变量以填充这些属性。这是一种将敏感数据从源代码仓库中排除的绝佳方式。
在 application.yaml 中配置 Twitter 凭证
要开始在应用程序中配置 Twitter API,我们必须提供凭证。我们将使用yaml文件来完成这项工作。让我们在我们的application.yaml中添加凭证:
twitter:
consumer-key: ${consumer-key}
consumer-secret: ${consumer-secret}
access-token: ${access-token}
access-token-secret: ${access-token-secret}
简单易懂。属性已经声明,然后我们使用$来指示 Spring 框架这个值将作为一个环境变量接收。记住,我们在上一章中已经配置了 Twitter 账户。
建模对象以表示 Twitter 设置
我们必须为我们的应用程序创建抽象和出色的数据模型。这将创建一些模型,使开发者的生活更容易理解和编码。让我们创建我们的 Twitter 设置模型。
Twittertoken
这个类表示之前在 Twitter 中配置的应用程序令牌。该令牌只能用于应用程序认证。我们的模型应该如下所示:
data class TwitterToken(val accessToken: String,val accessTokenSecret: String)
我喜欢 Kotlin 声明数据类的方式——完全不可变且没有样板代码。
TwitterAppSettings
TwitterAppSettings表示消费者密钥和消费者密钥。从 Twitter 的角度来看,这是我们应用程序的一种身份标识。我们的模型相当简单,必须如下所示:
data class TwitterAppSettings(val consumerKey: String,val consumerSecret: String)
干得好,我们的模型已经准备好了。现在是时候为 Spring 容器生成对象了。我们将在下一节中这样做。
为 Spring 容器声明 Twitter 凭证
让我们生成我们的 Twitter 配置对象。按照我们一直在使用的模式,我们将使用@Configuration类。该类应该如下所示:
package springfive.twittergathering.infra.twitter
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
open class TwitterConfiguration(@Value("\${twitter.consumer-key}") private val consumerKey: String,
@Value("\${twitter.consumer-secret}") private val consumerSecret: String,
@Value("\${twitter.access-token}") private val accessToken: String,
@Value("\${twitter.access-token-secret}") private val accessTokenSecret: String) {
@Bean
open fun twitterAppSettings(): TwitterAppSettings {
return TwitterAppSettings(consumerKey, consumerSecret)
}
@Bean
open fun twitterToken(): TwitterToken {
return TwitterToken(accessToken, accessTokenSecret)
}
}
非常简单,这是声明 Spring 中 bean 的一种 Spring 方式。我们正在逐步改进我们使用 Spring 的方式。做得好!
现在,我们已经完成了 Twitter 的配置。我们将使用 Spring WebFlux 的 WebClient 来消费 Twitter API,它支持响应式编程范式。在我们运行代码之前,让我们先了解一下。
Spring 响应式 Web 客户端
这是一个相当新的功能,它是在 Spring Framework 5 中添加的。它使我们能够使用响应式范式与 HTTP 服务交互。
然而,它并不是 Spring 提供的RestTemplate的替代品,而是一个用于处理反应式应用程序的补充。不用担心,RestTemplate是传统应用程序中与 HTTP 服务交互的出色且经过测试的实现。
此外,WebClient实现支持text/event-stream MIME 类型,这可以让我们消费服务器事件。
以 Spring 方式生成 WebClient
在我们开始调用 Twitter API 之前,我们希望在 Spring 中以某种方式创建一个WebClient实例。这意味着我们正在寻找一种方法来注入实例,使用依赖注入模式。
为了实现这一点,我们可以使用@Configuration注解并创建一个WebClient实例,使用@Bean注解来声明 Spring 容器中的 bean。让我们这样做:
package springfive.twittergathering.infra.web
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.reactive.function.client.WebClient
@Configuration
open class WebClientProducer {
@Bean
open fun webClient(): WebClient? {
return WebClient.create()
}
}
在这个类别中有一两个已知的注释;这是一种在 Spring 中以标准方式声明 bean 实例的相当常见方法。这使得在 Spring 管理的其他类中注入WebClient实例成为可能。
创建模型以收集推文
如果我们想要异步和反应式地消费 Twitter API,那么我们应该创建 API 客户端。在我们编写客户端代码之前,我们需要根据我们的要求创建我们的模型类。
我们不需要所有推文的属性。我们期望以下属性:
-
id -
text -
createdAt -
user
然后,我们将根据列出的属性来构建我们的班级模型。
让我们从用户属性开始。这个属性是一个 JSON 属性,我们将为它创建一个单独的类。这个类应该看起来像这样:
@JsonIgnoreProperties(ignoreUnknown = true)
data class TwitterUser(val id:String,val name:String)
我们使用了 Kotlin 的data class,它非常适合我们的用例,我们希望将其用作数据容器。此外,我们需要添加@JsonIgnoreProperties(ignoreUnknown = true),因为这个注解指示 Spring 转换器在 JSON 响应中缺少属性时忽略该属性。这是这部分代码的重要部分。
我们已经创建了TwitterUser类,它代表了创建推文的用户。现在,我们将创建一个代表推文的Tweet类。让我们创建我们的类:
@JsonIgnoreProperties(ignoreUnknown = true)
data class Tweet(val id:String, val text:String, @JsonProperty("created_at")val createdAt:String, val user:TwitterUser)
对于我们来说有一些常见的事情,还有一件新的事情。@JsonProperty允许开发者自定义类上的属性名,该类在 JSON 中有不同的属性名;这对于 Java 开发者来说很常见,因为他们通常使用CamelCase作为命名属性的命名方式,而在 JSON 表示法中,人们通常使用SnakeCase。这个注解可以帮助我们解决编程语言和 JSON 之间的这种不匹配。
我们可以在这里找到关于蛇形命名的更详细解释:en.wikipedia.org/wiki/Snake_case。同样,我们也可以在这里找到关于驼峰命名的完整解释:en.wikipedia.org/wiki/Camel_case。
很好。我们的 API 对象已经准备好了。有了这些对象,我们就可以与 API 交互。我们将创建一个服务来收集推文。我们将在下一节中这样做。
使用 Twitter API 进行认证
在我们的对象准备好后,我们需要创建一个类来帮助我们处理 Twitter 认证。我们将使用 Twitter 应用程序仅认证模型。这种认证应该用于后端应用程序。
使用这种认证方式的应用程序可以:
-
拉取用户时间线
-
访问任何账户的朋友和关注者
-
访问列表和资源
-
在推文中搜索
-
获取任何用户信息
如我们所见,该应用程序是一个只读的 Twitter API 消费者。
我们可以使用 Twitter 文档详细了解这种认证。文档可以在以下位置找到:developer.twitter.com/en/docs/basics/authentication/guides/authorizing-a-request.
我们将遵循 Twitter 文档来授权我们的请求,它类似于烹饪食谱,因此我们必须遵循所有步骤。最终的类应该看起来像这样:
package springfive.twittergathering.infra.twitter
import org.springframework.util.StringUtils
import springfive.twittergathering.infra.twitter.EncodeUtils.computeSignature
import springfive.twittergathering.infra.twitter.EncodeUtils.encode
import java.util.*
object Twitter {
private val SIGNATURE_METHOD = "HMAC-SHA1"
private val AUTHORIZATION_VERIFY_CREDENTIALS = "OAuth " +
"oauth_consumer_key=\"{key}\", " +
"oauth_signature_method=\"" + SIGNATURE_METHOD + "\", " +
"oauth_timestamp=\"{ts}\", " +
"oauth_nonce=\"{nonce}\", " +
"oauth_version=\"1.0\", " +
"oauth_signature=\"{signature}\", " +
"oauth_token=\"{token}\""
fun buildAuthHeader(appSettings: TwitterAppSettings, twitterToken: TwitterToken, method: String, url: String, query: String):String{
val ts = "" + Date().time / 1000
val nounce = UUID.randomUUID().toString().replace("-".toRegex(), "")
val parameters = "oauth_consumer_key=${appSettings.consumerKey}&oauth_nonce=$nounce&oauth_signature_method=$SIGNATURE_METHOD&oauth_timestamp=$ts&oauth_token=${encode(twitterToken.accessToken)}&oauth_version=1.0&track=${encode(query)}"
val signature = "$method&" + encode(url) + "&" + encode(parameters)
var result = AUTHORIZATION_VERIFY_CREDENTIALS
result = StringUtils.replace(result, "{nonce}", nounce)
result = StringUtils.replace(result, "{ts}", "" + ts)
result = StringUtils.replace(result, "{key}", appSettings.consumerKey)
result = StringUtils.replace(result, "{signature}", encode(computeSignature(signature, "${appSettings.consumerSecret}&${encode(twitterToken.accessTokenSecret)}")))
result = StringUtils.replace(result, "{token}", encode(twitterToken.accessToken))
return result
}
}
data class TwitterToken(val accessToken: String,val accessTokenSecret: String)
data class TwitterAppSettings(val consumerKey: String,val consumerSecret: String)
这是一个食谱。函数buildAuthHeader将使用授权请求的规则创建授权头。我们已对一些请求头进行了签名,并结合请求体。此外,用我们的 Twitter 凭证对象替换模板值。
有关服务器发送事件(SSE)的一些说明
服务器发送事件(SSE)是一种技术,其中服务器向客户端发送事件,而不是客户端轮询服务器以检查信息可用性。消息流不会中断,直到客户端或服务器关闭流。
在这里需要理解的最重要的事情是信息流的流向。服务器决定何时向客户端发送数据。
处理资源负载和带宽使用非常重要。客户端将接收数据块,而不是通过轮询技术将负载施加在服务器上。
Twitter 有一个流 API,Spring Framework WebClient 支持 SSE。现在是消费 Twitter 流的时候了。
创建收集服务
TweetGatherService将负责与 Twitter API 交互,并根据请求的标签收集请求推文。该服务将是一个带有一些注入属性的 Spring Bean。类应该看起来像这样:
package springfive.twittergathering.domain.service
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import org.springframework.http.MediaType
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Flux
import springfive.twittergathering.infra.twitter.Twitter
import springfive.twittergathering.infra.twitter.TwitterAppSettings
import springfive.twittergathering.infra.twitter.TwitterToken
@Service
class TweetGatherService(private val twitterAppSettings: TwitterAppSettings,
private val twitterToken: TwitterToken,
private val webClient: WebClient) {
fun streamFrom(query: String): Flux<Tweet> {
val url = "https://stream.twitter.com/1.1/statuses/filter.json"
return this.webClient.mutate().baseUrl(url).build()
.post()
.body(BodyInserters.fromFormData("track", query))
.header("Authorization", Twitter.buildAuthHeader(twitterAppSettings, twitterToken, "POST", url, query))
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve().bodyToFlux(Tweet::class.java)
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class Tweet(val id: String = "", val text: String = "", @JsonProperty("created_at") val createdAt: String = "", val user: TwitterUser = TwitterUser("", ""))
@JsonIgnoreProperties(ignoreUnknown = true)
data class TwitterUser(val id: String, val name: String)
这里有一些重要的要点。首先是函数声明;看看Flux<Tweet>,这意味着数据永远不会中断,因为它代表了 N 个值。在我们的情况下,我们将消费 Twitter 流,直到客户端或服务器中断数据流。
之后,我们配置了 HTTP 请求体,以获取我们想要的跟踪事件。之后,我们配置了 Accept HTTP 头;这是向 WebClient 指示它需要消费哪种 MIME 类型的关键。
最后,我们使用了 Twitter.buildAuthHeader 函数来配置 Twitter 认证。
太棒了,我们准备好开始消费 Twitter API,我们只需要编写触发器来使用该函数。我们将在下一节中这样做。
监听 RabbitMQ 队列并消费 Twitter API
我们将消费 Twitter API,但何时开始?
当跟踪标签的请求到达我们的应用程序时,我们需要开始获取推文。为了达到这个目标,当 TrackedHashTag 在我们的微服务上注册时,我们将实现 RabbitMQ 监听器。应用程序将向代理发送消息以开始消费 Twitter 流。
让我们看看代码,一步一步地理解其行为;最终的代码应该看起来像这样:
package springfive.twittergathering.domain.service
import org.springframework.amqp.rabbit.annotation.RabbitListener
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import springfive.twittergathering.domain.TrackedHashTag
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
@Service
class TwitterGatherRunner(private val twitterGatherService: TweetGatherService,private val rabbitTemplate: RabbitTemplate) {
@RabbitListener(queues = ["twitter-track-hashtag"])
fun receive(hashTag:TrackedHashTag) {
val streamFrom = this.twitterGatherService.streamFrom(hashTag.hashTag).filter({
return@filter it.id.isNotEmpty() && it.text.isNotEmpty() &&
it.createdAt.isNotEmpty()
})
val subscribe = streamFrom.subscribe({
println(it.text)
Mono.fromFuture(CompletableFuture.runAsync {
this.rabbitTemplate.convertAndSend("twitter-
exchange","track.${hashTag.queue}",it)
})
})
Schedulers.elastic().schedule({ subscribe.dispose() },10L,TimeUnit.SECONDS)
}
}
保持冷静。我们将涵盖整个代码。在 @RabbitListener 中,我们配置了我们想要消费的队列名称。Spring AMQP 模块将自动为我们配置监听器并开始消费所需的队列。正如我们所见,我们收到了 TrackedHashTag 对象;记住之前章节中的转换器。
第一条指令将开始消费 Twitter 流。该流返回一个 flux,其中可以有很多数据事件。在消费者之后,我们希望在流中过滤数据。我们想要 Tweet,其中 id、text 和 createdAt 都不为空。
然后,我们订阅这个流并开始接收流中的数据。此外,subscribes 函数返回一个可丢弃的对象,这将在下一步中很有帮助。我们创建了一个匿名函数,该函数将在控制台上打印 Tweet 并将 Tweet 发送到 RabbitMQ 队列,以便在另一个微服务中消费。
最后,我们使用调度器停止数据流并消费数据 10 秒。
在测试 Twitter 流之前,我们需要将跟踪标签服务更改为通过 RabbitMQ 发送消息。我们将在下一节中这样做。这些更改很小,我们会快速完成。
更改跟踪标签服务
为了运行整个解决方案,我们需要对跟踪标签服务项目做一些更改。这些更改很简单且基本;配置 RabbitMQ 连接并将服务更改为向代理发送消息。
让我们这样做。
添加 Spring Starter RabbitMQ 依赖
正如我们在 Twitter Gathering 项目中之前所做的那样,我们需要添加 spring-boot-starter-amqp 以提供一些自动配置。为此,我们需要将以下片段添加到我们的 pom.xml 文件中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
对了。现在,是时候配置 RabbitMQ 连接了。我们将在下一节中这样做。
配置 RabbitMQ 连接
我们将使用 application.yaml 来配置 RabbitMQ 连接。然后,我们需要在其中创建一些属性,Spring AMQP 模块将使用提供的配置来启动连接工厂。
配置它很简单。跟踪标签的最终yaml文件应该看起来像这样:
spring:
rabbitmq:
host: localhost
username: guest
password: guest
port: 5672
redis:
host: 127.0.0.1
port: 6379
server:
port: 9090
queue:
twitter: twitter-track-hashtag
exchange:
twitter: twitter-track-exchange
routing_key:
track: "*"
---
spring:
profiles: docker
rabbitmq:
host: rabbitmq
username: guest
password: guest
port: 5672
redis:
host: redis
port: 6379
server:
port: 9090
queue:
twitter: twitter-track-hashtag
exchange:
twitter: twitter-track-exchange
routing_key:
track: "*"
在这个 yaml 文件中有两个配置文件。看看 RabbitMQ 的不同主机。在默认配置文件中,我们能够连接到 localhost,因为我们已经在主机上公开了 RabbitMQ 端口。但在 Docker 配置文件中,我们无法连接到 localhost,我们需要连接到rabbitmq主机,这是 Twitter 网络的主机。
我们的 RabbitMQ 连接已准备好使用。让我们在下一节尝试它。让我们开始吧。
为 Twitter 标签服务创建交换、队列和绑定
让我们声明我们的 RabbitMQ 实体以用于跟踪标签。我们将使用@Configuration类来完成这项工作。
RabbitMQ 连接应该看起来像这样:
package springfive.twittertracked.infra.rabbitmq
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import org.springframework.amqp.core.Binding
import org.springframework.amqp.core.BindingBuilder
import org.springframework.amqp.core.Queue
import org.springframework.amqp.core.TopicExchange
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
open class RabbitMQConfiguration(@Value("\${queue.twitter}") private val queue:String,
@Value("\${exchange.twitter}") private val exchange:String,
@Value("\${routing_key.track}") private val routingKey:String){
@Bean
open fun queue():Queue{
return Queue(this.queue,false)
}
@Bean
open fun exchange():TopicExchange{
return TopicExchange(this.exchange)
}
@Bean
open fun binding(queue: Queue, exchange: TopicExchange): Binding {
return BindingBuilder.bind(queue).to(exchange).with(this.routingKey)
}
@Bean
open fun converter(): Jackson2JsonMessageConverter {
return Jackson2JsonMessageConverter(ObjectMapper().registerModule(KotlinModule()))
}
}
很简单。我们声明了一个交换、队列和绑定,就像我们之前做的那样。
向代理发送消息
这现在是最有趣的部分。当我们想要保存TrackedHashTag时,我们必须将这个全新的实体发送到 RabbitMQ。这个过程将发送消息,然后 Twitter 聚集微服务将在十秒后开始消费流。
我们需要稍微修改一下TrackedHashTagService;最终版本应该看起来像这样:
package springfive.twittertracked.domain.service
import org.springframework.amqp.rabbit.core.RabbitTemplate
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import springfive.twittertracked.domain.TrackedHashTag
import springfive.twittertracked.domain.repository.TrackedHashTagRepository
import java.util.concurrent.CompletableFuture
@Service
class TrackedHashTagService(private val repository: TrackedHashTagRepository,
private val rabbitTemplate: RabbitTemplate,
@Value("\${exchange.twitter}") private val exchange: String,
@Value("\${routing_key.track}") private val routingKey: String) {
fun save(hashTag: TrackedHashTag) {
this.repository.save(hashTag).subscribe { data ->
Mono.fromFuture(CompletableFuture.runAsync {
this.rabbitTemplate.convertAndSend(this.exchange, this.routingKey,
hashTag)
})
}
}
fun all() = this.repository.findAll()
}
干得好。当新的实体到来时,它将被发送到代理。我们已经完成了对跟踪标签服务的修改。
最后,我们能够测试整个流程。让我们开始玩耍并感知我们构建的应用程序的真正力量。
演示时间!!!
测试微服务的集成
现在,我们准备好测试整个解决方案。在你开始之前,我们需要检查以下基础设施项目:
-
Redis
-
RabbitMQ
如果项目正在运行,我们可以跳到下一节。
我们可以使用docker ps命令,该命令应该列出运行模式下的 Redis 和 RabbitMQ 容器。
运行跟踪标签服务
运行这个应用程序没有特别的事情要做。它包括在application.yaml的默认配置文件中配置的基础设施连接。
运行TrackedHashTagApplication上的主函数。我们可以使用 IDE 或命令行来完成这项工作。
检查控制台输出;输出将在 IDE 或命令行上显示。我们想要找到以下行:

这意味着第一个应用程序完全运行,我们可以运行 Twitter 聚集。请保持应用程序运行,因为它需要这样做。
让我们运行 Twitter 聚集!!!
运行 Twitter 聚集
这个应用程序运行起来稍微复杂一些。我们需要为它配置一些环境变量。这是必需的,因为我们不希望在我们的存储库中包含 Twitter 应用程序凭证。
在 IDE 中做这件事很简单。为此,我们可以配置运行配置。让我们来做这件事:
- 点击编辑配置...,如下图所示:

然后,我们能够看到像这样的环境变量:

-
我们需要点击 ...,如图中所示。
-
下一个屏幕将显示,我们可以配置环境变量:

-
我们需要配置以下环境变量:
-
consumer-key
-
consumer-secret
-
access-token
-
access-token-secret
-
这些值应该填写 Twitter 应用程序管理器的值。
然后,我们可以运行应用程序。运行它!!
现在,我们应该在控制台中看到以下行,这意味着应用程序正在运行:

太棒了,我们的两个微服务正在运行。让我们触发 Twitter 流。我们将在下一节中这样做。
运行应用程序还有其他方法,例如使用 Maven Spring Boot 目标或 Java 命令行。如果你更喜欢在 Java 命令行中运行,请记住使用 -D 参数来传递环境变量。
测试内容
我们非常期待测试完整的集成。我们可以使用 curl 工具向 Tracked Hashtag 服务发送请求数据。我们想跟踪来自 Twitter 的 "bitcoin"。
我们可以执行以下命令行:
curl -H "Content-Type: application/json" -X POST -d '{"hashTag":"bitcoin","queue":"bitcoin"}' \
http://localhost:9090/api/tracked-hash-tag
检查 HTTP 状态码;它应该是 HTTP 状态 200。之后,我们可以检查 Twitter Gathering 项目的控制台,并且应该有大量的推文被记录。
看看日志,日志中必须有像这样的推文:

太棒了!
伟大的工作,伙计们,我们已经将完整的应用程序与 RabbitMQ 和 Twitter 流集成在一起。
Spring Actuator
Spring Boot Actuator 是当应用程序在生产环境中运行时的一种辅助工具。该项目提供了已部署应用程序的内置信息。
在微服务世界中,监控应用程序的实例是获得成功的关键点。在这些环境中,通常有许多应用程序通过网络协议(如 HTTP)调用其他应用程序。网络是一个不稳定的 环境,有时它可能会失败;我们需要跟踪这些事件以确保应用程序处于运行状态并且完全可用。
Spring Boot Actuator 在这些情况下帮助开发者。该项目公开了一些包含应用程序信息的 HTTP API,例如内存使用情况、CPU 使用情况、应用程序健康检查以及应用程序的基础设施组件,例如与数据库和消息代理的连接等。
最重要的一点是,信息通过 HTTP 暴露。它有助于与外部监控应用程序(如 Nagios 和 Zabbix)的集成,例如。没有特定的协议用于暴露这些信息。
让我们将其添加到我们的项目中并尝试几个端点。
在我们的 pom.xml 中添加 Spring Boot Actuator
Spring Boot Actuator 在我们的 pom.xml 中配置起来非常简单。我们扩展了 Spring Boot 的父 pom,因此没有必要指定依赖项的版本。
让我们配置我们的新依赖项:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
太棒了,真的很容易。在我们测试之前,让我们更深入地了解一下。
Actuator 端点
项目有很多内置的端点,并且当应用程序启动时它们会启动。记住,我们已经使用了启动项目,这是自动为我们配置它的项目。
有几个端点用于不同的需求,我们将查看在生产微服务中最常用的端点。
-
/health:最知名的 actuator 端点;它显示了应用程序的健康状况,通常有一个status属性 -
/configprops:显示折叠的@ConfigurationProperties -
/env:公开 SpringConfigurableEnvironment的属性 -
/dump:显示线程转储 -
/info:我们可以在该端点放置一些任意信息 -
/metrics:运行应用程序的指标 -
/mappings:当前应用程序的@RequestMappings端点
另一个重要的端点可以通过 HTTP 接口显示应用程序日志。/logfile 端点可以帮助我们可视化日志文件。
Spring Boot Actuator 创建的端点列表可以在以下位置找到:docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html。
应用程序自定义信息
我们可以使用一个特定的端点来从我们的应用程序中公开自定义信息。这些信息将通过/info端点公开。
为了配置这一点,我们可以使用 application.yaml 文件并将所需信息按照以下模式放置,如下所示:
info:
project: "twitter-gathering"
kotlin: @kotlin.version@
所需的属性必须以 info. * 开头。然后,我们可以测试我们的第一个 actuator 端点并检查我们的 /info 资源。
让我们尝试访问http://localhost:8081/info。在application.yaml中填写的信息应该会显示出来,如下所示:

如我们所见,属性是从 HTTP 端点公开的。我们可以使用它来放置应用程序版本,例如。
测试端点
在 Spring Boot 的第 2 版中,Spring Actuator 管理端点默认是禁用的,因为这些端点可能包含运行中应用程序的敏感数据。然后,我们需要配置以正确启用这些端点。
有一个需要注意的特殊点。如果应用程序公开暴露,你应该保护这些端点。
让我们启用我们的管理端点:
management:
endpoints:
web:
expose: "*"
在前面的配置中,我们启用了所有管理端点,然后我们可以开始测试一些端点。
让我们测试一些端点。首先,我们将测试指标端点。此端点显示了运行应用程序可用的指标。访问 http://localhost:8081/actuator/metrics 并查看结果:

我们使用端口 8081 是因为我们已在 application.yaml 中配置了属性 server.port。端口可以根据你的需求进行更改。
已经为我们自动配置了许多指标。该端点仅公开可用的指标。要检查指标值,我们需要使用另一个端点。让我们检查 http.server.request 的值。
检查值的基端点是:http://localhost:8081/actuator/metrics/{metricName}。然后,我们需要访问:http://localhost:8081/actuator/metrics/http.server.requests。结果应该是:

正如你所见,服务器接收了八次调用。尝试再点击几次以查看指标的变化。
干得好。我们的微服务已准备好投入生产。我们有 Docker 镜像和用于监控服务的端点。
摘要
在本章中,我们学习了并将许多 Spring 高级概念付诸实践,例如 RabbitMQ 集成。
我们创建了一个完全响应式的 WebClient 并利用了响应式范式;它实现了资源计算优化并提高了应用程序的性能。
此外,我们通过 RabbitMQ 代理集成了两个微服务。这是集成应用程序的一个优秀解决方案,因为它解耦了应用程序,并且允许你非常容易地水平扩展应用程序。消息驱动是构建响应式应用程序的必要特性之一;它可以在《响应式宣言》中找到(www.reactivemanifesto.org/en)。
在下一章中,我们将改进我们的解决方案并创建一个新的微服务来向我们的客户流式传输过滤后的推文。我们还将再次使用 RabbitMQ。
第六章:玩转服务器端事件(Server-Sent Events)
在第四章,Kotlin 基础和 Spring Data Redis 以及第五章,反应式 Web 客户端中,我们创建了两个微服务。第一个负责在 Redis 上跟踪数据并触发第二个微服务,该微服务将消费 Twitter 流。这个过程是异步发生的。
在本章中,我们将创建另一个微服务,该服务将消费 Twitter Gathering 产生的数据,并通过 REST API 公开。将能够通过文本内容过滤推文。
我们已经使用服务器端事件(Server-Sent Events)(SSE)消费了 Twitter 流;我们创建了一个反应式 REST 客户端来消费它。现在,是时候创建我们的 SSE 实现了。我们将消费 RabbitMQ 队列并将数据推送到我们的连接客户端。
我们将探讨 SSE 并了解为什么这种解决方案非常适合我们的一些微服务。
在本章结束时,我们将对在 Spring 生态系统中使用 SSE 有信心。
在本章中,我们将学习以下内容:
-
使用 Spring 框架实现 SSE 端点
-
使用 Reactor Rabbit 客户端消费 RabbitMQ
创建 Tweet 分发器项目
现在,我们将创建我们的最后一个微服务。它将推送 Twitter Gathering 过滤后的推文给我们的连接客户端,在这种情况下,消费者。
在本章中,我们将使用 Spring Initializr 页面来帮助我们创建我们漂亮的新项目。让我们创建。
再次使用 Spring Initializr
如您所见,Spring Initializr 页面是创建 Spring 项目的合作伙伴。让我们再次使用它并创建一个项目:
前往start.spring.io并使用以下截图填写数据:

我们选择了反应式 Web 依赖项;我们还将继续使用 Kotlin 作为编程语言。最后,点击“生成项目”按钮。很好,对我们来说足够了。
有一些缺失的依赖项在 Spring Initializr 中没有显示。我们需要手动设置这些依赖项。我们将在下一节中完成这项任务。让我们去吧。
额外的依赖项
我们需要使用 Jackson Kotlin 模块作为依赖项来正确处理我们新微服务中的 JSON。此外,我们还将使用 Reactor RabbitMQ 依赖项,它允许我们在反应式范式与 RabbitMQ 代理交互。
要添加这些依赖项,我们需要将以下片段添加到 pom.xml:
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor.rabbitmq</groupId>
<artifactId>reactor-rabbitmq</artifactId>
<version>1.0.0.M1</version>
</dependency>
太棒了。我们的依赖项已配置。我们的项目准备就绪。
在开始之前,我们需要深入理解 SSE 的概念。我们将在下一节中学习。
服务器端事件
服务器端事件(Server-Sent Events)是从服务器向客户端发送数据流的标准方式。在下一节中,我们将学习如何使用 Spring 框架实现它。
此外,我们还将了解 SSE 和 WebSockets 之间的主要区别。
关于 HTTP 协议的一些话
HTTP 是 OSI 模型中的应用层协议。应用层是 OSI 模型中最后表示的一层。这意味着这一层更接近用户界面。这一层的主要目的是发送和接收用户输入的数据。通常,这通过用户界面,也称为应用程序,如文件传输和发送电子邮件来实现。
应用层有几种协议,例如域名系统(DNS),它将域名转换为 IP 地址,或者 SMTP,其主要目的是将电子邮件发送到邮件管理应用程序。
应用层直接与电子邮件客户端等软件交互,例如;与硬件部分没有交互。它是 OSI 模型的最后一层,也是离最终用户最近的一层。
所有这些层都处理软件,这意味着没有关于 OSI 模型中代表的物理部分的担忧。
可以在以下链接找到 OSI 模型的更详细解释:support.microsoft.com/en-us/help/103884/the-osi-model-s-seven-layers-defined-and-functions-explained。
以下是一个 OSI 模型的表示:

HTTP 协议使用 TCP 协议作为传输通道。然后,它将建立连接并开始在通道上传输数据。
TCP 协议是一种流协议和全双工通道。这意味着服务器和客户端可以通过连接发送数据。
HTTP 和持久连接
HTTP 协议是一种请求-响应模型,其中客户端提交消息(HTTP 请求)并处理此消息,然后将响应(HTTP 响应)发送给客户端。在发送响应后,连接将被关闭。
看看以下图表:

理解起来相当简单。客户端将发送请求,在这种情况下,连接将被打开。之后,服务器将接收请求以处理某些内容,并将答案发送给客户端。在整个过程完成后,连接将被关闭。如果客户端需要发送新的请求,则应再次打开连接,并且流程按照相同的顺序发生。
这里有一个明显的缺点,客户端需要为每个请求打开新的连接。从服务器的角度来看,服务器需要同时处理大量的新连接。这消耗了大量的 CPU 和内存。
在 HTTP 的 1.0 版本中,连接不是持久的。为了启用它,请求中应包含keep-alive头。头应如下所示:
Connection: keep-alive
这是唯一一种在 1.0 版本上使 HTTP 连接持久化的方法,如前所述;当这种情况发生时,服务器不会断开连接,客户端可以重用已打开的连接。
在 HTTP 1.1 中,连接默认是持久的;在这种情况下,与第一个版本相反,连接保持打开,客户端可以正常使用它。
这里有一个感知到的改进,它可以带来一些优势。服务器需要管理的连接更少,这减少了大量的 CPU 时间。HTTP 请求和响应可以在同一个连接中流水线化。
正如我们所知,天下没有免费的午餐。这也存在一些缺点;服务器需要保持连接打开,服务器将为客户端保留所需的连接。这可能在某些场景中导致服务器不可用。
持久连接对于在服务器和客户端之间维护流非常有用。
WebSocket
在 HTTP 协议中,通信支持全双工,这意味着客户端和服务器可以通过该通道发送数据。支持这种通信的标准方式是 WebSocket。在本规范中,客户端和服务器可以在持久连接中相互发送数据。看看下面的图示:

如我们所见,数据可以通过两个参与者,客户端和服务器发送和接收——这就是 WebSocket 的工作方式。
在我们的案例中,我们不需要在连接期间向服务器发送任何数据。因为这个特性,我们将选择 SSE。我们将在下一节中了解它们。
服务器发送事件
与 WebSocket 实现的全双工通信相反,SSE 使用半双工通信。
客户端向服务器发送请求,当需要时,服务器将数据推送到客户端。记住这里的主动参与者是服务器;数据只能由服务器发送。这是一个半双工行为。看看下面的图示:

小菜一碟。这是 SSE 技术的基础。SSE 是自解释的。我们将与 Spring 框架一起使用它。然而,在我们这样做之前,让我们看看一个 Reactor RabbitMQ 项目。
Reactor RabbitMQ
我们的解决方案是完全反应式的,因此我们需要使用 Reactor RabbitMQ,它允许我们使用反应式范式与 RabbitMQ 代理交互。
在这个新的微服务上,我们不需要通过消息代理发送消息。我们的解决方案将监听 RabbitMQ 队列,并将接收到的推文推送给已连接的客户端。
理解 Reactor RabbitMQ
Reactor RabbitMQ 尝试提供一个反应式库来与 RabbitMQ 代理交互。它使开发者能够创建基于反应式流的非阻塞应用程序,使用 RabbitMQ 作为消息代理解决方案。
正如我们之前所学的,这种解决方案通常不会占用很多内存。该项目基于 RabbitMQ Java 客户端,并且与阻塞解决方案具有相似的功能。
我们没有使用spring-amqp-starter,所以魔法不会发生。我们需要为 Spring 上下文编写 bean 声明,我们将在下一节中完成这项工作。
配置 RabbitMQ Reactor beans
在本节中,我们将配置 Spring 上下文中的 RabbitMQ 基础设施类。我们将使用一个@Configuration类来声明它。
配置类应该看起来像以下这样:
package springfive.twitterdispatcher.infra.rabbitmq
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.rabbitmq.client.ConnectionFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import reactor.rabbitmq.ReactorRabbitMq
import reactor.rabbitmq.Receiver
import reactor.rabbitmq.ReceiverOptions
@Configuration
class RabbitMQConfiguration(private @Value("\${spring.rabbitmq.host}") val host:String,
private @Value("\${spring.rabbitmq.port}") val port:Int,
private @Value("\${spring.rabbitmq.username}") val username:String,
private @Value("\${spring.rabbitmq.password}") val password:String){
@Bean
fun mapper(): ObjectMapper = ObjectMapper().registerModule(KotlinModule())
@Bean
fun connectionFactory():ConnectionFactory{
val connectionFactory = ConnectionFactory()
connectionFactory.username = this.username
connectionFactory.password = this.password
connectionFactory.host = this.host
connectionFactory.port = this.port
connectionFactory.useNio()
return connectionFactory
}
@Bean
fun receiver(connectionFactory: ConnectionFactory):Receiver{
val options = ReceiverOptions()
options.connectionFactory(connectionFactory)
return ReactorRabbitMq.createReceiver(options)
}
}
这里有两个重要的事情。第一个是我们为 Kotlin 配置了 Jackson 支持。它允许我们将ObjectMapper注入到我们的 Spring beans 中。下一个重要的事情与 RabbitMQ 连接的配置有关。
我们为 Spring 上下文声明了一个ConnectionFactory bean。我们使用@Value注解注入配置,并在构造函数中接收这些值。在 Kotlin 语言中,我们可以直接在属性中设置值;看看ConnectionFactory属性分配。
在配置ConnectionFactory之后,我们能够声明一个接收器,这是一个用于消费队列的Reactive抽象,使用响应式编程。我们接收之前创建的ConnectionFactory并将其设置为ReceiverOptions。
这就是 Reactor RabbitMQ 配置的全部内容。
响应式地消费 RabbitMQ 队列
现在,我们将消费 RabbitMQ 队列。实现与我们在阻塞实现中看到的方法非常相似,函数的名称也相似。
我们在之前的章节中消费了一些 RabbitMQ 消息,但这个解决方案相当不同。现在,我们将使用响应式 RabbitMQ 实现。主要思想是消费事件流;这些事件代表到达代理的消息。这些消息到达,Reactor RabbitMQ 将这些消息转换为Flux,以便我们能够在响应式范式下消费。
在响应式范式下,事件流(我们可以将队列中的消息视为事件)的表示是Flux。
然后,我们监听 RabbitMQ 的功能应该返回Flux,这是事件的无限表示。接收器实现返回消息的Flux,这对我们来说足够了,并且很好地符合我们的需求。
我们的实现应该看起来像以下这样:
package springfive.twitterdispatcher.domain.service
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.rabbitmq.Receiver
@Service
class TwitterDispatcher(private @Value("\${queue.twitter}") val queue: String,
private val receiver: Receiver,
private val mapper: ObjectMapper) {
fun dispatch(): Flux<Tweet> {
return this.receiver.consumeAutoAck(this.queue).flatMap { message ->
Mono.just(mapper.readValue<Tweet>(String(message.body)))
}
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class Tweet(val id: String = "",
val text: String = "", @JsonProperty("created_at")
val createdAt: String = "", val user: TwitterUser = TwitterUser("", ""))
@JsonIgnoreProperties(ignoreUnknown = true)
data class TwitterUser(val id: String, val name: String)
让我们更深入地了解一下。我们在构造函数中接收了Receiver作为注入。当有人调用dispatch()函数时,Receiver将开始消费队列,这个队列也作为构造函数中的注入。
Receiver 生成 Flux<Delivery>。现在,我们需要将代表消息抽象的 Flux<Delivery> 实例转换为我们的领域模型 Tweet。flatMap() 函数可以为我们完成这个任务,但首先,我们将 message.body 转换为字符串,然后我们使用了 Jackson 来读取 JSON 并将其转换为我们的 Tweet 领域模型。
看看代码的易读性;API 流畅且易于阅读。
消费者将在连接的客户端断开连接之前不会终止。我们很快就能看到这种行为。
过滤流
我们正在从 RabbitMQ 接收消息。现在,我们需要将消息返回给连接的客户。
为此,我们将使用 Spring WebFlux 的 SSE。这个解决方案非常适合我们,因为我们将生成 Flux<Tweet> 并开始向客户端推送推文。客户端将发送查询以过滤所需的推文。
应用程序将完全响应式。让我们看看我们的代码:
package springfive.twitterdispatcher.domain.controller
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Flux
import springfive.twitterdispatcher.domain.service.Tweet
import springfive.twitterdispatcher.domain.service.TwitterDispatcher
@RestController
@RequestMapping("/tweets")
class TweetResource(private val dispatcher: TwitterDispatcher) {
@GetMapping(produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun tweets(@RequestParam("q")query:String):Flux<Tweet>{
return dispatcher.dispatch()
.filter({ tweet: Tweet? -> tweet!!.text.contains(query,ignoreCase = true) })
}
}
非常容易理解。我们声明了 tweets() 函数;此函数映射到 GET HTTP 请求并生成 MediaType.TEXT_EVENT_STREAM_VALUE。当客户端连接到端点时,服务器将根据所需的参数开始发送推文。
当客户端断开连接时,Reactor RabbitMQ 将关闭请求的 RabbitMQ 连接。
整个解决方案的 Docker 化
现在,是时候封装整个解决方案并为所有项目创建 Docker 镜像了。在任意位置运行项目非常有用。
我们将逐步配置所有项目,然后在 Docker 容器中运行解决方案。作为一个挑战,我们可以使用 docker-compose 在单个 yaml 文件中编排整个解决方案。
对于跟踪标签服务,我们已经创建了 Docker 镜像。然后,我们将开始配置 Tweet 收集,最后一个是 Tweet 分发器。让我们立即进行配置。
你可以在以下位置找到更多 docker-compose 项目详情:docs.docker.com/compose/。此外,在新版本中,docker-compose 支持 Docker Swarm 在集群节点之间编排堆栈。在生产环境中部署 Docker 容器可能非常有用。
Tweet 收集
让我们为 Tweet 收集项目配置 pom.xml。
构建节点应该如下所示:
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.21.0</version>
<configuration>
<images>
<image>
<name>springfivebyexample/${project.build.finalName}</name>
<build>
<from>openjdk:latest</from>
<entryPoint>java -Dspring.profiles.active=container -jar
/application/${project.build.finalName}.jar</entryPoint>
<assembly>
<basedir>/application</basedir>
<descriptorRef>artifact</descriptorRef>
<inline>
<id>assembly</id>
<files>
<file>
<source>target/${project.build.finalName}.jar</source>
</file>
</files>
</inline>
</assembly>
<tags>
<tag>latest</tag>
</tags>
<ports>
<port>8081</port>
</ports>
</build>
<run>
<namingStrategy>alias</namingStrategy>
</run>
<alias>${project.build.finalName}</alias>
</image>
</images>
</configuration>
</plugin>
看看端口配置;它应该与我们配置的 application.yaml 中的相同。配置已完成,因此让我们创建我们的 Docker 镜像:
mvn clean install docker:build
命令输出应该如下所示截图:

最近创建了一个标记为最新的镜像;镜像已准备好运行。让我们为我们的 Tweet 分发器项目做同样的事情。
Tweet 分发器
我们的新插件条目应该如下所示:
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.21.0</version>
<configuration>
<images>
<image>
<name>springfivebyexample/${project.build.finalName}</name>
<build>
<from>openjdk:latest</from>
<entryPoint>java -Dspring.profiles.active=container -jar
/application/${project.build.finalName}.jar</entryPoint>
<assembly>
<basedir>/application</basedir>
<descriptorRef>artifact</descriptorRef>
<inline>
<id>assembly</id>
<files>
<file>
<source>target/${project.build.finalName}.jar</source>
</file>
</files>
</inline>
</assembly>
<tags>
<tag>latest</tag>
</tags>
<ports>
<port>9099</port> </ports>
</build>
<run>
<namingStrategy>alias</namingStrategy>
</run>
<alias>${project.build.finalName}</alias>
</image>
</images>
</configuration>
</plugin>
再次看看端口配置。它将被 Docker 用于暴露正确的端口。现在,我们可以运行镜像创建命令:
mvn clean install docker:build
然后,我们可以看到命令的输出,如下面的截图所示:

太棒了,所有镜像都已准备就绪。让我们运行它。
我们需要为所有项目创建 Docker 镜像。过程是相同的;配置 maven Docker 插件,然后在项目上使用mvn clean install docker:build。完整的源代码可以在 GitHub 上找到。跟踪哈希标签服务可以在github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter04找到,Tweet Gathering 可以在github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter05找到,最后,Tweet Dispatcher 可以在github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter06找到。
运行容器化解决方案
我们已经准备好在 Docker 容器中运行解决方案。我们一直使用 IDE 或命令行运行解决方案,但现在我们将启动一些容器并测试解决方案以及 Spring 配置文件。
在此之前,让我们快速回顾一下解决方案:

-
第一次操作,跟踪哈希标签服务,将哈希标签持久化到Redis数据库中。
-
之后,跟踪哈希标签服务将新跟踪的哈希标签发送到RabbitMQ代理的队列中。
-
Tweet Gathering 监听队列以跟踪推文并触发事件,并首先监听Twitter 流。
-
Tweet Gathering 开始从Twitter 流中获取推文。
-
Tweet Gathering 将推文发布到RabbitMQ 代理的队列中。
-
Tweet Dispatcher 消费消息。
-
Tweet Dispatcher 使用 SSE 将消息发送到客户端。
现在我们已经理解了解决方案,让我们开始容器化。
运行跟踪哈希标签服务容器
上一节已创建镜像,因此现在我们可以启动容器。启动容器的命令应该如下所示:
docker run -d --name tracked --net twitter -p 9090:9090 springfivebyexample/tracked_hashtag
让我们解释一下指令。-d告诉 Docker 引擎以后台模式或分离模式运行容器。另一个重要参数是--net,它将容器连接到所需的网络。
我们可以使用以下命令在运行时跟踪容器日志:
docker logs tracked -f
这个命令类似于 Linux 上的tail -f命令,它查看日志流的最后部分。我们可以移除标志-f以查看日志的最后几行。
docker logs 的输出应该如下所示:

查看日志中选择的配置文件:
INFO 7 --- [ main] s.t.TrackedHashTagApplication$Companion : The following profiles are active: docker
记住,我们在pom.xml文件中从跟踪哈希标签服务进行了参数化。让我们看一下以下片段:
<entryPoint>java -Dspring.profiles.active=docker -jar /application/${project.build.finalName}.jar</entryPoint>
干得漂亮。我们的第一个服务正在正常运行。让我们运行 Tweet Gathering;这里有一些有趣的配置。
我们在第四章创建了 Twitter 网络,Kotlin 基础和 Spring Data Redis,我们需要使用这个网络来使容器能够通过容器名称在我们的自定义网络中相互看到。
运行 Tweet Gathering 容器
要运行Tweet Gathering应用程序略有不同。这个容器需要环境变量,这些变量用于与 Twitter API 交互。我们可以在docker run命令中使用-e参数。让我们这么做:
docker run -d --name gathering --net twitter -e CONSUMER_KEY=gupfxwn43NBTdxCD3Tsf1JgMu \
-e CONSUMER_SECRET=pH4uM5LlYxKzfJ7huYRwFbaFXn7ooK01LmqCP69QV9a9kZrHw5 \
-e ACCESS_TOKEN=940015005860290560-m0WwSyxGvp5ufff9KW2zm5LGXLaFLov \
-e ACCESS_TOKEN_SECRET=KSofGB8aIwDmewceKXLbN8d5chvZkZyB31VZa09pNBhLo \
-p 8081:8081 springfivebyexample/tweet_gathering
查看我们在application.yaml文件中配置的环境变量。Docker 运行命令会将这些变量注入到系统中,然后我们可以在 Java 应用程序中使用它们。
让我们检查我们的容器日志。我们可以使用以下命令:

太棒了,我们的应用程序正在运行。如您所见,应用程序已连接到 RabbitMQ 代理。
RabbitMQ和Redis应该运行,以便您能够运行 Tweet Gathering。我们可以使用docker ps命令来检查它;它将列出正在运行的容器,RabbitMQ 和 Redis 需要在这个列表上。
现在,我们可以运行 Dispatcher 应用程序来完成整个解决方案。让我们这么做。
运行 Tweet Dispatcher 容器
运行 Tweet Dispatcher 容器没有秘密。我们可以使用以下命令来运行它:
docker run -d --name dispatcher --net twitter -p 9099:9099 springfivebyexample/tweet_dispatcher
它将启动容器,在运行过程中命名容器是个好主意。这可以帮助我们使用命令行工具(如docker container ls或docker ps)管理容器,因为它会在最后一列显示容器名称。然后,让我们检查我们的容器是否正在运行,所以输入以下命令:
docker container ls
或者,您也可以运行以下命令:
docker ps
我们应该能够看到 Gathering 容器正在运行,如下所示:

有五个容器,三个应用程序,以及两个基础设施服务,RabbitMQ和Redis。
在任何时候,我们都可以使用以下命令停止所需的容器:
docker stop gathering
docker stop只会停止容器;信息将保留在容器卷中。我们也可以使用容器名称或容器 ID,我们之前已经命名了。这对我们来说很容易。如果我们使用docker ps命令,最近停止的镜像永远不会出现在列表中。要显示所有容器,我们可以使用docker ps -a或docker container ls -a。
现在,我们将再次启动容器;命令是自解释的:
docker start gathering
容器再次运行。我们已经在 Docker 上练习得更多了。
伙计们,干得漂亮。整个应用程序已经容器化了。做得好。
我们可以使用 Linux 指令并执行一些批处理指令。例如,我们可以使用docker stop $(docker ps -q)——它将停止所有正在运行的容器。docker ps -q命令只会带来容器的 ID。
docker-compose 工具
在微服务架构风格中,整个解决方案被解耦成小型且定义良好的服务。通常,当我们采用这些风格时,我们会有多个工件需要部署。
让我们分析我们的解决方案;我们有三个组件需要部署。我们使用了 Docker 容器,并且使用docker run命令运行了这些容器。一个接一个地,我们使用了三次docker run。这相当复杂,在开发常规中很难做到。
docker-compose可以帮助我们在这种场景下。这是一个工具,可以帮助我们在像我们这样的复杂场景中编排 Docker 容器。
让我们假设我们的应用程序正在快速发展,我们需要构建四个更多的微服务来实现预期的业务案例,这将涉及到四个更多的docker run命令,并且可能很难维护,尤其是在开发生命周期中。有时,我们需要将工件提升到测试环境,我们可能需要修改我们的命令行来实现这一点。
docker-compose使我们能够通过单个yaml文件部署多个容器。这个yaml文件有一个定义的结构,允许我们在同一个文件中定义和配置多个容器。此外,我们可以通过单个命令运行此yaml文件中配置的解决方案,这使得开发生活变得简单。
工具可以在本地机器上工作,或者我们可以将其与 Docker Swarm 工具集成,该工具可以管理 Docker 主机集群。
Docker Swarm 是管理 docker 集群的本地工具。它使得在 Docker 集群上部署容器变得容易。在新版本中,docker-compose完全集成了 Docker Swarm。我们可以在docker-compose.yaml的 Docker Swarm 属性中定义它。Docker Swarm 文档可以在以下位置找到:docs.docker.com/engine/swarm/。
docker-compose的yaml有一个定义的结构要遵循;文档可以在以下位置找到:docs.docker.com/compose/compose-file/#compose-and-docker-compatibility-matrix.我们将创建一个简单的文件来理解docker-compose的行为。让我们创建我们的简单yaml——这个yaml应该看起来像这样:
version: '3'
services:
rabbitmq:
image: rabbitmq:3.7.0-management-alpine
ports:
- "5672:5672"
- "15672:15672"
redis:
image: "redis:alpine"
ports:
- "6379:6379"
前面代码中的yaml将创建以下图中详细的结构:

它简化了开发时间。现在,我们将学习如何安装docker-compose。
安装 docker-compose
docker-compose的安装相当简单且文档齐全。我们使用 Linux,所以我们将使用 Linux 说明。
打开终端并使用以下命令:
sudo curl -L https://github.com/docker/compose/releases/download/1.18.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
等待下载完成后,我们就可以执行以下指令为程序赋予可执行权限。让我们通过执行以下命令来完成:
sudo chmod +x /usr/local/bin/docker-compose
如您所知,您可能需要输入管理员密码。我们的 docker-compose 现在已经安装。让我们检查一下:
docker-compose --version
提示将显示安装的版本,如下所示:

docker-compose 已经启动并运行,所以让我们跳到下一部分,开始创建我们的 yaml 文件,并使用一条命令部署整个栈。
对于不同的操作系统,指令可以在以下位置找到:docs.docker.com/compose/install/#install-compose。然后,您可以浏览指令并点击所需的操作系统。
创建 docker-compose 文件
现在,我们已经安装了 docker-compose,我们可以尝试使用这个工具。我们希望用一条命令运行整个栈。我们将创建 yaml 文件来表示栈。我们的 yaml 文件应该包含 Redis 容器、RabbitMQ 容器、Tracked Hashtag 应用程序、Gathering 应用程序,最后是 Dispatcher 应用程序。
我们可以在任何想要的地方创建 docker-compose.yaml 文件,对此没有限制。
我们的 docker-compose.yaml 文件应该看起来像以下这样:
version: '3'
services:
rabbitmq:
image: rabbitmq:3.7.0-management-alpine
hostname: rabbitmq
ports:
- "5672:5672"
- "15672:15672"
networks:
- solution
redis:
image: "redis:4.0.6-alpine"
hostname: redis
ports:
- "6379:6379"
networks:
- solution
tracked:
image: springfivebyexample/tracked_hashtag
ports:
- "9090:9090"
networks:
- solution
gathering:
image: springfivebyexample/tweet_gathering
ports:
- "8081:8081"
networks:
- solution
environment:
- CONSUMER_KEY=gupfxwn43NBTdxCD3Tsf1JgMu
- CONSUMER_SECRET=pH4uM5LlYxKzfJ7huYRwFbaFXn7ooK01LmqCP69QV9a9kZrHw5
- ACCESS_TOKEN=940015005860290560-m0WwSyxGvp5ufff9KW2zm5LGXLaFLov
- ACCESS_TOKEN_SECRET=KSofGB8aIwDmewceKXLbN8d5chvZkZyB31VZa09pNBhLo
dispatcher:
image: springfivebyexample/tweet_dispatcher
ports:
- "9099:9099"
networks:
- solution
networks:
solution:
driver: bridge
如您所见,我们在 yaml 中定义了整个栈。需要注意的是,我们可以发现与 docker run 命令的一些相似之处,实际上,它将使用 Docker 引擎来运行。yaml 中的 environment 节点与 Docker run 命令中的 -e 参数具有相同的行为。
我们已经定义了应用程序端口、Docker 镜像,并且已经将容器连接到相同的网络。这非常重要,因为当我们使用网络上的 docker-compose 文件名时,它可以找到具有某种 DNS 行为的容器名称。
例如,在定义的网络 solution 中,容器可以通过名称 redis 找到 Redis 容器实例。
运行解决方案
docker-compose 简化了运行整个栈的过程。我们的 yaml 文件已正确配置和定义。
让我们开始解决方案。运行以下命令:
docker-compose up -d
命令相当简单,-d 参数指示 Docker 在后台运行命令。正如我们在 Docker run 命令中所做的那样。
此命令的输出应该如下所示:

看一下,docker-compose 为我们的栈创建了一个网络。在我们的例子中,网络驱动器是桥接,网络创建后,容器启动。
测试网络
让我们测试一下,找到 Gathering 容器——docker-compose 中的容器名称以启动 docker-compose 的文件夹名称为前缀。
例如,我在 compose 文件夹中启动了我的docker-compose堆栈。由于文件夹名称,我的容器名称将是compose_gathering_1。
然后,我们将连接 Gathering 容器。可以使用以下命令实现:
docker exec -it compose_gathering_1 /bin/bash
docker exec命令允许我们在容器内执行某些操作。在我们的情况下,我们将执行/bin/bash程序。
命令结构如下:
docker exec -it <container name or container id> <program or instruction>
太棒了,请注意命令行。它应该更改,因为我们现在处于容器命令行:

我们没有以 root 身份连接到我们的主机,但现在我们是在容器中的 root。这个容器与 Redis 容器实例位于同一网络中,称为redis。
让我们用ping命令测试一下;我们应该能够通过名称redis找到redis容器,让我们试试。输入以下内容:
ping redis
命令输出应该是以下内容:

太棒了,我们的容器可以通过名称找到 Redis 容器。yaml文件完全正常工作。
摘要
在本章中,我们完成了第二个解决方案。我们介绍了 RabbitMQ Reactor 库,它使我们能够使用响应式范式连接到 RabbitMQ。
我们已经将整个解决方案打包在 Docker 容器中,并将其连接到相同的网络,以便应用程序之间能够相互通信。
我们还学习了从服务器通过 HTTP 持久连接向客户端推送数据的重要模式,并且我们还学习了 WebSockets 和服务器发送事件之间的区别。
最后,我们学习了docker-compose如何帮助我们通过几个命令创建堆栈并运行整个解决方案。
在接下来的章节中,我们将构建一个完全的微服务解决方案,使用一些重要的模式,如服务发现、API 网关、断路器等。
第七章:航空票务系统
我们最后的几个项目——Twitter 消费者、Twitter 收集器和 Twitter 分发器——非常出色。我们学习了几个令人兴奋的功能,它们都是使用 Spring 5.0 中的新功能实现的。所有这些都是在响应式流中使用 Kotlin 作为编程语言实现的;它们是 Spring 5.0 中最热门的功能;这是一个令人印象深刻的进步。
然而,在这些项目中明显缺少一些部分;我们考虑到了微服务需求。没有像服务发现、分布式配置、API 网关、分布式跟踪和监控这样的基础设施服务。这些服务在微服务架构等分布式系统中是必需的。
有几个原因。首先,我们可以考虑配置管理。让我们想象以下场景——在开发周期中,我们有三个环境:开发(DEV)、测试(TST)和生产(PROD)。这是公司在标准中找到的相当简单的标准。此外,我们将应用程序解耦为 4 个微服务,然后使用最少的基础设施,我们有 12 个服务实例;记住,这是一个好场景,因为在实际情况中,我们可能会有几个微服务应用程序的实例。
在早期场景中,我们将为每个微服务维护至少三个配置文件,记住我们需要为三个环境保留配置。然后,我们将有 12 个版本的设置。维护配置是一项艰巨的任务,要保持文件同步和更新。这些文件可能包含敏感信息,如数据库密码和消息代理的配置,而且不建议将这些文件放在主机机器上。
在这种情况下,分布式配置可以轻松解决我们的问题。在本章中,我们将学习配置服务器以及其他基础设施服务。
让我们总结一下本章我们将学习的内容:
-
如何创建配置服务器
-
使用 Eureka 实现服务发现
-
使用 Spring Cloud Zipkin 监控应用程序
-
使用 Spring Cloud Gateway 暴露应用程序
航空票务系统
在这些最后几章中,我们将致力于航空票务系统。解决方案相当复杂,涉及大量的 HTTP 集成和基于消息的解决方案。我们将探索从本书旅程中学到的内容。
我们将使用 Spring Messaging、Spring WebFlux 和 Spring Data 组件来创建解决方案。应用程序将拆分为几个微服务,以确保系统的可伸缩性、弹性和容错性。
此外,我们还将有一些基础设施服务来帮助我们交付一个高效的系统。将引入一些新的模式,例如断路器和 OAuth。在基础设施层,我们将使用与 Spring 框架生态系统集成的 Netflix OSS 组件。
我们应用程序的主要目的是销售机票,但为了完成这个任务,我们需要构建一个完整的生态系统。我们将构建一个微服务来管理座位和飞机的特性。还将有一个微服务来管理可用的公司航班;基本想法是管理航班日期和路线。当然,我们还将有一个微服务来管理乘客、票价、预订和支付。最后,我们将有一个电子商务API,用户可以通过它购买机票。
航空公司功能
我们将创建一些微服务来构建解决方案,然后我们将解决方案分解成小块,即微服务。为此,我们将使用边界上下文模式,这是领域驱动设计(DDD)的一个基本组成部分。
让我们看一下下面的图表,以便了解我们将要构建的内容:

这是本章将要做什么的总结;我们已经为每个微服务定义了基本功能。
现在,我们将查看组件;让我们进入下一节。
解决方案图表
下面的图表展示了整个解决方案,我们将在接下来的章节中实现它:

如我们所见,有不同的组件。一些组件将通过网关向最终用户(在我们的案例中,是我们的客户)公开。还有一个类别,公司用户将使用它来注册航班,例如,这些微服务也将通过网关公开。
基础设施类别不会通过互联网公开,除了网关服务。这些服务帮助解决方案的基础设施,不应该公开,因为其中包含敏感数据。
有很多事情要做;让我们开始吧。
DDD 使我们能够轻松地处理微服务。一些 DDD 模式非常适合微服务架构风格。Packt 目录中有很多有趣的书籍。
Spring Cloud Config Server
当我们采用微服务架构风格时,有一些挑战需要解决。首先要解决的问题之一是如何在集群中管理微服务配置,以及如何使它们易于分布式?
Spring Cloud Config 提供了一个基于注解和 Spring bean 的 Spring 方式。这是一个在可生产模块中轻松解决这个问题的好方法。此模块有三个主要组件,即配置存储库,也就是版本控制系统,配置服务器,它将提供配置,最后是配置客户端,它将从配置服务器消费配置。
此模块通过 HTTP 接口提供配置文件。这是本项目提供的主要功能,它作为我们架构中配置的中心存储库。
我们希望从我们的类路径中移除application.yaml文件;我们不再需要在类路径中此文件,因此我们将使用配置服务器为我们应用程序提供此文件。
现在,我们的微服务将没有配置文件,即application.yaml。在应用程序引导过程中,应用程序将查看配置服务器以获取正确的配置,然后应用程序将完成引导以将它们启动并运行。
以下图表解释了配置服务器和配置客户端:

如我们所见,这里的基思想是尝试通过配置服务器分发配置。使用这种方法有一些优点。第一个优点是将配置保存在中央仓库中。这使得配置易于维护。第二个优点是配置通过标准协议(如 HTTP)提供服务。大多数开发者都知道这个协议,这使得交互易于理解。最后,也是最重要的,当属性更改时,它可以在其他微服务中立即反映。
是时候实现它了。让我们开始吧。
配置服务器通常在私有网络上维护,如果我们是在云环境中部署,尽管 Spring Cloud 配置支持基于对称密钥或非对称密钥的加密和解密。记住,微服务配置不应该发布在公共网络上。
创建配置服务器项目
让我们使用 Spring Initializr 创建我们的项目。转到 Spring Initializr (start.spring.io/) 并遵循图片说明:

点击生成项目,然后我们可以在 IDE 中打开项目。
启用 Spring Cloud 配置服务器
我们将使用 Git 仓库作为属性源,然后我们需要创建一个仓库来保存这些文件。然而,在此之前,让我们导航到pom.xml文件,看看一些有趣的内容。我们可以找到以下依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
它是一个配置服务器的依赖项。它使我们能够在我们的应用程序中使用配置服务器。记住,我们需要将此放入pom.xml文件中才能达到所需的配置服务器。
使用 GitHub 作为仓库
Spring Cloud 配置服务器使我们能够使用不同的数据存储技术作为属性存储库。社区提供了一些选项,例如 Git 仓库、文件系统或 SVN 等。
我们将选择 Git 仓库,并使用 GitHub 作为托管平台。
我们将使用包含书籍源代码的 Git 仓库。仓库位于:GitHub.com/PacktPublishing/Spring-5.0-By-Example/tree/master/config-files。
Spring Cloud Config Server 也支持私有仓库。为此,我们需要提供私有/公开密钥。
配置 Spring Boot 应用程序
启用和运行配置服务器以及提供我们的配置 HTTP 协议是一件轻而易举的事情。为了实现它,我们需要在我们的 Spring Boot 启动类中放置以下注解。实现如下:
package springfive.airline.configserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
太棒了。@EnableConfigServer为我们做了魔法。它会启动配置服务器并使应用程序准备好连接。
将 Git 仓库配置为属性源
我们的配置服务器需要被配置。为此,我们将使用application.yaml文件。这个文件应该简单,并且配置最少。配置文件应该看起来像这样:
server:
port: 5000
spring:
cloud:
config:
name: configserver
server:
git:
uri: https://github.com/PacktPublishing/Spring-5.0-By-Example search-paths: config-files*
我们已经配置了应用程序端口,这是一个常见任务。我们命名了我们的配置服务器,最重要的是server.git.uri配置属性,它指示 Spring 框架获取配置文件。
另一个配置是search-paths;它允许我们在git仓库文件夹中搜索配置,而不是在仓库的根地址中。
运行配置服务器
干得好;我们的配置服务器已经准备好使用。然后让我们运行它。我们可以使用 JAR 文件,或者通过 IDE 也可以,这取决于你选择哪种方式。
我们可以使用 Java 命令行或 IDE 来运行它。我更喜欢使用 IDE,因为它使我们能够调试并进行一些代码更改。
运行它。
输出应该看起来像这样:

Tomcat 启动成功;我们的配置服务器已启动并运行。我们可以在配置服务器中找到一些不同的端点。这些端点被暴露出来以提供配置文件。
Spring Cloud Config Server 也支持配置文件,为不同的环境提供不同的配置是很重要的。
配置服务器支持的模式如下:
<application-name>-<profile>.<properties|yaml>
这点非常重要,同时,这也使得在微服务中声明application.name属性成为强制性的,以识别应用程序。
我们可以在应用程序引导中找到 Spring Cloud Config Server 提供的端点。看看日志:

记住配置服务器支持环境;正因为如此,端点上有一种正则表达式。看看"/{name}-{profiles}.yml"端点。
测试我们的配置服务器
我们能够通过 REST API 测试我们的配置服务器。
让我们创建一个简单的yaml文件来创建测试;文件应该命名为dummy.yaml:
info:
message: "Testing my Config Server"
status: "It worked"
推送到 GitHub – 如果你使用的是 GitHub 书籍,这一步是不必要的。然后,我们可以使用以下命令调用配置服务器 API:
curl http://localhost:5000/dummy/default | jq
命令会在default配置文件中寻找名为dummy的配置;URL 是自解释的。以下输出应该被显示:

我们的配置服务器已完全运行。现在,我们将使用 Netflix Eureka 配置我们的服务发现。
Spring Cloud 服务发现
服务发现是微服务架构的关键点之一。微服务架构的基础是将单体应用程序解耦成更小的软件块,这些块具有明确的边界。
这影响了我们的单体应用程序的系统设计。一般来说,应用程序逻辑在代码方面保持在一个地方。这意味着当应用程序运行时,过程或方法调用是在相同的上下文中调用的。
当我们采用微服务架构风格时,这些调用通常是外部的,换句话说,它们将通过 HTTP 调用调用服务,例如,在另一个应用程序上下文或 Web 服务器中。
然后,服务需要通过 HTTP 调用其他服务,例如,但如果这些服务的实例以相当高的频率变化,服务又是如何调用其他服务的呢?记住,我们正在创建分布式和可扩展的系统,其中服务的实例可以根据系统使用情况进行增加。
服务需要知道其他服务在哪里运行,才能调用它们。让我们想象一下,如果我们考虑将服务的 IP 地址放入配置中;这将很难管理,并且在那段时间内无法跟踪机器的变化。
服务发现模式解决了这个挑战。一般来说,解决方案涉及服务注册表,它知道所有运行服务的位置。然后,客户端需要有一种服务注册表客户端,以便能够查询这个服务注册表以获取所需服务的有效地址;然后服务注册表将返回一个健康的地址,最后,客户端可以调用所需的服务。
让我们看看以下图表:

该模式的完整文档可以在microservices.io/patterns/client-side-discovery.html和www.nginx.com/blog/service-discovery-in-a-microservices-architecture/找到。针对该模式有如此多的实现。
Spring Cloud 服务发现支持一些服务发现实现,例如由 Spring Cloud Consul 提供的 Hashicorp Consul 和由 Spring Cloud Zookeeper 提供的 Apache Zookeeper。
我们正在使用 Netflix OSS 堆栈,我们将使用由 Spring Netflix OSS 提供的 Eureka 服务器。它使我们能够将 Eureka 服务器用作管理的 Spring Bean。
Spring Eureka 客户端提供了一个了解服务注册表的客户端,这可以通过几个注解和一些配置来实现——我们很快就会这么做。
在接下来的章节中,我们将开始创建和配置 Eureka 服务器。让我们这么做。
Spring Cloud Consul 的完整文档可以在cloud.spring.io/spring-cloud-consul找到,Spring Cloud Zookeeper 的文档可以在cloud.spring.io/spring-cloud-zookeeper找到。
创建 Spring Cloud Eureka
为了在我们的基础设施中启用服务发现,我们需要创建一个实例,该实例将作为服务发现。Spring Cloud Eureka 服务器使我们能够完成这项任务。让我们创建我们的项目。前往 Spring Initializr 并填写信息,如下面的截图所示:

看一下所需的依赖项。Eureka 服务器是允许我们启动服务发现服务器的依赖项。
让我们在 IDE 中打开项目并开始配置它。我们将在下一节中这么做。
创建 Eureka 服务器主类
在我们开始配置之前,我们将创建main类。这个类将启动 Spring Boot 应用程序。Eureka 服务器嵌入在应用程序中。这是一个相当标准的带有单个注解的 Spring Boot 应用程序。
main应用程序类应该看起来像这样:
package springfive.airline.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
@EnableEurekaServer注解将在我们的应用程序中启动内嵌的 Eureka 服务器,并使其准备好使用。它还将启用我们应用程序中的服务注册。
配置 Spring Cloud Eureka 服务器
我们的 Eureka 服务器需要使用之前章节中配置的 Spring Cloud Server 进行配置。然后,我们需要在我们的项目中保留application.yaml,以正确使用配置服务器。而不是application.yaml,我们需要放置bootstrap.yaml并将配置服务器地址放在它上面。
然后,我们需要:
-
在 GitHub 上创建
discovery.yaml -
在 classpath 项目中创建
bootstrap.yaml文件
让我们从discovery.yaml文件开始。文件应该看起来像这样:
server:
port: 8761
eureka:
instance:
hostname: localhost
health-check-url-path: /actuator/health
status-page-url-path: /actuator/info
client:
registerWithEureka: false
fetchRegistry: false
logging:
level:
com.netflix.discovery: 'ON'
org.springframework.cloud: 'DEBUG'
有一些有趣的事情可以探索。我们使用 localhost 作为hostname,因为我们是在开发机器上运行。有一些关于 URL 健康检查和状态页面的配置——请注意与服务器相关的配置。它们位于eureka.instance YAML 节点下方。配置是health-check-url-path和status-page-url-path。我们也可以使用默认值,但新的 Spring Boot Actuator 改变了这两个功能的 URL,因此我们需要正确配置它们。
eureka.client YAML 节点是关于客户端配置的;在我们的情况下,我们将registerWithEureka设置为 false。我们不希望 Eureka 服务器同时作为客户端。对于fetchRegistry配置也是如此,它是一个客户端配置,它将缓存 Eureka 注册表的信息。
logging 节点涉及日志配置。
太棒了——我们的 gateway.yaml 已经准备好了。
让我们在 Eureka 服务器项目的类路径中创建我们的 bootstrap.yaml 文件。文件应该看起来像这样:
spring:
application:
name: discovery
cloud:
config:
uri: http://localhost:5000
label: master
简单易行——我们已经配置了 spring.cloud.config。它指导 Spring 配置服务器的地址。此外,我们还配置了 label,这是我们使用 版本控制系统(VCS)作为仓库时的分支。
做得很好。配置已经准备好了。现在是时候运行它了。让我们在下一节中这样做。
运行 Spring Cloud Eureka 服务器
Eureka 服务器已经准备好使用。我们将启动 Spring Boot 应用程序并将我们的 Eureka 服务器上线。我们可以使用 Java 命令行或 IDE 来运行它。我更喜欢使用 IDE,因为它使我们能够调试并进行一些代码更改。
配置服务器需要运行,因为发现将找到配置文件以正确引导服务器。
运行它!
我们应该在应用程序启动日志中看到以下行:

太棒了。看看日志的下一行:
2018-01-07 14:42:42.636 INFO 11191 --- [ Thread-32] e.s.EurekaServerInitializerConfiguration : Started Eureka Server
这意味着我们的 Eureka 服务器已经准备好使用。为了检查解决方案,我们可以访问 Eureka 服务器的首页。访问 http://localhost:8761/,以下页面将会显示:

如我们所见,还没有任何服务实例可用。我们可以找到一些相关信息,例如服务器 Uptime、当前数据中心和当前时间。在“常规信息”部分有一些信息,关于运行 Eureka 服务器的服务器。
干得好。我们的服务发现服务正在运行。我们很快将使用这个基础设施。
Spring Cloud Zipkin 服务器和 Sleuth
我们解决方案涉及一些微服务;这使得我们的解决方案易于部署和编写代码。每个解决方案都有一个特定的仓库和代码库。
在单体解决方案中,整个问题都在要部署的同一工件中解决。通常,在 Java 中,这些工件是 .jar、.war 或 .ear,如果应用程序是根据 Java EE 5/6 规范编写的。
这些类型应用程序的日志策略相当容易处理(因此问题可以轻松解决),因为所有事情都发生在同一个上下文中;请求来自同一个应用程序服务器或网络服务器,它们具有业务组件。现在,如果我们查看日志,我们可能会找到我们想要的日志条目。这使得跟踪应用程序更容易找到错误和调试。
在微服务解决方案中,应用程序行为在分布式系统中被分割;这大大增加了跟踪任务,因为请求可能到达 API 网关并进入微服务。它们在不同的源中记录信息。在这种情况下,我们需要一种日志聚合器和一种识别服务之间整个事务的方法。
为了这个目的,Spring Cloud Sleuth 和 Spring Cloud Zipkin 可以帮助我们,使跟踪功能对开发者来说更加舒适。
在本节中,我们将查看和理解它的工作原理。
Zipkin 服务器的基础设施
在我们开始工作之前,我们需要配置一个 Zipkin 服务器需要的服务。默认情况下,Zipkin 服务器使用内存数据库,但生产环境中不建议使用;通常,开发者使用此功能来演示 Zipkin 功能。
我们将使用 MySQL 作为数据存储。Zipkin 服务器也支持不同的来源,例如 Cassandra 和 Elasticsearch。
Spring Cloud Sleuth 支持同步和异步操作。同步操作是通过 HTTP 协议进行的,异步可以通过 RabbitMQ 或 Apache Kafka 完成。
要使用 HTTP,即 REST API,我们应该使用 @EnableZipkinServer,它将通过 SpanStore 接口代理 REST 层的持久化。
我们将选择异步解决方案,因为它非常适合我们的项目,我们也不想跟踪收集器引起一些性能问题。异步解决方案使用 Spring Cloud Stream binder 来存储 Spans。我们选择 RabbitMQ 消息代理来完成这项工作。这可以通过使用 @EnableZipkinStreamServer 注解来实现,这些注解配置 Spring Sleuth 使用流来存储 Spans。
让我们创建我们的 docker-compose-min.yaml 文件来启动 RabbitMQ 和 MySQL 容器。该文件应如下所示:
version: '3'
services:
rabbitmq:
hostname: rabbitmq
image: rabbitmq:3.7.0-management-alpine
ports:
- "5672:5672"
- "15672:15672"
networks:
- airline
mysql:
hostname: mysql
image: mysql:5.7.21
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=zipkin
networks:
- airline
mongo:
hostname: mongo
image: mongo
ports:
- "27017:27017"
networks:
- airline
redis:
hostname: redis
image: redis:3.2-alpine
ports:
- "6379:6379"
networks:
- airline
networks:
airline:
driver: bridge
docker-compose-min.yaml 文件可以在 GitHub 找到,其中包含 MongoDB 和 Redis - 它们将在下一章中使用。
这里没有什么特别的。我们已经声明了两个容器 - RabbitMQ 和 MySQL - 并且在主机机器上公开了端口。此外,我们还创建了 airline 网络;我们将使用这个网络来附加我们的基础设施微服务。
现在,我们可以创建我们的 Zipkin 服务器,我们将在下一节中完成。
创建 Spring Cloud Zipkin 服务器
我们将在 Spring Initializr 中创建我们的 Zipkin 控制面板结构,然后我们需要遵循以下说明:

太棒了 - 查看所选依赖项部分,所有这些依赖项都是必需的。请注意 Spring Boot 版本。我们选择 1.5.9,因为在 Spring Boot 2 中没有对 Zipkin 服务器提供支持。这不是问题,因为我们不需要 Spring Boot 2 的特定功能。
点击“生成项目”按钮,等待下载完成。之后,在 IDE 中打开项目。
为了启用服务发现并在数据库中存储 Spans,我们需要在我们的 pom.xml 中添加以下依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>
第一个依赖项是为服务发现客户端,其余的是到 MySQL 的 JDBC 连接。这使得我们的项目依赖项完全配置。
让我们创建我们的 main 类来启动 Zipkin 服务器。这个类相当标准,但有一些新的注解:
package springfive.airline;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.sleuth.zipkin.stream.EnableZipkinStreamServer;
@SpringBootApplication
@EnableZipkinStreamServer
@EnableEurekaClient
public class ZipkinServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZipkinServerApplication.class, args);
}
}
@EnableEurekaClient 注解使应用程序能够连接到 Eureka 服务器。新的注解 @EnableZipkinStreamServer 指示框架连接到配置的代理以接收 Spans。记住,这可以通过 Spring Cloud Stream Binder 完成。
配置 boostrap.yaml 和 application.yaml
在该部分,我们创建了我们的 main 类。在我们运行它之前,我们应该创建我们的两个配置文件。位于 src/main/resources 目录中的 bootstrap.yaml 和位于我们的 GitHub 仓库上的 application.yaml。它们将通过 Config Server 下载并由 Zipkin 服务器项目提供。
让我们从 bootstrap.yaml 开始:
spring:
application:
name: zipkin
cloud:
config:
uri: http://localhost:5000
label: master
没有什么特别的,我们已配置了我们的 Config Server 地址。
让我们跳转到我们的 application.yaml:
server:
port: 9999
spring:
rabbitmq:
port: 5672
host: localhost
datasource:
schema: classpath:/mysql.sql
url: jdbc:mysql://${MYSQL_HOST:localhost}/zipkin?autoReconnect=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
initialize: true
continue-on-error: true
sleuth:
enabled: false
zipkin:
storage:
type: mysql
logging:
level:
ROOT: INFO
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
这里有一些有趣的事情。在 spring.rabbitmq 节点中,我们已配置了我们的 RabbitMQ 代理连接。它将被用来接收 Spans。在 spring.datasource 中,我们已配置了 MySQL 连接。Zipkin 服务器将使用它来存储数据。此外,我们还配置了如何执行 DDL 脚本来创建 zipkin 数据库。
spring.sleuth 节点被配置为不产生任何 Span,因为它是一个服务器,而不是客户端应用程序,并且我们不会在 Zipkin 服务器上执行跟踪。
zipkin 节点曾用来配置 Zipkin 服务器存储类型,在我们的例子中是 MySQL。
让我们运行它!!!
运行 Zipkin 服务器
我们已正确配置了 Zipkin 服务器,因此现在我们可以正常运行它。
我们可以运行主类 ZipkinServerApplication。我们可以使用 IDE 或 Java 命令行,运行以下输出后:

干得好——Zipkin 服务器现在正在运行。我们可以查看索引页面来查看它的样子。
前往 Zipkin 页面;页面应该看起来如下截图:

此外,我们还可以检查 RabbitMQ 面板以找到 Zipkin 服务器创建的队列。转到 RabbitMQ 队列(http://localhost:15672/#/queues)部分,页面应该看起来像这样:

查看队列,项目已创建了 sleuth.sleuth 队列,做得好。
Zipkin 服务器已准备就绪。目前,我们不会有任何 Span,因为没有应用程序向 Zipkin 发送数据。我们将在下一章中这样做。
Spring Cloud Gateway
API 网关模式帮助我们通过单个已知的入口点公开我们的微服务。通常,它充当外部访问的入口点并将调用重定向到内部微服务。
在我们的应用程序中采用 API 网关有许多好处。第一个好处很容易识别,它使 API 消费对客户端来说变得容易,这意味着客户端不需要知道不同的微服务端点。
其他好处是第一个好处的结果。当我们有一个唯一的入口点时,我们可以处理一些跨应用的问题,例如过滤、认证、节流和速率限制等。
这是我们采用微服务架构时的一个基本部分。
Spring Cloud Gateway 允许我们在 Spring 管理的 bean 中使用这些功能,以 Spring 的方式使用依赖注入和其他 Spring 框架提供的功能。
该项目基于 Spring Framework 5,它以 Project Reactor 为基础。提供了一些有趣的功能,例如 Hystrix 断路器集成以及与 Spring Cloud Discovery 客户端的集成。
看一下图表,了解 API 网关的好处:

API 网关模式的完整文档可以在以下网址找到:microservices.io/patterns/apigateway.html。
创建 Spring Cloud Gateway 项目
我们将使用 Spring Initializr 创建我们的 Spring Cloud Gateway 项目;我们需要手动添加一些依赖项。让我们转到 Spring Initializr 页面并创建我们的项目:

有一个新的依赖项 Gateway,它使我们能够与 Spring Cloud Gateway 一起工作。然后点击“生成项目”并等待下载完成。
之后,我们需要添加一个缺失的依赖项。这个缺失的依赖项是 Gateway 与 Eureka 服务器交互所必需的;依赖项的名称是 spring-cloud-starter-netflix-eureka-client。然后,让我们在我们的 pom.xml 中添加这个依赖项,我们需要添加以下片段:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
太好了,我们的项目已经正确配置,可以与 Eureka 服务器一起工作。在下一节中,我们将配置项目以与 Config Server 一起工作。
创建 Spring Cloud Gateway 的主类
这一部分没有秘密。Spring Cloud Gateway 与常见的 Spring Boot 应用程序以相同的方式工作。有一个 main 类,它将启动嵌入式服务器并启动整个应用程序。
我们的 main 类应该看起来像这样:
package springfive.airline.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient @SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
如我们所见,这是一个相当标准的 Spring Boot 应用程序,配置了 @EnableEurekaClient 以作为服务发现实现与 Eureka 服务器一起工作。
配置 Spring Cloud Gateway 项目
主要项目结构已经准备好。在本节中,我们将创建项目配置。为了实现这一点,我们需要执行以下步骤:
-
将
gateway.yaml文件添加到 GitHub -
在 Gateway 项目中创建
bootstrap.yaml
我们使用 Spring Cloud Config Server,因此有必要在 GitHub 上创建新文件,因为配置服务器将尝试在存储库中查找文件。在我们的例子中,我们使用 GitHub 作为存储库。
第二个任务是必要的,因为bootstrap.yaml文件在应用程序完全准备好运行之前被处理。然后,在这个阶段,应用程序需要查找配置文件,为了实现这一点,应用程序需要知道repository,在我们的例子中,是配置服务器。记住配置服务器的地址始终需要放在bootstrap.yaml上。
让我们创建我们的gateway.yaml文件——文件应该看起来像这样:
server:
port: 8888
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
logging:
level: debug
YAML 文件中的eureka.client节点负责配置 Eureka 客户端配置。我们需要配置我们的 Eureka 服务器地址实例。它应该指向正确的地址。
Eureka 配置客户端属性有更多选项。完整的文档可以在github.com/Netflix/eureka/wiki/Configuring-Eureka找到;Netflix 团队维护 Eureka。
然后,我们需要在网关项目中创建我们的bootstrap.yaml文件。此文件将指导 Spring 框架在配置服务器上查找配置文件,然后下载所需的文件以完成应用程序启动。我们的文件应该看起来像这样:
spring:
application:
name: gateway
cloud:
config:
uri: http://localhost:5000
label: master
非常简单。application.name是必需的,用于指导框架查找正确的文件。通常,不同应用程序和环境都有许多配置文件。
在cloud.config节点上,我们需要输入我们之前配置的 Spring Cloud Config Server 地址。
项目的最终结构应该看起来像这样:

看一下截图。类路径中没有application.yaml。这给我们带来了几个优点;类路径项目中没有配置文件,这在管理微服务配置方面对我们帮助很大。
在下一节中,我们将运行它并解释整个应用程序启动过程。让我们开始吧。
运行 Spring Cloud Gateway
项目配置良好,现在是时候运行它了。我们可以使用 Java 命令行或 IDE。两种方式没有区别。
配置服务器和 Eureka 服务器需要保持运行状态;网关项目正确运行是强制性的。然后,我们可以运行项目。
运行项目并查看日志。我们可以看到一些有趣的内容,例如项目连接到配置服务器并下载配置,然后连接到 Eureka 服务器并自我注册。以下图表解释了应用程序启动流程:

让我们看看不同的流程是什么,并理解它们:
-
网关应用程序请求配置文件
-
配置服务器提供配置文件
-
网关应用注册到 Eureka 服务器
太棒了,我们的网关应用已连接到我们的基础设施服务。
检查 Eureka 服务器
我们的网关正在运行。现在,我们可以检查 Eureka 服务器页面以确认此信息。
前往http://localhost:8761/,并检查 Eureka 当前注册的实例部分。我们应该看到网关应用,如下面的截图所示:

极好。它运行得很好。网关应用已成功注册,并且可以通过服务发现来查找。我们的网关将连接到 Eureka 服务器以获取可用的服务并将请求调用分发到正确的服务。
干得好。现在,我们可以在网关中创建我们的路由。我们将在下一章创建我们的航空公司微服务时进行此操作。
使用 Spring Cloud Gateway 创建我们的第一个路由
我们的网关正在运行。在我们开始为我们的航空公司应用程序设置真实路由之前,让我们尝试使用一些假路由来测试 Spring Cloud Gateway 的行为。我们将使用httpbin.org/网站,它帮助我们测试一些路由。
让我们创建一个带有@Configuration注解的类,为 Spring 容器提供路由。让我们创建一个名为springfive.airline.gateway.infra.route的包,然后创建以下类:
package springfive.airline.gateway.infra.route;
import java.util.function.Function;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.PredicateSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder.Builder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SampleRoute {
private Function<PredicateSpec, Builder> addCustomHeader = predicateSpec -> predicateSpec
.path("/headers")
.addRequestHeader("Book", "Spring 5.0 By Example")
.uri("http://httpbin.org:80");
@Bean
public RouteLocator sample(RouteLocatorBuilder builder) {
return builder.routes()
.route("custom-request-header", addCustomHeader)
.route("add-query-param", r -> r.path("/get").addRequestParameter("book", "spring5.0")
.uri("http://httpbin.org:80"))
.route("response-headers", (r) -> r.path("/response-headers")
.addResponseHeader("book","spring5.0")
.uri("http://httpbin.org:80"))
.route("combine-and-change", (r) -> r.path("/anything").and().header("access-key","AAA")
.addResponseHeader("access-key","BBB")
.uri("http://httpbin.org:80"))
.build();
}
}
有一些不同类型的配置路由;我们提取的第一个是到名为addCustomHeader的私有属性的函数,该函数将用于custom-request-header路由。我们将使用curl测试之前创建的一些路由。
我们将测试的第一个是custom-request-header,路由已配置为路由到:httpbin.org:80并且路径将是/headers。此服务将返回发送到服务器的请求头。看看addCustomHeader,我们已配置它向请求添加自定义头。它将以Book作为键,以Spring 5.0 By Example作为值。让我们使用 curl 调用网关 URL:
curl http://localhost:8888/headers
输出应该看起来像这样:

让我们分析输出。首先要注意的是我们调用了 localhost 地址。请求中的Host键显示为httpbin.org,这意味着 Spring Cloud Gateway 已更改了地址。太棒了,但我们预料到了。第二点是添加了Book键的地方,bingo,它就在请求头中。网关按预期工作,我们用几行代码做了些有趣的事情。
让我们再进行一次测试。我们将测试combine-and-change,此路由配置为用请求Header access-key: AAA回答/anything,所以命令行应该是:
curl -v -H "access-key: AAA" http://localhost:8888/anything
如我们所见,-v参数使调用以详细模式进行,这对于调试目的很有用,而-H表示请求头。让我们看看输出:

太棒了。如果你看access-key的值,网关已更改为请求的值BBB。干得好,大家。有一些端点要测试,请随意测试。
你可以在以下位置找到 httpbin 文档:httpbin.org/。还有一些有趣的 HTTP 测试方法。
将基础设施放在 Docker 上
我们的基础设施已经准备好,并使我们能够开发应用程序。我们可以创建一个 Docker compose 文件来启动基础设施服务;在开发生命周期中,Eureka、配置服务器、跟踪服务器和 API 网关等组件不会发生变化,因为它们作为基础设施进行交互。
然后,它使我们能够创建组件镜像并在docker-compose.yaml文件中使用它们。让我们列出我们的组件:
-
配置服务器
-
Eureka
-
Zipkin
-
RabbitMQ
-
Redis
我们知道如何使用 Fabric8 Maven 插件创建 Docker 镜像,我们在前面的章节中已经这样做了几次——让我们来做吧。
让我们以一个为例进行配置,记住我们需要对所有项目进行相同的配置,包括 Eureka、网关、配置服务器和网关。以下代码片段配置了docker-maven-plugin以生成 Docker 镜像:
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.21.0</version>
<configuration>
<images>
<image>
<name>springfivebyexample/${project.build.finalName}</name>
<build>
<from>openjdk:latest</from>
<entryPoint>java -Dspring.profiles.active=docker -jar /application/${project.build.finalName}.jar</entryPoint>
<assembly>
<basedir>/application</basedir>
<descriptorRef>artifact</descriptorRef>
<inline>
<id>assembly</id>
<files>
<file>
<source>target/${project.build.finalName}.jar</source>
</file>
</files>
</inline>
</assembly>
<tags>
<tag>latest</tag>
</tags>
<ports>
<port>8761</port>
</ports>
</build>
<run>
<namingStrategy>alias</namingStrategy>
</run>
<alias>${project.build.finalName}</alias>
</image>
</images>
</configuration>
</plugin>
这是一个相当简单的配置。一个简单的 Maven 插件,带有几个配置。然后,在插件配置之后,我们能够生成 Docker 镜像。生成 Docker 镜像的命令是:
mvn clean install docker:build
它将为我们生成一个 Docker 镜像。
配置的项目可以在 GitHub 上找到;与前面的章节一样,这里有很多配置要做。我们需要配置docker-maven-plugin并生成 Docker 镜像。
完全配置的项目可以在第七章文件夹中找到。GitHub 仓库是:github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter07.
在创建镜像之后,我们能够创建一个定义整个系统的 Docker compose 文件。docker-compose-infra-full.yaml文件应该看起来像这样:
version: '3'
services:
config:
hostname: config
image: springfivebyexample/config
ports:
- "5000:5000"
networks:
- airline
rabbitmq:
hostname: rabbitmq
image: rabbitmq:3.7.0-management-alpine
ports:
- "5672:5672"
- "15672:15672"
networks:
- airline
mysql:
hostname: mysql
image: mysql:5.7.21
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=zipkin
networks:
- airline
redis:
hostname: redis
image: redis:3.2-alpine
ports:
- "6379:6379"
networks:
- airline
zipkin:
hostname: zipkin
image: springfivebyexample/zipkin
ports:
- "9999:9999"
networks:
- airline
networks:
airline:
driver: bridge
这里有一些需要注意的有趣事情。非常重要的一点是,所有容器实例都连接到同一个名为airline的 Docker 网络。注意容器暴露的端口,在 Docker 中启用服务发现功能很重要。
然后,我们可以执行指令来启动整个基础设施;可以使用以下命令完成:
docker-compose -f docker-compose-infra-full.yaml up -d
以下输出应该出现:

此外,我们可以执行以下指令来检查容器的执行:
docker-compose -f docker-compose-infra-full.yaml ps
它将列出正在运行的容器,如下面的截图所示:

所有应用程序都已启动并运行。做得好。
要删除容器,我们可以使用:
docker-compose -f docker-compose-infra-full.yaml down
它将从堆栈中移除容器。
干得好,我们的基础设施在 Docker 容器中完全运行。它是开始创建我们的微服务的基础。
摘要
在本章中,我们采用了微服务架构风格构建了基本的基础设施服务。
我们已经学习了 Spring 框架如何从我们的微服务中消除基础设施代码,并使我们能够使用几个注解来创建这些服务。
我们理解了其内部的工作原理;当应用程序在生产阶段出现错误时,进行调试和故障排除至关重要。
现在,我们已经准备好创建可扩展、容错和响应式的系统。我们已经为我们系统建立了基础。
在下一章中,我们将开始构建我们的航空公司票务系统,了解如何将新的微服务与整个基础设施连接起来,并启用服务发现和其他令人惊叹的功能。
那里见。
第八章:断路器和安全
在前一章中,我们配置了将在我们的基础设施中运行的微服务,并创建了一个 Eureka 服务器作为我们解决方案的服务发现。此外,我们还创建了一个 Config Server 应用程序,它将为我们的微服务提供服务配置。
在本章中,我们将创建微服务来与我们的先前基础设施交互。我们将发现如何为我们的业务微服务应用服务发现功能,并了解断路器模式如何帮助我们为应用程序带来弹性。
在本章中,我们将了解微服务如何通过由 Spring WebFlux 客户端提供的 HTTP 异步调用来与其他服务进行通信。
到本章结束时,我们将学会如何:
-
使用服务发现连接微服务
-
从配置服务器拉取配置
-
了解
Hystrix如何为微服务带来弹性 -
展示边缘 API 策略
-
展示 Spring Boot Admin
理解服务发现的力量
我们将根据业务需求创建我们的第一个微服务。我们将创建一个 planes 微服务,该微服务将维护有关公司飞机的数据,例如特性、型号和其他属性。
planes 微服务将被用来为我们第二个微服务,即 flights 微服务,提供飞机特性。它需要获取一些飞机信息以便能够创建航班,例如座位数。
planes 微服务是一个很好的起点,因为没有需要创建与业务相关的依赖项。
我们的 planes 微服务很快就会变得有用。是时候创建它了。让我们开始吧。
创建 planes 微服务
正如我们在前几章中所做的那样,我们将使用 Spring Initializr 来实现这个目的。以下是一些应该选择的依赖项,如以下截图所示:

有一些必要的依赖项。Stream Binder Rabbit 和 Sleuth Stream 依赖项是必要的,使我们能够发送数据跨度,并启用应用程序跟踪,通过 RabbitMQ 消息代理。我们将使用 MongoDB 作为此特定应用程序的数据库,因此我们需要 Reactive MongoDB。Config Client 对于解决方案中存在的所有微服务都是强制性的。我们不会在类路径上有任何应用程序配置。Actuator 提供了生产就绪的指标和关于运行应用程序的信息;这是微服务架构风格的一个基本特征。此外,Zuul 将是连接应用程序与我们的边缘 API 的关键。我们将在本章的学习过程中了解更多关于它的信息。
我们现在可以按下“生成项目”按钮来下载项目。在 IDE 中打开项目。
将使用 Spring Boot 2 框架创建 planes 微服务,因为我们感兴趣的是为我们的飞机服务实现响应式基础。
此外,我们还需要包含一个额外的依赖项,这可以通过在 pom.xml 上的以下片段来完成:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
spring-cloud-starter-netflix-eureka-client 允许我们的应用程序通过 Eureka 服务器实现服务发现。
编写飞机微服务
我们将在应用程序中添加一些功能。对于这个特定的应用程序,我们将使用 Spring Reactive WebFlux 创建 CRUD 功能。
Plane 类代表我们的微服务中的飞机模型,该类应该像这样:
package springfive.airline.airlineplanes.domain;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import java.util.Set;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import springfive.airline.airlineplanes.resource.data.PlaneRequest;
@Data
@Document(collection = "planes")
@JsonInclude(Include.NON_NULL)
public class Plane {
@Id
String id;
String owner;
PlaneModel model;
Set<Seat> seats;
String notes;
@Builder
public static Plane newPlane(String owner,PlaneModel planeModel,Set<Seat> seats,String notes){
Plane plane = new Plane();
plane.owner = owner;
plane.model = planeModel;
plane.seats = seats;
plane.notes = notes;
return plane;
}
public Plane fromPlaneRequest(@NonNull PlaneRequest planeRequest){
this.owner = planeRequest.getOwner();
this.model = planeRequest.getModel();
this.seats = planeRequest.getSeats();
this.notes = planeRequest.getNotes();
return this;
}
}
有趣的点在于 @Document 注解。它使我们能够配置领域 MongoDB 集合的名称。@Builder 注解使用注解的方法创建构建者模式的实现。Project Lombok 库提供了这个功能 (projectlombok.org)。此外,该项目还有一些令人兴奋的功能,如 @Data,它为注解的类自动创建 getters/setters、equals 和 hashCode 实现。
正如我们所看到的,这个类中有一些领域模型。这些模型在这里不需要解释,完整的源代码可以在 GitHub 项目中找到:github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter08/airline-planes。
响应式仓库
我们的 Plane 类需要一个仓库将数据持久化到数据库中。我们将使用 Spring Reactive MongoDB 实现提供的响应式仓库。我们将使用 ReactiveCrudRepository,因为它使我们的仓库变得响应式。我们的仓库应该像这样:
package springfive.airline.airlineplanes.repository;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import springfive.airline.airlineplanes.domain.Plane;
public interface PlaneRepository extends ReactiveCrudRepository<Plane,String>{
}
实现与之前的 Spring Data 版本相同,只是新增了响应式接口。现在,我们可以在下一节创建我们的服务层。
创建飞机服务
我们的 PlaneService 将负责在 PlaneRepository 和 PlaneResource 之间创建一种粘合剂;后者我们将在下一节中创建。实现应该像这样:
package springfive.airline.airlineplanes.service;
import lombok.NonNull;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import springfive.airline.airlineplanes.domain.Plane;
import springfive.airline.airlineplanes.repository.PlaneRepository;
import springfive.airline.airlineplanes.resource.data.PlaneRequest;
@Service
public class PlaneService {
private final PlaneRepository planeRepository;
public PlaneService(PlaneRepository planeRepository) {
this.planeRepository = planeRepository;
}
public Flux<Plane> planes(){
return this.planeRepository.findAll();
}
public Mono<Plane> plane(@NonNull String id){
return this.planeRepository.findById(id);
}
public Mono<Void> deletePlane(@NonNull Plane plane){
return this.planeRepository.delete(plane);
}
public Mono<Plane> create(@NonNull PlaneRequest planeRequest){
final Plane plane = Plane.builder().owner(planeRequest.getOwner())
.planeModel(planeRequest.getModel()).seats(planeRequest.getSeats())
.notes(planeRequest.getNotes()).build();
return this.planeRepository.save(plane);
}
public Mono<Plane> update(@NonNull String id,@NonNull PlaneRequest planeRequest){
return this.planeRepository.findById(id)
.flatMap(plane -> Mono.just(plane.fromPlaneRequest(planeRequest)))
.flatMap(this.planeRepository::save);
}
}
这个类没有特别之处,PlaneService 将调用 PlaneRepository 将 Plane 持久化到数据库中。正如我们所看到的,我们广泛地使用了 lambda 表达式。Java 8 是运行 Spring Boot 2 应用程序的要求。
看看构建者模式如何使我们能够编写干净的代码。阅读这段代码要容易得多;我们使用了 Lombok 提供的 chaining 方法来编写它。
REST 层
我们将使用 Spring WebFlux 来公开我们的 REST 端点,然后我们需要在我们的方法中返回 Mono 或 Flux。REST 实现应该像这样:
package springfive.airline.airlineplanes.resource;
import java.net.URI;
import javax.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import springfive.airline.airlineplanes.domain.Plane;
import springfive.airline.airlineplanes.resource.data.PlaneRequest;
import springfive.airline.airlineplanes.service.PlaneService;
@RestController
@RequestMapping("/planes")
public class PlaneResource {
private final PlaneService planeService;
public PlaneResource(PlaneService planeService) {
this.planeService = planeService;
}
@GetMapping
public Flux<Plane> planes() {
return this.planeService.planes();
}
@GetMapping("/{id}")
public Mono<ResponseEntity<Plane>> plane(@PathVariable("id") String id) {
return this.planeService.plane(id).map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
public Mono<ResponseEntity<Void>> newPlane(
@Valid @RequestBody PlaneRequest planeRequest, UriComponentsBuilder uriBuilder) {
return this.planeService.create(planeRequest).map(data -> {
URI location = uriBuilder.path("/planes/{id}")
.buildAndExpand(data.getId())
.toUri();
return ResponseEntity.created(location).build();
});
}
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Object>> deletePlane(@PathVariable("id") String id) {
return this.planeService.plane(id).flatMap(data -> this.planeService.deletePlane(data)
.then(Mono.just(ResponseEntity.noContent().build())))
.defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
@PutMapping("/{id}")
public Mono<ResponseEntity<Object>> updatePlane(@PathVariable("id") String id,@Valid @RequestBody PlaneRequest planeRequest) {
return this.planeService.update(id,planeRequest)
.then(Mono.just(ResponseEntity.ok().build()));
}
}
看一下plane方法。当planeService.plane(id)返回空的 Mono 时,REST 端点将返回notFound,如下实现:ResponseEntity.notFound().build()。这使得代码极其易于理解。
在newPlane方法中,我们将返回带有新实体 ID 的location HTTP 头。
运行飞机微服务
在我们运行飞机微服务之前,我们将创建plane微服务的main类。它将负责启动应用程序。为此,我们需要包含几个 Spring 注解。类实现可以像这样:
package springfive.airline.airlineplanes;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableZuulProxy
@EnableEurekaClient @SpringBootApplication
public class AirlinePlanesApplication {
public static void main(String[] args) {
SpringApplication.run(AirlinePlanesApplication.class, args);
}
}
Spring 注解将与 Zuul 代理连接。此外,我们需要将应用程序与 Eureka 服务器连接,并自动配置应用程序。这些行为可以使用@EnableZuulProxy、@EnableEurekaClient和@SpringBootApplication来完成。
现在,我们将创建一个bootstrap.yaml文件来指导 Spring 框架在上一章创建的配置服务器上搜索配置文件。文件应该像这样:
spring:
application:
name: planes
cloud:
config:
uri: http://localhost:5000
label: master
我们已经配置了配置服务器的地址;这简直易如反掌。
现在,我们需要在 GitHub 仓库中添加application.yaml文件,因为配置服务器将尝试在仓库中找到该文件。
该文件可以在 GitHub 上找到,地址为github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/config-files/flights.yaml。
我们可以在 IDE 中运行应用程序或通过命令行运行;这取决于你。在尝试运行之前,请检查配置服务器、Eureka、MongoDB 和 RabbitMQ 是否正在运行。
我们可以使用位于 GitHub 上的 Docker Compose 文件(github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter07/docker/docker-compose-infra-full.yaml)。它包含 RabbitMQ、配置服务器、Eureka、MongoDB、MySQL、Redis 和 Zipkin 容器,可供使用。如果您正在使用它,请使用以下命令运行它:docker-compose -f docker-compose-infra-full.yaml up -d。
让我们检查输出。我们可以以不同的方式检查它:在控制台上,以及在 Eureka 服务器上。让我们这么做。
检查控制台。让我们尝试找到关于DiscoveryClient的一行。planes微服务正在尝试连接到 Eureka 服务器:

在日志文件中这里有一些重要信息。第一行指示哪个应用程序正在尝试注册到 Eureka 服务器。接下来的四行是关于 Sleuth 的。Sleuth 框架正在注册 RabbitMQ 队列和通道。
我们需要找到以下行:
Started AirlinePlanesApplication in 17.153 seconds (JVM running for 18.25)
此外,我们还可以检查 Eureka 服务器,并可以看到那里的 PLANES 应用程序,如下所示:

太棒了,我们的飞机微服务已经上线。
我们可以使用 Postman 尝试我们的微服务。这个应用程序使我们能够使用直观的 IDE 调用我们的 API,并与我们的微服务进行交互。该应用程序允许我们将一些 HTTP 调用分组到集合中。飞机集合可以在 GitHub 上找到,地址为 github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/postman/planes.postman_collection。
我们已经完成了我们的第一个微服务。在下一节中,我们将创建我们的 flights 微服务,它将消费飞机的数据。
飞行微服务
我们的飞机微服务已经启动并运行。现在这很重要,因为飞行微服务需要获取飞机的数据来创建飞行实体。
我们将介绍 Netflix Ribbon,它将作为我们的应用程序的客户端负载均衡器,我们将使用服务发现来从服务注册表中查找服务的地址。
克隆飞行微服务项目
我们在前一章中多次执行了这个任务。我们可以在 GitHub 上下载项目源代码,地址为 github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter08/airline-flights。在下一节中,我们将深入了解 Ribbon 以及它如何帮助我们解决分布式系统问题。
Netflix Ribbon
Ribbon 是由 Netflix 公司创建和维护的开源项目。该项目采用 Apache 2.0 许可,可用于商业目的。
Ribbon 为 IPC(进程间通信)提供客户端软件负载均衡算法。该项目以异步方式支持大多数流行的协议,如 TCP、UDP 和 HTTP。
还有更多有趣的功能,例如服务发现集成,这使得在动态和弹性的环境中(如云)进行集成成为可能。为此,我们将查看我们的 Eureka 服务器。这两个项目都由 Netflix 团队维护。它非常适合我们的用例。
另一个有趣的功能是容错性。Ribbon 客户端可以在配置的列表中找到活动服务器并发送请求。此外,下线服务器将不会收到任何请求。
下面的图解说明了 Ribbon 的工作原理:

正如我们所见,Ribbon 客户端可以与 Eureka 通信,然后重定向对所需微服务的请求。在我们的案例中,flights 微服务将使用 Ribbon 客户端,从 Eureka 获取服务注册表,并将调用重定向到活动的 planes 微服务实例。这听起来像是一个令人惊叹的解决方案。
理解发现客户端
现在,我们将了解服务发现以及它在复杂和动态环境中的工作方式。服务发现的基本思想是维护服务存储库并为调用者提供服务地址。
实现这个目标需要一些复杂的工作。有两个主要的行为需要理解:
-
第一个是要注册。正如我们所知,服务发现需要存储服务信息,例如地址和名称,然后在服务引导期间,它需要将信息发送到服务注册表。
-
在第二个操作中,服务发现客户端需要查询服务注册表,请求所需的服务名称,例如。然后服务注册表将向客户端发送服务信息。
现在我们已经了解了基础知识,如下面的图所示:

如前图所示:
-
第一部分是服务注册。
-
在第二阶段,服务客户端将从 Eureka 服务器获取服务地址。
-
然后,客户端可以根据服务信息进行调用。
让我们在代码中实现它。
实际中的服务发现和负载均衡
现在我们将编写一些代码来与我们的服务发现和负载均衡基础设施进行交互。现在我们知道了它是如何工作的,这将帮助我们理解源代码。
我们将创建一个DiscoveryService类,该类将根据请求的服务名称发现地址。类代码应该如下所示:
package springfive.airline.airlineflights.service;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class DiscoveryService {
private final LoadBalancerClient lbClient;
private final DiscoveryClient dClient;
public DiscoveryService(LoadBalancerClient lbClient, DiscoveryClient dClient) {
this.lbClient = lbClient;
this.dClient = dClient;
}
public Flux<String> serviceAddressFor(String service) {
return Flux.defer(() -> Flux.just(this.dClient.getInstances(service)).flatMap(srv ->
Mono.just(this.lbClient.choose(service))
).flatMap(serviceInstance ->
Mono.just(serviceInstance.getUri().toString())
));
}
}
如我们所见,我们注入了两个对象:LoadBalanceClient,它充当客户端负载均衡器,即 Netflix Ribbon;以及DiscoveryClient,它将找到请求的服务实例。
我们使用 lambda Flux.defer()来组织流程,然后我们将从 Eureka 服务器中查找服务实例。我们使用this.dClient.getInstances(service)来做这件事。在从负载均衡中查找服务 URI 之后,它将返回一个服务名称列表。这将是使用this.lbClient.choose(service).完成的。然后我们将返回服务实例地址的Flux。
是时候看看客户端代码如何使用DiscoveryService对象了。客户端代码可能如下所示:
public Mono<Plane> plane(String id) {
return discoveryService.serviceAddressFor(this.planesService).next().flatMap(
address -> this.webClient.mutate().baseUrl(address + "/" + this.planesServiceApiPath + "/" + id).build().get().exchange()
.flatMap(clientResponse -> clientResponse.bodyToMono(Plane.class)));
}
这段代码可以在项目的PlaneService类中找到。记住serviceAddressFor()方法返回一个服务地址的Flux。我们将使用next()方法获取第一个,然后我们能够将服务地址转换为一个有效的地址,以到达飞机微服务。
现在,我们将测试服务连接。我们需要完成以下任务:
-
运行配置服务器、Eureka、
planes微服务和flights微服务 -
在
planes微服务上创建一个plane实体 -
在
flights微服务上创建一个flight实体
检查之前列出的所有服务是否都在运行。然后我们将使用以下 JSON 创建一个plane实体:
{
"owner" : "Spring Framework Company",
"model" : {
"factory" : "Pivotal",
"model" : "5.0",
"name" : "Spring 5.0",
"reference_name" : "S5.0"
},
"seats" : [
{
"identity" : "1A",
"row" : "1",
"right_side" : { "seat_identity" : "2A"},
"category" : {
"id" : "A",
"name": "First Class"
}
},
{
"identity" : "2A",
"row" : "1",
"left_side" : { "seat_identity" : "1A"},
"category" : {
"id" : "A",
"name": "First Class"
}
},
{
"identity" : "3A",
"row" : "1",
"left_side" :{ "seat_identity" : "2A"},
"category" : {
"id" : "A",
"name": "First Class"
}
}
],
"notes": "The best company airplane"
}
我们需要使用 HTTP POST 方法在 http://localhost:50001/planes 上调用 planes 微服务。我们可以在 Postman 的 Planes Collection 中找到创建飞机的请求。当我们调用创建飞机 API 时,我们将获得一个新的飞机 ID。它可以在 HTTP 响应头中找到,如下面的图片所示,在 Postman 中:
Postman 是一个帮助开发者测试 API 的工具。Postman 提供了一个友好的 GUI(图形用户界面)来发送请求。此外,该工具支持环境,并且可以帮助测试不同的环境,如开发、测试和生产。

查看一下 location HTTP 响应头。HTTP 状态码同样重要。我们将使用刚刚创建的飞机 ID 5a6a6c636798a63817bed8b4 来创建一个新的航班。
我们可以在 W3 Org(www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)找到 HTTP 状态码列表。请记住这一点,因为它非常重要,遵循正确的状态码。当我们创建 REST API 时,这被认为是一种最佳实践。
Flight Collection 可以在 GitHub 上找到,地址为 github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/postman/flights.postman_collection。我们想要执行一个创建航班的请求,但在那之前,我们需要更改之前创建的飞机 ID。查看以下截图:

飞机 ID 已经变更为之前创建的飞机 ID。现在我们可以执行请求。flights 微服务与 planes 微服务具有相同的行为。它将返回带有新航班 ID 的位置响应。在我的情况下,生成的新 ID 如下所示:

现在,我们可以通过 ID 查找航班。请求可以在 Flight Collection 中找到;名称为 Flight by Id。我们可以执行这个请求,结果应该如下所示:

查看一下 plane JSON 节点。我们在 flight 微服务中没有关于飞机的数据。这些信息来自 planes 微服务。我们已经使用了服务发现和客户端负载均衡。做得好!
让我们看一下 IDE 提供的调试信息。我们想要查看飞机服务地址:

在变量面板上,我们可以看到地址变量。其值来自服务发现和客户端负载均衡。它是 服务 IP 或 域名。现在我们能够通过转换 URL 来调用所需的服务。
太棒了,我们的基础设施工作得非常好,现在我们能够使用基础设施查找服务,但有一些重要的事情需要注意。我们将在下一节中找到它。
当服务失败时,hello Hystrix
有时基础设施可能会失败,尤其是网络。它可能会在微服务架构中引起一些问题,因为通常服务之间存在许多连接。这意味着在运行时,微服务依赖于其他微服务。通常这些连接是通过 HTTP 协议通过 REST API 完成的。
它可能导致一种称为 级联失败 的行为;也就是说,当微服务系统的一部分失败时,它可能会触发其他微服务的失败,因为存在依赖关系。让我们举例说明:

如果 服务 Y 失败,服务 A 和 服务 M 可能也会失败。
我们有一个模式可以帮助我们处理这种情况:电路断路器。
Hystrix 简述
Hystrix 是一个帮助开发者管理服务之间交互的库。该项目是开源的,由社区维护,并位于 Netflix GitHub 上。
电路断路器模式是一种帮助控制系统集成模式的模式。这个想法相当简单:我们将远程调用封装在函数或对象中,并将监控这些调用以跟踪失败。如果调用达到限制,电路将打开。其行为类似于电路断路器,其想法相同——保护某物以避免破坏电气系统:

Hystrix 实现了电路断路器模式,并有一些有趣的行为,例如回退选项。Hystrix 为我们的应用程序提供弹性。我们能够提供回退,停止级联失败,并给出操作控制。
该库提供高级配置,如果我们使用 Spring Cloud Hystrix,则可以通过注解进行配置。
电路断路器模式由 Martin Fowler 描述。您可以在 Martin Fowler 的页面 martinfowler.com/bliki/CircuitBreaker.html 上找到更多关于它的信息。
Spring Cloud Hystrix
如我们所预期,Spring Boot 与 Netflix Hystrix 集成。集成可以通过几个注解完成,并通过配置注解与 Hystrix 属性进行配置。我们将保护在 flight 服务中编码的 planes 微服务交互。我们现在有一个尝试获取飞机数据的函数。
让我们看看那个方法:
@HystrixCommand(commandKey = "plane-by-id",groupKey = "airline-flights",fallbackMethod = "fallback",commandProperties = {
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value="10"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "10"),
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value="10000"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "800"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "10000")
})
public Mono<Plane> plane(String id) {
return discoveryService.serviceAddressFor(this.planesService).next().flatMap(
address -> this.webClient.mutate().baseUrl(address + "/" + this.planesServiceApiPath + "/" + id).build().get().exchange()
.flatMap(clientResponse -> clientResponse.bodyToMono(Plane.class)));
}
对于这个命令有一些配置。第一个配置是commandKey.这里的想法是为命令创建一个名称。这对于面板控制将很有用。第二个是groupKey,这是用于分组命令的命令。它也有助于在仪表板上将命令数据分组在一起。有一个滚动窗口的概念。其想法是在时间间隔内分组请求;它用于启用指标和统计。
circuitBreaker.requestVolumeThreshold配置了在滚动窗口中触发的请求数量。例如,如果我们配置的滚动窗口为 10 秒开放,如果在 10 秒的间隔内有九个请求,则电路不会打开,因为我们已经在我们的命令中将其配置为 10。另一个配置是circuitBreaker.sleepWindowInMilliseconds,其基本思想是在触发电路后给予一定的时间,在此期间拒绝请求,然后再尝试允许尝试。
最后一个是execution.isolation.thread.timeoutInMilliseconds.这个属性配置了命令的超时时间。这意味着如果达到配置的时间,断路器系统将执行回退逻辑,并将命令标记为超时。
Hystrix库高度可定制,有很多属性可以使用。完整的文档可以在github.com/Netflix/Hystrix/wiki/configuration.找到。我们可以根据不同的用例使用这些属性。
Spring Boot Admin
Spring Boot Admin 项目是一个帮助生产环境中开发者的工具。该工具以有组织的仪表板显示 Spring Boot 应用程序指标,并且使查看应用程序指标和更多信息变得极其容易。
工具使用 Spring Boot Actuator 的数据作为信息源。该项目是开源的,有很多贡献者,并且在社区中也是一个活跃的项目。
运行 Spring Boot Admin
设置应用程序非常简单。我们需要一个新的 Spring Boot 应用程序,并将其与我们的服务发现实现连接。让我们现在就做吧。
我们可以在 GitHub 上找到代码github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter08/admin。如果你想创建一个新的应用程序,请继续;过程与我们在前面的章节中所做的是相似的。
该项目是一个 Spring Boot 常规应用,包含两个新的依赖项:
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-server</artifactId>
<version>1.5.6</version>v
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-server-ui</artifactId>
<version>1.5.6</version>
</dependency>
这些依赖项是关于admin-server和admin-server-ui的。该项目目前不支持 Spring Boot 2,但这不是问题,因为我们不需要对此使用响应式功能;它是一个监控工具。
我们已经配置了我们的必需依赖项。由于我们在基础设施中有一个服务发现,我们需要它来提供服务发现功能,并最小化我们对 Spring Boot Admin 应用程序的配置。让我们添加 Eureka 客户端依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
太棒了,我们的依赖项配置正确。然后我们可以创建我们的主类。主类应该是这样的:
package springfive.airline.admin;
import de.codecentric.boot.admin.config.EnableAdminServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableAdminServer
@EnableEurekaClient
@SpringBootApplication
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}
这里的主要区别在于@EnableAdminServer将配置 Spring Boot Admin 应用程序并为我们设置服务器。正如我们所预期的那样,我们将使用配置服务器应用程序来存储我们的application.yaml。为了实现这一点,我们需要创建我们的bootstrap.yaml,它应该是这样的:
spring:
application:
name: admin
cloud:
config:
uri: http://localhost:5000
label: master
没有任何区别,bootstrap.yaml被配置为从配置服务器查找配置文件。
是时候创建我们的application.yaml文件了,我们需要添加一些配置来设置新的健康检查 URL,因为 Spring Boot 2 中的 actuator 被移动,并以前缀actuator开头。我们新的健康检查 URL 应该是/actuator/health。
我们的配置文件应该是这样的:
server:
port: 50015
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
boot:
admin:
discovery:
converter:
health-endpoint-path: /actuator/health
我们已经配置了 Eureka 服务器地址并设置了健康检查 URL。
现在我们可以运行我们称为AdminApplication的主类。我们可以使用 Java 命令行或 IDE;两者之间没有任何区别。
运行它!
我们应该在日志文件中看到以下行:

太棒了,我们的应用程序已经准备好使用。现在我们可以进入主页。访问http://localhost:50015/#/(主页),然后我们可以看到以下页面:

看看这如何更容易地看到我们的微服务中的任何故障或异常行为。记住,微服务架构中的关键点是监控。为了有一个良好的环境,这真的是必要的。
Spring Cloud Zuul
当我们采用微服务架构时,Spring Cloud Gateway 是自然的选择,但如今 Spring Cloud Gateway 并没有启用对服务发现功能的支持,例如 Eureka 服务器。这意味着我们不得不逐个配置路由。这听起来并不好。
我们有 Zuul 代理作为我们的微服务环境的网关,但请记住,当项目支持服务发现时,Spring Cloud Gateway 是最好的选择。
让我们创建 Zuul 代理项目。
理解 EDGE 服务项目
EDGE 服务是一个提供动态路由、监控、弹性和安全性的服务。基本思想是为我们的微服务创建一个反向代理。
此服务将作为我们的微服务的代理,并作为中央访问点公开。Spring Cloud Zuul 与 Eureka 服务器集成。它将提高我们的弹性,因为我们将使用 Eureka 服务器提供的服务发现功能。
以下图片展示了我们如何在我们的架构中使用Edge 服务:

如我们所见,Zuul 服务器将连接到服务发现服务器,以获取可用服务的列表。之后,Zuul 服务将重定向到请求的服务。
看看这个图。它与客户端没有交互,也就是说,移动和浏览器以及我们的微服务。
Spring Cloud Zuul 还支持一些有趣的功能,例如:
-
pre:这可以用来在
RequestContext中设置一些数据;它在请求被路由之前执行 -
route:这个处理请求路由
-
post:这个过滤器在请求被路由后执行
-
error:当发生某些错误时,我们可以使用错误功能来处理请求
我们将不会使用这些功能,但请记住,它们可能非常有用。记住,我们的 Zuul 服务器是通向互联网的网关。
创建 EDGE 服务器
我们将使用 Zuul 服务器作为我们应用程序的 API 网关。现在是我们创建项目的时候了。由于创建此项目没有涉及任何相关的差异,我们将查看特定的 Zuul 部分。
所需的依赖项是:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
它将为我们配置 Zuul 服务器依赖项。
现在,我们可以添加项目的主类。这个类应该是这样的:
package springfive.airline.edge;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.stereotype.Controller;
@Controller
@EnableZuulProxy
@EnableEurekaClient
@SpringBootApplication
public class EdgeServerApplication {
public static void main(String[] args) {
SpringApplication.run(EdgeServerApplication.class, args);
}
}
这里的新功能是 @EnableZuulProxy。它将设置 Zuul 服务器端点并配置反向代理过滤器。然后,我们将能够将请求转发到微服务应用程序。Zuul 与 Eureka 服务器集成,因此我们不需要手动配置它。自动配置将在发现客户端实现时找到服务。
我们可以通过命令行或 IDE 运行应用程序,这取决于你。
然后,我们可以看到配置的路由。转到 http://localhost:8888/routes,我们将能够看到路由:

我们已经配置了一些路由。我们使用 application.yaml 文件来完成这项工作。文件应该是这样的:
zuul:
routes:
planes:
path: /api/v1/planes/**
serviceId: planes
flights:
path: /api/v1/flights/**
serviceId: flights
fares:
path: /api/v1/fares/**
serviceId: fares
passengers:
path: /api/v1/passengers/**
serviceId: passengers
让我们了解这个配置。我们创建了一个名为 planes 的节点。这个节点配置了一个 path(即 URI)并配置了通过 serviceId 在 Eureka 服务器上注册的服务名称。
让我们做一个简单的测试。我们将:
-
为飞机服务配置新的 URL 路径
-
使用 Zuul 服务器测试请求
打开位于 planes 微服务项目中的 PlaneResource 类。
RequestMapping 的配置如下:
@RequestMapping("/planes")
改成这样:
@RequestMapping("/")
记住我们可以使用 Zuul 服务器作为路由器,因此我们不再需要这些信息了。在源代码的 URI 路径上,我们能够使用配置文件。
再次运行 planes 微服务。以下服务需要运行:
-
配置服务器
-
Eureka 服务器
-
飞机微服务
-
API Edge
然后,我们可以使用 Zuul 代理调用 planes 微服务。让我们使用 cURL 来做这件事:
curl http://localhost:8888/api/v1/planes
让我们稍微了解一下。端口 8888 指向 Zuul 服务器,我们在 application.yaml 中进行了配置。当路径是 '/api/v1/planes/**' 时,Zuul 服务器 将重定向到 planes 微服务。基本流程是:

请求将到达 Zuul 服务器,然后 Zuul 服务器 将将其重定向到请求的微服务。结果应该像这样;在我的情况下,数据库中有些飞机:

太棒了,我们的 API 网关完全运行正常。我们将使用它来处理同一端口的全部服务,只需更改 URI 以指向所需的 serviceId。
我们可以像在其他 Spring Boot 应用程序中一样配置端口。在这种情况下,我们选择了 8888 端口。
摘要
在本章中,我们了解了一些重要的微服务模式和它们如何帮助我们交付具有容错性、弹性和易于出错的程序。
我们练习了如何使用 Spring 框架提供的服务发现功能以及它在应用程序运行时的运作方式,我们还进行了一些调试任务,以帮助我们理解它在底层是如何工作的。
由 Netflix 托管的 Hystrix 项目可以提高我们应用程序的弹性和容错性。在本节中,当处理远程调用时,我们创建了一些 Hystrix 命令,并了解了 Hystrix 是断路器模式的有用实现。
在本章的结尾,我们能够理解微服务的缺点以及如何在分布式环境中解决常见问题。
现在我们知道了如何使用 Spring 框架解决微服务架构风格中的常见问题。
在下一章中,我们将完成我们的 航空票务系统,使用配置的工具监控微服务的健康状态,并查看它在微服务在生产阶段运行时的操作时间如何帮助开发者。
那里见。
第九章:整合所有内容
在采用微服务架构风格时,我们会面临一些挑战。第一个是处理操作复杂性;如服务发现和负载均衡器等这样的服务帮助我们解决这些问题。我们在前面的章节中解决了这些挑战,并在解决这些挑战的过程中了解了一些重要的工具。
在采用微服务架构时,还有一些其他重要的关键点需要处理。有效监控我们微服务环境中发生的事情的方法是监控微服务消耗其他微服务资源(如 HTTP API)的次数以及它们失败的次数。如果我们有接近实时的统计数据,可以节省开发者几天的时间进行故障排除和错误调查。
在本章中,我们将创建一些服务,这些服务可以帮助我们监控 Hystrix 命令并在分布式环境中聚合命令的统计信息。
安全性是微服务架构中的一个重要特性,尤其是在微服务架构采用的分布式特性方面。在我们的架构中有很多微服务;我们无法在服务之间共享状态,因此无状态安全非常适合我们的环境。
OAuth 2.0 协议规范具有这个重要的特性:无状态实现。Spring Cloud Security 提供了对 OAuth 2.0 的支持。
最后,我们将使用 Docker 将我们的微服务 Docker 化,以便使用 Docker Compose 文件中的镜像。
在本章中,我们将了解:
-
实现聚合 Hystrix 流的 Turbine 服务器
-
配置 Hystrix Dashboard 以使用 Turbine 和输入数据
-
创建一个将集成电子邮件 API 的邮件服务
-
理解 Spring Cloud Security
-
将我们的微服务 Docker 化
航空公司预订微服务
航空公司 预订 微服务是一个标准的 Spring Boot 应用程序。它与其他服务有一些交互,例如 航班 微服务。
这些交互是通过 Hystrix 创建的,为航空公司 预订 微服务带来了一些期望的行为,例如容错和弹性。
这个服务有一些业务规则,它们现在对学习上下文并不重要,所以我们将在项目创建和执行部分跳过。
完整的源代码可以在 GitHub 上找到(github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter09/airline-booking);让我们查看它并看看一些代码。
航空公司支付微服务
航空公司 支付 是一个微服务,为我们的航空公司票务系统提供支付确认。出于学习目的,我们将跳过这个项目,因为其中有一些业务规则,在 Spring 框架的上下文中并不重要。
我们可以在 GitHub 上找到完整的源代码(github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter09/airline-payments)。
了解涡轮服务器
在我们的微服务组中,有一些集成;Bookings微服务调用Fares微服务和Passengers微服务,这些集成是通过 Hystrix 来实现的,使其更具弹性和容错性。
然而,在微服务世界中,存在多个服务实例。这将需要我们按实例聚合 Hystrix 命令指标。逐个管理实例不是一个好主意。在这种情况下,涡轮服务器可以帮助开发者。
默认情况下,涡轮从由 Hystrix 运行的服务器中拉取指标,但在云环境中不推荐这样做,因为它可能会消耗大量的网络带宽,并增加流量成本。我们将使用 Spring Cloud Stream RabbitMQ 通过高级消息队列协议(AMQP)将指标推送到涡轮。因此,我们需要配置 RabbitMQ 连接,并在我们的微服务中添加两个额外的依赖项,这些依赖项是:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-hystrix-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
这些依赖项将使指标可以通过 AMQP 协议发送到涡轮服务器。
默认情况下,涡轮流使用端口8989。我们将将其配置为在8010上运行,并且我们可以使用application.yaml中的turbine.stream.port属性来自定义它。
涡轮流将成为 Hystrix 仪表板的数据输入,以显示命令指标。
完整的源代码可以在 GitHub 上找到(github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter09/turbine)。
有许多配置可以自定义涡轮服务器。这使得服务器能够适应不同的用例。
我们可以在Spring Cloud Turbine部分找到涡轮文档(cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#_turbine)。这里有大量的信息,特别是如果你需要自定义一些配置的话。
创建涡轮服务器微服务
让我们创建我们的涡轮服务器。我们将创建一个标准的 Spring Boot 应用程序,并添加一些注解来启用涡轮流和发现客户端。
主类应该是:
package springfive.airline.turbine;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.turbine.stream.EnableTurbineStream;
@EnableEurekaClient
@EnableTurbineStream
@SpringBootApplication
public class AirlineTurbineApplication {
public static void main(String[] args) {
SpringApplication.run(AirlineTurbineApplication.class, args);
}
}
如我们所见,@EnableTurbineStream将使我们能够通过 RabbitMQ 消息代理推送 Hystrix 命令指标,这对我们来说已经足够了。
Turbine 服务器的application.yaml文件可以在 GitHub 上找到(github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/config-files/turbine.yaml)。有几个配置,例如发现客户端和 Turbine 服务器配置。
我们可以通过命令行或 IDE 运行应用程序。运行它!
对flights微服务进行一些调用。创建航班 API 将调用planes微服务,该微服务使用 Hystrix 命令,并将触发一些 Hystrix 命令调用。
我们可以使用位于 GitHub 上的 Postman 集合(github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/postman/flights.postman_collection)。这个集合有一个创建航班请求,该请求将调用planes微服务以获取飞机详情。收集指标就足够了。
现在,我们可以测试我们的 Turbine 服务器是否运行正确。转到 Turbine 流端点,然后应该显示带有指标的 JSON 数据,如下所示:

有一些 Hystrix 命令信息,但正如我们所见,这些信息需要组织以便对我们有用。Turbine 使用服务器端事件(SSE)技术,该技术在第六章中介绍,即与服务器端事件玩耍。
在下一节中,我们将介绍 Hystrix 仪表板。它将帮助我们组织和使这些信息对我们有用。
让我们跳到下一节。
Hystrix 仪表板
Hystrix 仪表板将帮助我们组织 Turbine 流信息。正如我们在上一节中看到的,Turbine 服务器通过 SSE 发送信息。这是通过 JSON 对象完成的。
Hystrix 流为我们提供了一个仪表板。让我们创建我们的 Hystrix 仪表板微服务。该应用程序是一个标准的带有@EnableHystrixDashboard注解的 Spring Boot 应用程序。让我们添加依赖项以启用它:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
好的,现在我们可以创建我们应用程序的主类。主类应该看起来像这样:
package springfive.airline.hystrix.ui;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@EnableEurekaClient
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixApplication.class, args);
}
}
完整的源代码可以在 GitHub 上找到:github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter09/hystrix-ui。
如我们所见,这是一个相当标准的带有@EnableHystrixDashboard注解的 Spring Boot 应用程序。它将为我们提供 Hystrix 仪表板。
现在,我们可以通过 IDE 或 Java 命令行运行应用程序。运行它!
Hystrix 仪表板可以通过以下 URL 访问:http://localhost:50010/hystrix。
然后,转到 Hystrix 仪表板的主页面。以下页面应该会显示:

太棒了——我们的 Hystrix 仪表板已经启动并运行。在这个页面上,我们可以指向 hystrix.stream 或 turbine.stream 来消费并显示命令的指标。
保持此应用程序运行,我们将在本章后面使用它。
干得好,让我们进入下一节。
创建 Mail 微服务
现在,我们将创建我们的 Mail 微服务。这个名字很直观,这个组件将负责发送电子邮件。我们不会配置一个 SMTP (简单邮件传输协议) 服务器,我们将使用 SendGrid。
SendGrid 是一个用于电子邮件的 SaaS (软件即服务) 服务,我们将使用此服务向我们的航空公司票务系统发送电子邮件。有一些触发器用于发送电子邮件,例如,当用户创建预订和当付款被接受时。
我们的 Mail 微服务将监听一个队列。然后,我们将使用消息代理来完成集成。我们选择这种策略是因为我们不需要能够同步回答的功能。另一个基本特征是在通信中断时的重试策略。这种行为可以通过消息策略轻松实现。
我们正在使用 RabbitMQ 作为消息代理。对于此项目,我们将使用 RabbitMQ Reactor,这是 RabbitMQ Java 客户端的响应式实现。
创建 SendGrid 账户
在我们开始编码之前,我们需要创建一个 SendGrid 账户。我们将使用试用账户,这对于我们的测试来说足够了。访问 SendGrid 门户 (sendgrid.com/) 并点击免费试用按钮。
填写所需信息并点击创建账户按钮。
在主页面上,在左侧点击设置,然后转到 API 密钥部分,按照这里显示的图片操作:

然后,我们可以点击右上角的创建 API 密钥按钮。页面应该看起来像这样:

填写 API 密钥信息并选择完全访问。之后,API 密钥将出现在您的屏幕上。请将其记在一个安全的地方,因为我们很快就会将其用作环境变量。
干得好,我们的 SendGrid 账户已经准备好使用,现在我们可以编写我们的 Mail 微服务了。
让我们在下一节中这样做。
创建 Mail 微服务项目
正如我们在 第八章 中所做的那样,断路器和安全,我们将查看基本项目部分。我们将使用 Spring Initializr,就像我们在前面的章节中多次做的那样。
完整的源代码可以在 GitHub 上找到 (github.com/PacktPublishing/Spring-5.0-By-Example/tree/master/Chapter09/mail-service)。
添加 RabbitMQ 依赖项
让我们添加 RabbitMQ 所需的依赖项。以下依赖项应该添加:
<dependency>
<groupId>io.projectreactor.rabbitmq</groupId>
<artifactId>reactor-rabbitmq</artifactId>
<version>1.0.0.M1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
第一个关于 RabbitMQ 的反应式实现,第二个是 starter AMQP,它将自动设置一些配置。
配置一些 RabbitMQ 东西
我们想要配置一些 RabbitMQ 交换机、队列和绑定。这可以通过 RabbitMQ 客户端库来完成。我们将为 Mail 微服务配置所需的基础设施。
我们的基础设施配置类应该看起来像这样:
package springfive.airline.mailservice.infra.rabbitmq;
// imports are omitted
@Configuration
public class RabbitMQConfiguration {
private final String pass;
private final String user;
private final String host;
private final Integer port;
private final String mailQueue;
public RabbitMQConfiguration(@Value("${spring.rabbitmq.password}") String pass,
@Value("${spring.rabbitmq.username}") String user,
@Value("${spring.rabbitmq.host}") String host,
@Value("${spring.rabbitmq.port}") Integer port,
@Value("${mail.queue}") String mailQueue) {
this.pass = pass;
this.user = user;
this.host = host;
this.port = port;
this.mailQueue = mailQueue;
}
@Bean("springConnectionFactory")
public ConnectionFactory connectionFactory() {
CachingConnectionFactory factory = new CachingConnectionFactory();
factory.setUsername(this.user);
factory.setPassword(this.pass);
factory.setHost(this.host);
factory.setPort(this.port);
return factory;
}
@Bean
public AmqpAdmin amqpAdmin(@Qualifier("springConnectionFactory") ConnectionFactory connectionFactory) {
return new RabbitAdmin(connectionFactory);
}
@Bean
public TopicExchange emailExchange() {
return new TopicExchange("email", true, false);
}
@Bean
public Queue mailQueue() {
return new Queue(this.mailQueue, true, false, false);
}
@Bean
public Binding mailExchangeBinding(Queue mailQueue) {
return BindingBuilder.bind(mailQueue).to(emailExchange()).with("*");
}
@Bean
public Receiver receiver() {
val options = new ReceiverOptions();
com.rabbitmq.client.ConnectionFactory connectionFactory = new com.rabbitmq.client.ConnectionFactory();
connectionFactory.setUsername(this.user);
connectionFactory.setPassword(this.pass);
connectionFactory.setPort(this.port);
connectionFactory.setHost(this.host);
options.connectionFactory(connectionFactory);
return ReactorRabbitMq.createReceiver(options);
}
}
这里有一些有趣的东西,但所有这些都关于 RabbitMQ 的基础设施。这很重要,因为当我们的应用程序处于引导时间时,这意味着我们的应用程序正在准备运行。这段代码将被执行并创建必要的队列、交换机和绑定。一些配置由 application.yaml 文件提供,请查看构造函数。
模型邮件消息
我们的 Mail 服务是抽象的,可以用于不同的目的,因此我们将创建一个简单的类来代表我们系统中的邮件消息。我们的 Mail 类应该看起来像这样:
package springfive.airline.mailservice.domain;
import lombok.Data;
@Data
public class Mail {
String from;
String to;
String subject;
String message;
}
很简单,这个类代表了我们系统中的一个抽象消息。
MailSender 类
如我们所预期,我们将通过 REST API 与 SendGrid 服务集成。在我们的案例中,我们将使用 Spring WebFlux 提供的反应式 WebClient。
现在,我们将使用上一节中创建的 SendGrid API 密钥。我们的 MailSender 类应该看起来像这样:
package springfive.airline.mailservice.domain.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import springfive.airline.mailservice.domain.Mail;
import springfive.airline.mailservice.domain.service.data.SendgridMail;
@Service
public class MailSender {
private final String apiKey;
private final String url;
private final WebClient webClient;
public MailSender(@Value("${sendgrid.apikey}") String apiKey,
@Value("${sendgrid.url}") String url,
WebClient webClient) {
this.apiKey = apiKey;
this.webClient = webClient;
this.url = url;
}
public Flux<Void> send(Mail mail){
final BodyInserter<SendgridMail, ReactiveHttpOutputMessage> body = BodyInserters
.fromObject(SendgridMail.builder().content(mail.getMessage()).from(mail.getFrom()).to(mail.getTo()).subject(mail.getSubject()).build());
return this.webClient.mutate().baseUrl(this.url).build().post()
.uri("/v3/mail/send")
.body(body)
.header("Authorization","Bearer " + this.apiKey)
.header("Content-Type","application/json")
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse ->
Mono.error(new RuntimeException("Error on send email"))
).bodyToFlux(Void.class);
}
}
我们在构造函数中收到了配置,即 sendgrid.apikey 和 sendgrid.url。它们将很快被配置。在 send() 方法中,有一些有趣的结构。看看 BodyInserters.fromObject():它允许我们在 HTTP 主体中发送一个 JSON 对象。在我们的案例中,我们将创建一个 SendGrid 邮件对象。
在 onStatus() 函数中,我们可以传递一个谓词来处理 HTTP 错误系列。在我们的案例中,我们对 4xx 错误系列感兴趣。
这个类将处理发送邮件消息,但我们需要监听 RabbitMQ 队列,这将在下一节中完成。
创建 RabbitMQ 队列监听器
让我们创建我们的 MailQueueConsumer 类,它将监听 RabbitMQ 队列。这个类应该看起来像这样:
package springfive.airline.mailservice.domain.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import reactor.rabbitmq.Receiver;
import springfive.airline.mailservice.domain.Mail;
@Service
@Slf4j
public class MailQueueConsumer {
private final MailSender mailSender;
private final String mailQueue;
private final Receiver receiver;
private final ObjectMapper mapper;
public MailQueueConsumer(MailSender mailSender, @Value("${mail.queue}") String mailQueue,
Receiver receiver, ObjectMapper mapper) {
this.mailSender = mailSender;
this.mailQueue = mailQueue;
this.receiver = receiver;
this.mapper = mapper;
}
@PostConstruct
public void startConsume() {
this.receiver.consumeAutoAck(this.mailQueue).subscribe(message -> {
try {
val mail = this.mapper.readValue(new String(message.getBody()), Mail.class);
this.mailSender.send(mail).subscribe(data ->{
log.info("Mail sent successfully");
});
} catch (IOException e) {
throw new RuntimeException("error on deserialize object");
}
});
}
}
注解了 @PostConstruct 的方法将在 MailQueueConsumer 准备就绪后调用,这意味着注入已经处理。然后 Receiver 将开始处理消息。
运行 Mail 微服务
现在,我们将运行我们的 Mail 微服务。找到 MailServiceApplication 类,这是我们的项目的主类。主类应该看起来像这样:
package springfive.airline.mailservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableHystrix
@EnableZuulProxy
@EnableEurekaClient
@SpringBootApplication
public class MailServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MailServiceApplication.class, args);
}
}
这是一个标准的 Spring Boot 应用程序。
我们可以在 IDE 中或通过 Java 命令行运行应用程序。
运行它!
我们需要传递 ${SENDGRID_APIKEY} 和 ${SENDGRID_URL} 作为环境变量。如果你使用 Java 命令行运行应用程序,-D 选项允许我们传递环境变量。如果你使用 IDE,你可以在运行/调试配置中配置。
创建身份验证微服务
我们想要保护我们的微服务。对于微服务应用程序来说,安全性至关重要,尤其是由于分布式特性。
在微服务架构风格中,通常有一个服务将充当认证服务。这意味着此服务将认证我们微服务组中的请求。
Spring Cloud Security 提供了一个声明性模型,以帮助开发者启用应用程序的安全性。它支持如 OAuth 2.0 等常见模式。此外,Spring Boot Security 还支持单点登录(SSO)。
Spring Boot Security 还支持与 Zuul 代理集成的中继 SSO 令牌。这意味着令牌将被传递到下游微服务。
对于我们的架构,我们将使用 OAuth 2.0 和 JWT 模式,两者都与 Zuul 代理集成。
在这样做之前,让我们了解 OAuth 2.0 流程中的主要实体:
-
受保护资源:此服务将应用安全规则;在我们的案例中是微服务应用程序
-
OAuth 授权服务器:认证服务器是位于应用程序(可以是前端或移动端)和应用程序想要调用的服务之间的一个服务
-
应用程序:将要调用服务的应用程序,即客户端。
-
资源所有者:将授权客户端应用程序访问其账户的用户或机器
让我们绘制基本的 OAuth 流程:

我们可以从这张图中观察到以下内容:
-
客户端请求授权
-
资源所有者发送授权许可
-
应用程序客户端从授权服务器请求访问令牌
-
如果授权许可有效,授权服务器将提供访问令牌
-
应用程序调用受保护的资源并发送访问令牌
-
如果资源服务器识别了令牌,该资源将为应用程序提供服务
这些是 OAuth 2.0 授权流程的基础。我们将使用 Spring Cloud Security 实现此流程。让我们开始做。
创建 Auth 微服务
正如我们在本章中所做的那样,我们将查看重要部分。让我们从我们的依赖项开始。我们需要添加以下依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
这些依赖项将使我们能够使用 Spring Cloud Security 功能。让我们开始编写我们的认证微服务。
配置安全
让我们开始编写我们的Auth微服务。我们将从授权和认证开始,因为我们想要保护我们微服务中的所有资源,然后我们将配置WebSecurityConfigureAdapter。该类应该看起来像这样:
package springfive.airline.authservice.infra.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import springfive.airline.authservice.service.CredentialsDetailsService;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final PasswordEncoder passwordEncoder;
private final CredentialsDetailsService credentialUserDetails;
public SecurityConfig(PasswordEncoder passwordEncoder,
CredentialsDetailsService credentialUserDetails) {
this.passwordEncoder = passwordEncoder;
this.credentialUserDetails = credentialUserDetails;
}
@Override
@Autowired
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.credentialUserDetails).passwordEncoder(this.passwordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/login", "/**/register/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().permitAll();
}
}
这里有很多内容。让我们从 @EnableWebSecurity 注解开始,这个注解使 Spring Security 能够与 Spring MVC 集成。@EnableGlobalMethodSecurity 提供了 AOP 拦截器,以使用注解启用方法安全。我们可以通过在控制器上的方法上注解来使用此功能,例如。基本思想是将方法调用包装在 AOP 拦截器中,并在方法上应用安全。
WebSecurityConfigurerAdapter 允许我们配置安全的端点和一些关于如何认证用户的内容,这可以通过使用 configure(AuthenticationManagerBuilder auth) 方法来完成。我们已经配置了我们的 CredentialsDetailsService 和 PasswordEncoder 以避免在应用程序层之间传递明文密码。在这种情况下,CredentialsDetailsService 是我们用户数据的来源。
在我们的 configure(HttpSecurity http) 方法中,我们配置了一些 HTTP 安全规则。正如我们所看到的,所有用户都可以访问 /login 和 /**/register/**。这是关于 登录 和 注册 功能。所有其他请求都需要通过授权服务器进行认证。
CredentialsDetailsService 应该看起来像这样:
package springfive.airline.authservice.service;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import springfive.airline.authservice.domain.Credential;
import springfive.airline.authservice.domain.data.CredentialData;
import springfive.airline.authservice.repository.CredentialRepository;
@Component
public class CredentialsDetailsService implements UserDetailsService {
private final CredentialRepository credentialRepository;
public CredentialsDetailsService(CredentialRepository credentialRepository) {
this.credentialRepository = credentialRepository;
}
@Override
public CredentialData loadUserByUsername(String email) throws UsernameNotFoundException {
final Credential credential = this.credentialRepository.findByEmail(email);
return CredentialData.builder().email(credential.getEmail()).password(credential.getPassword()).scopes(credential.getScopes()).build();
}
}
这里没有什么特别之处。我们需要重写 loadUserByUsername(String email) 方法来提供用户数据给 Spring Security。
让我们配置我们的令牌签名者和令牌存储。我们将使用 @Configuration 类提供这些 Bean,就像我们在前面的章节中所做的那样:
package springfive.airline.authservice.infra.oauth;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
public class OAuthTokenProducer {
@Value("${config.oauth2.privateKey}")
private String privateKey;
@Value("${config.oauth2.publicKey}")
private String publicKey;
@Bean
public JwtTokenStore tokenStore(JwtAccessTokenConverter tokenEnhancer) {
return new JwtTokenStore(tokenEnhancer);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAccessTokenConverter tokenEnhancer() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(privateKey);
converter.setVerifierKey(publicKey);
return converter;
}
}
我们在 application.yaml 文件中配置了我们的私有和公开密钥。可选地,我们还可以从类路径中读取 jks 文件。然后,我们使用 JwtAccessTokenConverter 类提供了我们的令牌签名者或令牌增强器,其中我们使用了私有和公开密钥。
在我们的令牌存储中,Spring Security 框架将使用此对象从令牌中读取数据,然后在 JwtTokenStore 实例上设置 JwtAccessTokenConverter。
最后,我们使用 BCryptPasswordEncoder 类提供了密码编码器类。
我们最后的类是授权服务器配置。配置可以使用以下类来完成:
查看位于 GitHub 上的 OAuth2AuthServer 类(github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/auth-service/src/main/java/springfive/airline/authservice/infra/oauth/OAuth2AuthServer.java)。
我们在 Auth 微服务中使用了 @EnableAuthorizationServer 来配置授权服务器机制。这个类与 AuthorizationServerConfigurerAdapter 一起工作,以提供一些自定义设置。
在 configure(AuthorizationServerSecurityConfigurer oauthServer) 上,我们配置了令牌端点的安全设置。
在 configure(AuthorizationServerEndpointsConfigurer endpoints) 中,我们已配置了令牌服务的端点,例如 /oauth/token 和 /oauth/authorize。
最后,在配置 (ClientDetailsServiceConfigurer clients) 中,我们已配置了客户端的 ID 和密钥。我们使用了内存数据,但也可以使用 JDBC 实现。
Auth 微服务的主类应该是:
package springfive.airline.authservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@EnableZuulProxy
@EnableEurekaClient @SpringBootApplication
public class AuthServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServiceApplication.class, args);
}
}
在这里,我们创建了一个带有服务发现和 Zuul 代理的标准 Spring Boot 应用程序。
测试 Auth 微服务
如我们所见,Auth 微服务已准备好测试。我们的微服务正在监听端口 7777,这是我们在 GitHub 上的 application.yaml 文件中配置的。
客户端凭证流程
让我们从客户端凭证流程开始。
我们的应用程序需要运行在端口 7777 上,然后我们可以使用以下命令行来获取令牌:
curl -s 442cf4015509eda9c03e5ca3aceef752:4f7ec648a48b9d3fa239b497f7b6b4d8019697bd@localhost:7777/oauth/token -d grant_type=client_credentials -d scope=trust | jq .
如我们所见,这个 客户端 ID 和 客户端密钥 来自 planes 微服务。我们在 OAuth2AuthServer 类中进行了此配置。让我们记住确切的位置:
....
@Override
public void configure(ClientDetailsServiceConfigurer clients)throws Exception {
clients
.inMemory()
.withClient("ecommerce") // ecommerce microservice
.secret("9ecc8459ea5f39f9da55cb4d71a70b5d1e0f0b80")
.authorizedGrantTypes("authorization_code", "refresh_token", "implicit",
"client_credentials")
.authorities("maintainer", "owner", "user")
.scopes("read", "write")
.accessTokenValiditySeconds(THREE_HOURS)
.and()
.withClient("442cf4015509eda9c03e5ca3aceef752") // planes microservice
.secret("4f7ec648a48b9d3fa239b497f7b6b4d8019697bd")
.authorizedGrantTypes("authorization_code", "refresh_token", "implicit",
"client_credentials")
.authorities("operator")
.scopes("trust")
.accessTokenValiditySeconds(ONE_DAY)
....
在调用上述命令后,结果应该是:

如我们所见,令牌已成功获取。做得好,我们的客户端凭证流程已成功配置。让我们转到隐式流程,这将在下一节中介绍。
隐式授权流程
在本节中,我们将探讨如何使用隐式流程在我们的 Auth 微服务中进行身份验证。
在我们测试我们的流程之前,让我们创建一个用户以在 Auth 微服务中启用身份验证。以下命令将在 Auth 服务中创建一个用户:
curl -H "Content-Type: application/json" -X POST -d '{"name":"John Doe","email":"john@doe.com", "password" : "john"}' http://localhost:7777/register
如我们所见,电子邮件是 john@doe.com,密码是 john。
我们将使用浏览器来完成这个任务。让我们访问以下 URL:
http://localhost:7777/oauth/authorize?client_id=ecommerce&response_type=token&scope=write&state=8777&redirect_uri=https://httpbin.org/anything
让我们了解这些参数:
第一部分是服务地址。要使用隐式授权流程,我们需要路径 /oauth/authorize。我们还将使用 ecommerce 作为客户端 ID,因为我们之前已经配置了它。response_type=token 通知隐式流程,scope 是我们想要的范围,在我们的案例中是写权限,state 是一个随机变量,redirect_uri 是 oauth 登录过程之后的 URI。
将 URL 放入网页浏览器中,应该会显示以下页面:

在输入用户名和密码后,将显示以下页面以授权我们的受保护资源:

点击授权按钮。然后我们将在浏览器 URL 中看到令牌,如下所示:

如果我们复制浏览器 URL,可以查看完整的令牌。
伙计们,干得好,我们的 Auth 微服务完全可用。
在接下来的几节中,我们将配置Auth微服务以保护 Zuul 代理下游微服务,例如planes微服务。让我们跳到下一节。
使用 OAuth 2.0 保护微服务
现在,我们将配置 OAuth 2.0 来保护我们的微服务;在我们的案例中,我们的微服务是资源服务器。让我们从planes微服务开始。我们将添加新的依赖项并配置私钥和公钥。同时,我们还将配置我们的JwtTokenStore。
让我们开始吧。
添加安全依赖
为了添加新要求的依赖项,我们将更改planes微服务的pom.xml文件。我们将添加以下依赖项:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
小菜一碟——我们所需的依赖项已经正确配置。
在下一节中,我们将配置application.yaml文件。
配置 application.yaml 文件
为了配置我们的私钥和公钥,我们将使用application.yaml文件。我们在Auth微服务中进行了此配置。配置相当简单。我们需要添加以下片段:
config:
oauth2:
privateKey: |
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDNQZKqTlO/+2b4ZdhqGJzGBDltb5PZmBz1ALN2YLvt341pH6i5
mO1V9cX5Ty1LM70fKfnIoYUP4KCE33dPnC7LkUwE/myh1zM6m8cbL5cYFPyP099t
hbVxzJkjHWqywvQih/qOOjliomKbM9pxG8Z1dB26hL9dSAZuA8xExjlPmQIDAQAB
AoGAImnYGU3ApPOVtBf/TOqLfne+2SZX96eVU06myDY3zA4rO3DfbR7CzCLE6qPn
yDAIiW0UQBs0oBDdWOnOqz5YaePZu/yrLyj6KM6Q2e9ywRDtDh3ywrSfGpjdSvvo
aeL1WesBWsgWv1vFKKvES7ILFLUxKwyCRC2Lgh7aI9GGZfECQQD84m98Yrehhin3
fZuRaBNIu348Ci7ZFZmrvyxAIxrV4jBjpACW0RM2BvF5oYM2gOJqIfBOVjmPwUro
bYEFcHRvAkEAz8jsfmxsZVwh3Y/Y47BzhKIC5FLaads541jNjVWfrPirljyCy1n4
sg3WQH2IEyap3WTP84+csCtsfNfyK7fQdwJBAJNRyobY74cupJYkW5OK4OkXKQQL
Hp2iosJV/Y5jpQeC3JO/gARcSmfIBbbI66q9zKjtmpPYUXI4tc3PtUEY8QsCQQCc
xySyC0sKe6bNzyC+Q8AVvkxiTKWiI5idEr8duhJd589H72Zc2wkMB+a2CEGo+Y5H
jy5cvuph/pG/7Qw7sljnAkAy/feClt1mUEiAcWrHRwcQ71AoA0+21yC9VkqPNrn3
w7OEg8gBqPjRlXBNb00QieNeGGSkXOoU6gFschR22Dzy
-----END RSA PRIVATE KEY-----
publicKey: |
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNQZKqTlO/+2b4ZdhqGJzGBDlt
b5PZmBz1ALN2YLvt341pH6i5mO1V9cX5Ty1LM70fKfnIoYUP4KCE33dPnC7LkUwE
/myh1zM6m8cbL5cYFPyP099thbVxzJkjHWqywvQih/qOOjliomKbM9pxG8Z1dB26
hL9dSAZuA8xExjlPmQIDAQAB
-----END PUBLIC KEY-----
此外,用户信息 URI 将通过以下 YAML 配置完成:
oauth2:
resource:
userInfoUri: http://localhost:7777/credential
太棒了——我们的应用程序已经完全配置好了。现在,我们将进行最后一部分:配置以获取信息令牌。
让我们这样做。
创建 JwtTokenStore Bean
我们将创建JwtTokenStore,它将被用来获取令牌信息。这个类应该看起来像这样:
package springfive.airline.airlineplanes.infra.oauth;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
public class OAuthTokenConfiguration {
@Value("${config.oauth2.privateKey}")
private String privateKey;
@Value("${config.oauth2.publicKey}")
private String publicKey;
@Bean
public JwtTokenStore tokenStore() throws Exception {
JwtAccessTokenConverter enhancer = new JwtAccessTokenConverter();
enhancer.setSigningKey(privateKey);
enhancer.setVerifierKey(publicKey);
enhancer.afterPropertiesSet();
return new JwtTokenStore(enhancer);
}
}
太棒了——我们的令牌签名者已经配置好了。
最后,我们将向主类添加以下注解,它应该看起来像这样:
package springfive.airline.airlineplanes;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
@EnableZuulProxy
@EnableEurekaClient
@EnableResourceServer
@SpringBootApplication
public class AirlinePlanesApplication {
public static void main(String[] args) {
SpringApplication.run(AirlinePlanesApplication.class, args);
}
}
它将保护我们的应用程序,并且访问应用程序端点需要访问令牌。
记住,我们需要为我们想要保护的所有的微服务执行相同的任务。
监控微服务
在微服务架构风格中,监控是一个关键部分。当我们采用这种架构时,有很多好处,比如上市时间、源维护和业务性能的提升。这是因为我们可以将业务目标分配给不同的团队,每个团队将负责一些微服务。另一个重要特征是计算资源的优化,比如云计算成本。
如我们所知,没有免费的午餐,这种风格带来了一些缺点,比如操作复杂性。有很多小服务需要监控。可能有数百个不同的服务实例。
我们在我们的基础设施中实现了一些这些服务,但直到现在,我们还没有数据来分析我们的系统健康。在本节中,我们将探索我们配置的服务。
让我们立即分析!
使用 Zipkin 收集指标
我们在上一章中已经配置了我们的 Zipkin 服务器。现在我们将使用这个服务器来分析我们的微服务数据。让我们开始吧。
进行一些调用以创建航班。创建航班 API 将调用认证服务和航班服务。查看以下图表:

我们将查看flights微服务和planes微服务之间的通信。让我们来分析它:
前往 Zipkin 主页面,http://localhost:9999/,选择航班,然后点击“查找跟踪”。页面应该看起来像这样:

如我们所见,我们的 Zipkin 服务器上有一些数据。点击“跨度”,它带有flights和planes标签,然后我们将查看这个特定的跟踪,并且将被重定向到另一个页面,其中包含特定的跨度数据,如下所示:

在这个页面上,我们可以看到一些重要信息,例如总请求时间。然后点击“平面”行,我们将会看到以下图像中的详细信息:

查看请求信息。这里有一些有趣的内容,例如mvc.controller.class和mvc.controller.method。这些内容有助于开发者排查错误。在第一个面板中,我们还看到了服务交互的时间。这对于查找微服务网络延迟非常有帮助;例如,它使得环境管理变得更加容易,因为我们有可视化工具来更好地理解数据。
此外,Zipkin 服务器还提供了其他一些有趣的功能来查找微服务统计信息,例如查找延迟超过特定时间的请求。这对运维人员非常有帮助。
我们可以在文档页面(cloud.spring.io/spring-cloud-static/spring-cloud-sleuth/2.0.0.M5/single/spring-cloud-sleuth.html)或 GitHub(github.com/spring-cloud/spring-cloud-sleuth)项目页面上找到更多关于 Spring Cloud Sleuth 的信息。
使用 Hystrix 收集命令统计
现在,我们想要监控我们的 Hystrix 命令。在我们的微服务中存在多个命令,可能最常用的是 OAuth 令牌请求者,因为我们总是需要有一个令牌来调用我们系统中的任何微服务。我们的 Turbine 服务器和 Hystrix UI 在本章的开头就已经配置好了,我们现在将使用这些服务。
记住,我们正在使用spring-cloud-netflix-hystrix-stream作为实现,将 Hystrix 数据发送到 Turbine 服务器,因为它比 HTTP 性能更好,同时也带来了一些异步特性。
异步调用可以使微服务更加健壮。在这种情况下,我们不会使用 HTTP 调用(同步调用)来注册 Hystrix 命令统计信息。我们将使用 RabbitMQ 队列来注册它。在这种情况下,我们将消息放入队列。此外,异步调用使我们的应用程序更优化地使用计算资源。
运行 Turbine 服务器应用程序和 Hystrix UI 应用程序。Turbine 将聚合来自服务器的指标。可选地,你可以运行相同服务的多个实例,例如flights。Turbine 将正确地聚合统计信息。
让我们调用创建航班 API;我们可以使用 Postman 来完成这个操作。
然后,我们可以看到实时的命令统计信息。在此之前,我们将在 Hystrix 仪表板中配置turbine.stream。
前往 Hystrix 仪表板页面:http://localhost:50010/hystrix/。以下页面将会显示:

然后,我们有一些工作要做。让我们配置我们的 Turbine 服务器流。我们的 Turbine 流运行在http://localhost:8010/turbine.stream。将此信息放在 Hystrix 仪表板信息下方,然后我们可以点击监控流按钮。
我们将重定向到 Hystrix 命令仪表板;我们之前调用过创建航班 API 几次。命令指标将显示,如下面的图像所示:

如我们所见,我们调用了创建航班 API 八次。这个 API 使用了一些命令,例如flights.plane-by-id,它调用飞机微服务,而flights.request-token调用Auth服务。
看看监控命令有多简单。运维人员喜欢使用 Zipkin 服务器这样的页面。
了不起的工作,大家,我们的服务集成得到了充分的监控,这使得我们的微服务采用更加舒适,因为我们有有用的应用程序来监控我们的服务实例。
微服务 Docker 化
在前面的章节中,我们使用了 Fabric8 Maven Docker 插件来启用我们使用 Maven 目标创建 Docker 镜像。
现在,我们需要配置我们的微服务以使用此插件,以便轻松为我们创建镜像。与一些持续集成和持续交付工具(如 Jenkins)集成可能会有所帮助,因为我们可以轻松调用docker: build目标。
每个项目都有自定义配置,例如端口和镜像名称。我们可以在 GitHub 仓库中找到配置。记住,配置是通过pom.xml完成的。
以下列表包含了所有项目的 GitHub 仓库地址;pom.xml文件中包含了 Maven Docker 插件的配置:
-
航班:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/airline-flights/pom.xml -
飞机:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/airline-planes/pom.xml -
票价:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/airline-fare/pom.xml -
预订:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/airline-booking/pom.xml -
管理员:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/admin/pom.xml -
边缘:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/api-edge/pom.xml -
乘客:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/airline-passengers/pom.xml -
认证:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/auth-service/pom.xml -
邮件:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/mail-service/pom.xml -
涡轮机:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/turbine/pom.xml -
Zipkin:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/zipkin-server/pom.xml -
支付:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/airline-payments/pom.xml -
Hystrix 仪表板:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/hystrix-ui/pom.xml -
发现:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/eureka/pom.xml -
配置服务器:
github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/Chapter09/config-server/pom.xml
运行系统
现在,我们可以使用上一节创建的镜像来运行我们的 Docker 容器。
我们将服务分成两个 Docker Compose 文件。第一个是关于基础设施服务。第二个是关于我们的微服务。
这些堆栈必须在同一个 Docker 网络上运行,因为服务应该通过容器主机名连接。
基础设施的 Docker Compose 文件可以在 GitHub 上找到:github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/stacks/docker-compose-infra.yaml。
微服务的 Docker Compose 文件可以在 GitHub 上找到:github.com/PacktPublishing/Spring-5.0-By-Example/blob/master/stacks/docker-compose-micro.yaml。
现在,我们可以使用 docker-compose 命令运行这些文件。请输入以下命令:
docker-compose -f docker-compose-infra.yaml up -d
docker-compose -f docker-compose-micro.yaml up -d
然后,完整的应用程序将启动并运行。
干得好,伙计们。
摘要
在本章中,我们学习了关于微服务架构的一些重要要点。
我们被介绍了一些用于监控微服务环境的工具。我们学习了如何使用 Turbine 服务器在分布式环境中帮助我们监控 Hystrix 命令。
我们还介绍了 Hystrix 仪表板功能,它帮助开发者和运维人员提供一个包含命令统计信息的丰富仪表板,这些统计信息几乎可以实时提供。
我们学习了如何使用 Spring Cloud Security 为我们的微服务启用安全功能,并实现了 OAuth 2 服务器,使用 JWT 为我们的安全层提供弹性。


浙公网安备 33010602011771号