Wildfly-云端开发实用指南-全-

Wildfly 云端开发实用指南(全)

原文:zh.annas-archive.org/md5/89e1c16ed868ece8a825673d0f3109ee

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书的目标是使您熟悉可用于在云中开发和部署 Java EE 应用程序的工具。您将全程参与应用程序开发过程:创建应用程序、在云中部署、配置持续集成以及创建服务之间的安全性和容错通信。结果,您将获得 Java EE 云开发的实际知识,这可以作为您未来项目的参考。

本书面向对象

如果您是一位熟悉 Java EE 技术且希望了解如何使用这些技术在 WildFly 和 OpenShift 的云环境中,那么这本书是为您准备的。

本书涵盖内容

第一章,Java EE 和现代架构方法,为用户提供 Java EE 当前状态的概述及其与现代架构方法(即微服务和云计算)的相关性。我们将介绍本书中将使用的工具以及我们将要开发的应用程序。

第二章,熟悉 WildFly Swarm,涵盖 WildFly 及其与 Java EE 及其主要特性的关系。我们将介绍 WildFly Swarm——WildFly 的侧项目,描述其目的,并展示如何使用它来开发微服务。

第三章,适当规模您的服务,专注于 Swarm 如何仅使用对服务必要的依赖来创建服务。您将更详细地了解什么是分数,Swarm 如何检测应使用哪些分数,以及您如何修改分数发现行为。

第四章,调整您的服务配置,帮助您学习如何配置 Swarm 服务。我们将向您展示不同配置工具的实际案例,以及您如何使用它们来引导应用程序的行为。

第五章,使用 Arquillian 测试您的服务,教您如何测试您的微服务。本章将介绍 Arquillian,即将要使用的测试框架,并展示项目的目的及其主要特性。随后,您将通过实际案例学习如何基于实际案例开发、编写和配置服务的测试。

第六章,使用 OpenShift 在云中部署应用程序,讨论如何将服务部署到云中,本章使用 OpenShift 来实现这一点。

第七章,为您的应用程序配置存储,首先帮助您学习 OpenShift 存储配置的理论基础。随后,我们将向您展示如何在云中部署数据库,并配置您的云应用程序使用它。

第八章,扩展和连接您的服务,更详细地探讨了在 OpenShift 环境中部署、扩展和连接应用程序的过程。

第九章,使用 Jenkins 配置持续集成,教您如何将宠物商店应用程序与 Jenkins,一个持续集成服务器集成。我们将介绍 CI 概念,并展示如何使用 Jenkins 实现。

第十章,使用 Keycloak 提供安全功能,讨论了基于令牌的分布式安全基础。我们将介绍 Keycloak,一个身份验证服务器,可用于保护分布式云应用程序。作为一个实际示例,我们将确保 Petstore 应用程序的部分 API 安全。

第十一章,使用 Hystrix 增强容错性,讨论了如何在分布式环境中处理不可避免的网络故障。为了做到这一点,我们将介绍断路器架构模式,并涵盖何时应该使用它及其优势。我们将查看其 Netflix 实现,Hystrix。我们将介绍其实现方式和如何使用它。

第十二章,未来方向,简要描述了 Java EE 开发的未来可能看起来如何,例如,平台演变的计划以及本书中描述的应用程序提供概念的未来标准化。我们还将探讨 MicroProfile 和 Jakarta EE,描述它们的目的,并强调它们如何帮助您以更快的速度推进平台。

为了充分利用本书

本书假设您熟悉 Java EE 技术。虽然我们将在示例中简要回顾 Java EE 构造的功能,但不会进行详细解释。

代码存储库包含所有章节的示例。为了帮助您导航,示例从章节内部进行索引。

下载示例代码文件

您可以从 www.packtpub.com 的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packtpub.com 登录或注册。

  2. 选择“支持”标签。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

文件下载后,请确保您使用最新版本解压缩或提取文件夹:

  • 适用于 Windows 的 WinRAR/7-Zip

  • 适用于 Mac 的 Zipeg/iZip/UnRarX

  • 适用于 Linux 的 7-Zip/PeaZip

本书代码包托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Cloud-Development-with-WildFly。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/上找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:

www.packtpub.com/sites/default/files/downloads/HandsOnCloudDevelopmentwithWildFly_ColorImages.pdf

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“计算机编程书籍通常从Hello World应用程序开始。”

代码块设置如下:

package org.packt.swarm;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

package org.packt.swarm;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/")
public class HelloWorldApplication extends Application {
}

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

mvn wildfly-swarm:run

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“我们必须点击网页控制台中的服务菜单中的创建路由。”

警告或重要注意事项看起来是这样的。

小技巧和窍门看起来是这样的。

联系我们

我们始终欢迎读者的反馈。

一般反馈:请通过feedback@packtpub.com发送电子邮件,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过questions@packtpub.com与我们联系。

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上以任何形式遇到我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并提供材料的链接。

如果您有兴趣成为一名作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com.

评论

请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

如需了解更多关于 Packt 的信息,请访问 packtpub.com.

第一章:Java EE 和现代架构方法论

在本章中,我们将向用户概述当前Java 企业版EE)的状态及其在现代架构方法论中的相关性,即微服务云计算。我们将介绍本书中将使用的工具以及我们将要开发的应用程序。

让我们先回顾一下关于 Java EE 的几个基本事实。

Java EE

在绘制 Java EE 架构之前,让我们快速了解一下该标准是如何创建的过程。

Java 社区进程

Java EE 是一个为使用 Java 编程语言构建企业应用而设计的标准。它包含一系列规范,这些规范定义了标准实现所需的功能。

构成 Java EE 的规范是在一个开放、基于社区的过程中开发的。组织和个人用户都可以加入其中并参与开发。

作为一种标准,Java EE 可能具有多个实现。愿意创建 Java EE 认证产品的供应商必须通过一项技术合规性测试,这保证了产品与标准的一致性。

该标准为企业应用开发者和标准实现供应商之间的合同提供保障。应用开发者可以确信他们的应用将得到支持并且是可移植的,因为存在多个标准实现;他们不依赖于单一供应商。应用开发者可以自由地在不同的标准实现之间迁移他们的应用。

重要的是要注意,该标准并不决定服务器实现的细节。因此,供应商必须竞争以提供最有效、最稳健且易于使用的实现。

总结来说,Java EE 标准为企业应用开发者提供了编写受支持和可移植应用的能力。此外,基于社区的规范开发过程和供应商之间的竞争有助于标准的演变,并使用户能够根据需要选择最佳实现。

另一方面,Java EE 作为一种标准实现的事实导致其演变和决策过程比替代框架慢。在一个技术发展迅速的世界里,这成为一个更大的问题。因此,最近,人们努力重构标准和规范创建的方式。Java EE 正在转型为 EE4J,这是一个在 Eclipse 基金会治理下开发的标准。我们将在最终的第十二章:未来方向中回到这个话题。

Java EE 应用程序的基本架构

Java EE 应用程序是用 Java 语言编写的,并在Java 虚拟机JVM)上运行。在标准 Java SE 功能之上,Java EE 实现提供者实现了一系列服务,这些服务可以被这些应用程序使用。此类服务的例子可能包括安全、事务或依赖注入。

应用程序不直接与企业服务交互。相反,规范定义了组件容器的概念。组件是用 Java 语言编写的软件单元,其配置和构建方式与标准 Java 类类似。区别在于组件提供的元数据允许它使用 Java EE 实现提供的运行时运行。这种可能因组件类型而异运行时称为容器。容器负责提供组件所需的所有企业服务的访问。

例如,让我们看一下以下组件:

package org.tadamski.examples.javaee;

import org.tadamski.examples.java.ee.model.Foo;

import javax.ejb.Stateless;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import javax.persistence.EntityManager;

//1
@Stateless
public class FooDaoBean implements FooDao {

    //2
    @Inject
    private EntityManager em;

    public void save(Foo foo) throws Exception {
        //3
        em.persist(foo);
    }
}

前面的脚本展示了一个ejb组件(1),即FooDaoBean,它负责将Foo类型的对象保存到数据库中。

运行此组件的ejb容器将负责管理此组件的所有实例的池化以及它们的生命周期。此外,此具体组件利用了企业服务数量:依赖注入(2)、ORM 持久化(3)和事务(此类组件的默认设置)。

通常,Java EE 运行时的目标是处理企业应用的所有技术方面,以便应用开发者可以专注于编写业务代码。前面的例子演示了在 Java EE 中是如何实现的:应用开发者使用 POJOs 编写代码,配置最小化(主要由注解提供)。应用开发者编写的代码以声明性方式实现业务功能,向中间件告知其技术需求。

Java EE 标准的范围

传统上,用 Java EE 技术编写的业务应用基于三层架构,即 Web 层、业务层和企业信息系统层:

图片

应用服务器实现了 Web 和业务层。它可以被各种类型的客户端访问

Web 组件,如Servlets、JSPs 或 JAX-RS,允许实现 Web 层。它们能够响应来自不同类型客户端的 HTTP 请求。例如,JSF 可以方便地创建 Web 用户界面,而 JAX-RS API 允许实现 RESTful 服务。

业务层由 EJB 实现,这些是基于 POJO 的池化组件,允许轻松实现事务操作,并提供广泛的特性,如安全、数据库、外部系统集成、远程访问或依赖注入。

尽管从宏观角度看架构非常直接,但它非常灵活,允许实现各种企业应用程序。此外,该标准在多年中不断发展,为广泛的业务使用提供了工具。

如果你查看 Java EE 规范(进一步阅读,链接 1),你将能够看到所有属于标准规范的部分。它们的共享量一开始可能会让人感到有些吓人。需要注意的是,在大多数情况下,你只需要处理其中的一小部分。另一方面,当你的应用程序需要任何类型的业务功能时,所需工具很可能已经集成在整个平台中,并且易于使用。

Java EE 标准的实现

Java EE 标准实现是运行时,允许我们运行组件,并为他们提供 Java EE 标准中指定的服务。这样的运行时被称为应用程序服务器

应用程序开发人员根据规范创建组件。这些组件被组装成存档,可以在应用程序服务器上部署。

应用程序服务器允许部署多个应用程序。此外,正如本章开头所暗示的,一个应用程序可以更改服务器实现,并使用来自其他供应商的应用程序服务器部署存档:

图片

当前开发趋势

应用程序的开发方式随着时间的推移而演变。让我们概述一下近年来对软件开发产生重大影响的几个概念:云计算和微服务。

云计算

云计算是一种基础设施,使得能够按需自动配置计算资源。提供的资源类型取决于云服务提供商与客户之间的合同——云服务提供商可以提供软件服务,如电子邮件或磁盘存储,软件开发平台,虚拟机的访问,或运行软件应用程序的基础设施。

资源通过互联网动态和快速地提供,因此客户能够使用(并支付)他们当前使用的资源。另一方面,云服务提供商可以利用规模经济:专业化和资源的最优使用将导致质量提升和成本优化。

那么,从开发者的角度来看,与云计算基础设施的交互是什么样的呢?在开发过程中,云服务提供商提供了一个包含许多工具的平台:它使开发者能够运行多个应用程序框架、独立服务和数据库等。它提供了这些应用程序所需的功能和工具:扩展、网络、安全和通信。此外,如之前所暗示的,用户只为使用的资源付费;云基础设施将根据你的应用程序使用的负载调整提供的资源。

上述描述听起来很有希望,但它立即引发了许多问题,例如资源是如何分配和扩展的,我可以使用哪些类型的工具,以及提供的工具的 API 是什么。

本书的一个目标是在整个过程中为你提供所有这些信息。为了介绍章节的目的,承认最重要的信息就足够了:云计算基础设施将使我们能够使用按需提供的计算资源,利用各种工具进行开发和部署。

微服务

微服务架构是一种软件开发方法,它提倡从松散耦合的服务中创建一个应用程序,这些服务相互协作。

这样一种架构在长时间内被研究和宣传:不久前,人们对面向服务的架构SOA)给予了大量的关注。更早之前,用于分布式计算的CORBA标准已经被设计出来。此外,使用松散耦合、高度内聚的服务来构建你的应用程序是一种良好的软件开发实践,并且它(以及应该)也可以应用于传统的单体应用程序。那么,为什么会有这个新概念的产生,它与什么不同?

在近年来,许多构建大型分布式系统的公司发现使用传统的单体软件架构来构建和维护他们的系统变得越来越困难,并决定将他们的系统重构为松散耦合的模块化分布式系统。观察那些成功做到这一点的公司的经验,我们能够收集到他们在构建的系统中的共同架构模式。这催生了微服务架构的概念。换句话说,微服务可以被看作是分布式计算系统的另一轮迭代,其特性是从实践经验中得出的。因此,而不是提供一个所有有志于实施者都必须遵守的微服务架构的定义,更容易提供一个微服务系统共有的特征集(进一步阅读,链接 2)。现在就让我们来做这件事。

微服务被构建为独立的、可独立部署的服务。从技术角度来看,这意味着它们在不同的进程中运行,并通过它们的 API 通过网络进行通信。每个服务都可以独立启动、停止和更新。每个服务对其自己的数据负责,并且只能通过它们的 API 修改其他服务的数据。

系统被分解为围绕业务功能的微服务。每个微服务都是由一个由所有必要的专业技术专家组成的小团队构建的。例如,在一个商店应用程序中,可能有一个评论服务。评论服务团队可能包括程序员、数据库工程师、测试员和领域专家。该团队负责该服务的各个方面——从获取客户反馈到数据库管理。

正如你所见,成功的微服务实践者并没有宣传一套应用应该遵守的推荐特性,而是创造了一个强制模块化和松散耦合的技术环境。

那么,如果你成功实施了微服务架构,你将获得哪些好处?

实施微服务的优势

首先要强调的是,如果你成功创建了一个具有强制模块化和松散耦合的架构特性的系统,你将获得一个高度模块化的系统,因为即时的修复和扩展不会妥协,并且在整个开发过程中有效地放弃服务之间的边界。

由于开发应用程序的模块化特性,构成它们的组件可以更有效地开发:由于每个服务都有一个小型、交叉团队在开发,其成员可以相对独立于其他团队地专注于自己的工作领域。正如实践所表明的,随着团队的增长,沟通开始越来越多地阻碍工作。小型、专注的团队对领域非常了解,他们也彼此了解,可以立即沟通,并推进工作。

此外,一个关键的事实是,该服务可以独立于其他服务进行部署。一个成功的微服务架构没有大系统发布的概念,即所有团队将他们的最新更新集中在一起,创建整个系统的一个主要版本。相反,所有团队都能够独立于其他服务发布和部署他们的新功能。团队之间没有同步,如果有服务的某个新版本可以发布,该服务的团队可以独立设计并执行。这种特性是持续集成的催化剂。团队能够构建管道,以便每个代码触发测试、审查和部署过程。

前一段描述的特征——小型、专注的团队和独立且自动化的构建和部署流程——导致了基于微服务的成功系统的重要特征:能够非常快速地实施所需变更。这是至关重要的,因为它允许立即响应用户需求。这缩短了客户和开发者之间的反馈循环,并允许系统快速进化以满足客户需求。

最后但同样重要的是,我们应该提到直接的技术后果。微服务可以更有效地进行扩展:当扩展传统的单体应用程序时,我们需要有效地复制多个应用服务器,复制应用程序中实现的所有功能。扩展微服务可以更加细致;我们能够只复制在不同服务器上需要更多实例的服务。

此外,微服务架构往往可以提高可用性:如果一个审查服务宕机,其他存储服务仍然可以正常工作,不受其影响。显然,这种情况远非理想,但比整个系统关闭要好得多。

在前一段中,我们提到前面的特征适用于成功的微服务实施。实际上,创建这样的系统并不简单。让我们来看看原因。

实施微服务的挑战

实施微服务架构所面临的挑战可以总结为一个词:分布式系统。

你将要实现的功能是否将在整个网络中使用多个服务。你将不得不处理网络延迟和故障。如果响应不是即时的怎么办?目标服务是否宕机或忙碌?我们应该如何找出原因,以及我们应该采取什么措施?

数据是否应该属于一个微服务?说起来容易,做起来难。我们可以使服务底层的数据库保持一致,但如何将这一信息传播到依赖这些数据的其他服务呢?

此外,每个团队可以独立工作是一件好事,但如果我们真的需要实现跨服务功能呢?那可能会变成一件头疼的事情:一个可能引入重大架构变化并严重影响整个架构的跨团队项目。

假设我们已经成功解决了前面的问题,并且系统正在运行。当发生错误时会发生什么?我们不得不分析散布在多个服务中的日志,还要追踪它们之间的网络交互。

那么,你应该如何决定微服务架构是否适合你的应用程序?

何时采用微服务架构

应主要考虑将微服务用于管理传统单体应用程序变得过于复杂,难以开发和维护的系统。如果你正在开发一个小型应用程序,前面段落中描述的额外复杂性可能会超过模块化带来的好处,并抑制而不是放大你的开发过程。

有建议(进一步阅读,链接 3)称,微服务架构应该是单体应用程序的演变。大多数系统应该从单体开始,只有当系统增长到难以开发和维护的程度时,才应考虑过渡到微服务。

最后但同样重要的是,如果系统设计得不好,过渡到微服务也不会神奇地解决其问题。更直白地说,分散一个混乱的系统只会导致更大的混乱。正如我们之前提到的,当系统的复杂性需要强加模块化时,应将微服务视为一种解决方案,而不是作为糟糕软件的神奇修复。

微服务与云

为了实现一个成功的微服务架构,我们需要尽可能自动化基础设施的大部分工作。最终,我们将处理一个包含大量独立服务,这些服务在网络中的某个地方运行的系统。手动维护这样的系统几乎是不可能的。

我们希望每个服务都能自动构建、测试、扩展和监控。云基础设施是一个自然的微服务环境,这允许你实现这一点。每个服务都可以在按需提供的资源上运行和扩展,可用的工具将允许我们以容错的方式构建、测试和连接服务。

你将在本书中学到所有这些内容。

是时候看看 Java EE 如何融入云微服务场景了。

Java EE 微服务

Java EE 应用程序的基本架构部分所述,在 Java EE 中,你通常创建包含应用程序的 JAR 文件,并在应用服务器上部署它们。在微服务中,我们希望将同类的 JAR 文件转换成可运行的服务:

图片

在传统场景中,应用服务器必须支持标准中指定的所有 API。

在微服务场景中,我们希望将每个 JAR(微服务的实现)转换成一个可运行的 JAR。这可以通过为给定的微服务创建一个运行时,并将这个运行时和服务的存档组装成一个可运行的 JAR 来实现。由于组装的运行时只为一个服务使用,我们不需要在其中包含所有的 Java EE 模块。构建你的微服务的工具将不得不分析你的服务存档并创建一个运行时,其中只包含它所需的那些功能。

我们已经概述了如何使用 Java EE 作为微服务架构的基础,但这样做的好处是什么?首先,您将能够立即利用经过验证的技术和您对它们的经验。此外,还有一个可移植性方面。正如我们在前面的章节中提到的,我们鼓励您从单体应用程序开始,并在必要时将其重构为微服务。由于两种情况下都使用了共同的技术集合和标准归档格式,您可以轻松地在两者之间迁移,创建一个在必要时可以更改和重构的弹性架构。

本书的目标

正如您将在本书中学到的,您可以使用现有的 Java EE 知识来创建微服务架构。然而,这种知识是不够的,因为正如我们在“微服务”部分中提到的,这种架构引入了自己的复杂性,必须加以处理。

本书的目标是通过向您提供关于在云基础设施上运行基于微服务的应用程序的实用、动手实践介绍,来填补这一知识空白。本书假设您熟悉 Java EE 以及传统的 Java EE 应用程序开发方式。它将通过提供一组具体工具的信息来补充这一知识,这些工具将使您能够立即利用云计算和微服务。

我们想强调的是,本书不宣传任何特定的方法,正如我们在“微服务”部分中提到的,任何架构决策都应基于具体项目,考虑到所有优点和缺点。我们的目标是为您提供一套工具,以便如果您决定进行这种转型,您将立即知道该怎么做。

在本书中,我们将开发一个示例应用程序,它将作为我们所有示例的基础。现在让我们更深入地了解它。

宠物店应用程序

计算机编程书籍通常从Hello World应用程序开始。同样,描述框架的书籍通常开发宠物店应用程序。我们将遵循这一传统。我们将开发的宠物店将是一个简单的应用程序,允许您浏览宠物目录,将一些宠物添加到购物车,并完成支付。

在应用程序的开发过程中,我们将专注于云和微服务方面。服务代码简单,使用基本的 Java EE 技术,以便读者可以专注于本书所教授的内容:云集成和微服务开发。

让我们从应用程序的宏观角度来审视:

图片

后端服务(红色)、网关(黄色)和安全服务器(蓝色)部署在云端。UI 应用程序(绿色)部署在云外。

网关服务负责为不同用户提供 API。客户网关为顾客提供一个 API,该 API 被实现商店接口的 web-client 宠物商店使用。客户网关协调对底层基础服务的调用,并且可以从云外访问。

安全服务负责 API 不同部分的访问认证和授权。它被所有其他组件使用。安全服务可以从云外访问。

核心功能由后端服务实现。后端服务不可从网关服务访问。让我们看看它们的函数:

  • 目录服务:提供商店中可用的宠物信息

  • 定价服务:负责提供特定宠物的价格

  • 购物车服务:负责维护特定客户的购物车信息

我们将在整本书中逐步开发应用程序。应用程序附在书中,因此,在学习书中描述的各种概念的同时,您可以立即与之一起工作。

使用的技术

我们将使用 WildFly Swarm 将我们的传统 Java EE JAR 转换为可运行的 JAR,WildFly Swarm 是我们在第二章,熟悉 WildFly Swarm中将要介绍的工具。WildFly Swarm 能够将我们的应用程序包装成一个包含所需的最小数量库的 JAR,有效地从可部署的 JAR 中创建微服务。我们将在第三章,调整应用程序大小中介绍 Swarm 是如何做到的,以及在第四章,调整您服务的配置中讨论如何配置创建的服务。

在编写服务之后,我们必须为它们编写测试。我们将使用 Arquillian 库来完成这项工作。我们将在第五章,使用 Arquillian 测试您的服务中讨论如何使用它。

我们将使用 OpenShift 在云中部署创建的服务。在第六章,使用 OpenShift 在云中部署应用程序中,我们将向您介绍平台、API 以及它提供的工具的理论介绍。在第七章,为您的应用程序配置持久存储中,我们将讨论如何在 OpenShift 上为我们的应用程序配置持久存储,以及如何扩展和连接我们的服务。

在云中创建和部署应用程序对于它们准备就绪并不足够。我们还需要确保它们的安全,监控它们,并处理网络故障。

为了提供安全,我们将利用 Keycloak 服务器。为了处理网络故障,我们将使用 Hystrix 库。为了提供监控。

摘要

本章旨在为您概述阅读本书可以期待的内容。

我们回顾了关于 Java EE 及其传统开发企业应用方式的基本信息。随后,我们介绍了软件开发中的现代趋势:云计算和微服务架构。

然后,我们介绍了宠物商店——我们将在这本书中开发的全书示例应用程序。

最后,我们介绍了书中使用到的所有技术和工具,例如 WildFly Swarm、OpenShift、Hystrix、Jenkins 和 Keycloak。

进一步阅读

  1. www.oracle.com/technetwork/java/javaee/overview/index.html

  2. www.youtube.com/watch?v=wgdBVIX9ifA

  3. martinfowler.com/bliki/MonolithFirst.html

第二章:熟悉 WildFly Swarm

在本章中,我们将介绍 WildFly——它如何与 Java EE 相关以及其主要功能。我们将介绍 WildFly Swarm——WildFly 的子项目——描述其目的,并展示如何使用它来开发微服务。我们将使用 Swarm 创建和部署我们的第一个应用程序。

介绍 WildFly

大多数人可能都听说过 JBoss 应用服务器;WildFly 是其继任者。它是对 Java EE 规范的开放源代码实现,更重要的是,在本书的上下文中,它是 Swarm 项目的基石。

WildFly 具有可扩展的架构,这使其能够在高性能核心之上构建不同大小的发行版,正如我们将在下一章中学习的,Swarm 在很大程度上利用了这一点。

性能

当你听到“Java EE 应用服务器”这个短语时,你可能会首先想到“重量级”这个词,因为应用服务器通常就是这样描述的。然而,值得注意的是,Java EE 规范并没有规定其实施必须是缓慢和臃肿的,实际上,许多现代应用服务器(包括 WildFly)确实没有遵循这个不存在的规则。

WildFly 启动仅需几秒钟,在资源使用方面进行了高度优化。你将在整本书中多次看到它。我们将运行多个基于 WildFly 的服务和测试,所有这些服务和测试都将立即启动,并且占用很小的内存。

可扩展性

如前所述,WildFly 的默认发行版是一个包含所有必要库的 Java EE 应用服务器。由于 WildFly 的可扩展性,你可以轻松创建自己的服务器发行版。你可以删除未使用的子系统;这里的一个好例子可能是 Web 配置文件,它只包含用于服务网页所需的子系统,可能被视为一个 Web 服务器。添加自己的扩展以提供额外功能也很容易。

正如你将在本书后面学到的那样,Swarm 利用了这两种能力的许多细节,自动裁剪服务器,使其仅使用你的服务所需的库,并提供了一组针对微服务的专用扩展。

无论你打算使用哪种发行版,无论是经过裁剪的 Web 服务器、扩展了你自己子系统的完整发行版,还是 Swarm 微服务,它都可以利用核心提供的所有功能,例如高性能、模块化类加载和成熟的管理层。

部署模型

应用服务器提供在 JVM 中协同定位的企业功能,这些功能可以被多租户应用程序使用。这些应用程序可以共享服务,实时部署和卸载,并在 JVM 中相互通信。

在这本书中,我们将专注于基于 Swarm 的微服务,但请注意,这是一个有效的架构模型,其好处在选择适合您所解决问题的正确架构风格时应予以考虑。

单体应用程序的组件位于同一个 JVM 中,并且它们可以在其边界内直接通信。在这样的应用程序中,你不必考虑分布式系统固有的许多问题。如果你决定分发你的应用程序,你必须注意网络故障、服务发现、监控服务可用性以及处理它们的故障,仅举几个问题。此外,在单体应用程序中,你可以使用现成的技术,如事务或安全,这些技术已经过彻底测试,并且已被证明可以很好地工作。

介绍 WildFly Swarm

如我们之前讨论的,应用程序服务器提供了在同一个实例中部署和管理多个应用程序的可能性。此外,Java EE 兼容的应用程序服务器提供了 Java EE 伞下所有规范的实现,以便每个符合该规范的应用程序都可以使用它。

这样的功能对于所有应用程序架构来说并不是必需的。在我们示例应用程序中开发的服务中,我们可能不太关心管理、热部署以及所有 Java EE 库的支持。原因是我们将开发小型专注的微服务。如果微服务被更新,我们只需终止其容器并重新启动其新版本。此外,在服务创建时,我们将能够确定它在操作期间将使用的所有库。正因为如此,我们才能仅使用那些必要的依赖项构建可执行的 JAR 文件,从而最小化运行时大小和内存使用。最适合这种目的的工具是 WildFly Swarm。

WildFly Swarm 是 WildFly 的一个子项目,其目标是使微服务应用程序开发变得简单。在我们更深入地了解 Swarm 行为之前,让我们通过我们的第一个Hello World JAX-RS Swarm 服务来感受一下它。

Java EE 应用程序

让我们创建一个简单的 Java EE 应用程序,其中包含一个 REST 资源,它使用GET方法来提供Hello world!消息:

package org.packt.swarm;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

//1
@Path("/")
public class HelloWorldResource {

 //2
    @GET
    //3
    @Path("hello")
 @Produces({ "text/plain" })
 public String hello() {
 return "Hello World!";
    }
}

在上面的列表中,我们创建了一个简单的资源,利用了 JAX-RS 注解;我们定义了整个类的主要路径(1),并创建了被GET(2)和Path(3)注解的"hello"方法,这样当在"/hello"路径上调用 HTML get 方法时,就会执行"hello"方法。

此外,我们必须在Path(1)根上定义应用程序以启动 Web 应用程序:

package org.packt.swarm;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

//1
@ApplicationPath("/")
public class HelloWorldApplication extends Application {
}

最后,我们必须配置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>

 <!-- 1 -->
 <groupId>org.packt.swarm</groupId>
    <artifactId>swarm-hello-world</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>

    (...)

 <!-- 2 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jboss.spec</groupId>
                <artifactId>jboss-javaee-7.0</artifactId>
                <version>${version.jboss.spec.javaee.7.0}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies> </dependencyManagement>

 <!-- 3 -->
 <dependencies>
        <dependency>
            <groupId>org.jboss.spec.javax.ws.rs</groupId>
            <artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
            <scope>provided</scope>
        </dependency> </dependencies>

    <build>
 <!-- 4 -->
 <plugins>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>${version.war.plugin}</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin> </plugins>
    </build>

</project>

我们正在使用战争类型(1)创建应用程序,以便它可以作为一个 Web 应用程序使用。我们正在引用 Java EE(2)和jaxrs API(3),这样我们就可以使用前面段落中提到的注解。最后,我们必须调整 war 插件,通知它我们不会使用web.xml文件。

就这样。这是一个简单的 REST HelloWorld 资源。我们现在将能够构建它并将其部署到 Java EE 应用程序服务器上。

适应 WildFly Swarm

现在我们都知道如何创建之前描述的 Java EE 应用程序,但我们是来这里学习如何使用 WildFly Swarm 的,所以让我们采用前面的应用程序来适应它。让我们卷起袖子,因为我们现在有一些艰苦的工作要做。

我们必须修改pom.xml

(...)

    <dependencies>
 <!-- 1 -->
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>jaxrs</artifactId>
            <version>${version.wildfly.swarm}</version> </dependency>
    </dependencies>

    <build>
        <plugins>
            (...)
 <!-- 2 -->
 <plugin>
                <groupId>org.wildfly.swarm</groupId>
                <artifactId>wildfly-swarm-plugin</artifactId>
                <version>${version.wildfly.swarm}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions> </plugin>
        </plugins>
    </build>

</project>

我们不得不向 Swarm 的 JAX-RS 模块(1)添加依赖项。这样的模块被称为分数,你将在下一章中了解更多关于它们的内容。请注意,我们不需要直接配置 JAX-RS API 依赖项,因为它将作为 JAX-RS 分数依赖项提供。

后来,我们不得不配置 WildFly Swarm 的 Maven 插件,该插件负责构建 Swarm 微服务(2)。你将在下一章中了解更多关于它的内容。

就这样。恭喜!你刚刚创建了你第一个 WildFly Swarm 应用程序。

示例参考:chapter2/swarm-hello-world(整个示例在附带的代码中可用,在目录:chapter2/swarm-hello-world目录中。)

这真的有效吗?

在我们更详细地查看发生了什么之前,让我们运行应用程序以证明它确实在运行。打开控制台,进入应用程序的根目录,并运行以下命令:

mvn wildfly-swarm:run

Maven 命令运行成功:

截图

swarm-hello-world 示例的控制台输出

我们可以打开网页浏览器并输入我们应用程序的地址,如下面的截图所示:

截图

这里发生了什么?

应用程序确实在运行。让我们一步一步地看看刚才发生了什么:

  1. Maven 构建已运行。一个标准的 maven 包插件创建了一个 war 存档,其中包含了之前描述的类(并且也可以部署到标准应用程序服务器)。

  2. Swarm 插件为我们的应用程序构建了一个运行时。这个运行时基于 WildFly-core,只包含服务应用程序所需的库。

  3. 插件构建了一个可运行的 JAR 文件,它结合了运行时和应用程序。

  4. 由于我们指定了运行目标,插件在创建后立即启动了服务。

让我们看一下目标目录,以记录构建输出:

截图

如您在前面的屏幕截图中所见,除了标准的 Maven 目标构件之外,还创建了一个额外的 JAR 文件:swarm-hello-world-1.0-swarm.jar。这是一个可运行的微服务。其名称由存档名称生成,并添加了 Swarm 后缀。此外,请注意,服务的大小为 47.5 MB。它比 WildFly 的 web 服务器配置略大。原因是必须添加一些额外的库(启用 REST 服务)到服务器中。

这个例子旨在让您对 WildFly Swarm 有一个初步的了解。如您所见,开发者的责任是实现业务功能并配置 Swarm Maven 插件。Swarm 负责其余工作:它创建了一个包含所有必要库的服务器,以使这些功能正常工作,并将存档与该服务器连接以创建一个可运行的微服务。由于这种约定优于配置的风格和自动服务器创建,许多配置负担从开发者那里移除,使他们可以专注于业务功能开发。显然,这种标准行为可以更改——您将在本书的后续部分了解更多相关信息。

摘要

本章的目标是向您介绍 WildFly Swarm——将其置于 WildFly 及其父项目的大背景下,展示其架构、特性和优势。最后,为了让您初步体验 WildFly Swarm,我们创建了一个简单的 Web 应用程序,并使用 Swarm 将其转换为一个微服务。在下一章中,我们将更深入地了解 WildFly Swarm 的模块化特性。

进一步阅读

    1. wildfly.org/

    2. wildfly-swarm.io/

第三章:适当调整服务大小

在本章中,你将了解 Swarm 如何仅使用对服务必要的依赖项来创建你的服务。你将更详细地了解什么是分数,Swarm 如何检测应该使用哪些分数,以及你如何修改分数发现行为。最后,你将了解如何使用空 JAR 和瘦 JAR 进一步修改服务创建的大小和行为。

在解释所有这些之前,我们将介绍我们将要工作的服务。

目录服务

在第一章中,你学习了宠物商店示例应用程序的基本架构以及构成它的服务。在本章和下一章中,我们将使用目录服务。为了回忆,这是负责提供商店中可用宠物信息的那个服务。现在我们将介绍这个简单的功能。在接下来的三个章节中,我们将修改这段代码,以展示 WildFly Swarm 的不同功能和配置选项。让我们看看初始版本。

草稿版本

我们将首先介绍服务的第一个草稿版本,我们将在稍后对其进行检查和扩展。

示例参考:chapter3/catalog-service-jaxrs

正如前一章所述,我们必须从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>org.packt.swarm.petstore</groupId>
    <artifactId>catalog-service-jaxrs</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>

    (...)

    <dependencies>
 <!-- 1 -->
 <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>jaxrs</artifactId>
            <version>${version.wildfly.swarm}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>${version.war.plugin}</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
 <!-- 2 -->
 <plugin>
                <groupId>org.wildfly.swarm</groupId>
                <artifactId>wildfly-swarm-plugin</artifactId>
                <version>${version.wildfly.swarm}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions> </plugin>
        </plugins>
    </build>

</project>

我们必须添加 JAX-RS 分数的依赖项(1)并配置 WildFly Swarm 插件(2)。现在让我们转向代码。

我们将从一个简单的域类Item开始,它包含关于商店中可用的宠物的信息:

package org.packt.swarm.petstore.catalog.model;

public class Item {

    private String itemId;
    private String name;
    private int quantity;

    private String description;

    public String getItemId() {
        return itemId;
    }

    public void setItemId(String itemId) {
        this.itemId = itemId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getQuantity() {
        return quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

如前述代码所示,这是一个简单的类,包含itemIdname、宠物的描述和商店中可用的数量。正如在Hello World示例中一样,我们必须初始化我们的 JAX-RS 应用程序:

package org.packt.swarm.petstore.catalog;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/")
public class CatalogApplication extends Application {
}

最后,我们准备好编写一个简单的 JAX-RS 资源,该资源将从内存中的HashMap提供有关可用宠物的信息:

package org.packt.swarm.petstore.catalog;

import org.packt.swarm.petstore.catalog.model.Item;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.Map;

//1
@Path("/")
public class CatalogResource {

 //2
 private Map<String, Item> catalog = new HashMap<>();

 public CatalogResource(){
 Item turtle = new Item();
        turtle.setItemId("turtle");
        turtle.setName("turtle");
        turtle.setQuantity(5);
        turtle.setDescription("Slow, friendly reptile. Let your busy self see how it spends 100 years of his life laying on sand and swimming.");
        catalog.put("turtle", turtle);
    }

 //3
 @GET
    @Path("item/{itemId}")
 @Produces(MediaType.APPLICATION_JSON)
 public Response searchById(@PathParam("itemId") String itemId) {
 try {
 Item item = catalog.get(itemId);
            return Response.ok(item).build();
        } catch (Exception e) {
 return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
        }
 }

}

我们的资源位于应用程序的根路径(1)。在第一个版本中,我们将目录实现为一个HashMap,并用第一个宠物turtle填充它(2)。当使用"item"地址和itemId参数调用GET方法时,将调用searchById方法(3)。

我们可以像第一章中那样构建和部署应用程序:

mvn wildfly-swarm:run

如果我们在网络浏览器中输入目录服务的地址,我们就能在目录中找到我们的第一个宠物:

图片

分数

在前一个例子中,我们做了以下操作:我们用 JAX-RS 注解注解了我们的类,使用 Swarm Maven 插件构建了代码,并获得了可运行的基于 Swarm 的 JAR。生成的 JAR 比完整的应用程序服务器小得多。这是因为 Swarm 只包装了我们需要的 WildFly 的部分来工作。现在,我们将更详细地研究这个声明。

让我们再次运行前一章创建的应用程序:

mvn wildfly-swarm:run

让我们看看控制台输出的开头:

看看红色矩形中的日志行。Swarm 在通知我们它已安装了四个分数:JAX-RS、Undertow、Elytron 和 Logging。然而,这意味着什么,实际上分数是什么?

分数是应用程序所需功能的一部分。更准确地说,分数收集了企业功能某部分工作所需的代码和配置。

由于我们在服务中使用了 JAX-RS,我们添加了 JAX-RS 分数作为 Maven 依赖项。回想一下,这是pom.xml中的以下依赖项:

(...) <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>jaxrs</artifactId>
            <version>${version.wildfly.swarm}</version>
        </dependency> (...)

因此,Swarm 构建了一个包含此分数的服务。然而,再次查看前面的截图,我们可以看到 JAX-RS 并不是唯一安装的分数,因为还有 Undertow、Elytron 和 Logging 分数存在。

日志分数存在的原因是,有一些分数对于所有配置都是必要的——日志就是其中之一。那么 Undertow 分数呢?分数可以依赖于其他分数。正如你可能知道的,JAX-RS 需要使用一个网络服务器来服务它生成的网页,因此,JAX-RS 分数需要依赖于 Undertow 插件。Swarm 发现我们正在使用 JAX-RS,因此它将其包含在生成的应用程序中,但它还必须分析该分数的依赖项。分析的结果显示,另一个分数,即 Undertow,也必须包含在内。同样,JAX-RS 和 Undertow 都依赖于负责实现安全的 Elytron 分数,因此它也被添加到了创建的服务中。

现在,让我们看看如果我们决定重构我们的目录服务并使用 CDI 会发生什么:

示例参考:chapter3/catalog-service-jaxrs-cdi/

让我们将搜索功能从 JAX-RS 资源移动到 CDI 服务:

package org.packt.swarm.petstore.catalog;

import org.packt.swarm.petstore.catalog.model.Item;

import javax.enterprise.context.ApplicationScoped;
import java.util.HashMap;
import java.util.Map;

//1
@ApplicationScoped
public class CatalogService {

    private Map<String, Item> catalog = new HashMap<>();

    public CatalogService(){
        Item turtle = new Item();
        turtle.setItemId("turtle");
        turtle.setName("turtle");
        turtle.setQuantity(5);
        turtle.setDescription("Slow, friendly reptile. Let your busy self see how it spends 100 years of his life laying on sand and swimming.");
        catalog.put("turtle", turtle);
    }

 //2
 public Item searchById(String itemId){
 return catalog.get(itemId);
    }

}

我们创建了一个应用程序范围的 bean(1)并将Search方法作为其 API 的一部分(2)。此外,我们还需要修改CatalogResource

package org.packt.swarm.petstore.catalog;

import org.packt.swarm.petstore.catalog.model.Item;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/")
public class CatalogResource {

 //1
 @Inject
    private CatalogService catalogService;

    @GET
    @Path("item/{itemId}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response searchByName(@PathParam("itemId") String itemId) {
        try {
 //2
 Item item = catalogService.searchById(itemId);
            return Response.ok(item).build();
        } catch (Exception e) {
            return
Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
        }
    }

}

我们向其中注入了我们刚刚创建的CatalogService(1)并使用它来查找宠物(2)。最后,我们必须修改pom.xml

(...)

    <dependencies>
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>jaxrs</artifactId>
            <version>${version.wildfly.swarm}</version>
        </dependency>
 <!-- 1 -->
 <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>cdi</artifactId>
            <version>${version.wildfly.swarm}</version> </dependency>

    </dependencies>

(...)

我们必须添加 Swarm 的 CDI 分数(2)。

在本章中提到的所有事情都完成后,我们可以构建我们的应用程序并看到与前面示例类似的结果。

让我们再次查看 WildFly-Swarm 插件的日志:

现在,我们有八个分数存在。除了前面应用程序中引入的 CDI、CDI-config、Bean ValidationTransactions之外,CDI、CDI-config、Bean ValidationTransactions也被添加了。再次,Swarm 扫描了应用程序并发现它依赖于 JAX-RS 和 CDI;它添加了那些分数及其所有依赖项。

如你或许已经注意到的,我们现在看到的分数与 Java EE 规范紧密相关。那么,我们可以把它们看作是添加到服务器核心的特定 Java EE 规范实现吗?不。因为我们已经知道,Swarm 基于 Java EE 服务器,其部分用途是使单体应用程序能够过渡到微服务,存在一大组分数映射到某些 Java EE 功能的实现。尽管如此,它们并不局限于这一点。还有另一组分数提供 Java EE 之外的功能。更重要的是,如果你在用例中需要,你也能够实现自己的分数。

查看 WildFly 的内部结构,了解 WildFly 插件是如何工作的,以便创建你的精简 Swarm 应用程序。让我们首先解释分数检测是如何工作的,以及你如何通过修改 Swarm 的配置参数来改变其行为。

分数检测

让我们回到我们最新的CatalogService。正如你所回忆的,它使用了 JAX-RS 和 CDI。我们通过编辑pom.xml文件手动提供了依赖项:

(...)

    <dependencies>
 <!-- 1 -->
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>jaxrs</artifactId>
            <version>${version.wildfly.swarm}</version>
        </dependency>

        <!-- 2 -->
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>cdi</artifactId>
            <version>${version.wildfly.swarm}</version> </dependency>
    </dependencies>

(...)

我们为两个分数提供了依赖项:JAX-RS(1)和 CDI(2)。我们可以运行应用程序并注意到它确实在运行。

让我们继续我们的实验。如果我们只配置一个分数会发生什么?

(...)
<dependencyManagement>
    <dependencies>
 <!-- 2 -->
 <dependency>
            <groupId>org.jboss.spec</groupId>
            <artifactId>jboss-javaee-7.0</artifactId>
            <version>${version.jboss.spec.javaee.7.0}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement> 

<dependencies>
 <!-- 1 -->
   <dependency>
       <groupId>org.wildfly.swarm</groupId>
       <artifactId>jaxrs</artifactId>
       <version>${version.wildfly.swarm}</version>
   </dependency>

   <!-- 2 -->
   <dependency>
       <groupId>javax.enterprise</groupId>
       <artifactId>cdi-api</artifactId>
       <scope>provided</scope> </dependency>
</dependencies>

(...)

在前面的代码中,只配置了 JAX-RS 依赖项(1)。请注意,在这种情况下,我们必须明确定义对 CDI-API 的依赖项(2)。当我们运行应用程序时,我们看到以下日志:

目前你还没有看到错误,但你的日志的前几行已经预示着问题将会发生。尽管使用了 CDI,但其分数(及其依赖项)尚未添加。如果我们打开浏览器并输入我们服务的地址,我们会看到一个错误的请求错误。在服务类中添加一个临时日志:

package org.packt.swarm.petstore.catalog;

import org.jboss.logging.Logger;
import org.packt.swarm.petstore.catalog.model.Item;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/")
public class CatalogResource {

    private final Logger log = Logger.getLogger(getClass());

    @Inject
    private CatalogService catalogService;

    @GET
    @Path("item/{itemId}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response searchByName(@PathParam("itemId") String itemId) {
        try {
            Item item = catalogService.searchById(itemId);
            return Response.ok(item).build();
        } catch (Exception e) {
            log.error("BAD REQUEST", e);
            return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
        }
    }

}

我们将能够注意到我们问题的原因:

ERROR [org.packt.swarm.petstore.catalog.CatalogResource] (default task-1) Bad request: java.lang.NullPointerException
        at org.packt.swarm.petstore.catalog.CatalogResource.searchByName(CatalogResource.java:27)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:140)
        at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:295)
        at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:249)
        at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:236)
        at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:406)
        at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:213)
        at org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:228)
        at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:56)
        at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:51)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)

由于缺少 CDI 分数,没有进行豆子解析和注入。因此,服务对象没有被注入到CatalogResource资源中,导致NullPointerException

让我们更进一步,移除所有的分数:


<dependencies>
 <!-- 2 -->
 <dependency>
 <groupId>org.jboss.spec.javax.ws.rs</groupId>
 <artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
 <scope>provided</scope>
 </dependency>    <dependency>
        <groupId>javax.enterprise</groupId>
        <artifactId>cdi-api</artifactId>
        <scope>provided</scope>
    </dependency>
 <!-- 1 -->
 <!-- no fractions here ... -->
    </dependencies>

(...)

我们已经移除了所有的分数(1)。请注意,在这种情况下,我们必须手动提供所有的 Java EE API(2)。

示例参考:chapter3/catalog-service-auto-detect/

当我们以这种方式配置项目进行构建时,日志中会出现一些有趣的内容:

在前面的例子中,Swarm 已经执行了自动分数检测。它是如何工作的?

Swarm 发现 org.packt.swarm.petstore.catalog.CatalogResource 正在使用 javax.ws.rs 包中的类,这导致了 JAX-RS 的包含。同样,使用 javax.inject 包导致了 CDI 分数的包含。后来,在手动示例中,Swarm 有一个包含检测到的分数、它们的依赖项以及始终需要的分数的构建服务。如果您现在运行该服务,您会注意到它确实正在正确工作。

为了理解 Swarm 在最近示例中的行为,我们必须了解分数检测模式。现在让我们来做这件事。

分数检测模式

Swarm Maven 插件可以在不同的分数检测模式下工作。如果您没有手动提供分数依赖项,它将在当缺失模式下运行。我们已经在之前的示例中看到了这种模式的行为:当没有直接提供分数依赖项时,插件会执行自动检测。另一方面,如果我们至少手动提供了一个分数依赖项,则自动检测模式将关闭。这就是为什么我们的最后一个示例没有包含 CDI 分数的原因:手动添加 JAX-RS 分数关闭了自动检测。

我们能对此做些什么吗?是的,我们可以使用不同的检测模式:force。此模式使自动检测每次都工作。在检测到使用的分数后,它将检测结果与用户配置的分数合并。

示例参考:chapter3/catalog-service-force-detect.

让我们重新配置我们的示例以使其工作:

(...)

    <dependencies>
        <dependency>
            <groupId>javax.enterprise</groupId>
            <artifactId>cdi-api</artifactId>
            <scope>provided</scope>
        </dependency>
 <!-- 1 -->
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>jaxrs</artifactId>
            <version>${version.wildfly.swarm}</version> </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>${version.war.plugin}</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.wildfly.swarm</groupId>
                <artifactId>wildfly-swarm-plugin</artifactId>
                <version>${version.wildfly.swarm}</version>
 <!-- 2 -->
 <configuration>
                    <fractionDetectMode>force</fractionDetectMode> </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

再次,只配置了 JAX-RS 分数(1);然而,因为我们已经使用 force 检测模式配置了 Maven 插件(2),Swarm 也会检测之前缺失的 CDI 分数。如果我们再次运行我们的应用程序,我们会看到所有必要的分数都被检测到,并且应用程序可以正常工作。

我们已经看到了两种分数检测模式:当缺失和 force。还有其他吗?是的,还有一个:never。在此模式下,正如其名称所暗示的,分数永远不会被检测到,您必须始终手动提供所有这些。

薄的和空壳 JAR

正如我们之前所说的,在标准的 Maven 插件操作期间,生成的应用程序包含 Swarm 服务器和部署在其上的应用程序。我们可以改变这种行为。让我们假设我们将应用程序部署在云中,然后稍后推送新的代码更改。由于大多数情况下是应用程序代码发生变化,我们希望创建一个包含服务器的容器,并将其部署在云中,然后只推送代码到它。我们如何做到这一点?通过使用空壳 JAR。

使用空壳 JAR

您可以配置 Maven 插件来构建空壳 JAR,其中包含 swarm 服务器,但上面没有实际部署的应用程序。让我们再次回到 JAX-RS + CDI 示例,以展示它是如何工作的。

示例参考:chapter3/catalog-service-hollow-jar.

我们首先需要做的是配置 Maven 插件:

(...)

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>${version.war.plugin}</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.wildfly.swarm</groupId>
                <artifactId>wildfly-swarm-plugin</artifactId>
                <version>${version.wildfly.swarm}</version>
 <!-- 1 -->
                <configuration>
                    <hollow>true</hollow> </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
(...)

我们唯一需要做的就是启用空心配置参数(1)。当我们构建应用程序并导航到目标目录时,我们将看到以下输出:

图片

如您在前面的屏幕截图中所见,一个目录以 -hollow-swarm 后缀结尾。这是我们不带部署应用程序的空心 JAR。在运行它时,我们必须提供将要部署在创建的服务器上的应用程序名称。我们将会以下这种方式做到:

java jar catalog-1.0-hollow-swarm.jar catalog-1.0.war

这将启动容器并运行我们的应用程序。结果,它将以与原始示例相同的方式运行。

使用瘦 JAR

您将能够创建一个瘦 JAR。瘦 JAR 不包含其 Maven 依赖项,并在应用程序启动期间从本地或远程 Maven 仓库加载它们。

示例参考:chapter3/catalog-service-thin-jar

让我们来看一个例子:

(...)

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>${version.war.plugin}</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.wildfly.swarm</groupId>
                <artifactId>wildfly-swarm-plugin</artifactId>
                <version>${version.wildfly.swarm}</version>
 <!-- 1 -->
                <configuration>
                    <bundleDependencies>false</bundleDependencies> </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

(...)

当我们构建应用程序并查看目标目录时,我们得到以下结果:

图片

注意,在前面的场景中,所有的 JAR 文件都非常小,可运行的 JAR 文件大小为 744 KB。

您还有可能将瘦 JAR 与空心 JAR 混合。可运行的 JAR 不包含必须部署在其上的应用程序,因此它必须以与前面示例相同的方式运行:

java jar catalog-1.0-hollow-swarm.jar catalog-1.0.war

服务器和部署都不包含捆绑的依赖项,因此它们必须通过应用程序部署从 Maven 仓库加载。

摘要

在本章中,您学习了 Swarm 如何创建可运行、大小合适的服务。您学习了什么是分数,分数检测过程是什么样的,以及您如何修改它。最后,您学习了如何创建空心和瘦 JAR。

在下一章中,我们将向您展示如何配置您的微服务。

第四章:调整您服务的配置

在本章中,你将学习如何配置你的 Swarm 服务。我们将展示不同配置工具的实际示例,以及你如何使用它们来引导应用程序的行为。

修改 Swarm 配置

Swarm 中可用的分数都带有合理的默认值。在我们迄今为止看到的示例中,我们没有触摸任何配置,但我们仍然能够看到应用程序在工作。现在,我们将向你展示如何调整 Swarm 创建的服务配置。

Swarm 提供了一套工具,允许你修改应用程序的配置。在下一节中,我们将逐一介绍它们,并展示它们在不同场景中的使用。让我们从最简单的一个开始:系统属性。

系统属性

你可以通过指定系统属性来修改配置。让我们回到我们的 catalog-service。正如你在上一章的 catalog-service 示例中所看到的,JAX-RS 应用程序正在监听 8080 端口的 HTTP 请求,这是默认配置。让我们假设我们想要更改该端口。

我们必须做的是在应用程序执行期间指定swarm.http.port属性,如下所示:

mvn clean wildfly-swarm:run -Dswarm.http.port=12345

当运行网络浏览器时,我们可以看到,确实,应用程序运行的端口已经发生了变化:

图片

那么,这里发生了什么?Undertow 分数发现有一个配置属性覆盖了标准的 HTTP 端口,并相应地修改了套接字配置。结果,运行中的应用程序正在使用指定的端口。

每个分数包含一组可以用来配置它的属性。你将能在 Swarm 文档中找到它们。

编辑属性的方法非常简单,在许多情况下可能足够,但更复杂的程序化配置的入口点可能更可行,让我们学习如何做到这一点。

实现自己的main

每个 Swarm 服务都包含一个main类,该类负责为服务创建和配置运行时,并在其上运行服务代码。Swarm 创建了main类的默认实现(实际上,到目前为止所有示例都使用了默认类),但如果你想要修改默认行为,你可以提供自己的Main类实现。这种修改的一个例子可能是提供额外的配置。

让我们回到 catalog-service。让我们回顾一下它的当前操作:我们创建了一个jaxrs资源,并使用 CDI 注入了提供邀请消息的服务。现在,让我们修改这个示例以提供我们自己的main类。

示例参考:chapter4/catalog-service-first-main

为了做到这一点,我们必须按照以下方式修改 catalog-service 的pom.xml文件:

(...)

    <dependencies>
 <!-- 2 -->
 <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>jaxrs</artifactId>
            <version>${version.wildfly.swarm}</version>
        </dependency>
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>cdi</artifactId>
            <version>${version.wildfly.swarm}</version> </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>${version.war.plugin}</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.wildfly.swarm</groupId>
                <artifactId>wildfly-swarm-plugin</artifactId>
                <version>${version.wildfly.swarm}</version>
 <!-- 1 -->
 <configuration>
                    <mainClass>org.packt.swarm.petstore.catalog.Main</mainClass> </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

我们必须修改 Swarm 插件,使其配置包含具有我们main方法的类(1)。当使用自己的main方法时,你必须手动指定你的服务依赖于哪些部分(2)。

现在,让我们看看实现了main方法的org.packt.swarm.petstore.Main类:

package org.packt.swarm.petstore.catalog;

import org.jboss.logging.Logger;
import org.wildfly.swarm.Swarm;

public class Main {

    public static void main(String[] args) throws Exception {
 //1
        new Swarm().start().deploy();
        //2
        Logger.getLogger(Main.class).info("I'M HERE!");
    }
}

我们创建了org.wildfly.swarm.Swarm类的实例(1)。start方法创建了容器,deploy方法将创建的存档部署到容器上。我们还创建了(2)日志输出以证明该类确实在运行。我们将在稍后更详细地查看Swarm类,但在那之前,这里是有提到的证明:

图片

消息已经存在,方法已经执行。

Swarm 类

正如我们在前面的章节中看到的,如果你正在实现自己的main方法,你将与org.wildfly.swarm.Swarm类进行交互。这个类负责根据提供的配置实例化容器,并创建和部署包含你的应用程序的存档。这两个步骤都可以通过Swarm类的操作进行修改。让我们更深入地了解它们。

提供配置

Swarm类提供了一组方法,允许你使用 Java API 修改配置,例如fractionsocketBindingoutboundSocketBinding。后两个方法,正如它们的名称所暗示的,允许你创建自己的套接字绑定和出站套接字绑定组。对我们来说最有趣的方法是fraction方法。它接受一个参数,即org.wildfly.swarm.spi.api.Fraction类实现的fraction。你将能够修改和重新配置所有部分,并将它们提供给 Swarm。让我们通过我们最喜欢的示例,即更改CatalogService的 HTTP 端口,来初步了解这个功能。

示例参考:chapter4/catalog-service-config-main

首先,我们必须将UndertowFraction依赖项添加到我们的pom.xml中:

(...)

    <dependencies>
        <dependency>
            <groupId>org.jboss.spec.javax.ws.rs</groupId>
            <artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.enterprise</groupId>
            <artifactId>cdi-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>jaxrs</artifactId>
            <version>${version.wildfly.swarm}</version>
        </dependency>
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>cdi</artifactId>
            <version>${version.wildfly.swarm}</version>
        </dependency>
 <!-- 1 -->
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>undertow</artifactId>
            <version>${version.wildfly.swarm}</version> </dependency>
        <dependency>
            <groupId>org.jboss.logging</groupId>
            <artifactId>jboss-logging</artifactId>
            <version>3.3.0.Final</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>

(...)

其次,让我们重新实现main方法:

package org.packt.swarm.petstore.catalog;

import org.wildfly.swarm.Swarm;
import org.wildfly.swarm.undertow.UndertowFraction;

public class Main {

    public static void main(String[] args) throws Exception {
 //1
        UndertowFraction undertowFraction = new UndertowFraction();
        //2
        undertowFraction.applyDefaults();
        //3
        undertowFraction.httpPort(12345);
        //4
        Swarm swarm = new Swarm();
        //5
        swarm.fraction(undertowFraction);
        //6
        swarm.start().deploy();
    }
}

如果你运行前面的代码,你确实会看到与属性示例中相同的结果:应用程序正在12345端口上运行。那么,刚才发生了什么?

在前一段代码的开始部分,我们创建了UndertowFraction(1)并运行了applyDefaults方法(2)。如果fraction是由 Swarm 自动创建的,则默认配置将应用于它。另一方面,如果你手动创建fraction,你将创建一个没有任何配置的空fraction对象。这就是applyDefaults方法的作用。它将默认配置应用于fraction对象。因此,每次你不想从头开始创建配置而只是修改它时,你必须首先调用applyDefaults方法,然后在此之后应用你的配置更改。这正是我们简单示例中的情况。我们不想手动创建完整的配置。相反,我们只想更改一个配置参数——监听端口。因此,我们将默认配置应用于fraction对象,然后只更改了 HTTP 端口。

我们创建了代表 Undertow 分片配置的UndertowFraction对象。我们必须将此配置提供给将运行服务的容器。为了做到这一点,我们使用了 Swarm 的fraction方法(4)。这里值得提一下,应用程序仍然由许多fractions 组成,但我们只提供了Undertowfraction配置。如果我们不向Swarm类添加自定义的fraction配置,则将使用默认配置。Swarm 仍然会启动 CDI 和 JAX-RS 等,但它们的配置将自动创建,就像我们第一个例子中那样。另一方面,Undertowconfiguration对象是由我们手动提供的,Swarm 将使用它。

在应用程序配置完成后,我们准备启动和部署(5)它,就像我们在前面的例子中所做的那样。如果我们运行我们的应用程序,我们将看到我们在使用系统属性的那个例子中获得的结果——应用程序在端口12345上运行。

然而,在属性示例中,我们只需要添加一个配置参数,而在这里,我们必须做很多事情。你可能想知道是否可以使用 Java API 提供更详细的配置,但在像 HTTP 端口这样的情况下仍然求助于属性;这是一个好问题。让我们找出答案。

使用自己的主程序以及属性

让我们将Main类修改为最简单的形式:

package org.packt.swarm.petstore;

import org.jboss.logging.Logger;
import org.wildfly.swarm.Swarm;

public class Main {

    public static void main(String[] args) throws Exception {
        new Swarm().start().deploy();
    }
}

然后,使用 HTTP 端口属性运行它:

mvn clean wildfly-swarm:run -Dswarm.http.port=12345

此外,我们将在浏览器中检查:

图片

嗯,它不起作用。所以,正如刚刚发生的那样,你无法做到这一点,很抱歉。

我当然是在开玩笑。你可以做到,但正如我们所知,我们完全意外地在上一列表中的代码中犯了一个小错误。问题出在哪里?执行main方法时使用的系统属性没有以任何方式传播到 Swarm。考虑另一方面,我们是这样编写我们的代码的:

package org.packt.swarm.petstore;

import org.jboss.logging.Logger;
import org.wildfly.swarm.Swarm;

public class Main {

 public static void main(String[] args) throws Exception {
 //1
 new Swarm(args).start().deploy();
 Logger.getLogger(Main.class).info("I'M HERE!");
 }
}

应用程序将使用指定的属性并展示应用程序的行为,我们将能够看到它是否正在正确运行。

总结来说,你现在可以将 Java API 与基于属性的配置混合使用,但必须记住使用带有main函数参数创建 Swarm。

Java API

让我们回到Swarm类。我们已经看到,我们能够使用自己的配置创建分数类并将其传递给Swarm类。实际上,我们能够通过编程方式控制整个 Swarm 配置。为了创建一个更详细的示例,让我们扩展我们的CatalogService,使其将数据存储在数据库中。

示例参考:chapter4/catalog-service-database

让我们从编辑pom.xml开始:

(...)

    <properties>
        (...)
 <version.hibernate.api>1.0.0.Final</version.hibernate.api>
        <version.h2>1.4.187</version.h2>
    </properties>

    (...)

    <dependencies>
        (...)
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>cdi</artifactId>
            <version>${version.wildfly.swarm}</version>
        </dependency>
 //1
 <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>datasources</artifactId>
            <version>${version.wildfly.swarm}</version>
        </dependency>
        //2
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>jpa</artifactId>
            <version>${version.wildfly.swarm}</version>
        </dependency>
        //3
        <dependency>
            <groupId>org.hibernate.javax.persistence</groupId>
            <artifactId>hibernate-jpa-2.1-api</artifactId>
            <version>${version.hibernate.api}</version>
        </dependency>
        //4
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>${version.h2}</version>
        </dependency> 
    </dependencies>

   (...)

</project>

我们添加了四个新的 Maven 依赖项。为了配置我们自己的datasource,我们必须添加datasource分数(1)。由于我们将使用 Java 持久化 API,我们需要jpa分数和 JPA API(2)。我们还将使用h2内存数据库,并且需要它的dependency(3)。最后,我们提供了dependencyh2数据库(4)。

由于我们打算持久化商店中可用的宠物数据,我们必须修改Item类,使其成为一个实体,一个表示将在关系数据库中持久化的状态的 JPA 对象:

package org.packt.swarm.petstore.catalog.model;

import com.fasterxml.jackson.annotation.JsonIgnore;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;

//1
@Entity
//2
@Table(name = "item")
//3
@NamedQueries({
 @NamedQuery(name="Item.findById",
                query="SELECT i FROM Item i WHERE i.itemId = :itemId"),
})
public class Item {

 //4
    @Id
    @JsonIgnore
    private int id;

    //5
    @Column(length = 30)
 private String itemId;

    //6
    @Column(length = 30)
 private String name;
    @Column
    private int quantity;

    @Column
    private String description;

    public String getItemId() {
        return itemId;
    }

    public void setItemId(String itemId) {
        this.itemId = itemId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getQuantity() {
        return quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

这是一个简单的jpa实体(1),对应的表名为"ITEM"(2)。我们创建了NamedQuery(3)来通过name查找宠物。我们添加了数据库 ID 字段(4)。此外,我们还添加了@Column注解,以便namequantity字段被持久化到数据库中(5)。

我们还必须修改我们的CatalogService类,使其能够从数据库中加载数据:

package org.packt.swarm.petstore.catalog;

import org.packt.swarm.petstore.catalog.model.Item;

import javax.enterprise.context.ApplicationScoped;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@ApplicationScoped
public class CatalogService {

 //1
 @PersistenceContext(unitName = "CatalogPU")
 private EntityManager em;

 //2

 public Item searchById(String itemId) {
 return em.createNamedQuery("Item.findById", Item.class).setParameter("itemId", itemId).getSingleResult();
>    }

}

我们引用了CatalogPU持久化上下文(我们将在稍后进行配置)并使用在Item类中定义的命名查询通过id查找宠物(2)。

好的,让我们转到有趣的部分。我们将创建并使用内存中的h2数据源;以下是如何做到这一点的代码:

package org.packt.swarm.petstore.catalog;

import org.wildfly.swarm.Swarm;
import org.wildfly.swarm.datasources.DatasourcesFraction;

public class Main {

    public static void main(String[] args) throws Exception {
        DatasourcesFraction datasourcesFraction = new DatasourcesFraction()
 //1
                .jdbcDriver("h2", (d) -> {
 d.driverClassName("org.h2.Driver");
                    d.xaDatasourceClass("org.h2.jdbcx.JdbcDataSource");
                    d.driverModuleName("com.h2database.h2");
                }) //2
                .dataSource("CatalogDS", (ds) -> {
 ds.driverName("h2");
                    ds.connectionUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
                    ds.userName("sa");
                    ds.password("sa");
                });

        Swarm swarm = new Swarm();
        swarm.fraction(datasourcesFraction);
        swarm.start().deploy();
    }
}

datasourcesFraction的配置比简单的端口更改要复杂一些,让我们更详细地看看。在(1)中,我们定义了一个名为"h2"Java 数据库连接(JDBC)驱动程序,并提供了实现org.wildfly.swarm.config.JDBCDriverConsumer类的 lambda 表达式——这基本上是一个接受者,允许你将额外的配置应用到创建的 JDBC 驱动程序上。在(2)中发生类似的情况。在这里,我们创建了CatalogDS数据源,并使用org.wildfly.swarm.config.DatasourcesConsumer类应用了额外的配置。

如前述代码所示,这个配置并不像Undertowport更改那样简单,但不用担心。Swarm 在每个版本中都附带当前的 Java API 库,并且由于所有配置选项都在那里描述,你不需要在配置应用程序时依赖猜测。[1]

我们仍然需要做更多的事情来使我们的示例工作,比如提供persistence.xml并在启动时填充数据库中的一组消息。

让我们从第一件事开始。以下是我们persistence.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<persistence
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        version="2.1"
        xmlns="http://xmlns.jcp.org/xml/ns/persistence"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
 <!-- 1 -->
    <persistence-unit name="CatalogPU" transaction-type="JTA">
        <!-- 2 -->
        <jta-data-source>java:jboss/datasources/CatalogDS</jta-data-source>
        <properties>
            <!-- 3 -->
            <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
            <property name="javax.persistence.schema-generation.create-source" value="metadata"/>
            <property name="javax.persistence.schema-generation.drop-source" value="metadata"/>
            <!-- 4 -->
            <property name="javax.persistence.sql-load-script-source" value="META-INF/load.sql"/>
        </properties>
    </persistence-unit>
</persistence>

在前述配置中,我们创建了一个名为CatalogPU的持久单元,它使用JTA事务(1),使持久单元使用之前创建的CatalogDS数据源(2),提供了一个配置,将使数据库在部署时创建新数据库,在卸载时使用实体类元数据删除它(3),最后,提供了加载脚本(4)。

问题是我们还没有它;让我们先添加它:

INSERT INTO ITEM(id, itemId, name, description, quantity) VALUES (1, 'turtle', 'turtle',  'Slow friendly reptile. Let your busy self see how it spends a hundred years of his life laying on sand and swimming.', 5);
INSERT INTO ITEM(id, itemId, name, description, quantity) VALUES (2, 'hamster', 'hamster', 'Energetic rodent - great as a first pet. Will be your only inmate that takes his fitness training serviously.', 10);
INSERT INTO ITEM(id, itemId, name, description, quantity) VALUES (3, 'goldfish', 'goldfish', 'With its beauty it will be the decoration of you aquarium. Likes gourmet fish feed and postmodern poetry.', 3);
INSERT INTO ITEM(id, itemId, name, description, quantity) VALUES (4, 'lion', 'lion', 'Big cat with fancy mane. Loves playing the tag and cuddling with other animals and people.', 9);

在所有这些工作都最终完成之后,我们应该能够看到我们的应用程序正在运行。现在让我们试试:

图片

哎呀!不是浏览器页面上的消息,而是出现了一个可怕的红色日志。出了什么问题?让我们看看第一条读取消息:"WFLYJCA0041: Failed to load module for driver [com.h2database.h2]"。确实如此,因为这个是一个自定义驱动模块,我们必须手动将其添加到我们的应用程序中。我们如何做到这一点呢?其实很简单。

要将一个额外的自定义模块添加到我们的应用程序中,我们必须将其添加到应用程序的resources目录:

图片

如前一个屏幕截图所示,modules目录必须放置在我们的应用程序中 Maven 的resources目录内部,并且目录结构必须与模块名称匹配。让我们看看模块描述符:

<?xml version="1.0" encoding="UTF-8"?>
<!-- 1 -->
<module  name="com.h2database.h2">

  <resources>
    <!-- 2 -->
    <artifact name="com.h2database:h2:1.4.187"/>
  </resources>
  <!-- 3 -->
  <dependencies>
    <module name="javax.api"/>
    <module name="javax.transaction.api"/>
    <module name="javax.servlet.api" optional="true"/>
 </dependencies>
</module>

为了回忆,这和我们之前在第二章《熟悉 WildFly Swarm》中介绍的是同一种描述符,我们描述了模块化类加载的概念。在上一个文件中,我们创建了一个名为"com.h2database.h2"的模块(1),指定唯一资源是h2数据库组件。请注意,我们使用 Maven 坐标引用了该组件。最后,我们必须指定所有模块依赖项(3)。

让我们再次构建并运行应用程序。现在我们确实能够查找我们的宠物了:

图片

我们现在确实能够通过id搜索宠物了。

让我们继续使用Swarm类的用法。接下来我们将查看它的deploy方法。

修改你的存档

在我们之前的示例中,每次我们创建Swarm实例并在其上应用一些配置时,我们都使用了无参数的deploy方法。该方法接受由标准 Maven 构建生成的存档,并将其部署到之前配置的容器上。尽管如此,deploy方法不止这一种版本。你可以创建自己的存档(或存档),并将它们部署到 Swarm 容器中。如何做到?可以使用ShrinkWrap API。

ShrinkWrap API

如果你曾经与 WildFly AS 一起工作过,尤其是它的测试框架 Arquillian,你可能也对ShrinkWrap API 很熟悉,该 API 用于在测试环境中部署应用程序存档之前构建应用程序存档。然而,如果你从未使用过它,不要担心——API 非常简单直观。

API 中的核心类是org.jboss.shrinkwrap.api.Archive实例。它是一个抽象类,代表存档。我们最感兴趣的实体实现是org.jboss.shrinkwrap.api.spec.JavaArchiveorg.jboss.shrinkwrap.api.spec.WebArchive,正如你可能猜到的,它们代表 JAR 和 WAR。API 很简单;它包含了一组方法,允许你向存档中添加资源。让我们看看它在实际操作中的表现。

为了这个示例,让我们回到第一个CatalogService版本,它只包含jaxrs资源和应用程序。

示例参考:chapter4/catalog-service-shrinkwrap

要看到ShrinkWrap的实际应用,我们必须修改pom.xml文件:

(...)

    <dependencies>
        <dependency>
            <groupId>org.jboss.spec.javax.ws.rs</groupId>
            <artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
            <scope>provided</scope>
        </dependency>
 <!-- 1 -->
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>jaxrs</artifactId>
            <version>${version.wildfly.swarm}</version> </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>${version.war.plugin}</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.wildfly.swarm</groupId>
                <artifactId>wildfly-swarm-plugin</artifactId>
                <version>${version.wildfly.swarm}</version>
 <!-- 2 -->
                <configuration>
                    <mainClass>org.packt.swarm.petstore.catalog.Main</mainClass> </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

(...)

由于我们提供了自己的main方法,我们必须显式添加对jaxrs分片的依赖(1)。我们还需要将方法添加到 Swarm 插件配置(2)中。

让我们看看ShrinkWrap API 在org.packt.swarm.petstore.Main类中的使用:

package org.packt.swarm.petstore.catalog;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.wildfly.swarm.Swarm;
import org.wildfly.swarm.jaxrs.JAXRSArchive;

public class Main {

    public static void main(String[] args) throws Exception {

        Swarm swarm = new Swarm();
        swarm.start();

 //1
        JAXRSArchive deployment = ShrinkWrap.create(JAXRSArchive.class, "deployment.war");
        //2
        deployment.addClasses(CatalogApplication.class, CatalogResource.class, Item.class);
        swarm.deploy(deployment);
    }
}

我们创建了 Web 存档(1),添加了我们的示例所包含的类(2),并将它们部署到创建的容器上(3)。结果,我们手动完成了 Swarm 为我们自动完成的事情。

我们已经使用了addClass方法将创建的类添加到存档中。以类似的方式,你也能够使用其他ShrinkWrap API 方法。org.jboss.shrinkwrap.api.spec.JavaArchive类除了包含原生存档方法(addaddDirectory)外,还包含使处理类(addClassaddPackage)、资源(addResource)和清单(setManifestaddManifestResource)变得容易的方法。org.jboss.shrinkwrap.api.spec.WebArchive类还额外增加了网络资源方法(addWebResourcesetWebXML)。与前面的例子一样,使用这些方法通常很简单,但在任何疑问的情况下,你可以利用ShrinkWrap Java API。

获取默认存档

ShrinkWrap不是太繁琐,以至于在现实生活中的任何情况下都派得上用场吗?毕竟,我们不想手动将应用程序中的所有类和资源添加到存档中。你不必担心这个问题——你将能够从 Swarm 实例中获取默认部署:

package org.packt.swarm.petstore.catalog;

import org.jboss.shrinkwrap.api.Archive;
import org.wildfly.swarm.Swarm;

public class Main {

 public static void main(String[] args) throws Exception {

 Swarm swarm = new Swarm();
 swarm.start();

 //1
 Archive<?> deployment = swarm.createDefaultDeployment();
 swarm.deploy(deployment);

 }

}

正如前一个例子所示,我们通过调用createDefaultDeployment()方法获得了默认部署。在获得它之后,我们只能向其中添加额外的所需资源。

Swarm ShrinkWrap 扩展

Swarm 添加了自己的类来补充ShrinkWrap API。让我们来介绍它们。

JARArchive

org.wildfly.swarm.spi.api.JARArchiveJavaArchive的替代品。除了它提供的所有功能外,JARArchive还增加了一个 API,可以轻松添加模块、Maven 依赖项和服务提供者实现。

WARArchive

由于WebArchiveJavaArchive的基础上增加了功能,WARArchive则在JARArchive的基础上增加了新特性。除了提供一个用于处理网络资源的接口外,它还增加了轻松添加静态网络内容的功能。让我们通过一个例子来看看。

如同往常,我们需要pom.xml文件:

(...)

    <!-- 1 -->
    <dependencies>
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>undertow</artifactId>
            <version>${version.wildfly.swarm}</version>
        </dependency>
 </dependencies>

(...)

由于我们正在使用自己的main,我们需要添加一个undertow分数依赖(1)并配置main方法(2)。

我们的静态内容将是一个简单的 Hello World 页面:

<html>
<body>
<h1>Hello World!</h1>
</body>
</html>

我们将把这个类添加到应用程序资源中的webpage目录下:

图片

main类看起来是这样的:

package org.packt.swarm.petstore.catalog;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.wildfly.swarm.Swarm;
import org.wildfly.swarm.undertow.WARArchive;

public class Main {

    public static void main(String[] args) throws Exception {

        Swarm swarm = new Swarm();

 //1
        WARArchive deployment = ShrinkWrap.create(WARArchive.class);
        //2
        deployment.staticContent("webpage");

        swarm.start().deploy(deployment);

    }
}

我们已经创建了WARArchive并调用了staticContent方法。当我们打开网络浏览器时,我们将看到 Hello World 页面:

图片

发生了什么?静态内容方法已经将webpage目录(在我们的例子中是一个文件)中的所有非 Java 文件复制到了创建的存档中,以便它们可以被undertow看到。

JAXRSArchive

我们现在想要查看的 Swarm 存档的最后一类是org.wildfly.swarm.JAXRSArchive。这个存档增加了创建默认 JAX-RS 应用程序的能力,应用程序路径设置为"/"。到目前为止,我们一直在所有示例中手动完成这项操作。有了 JAX-RS 存档,这个类将自动添加。

XML 配置

虽然 Java API 很方便,但这并不是我们唯一的选择。如果你熟悉 WildFly 的 XML 配置,或者如果你正在将你的应用程序迁移到 Swarm 并且有一个可工作的 XML 文件,你不需要将其转换为 Java API,因为你可以直接使用它。

示例参考:chapter4/catalog-service-xmlconfig

让我们回到我们的数据库示例。你可以使用 XML 配置数据源。在这种情况下,XML 配置看起来像这样:

<subsystem xmlns="urn:jboss:domain:datasources:4.0">
    <datasources>
        <drivers>
            <driver name="h2" module="com.h2database.h2">
                <driver-class>org.h2.Driver</driver-class>
                <xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
            </driver>
        </drivers>
        <datasource jndi-name="java:jboss/datasources/CatalogDS" pool-name="CatalogDS" enabled="true" use-java-context="true">
            <connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
            <driver>h2</driver>
        </datasource>
        <datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
            <connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
            <driver>h2</driver>
            <security>
                <user-name>sa</user-name>
                <password>sa</password>
            </security>
        </datasource>
    </datasources>
</subsystem>

我们必须将此配置文件添加到resources目录中:

图片

最后,我们还需要告诉 Swarm 使用配置文件。以下是被修改的Main类:

package org.packt.swarm.petstore.catalog;

import org.jboss.shrinkwrap.api.Archive;
import org.wildfly.swarm.Swarm;
import org.wildfly.swarm.datasources.DatasourcesFraction;
import org.wildfly.swarm.jaxrs.JAXRSArchive;
import org.wildfly.swarm.undertow.UndertowFraction;
import org.wildfly.swarm.undertow.WARArchive;

import java.net.URL;

public class Main {

    public static void main(String[] args) throws Exception {

        Swarm swarm = new Swarm();

 //1
        ClassLoader cl = Main.class.getClassLoader();
        URL xmlConfig = cl.getResource("datasources.xml");

 //2
        swarm.withXmlConfig(xmlConfig);

        swarm.start().deploy();

    }

我们获取了类加载器以便定位配置文件(1)。在读取文件后,我们指示 Swarm 使用其中的配置(2)。

然而,我们已经使用了整个配置文件——Swarm 现在会使用所有子系统吗?答案是:不会;只有那些已经指定了依赖关系的部分才会被添加到容器中。给定 XML 文件,Swarm 只会读取构成它的那些子系统的配置。你也可以提供一个只包含你想要使用 XML 配置的子系统的文件。

YAML 配置

你还可以通过 YAML 数据序列化语言提供 Swarm 配置。

再次,让我们从端口更改示例开始。我们将再次从 JAX-RS 示例开始,并修改它以使用 YAML 配置。

首先,让我们在resources目录内创建 HTTP-port.yml配置文件:

swarm:
  http:
    port: 12345

Swarm 会将嵌套属性转换为平面属性。因此,前面文件中指定的属性被转换为swarm.http.port,这是我们非常熟悉的。

要使用以下配置,我们必须修改我们的Main类:

package org.packt.swarm.petstore.catalog;

import org.wildfly.swarm.Swarm;

import java.net.URL;

public class Main {

    public static void main(String[] args) throws Exception {

        Swarm swarm = new Swarm();

        //1
 ClassLoader cl = Main.class.getClassLoader();
        URL yamlConfig = cl.getResource("http-port.yml");

        //2
        swarm.withConfig(yamlConfig);

        swarm.start().deploy();
    }
}

在从classpath(1)获取配置后,我们使用withConfig方法通知 Swarm 使用它。就是这样;现在,Swarm 将使用12345端口。

项目阶段

YAML 配置的强大之处在于它能够为不同的项目阶段提供不同的组属性。再次,让我们先看看示例。

新的配置文件看起来像这样:

swarm:
  http:
    port: 8080
---
project:
    stage: test
swarm:
    http:
        port: 12345
---
project:
    stage: QA
swarm:
    http:
        port: 12346

文件的不同部分收集了不同项目阶段的配置。第一组是默认配置。当没有提供阶段名称时使用。其他两个指定了测试和 QA 阶段的配置。然而,你如何知道应用程序当前运行在哪个阶段?你必须提供 swarm.project.stage 属性。所以,例如,我们使用以下命令运行前面的例子:

mvn wildfly-swarm:run -Dswarm.project.stage=QA

然后,我们将能够通过 12346 端口访问我们的应用程序。

正如你将在前面的代码中注意到的那样,YAML 配置使得为不同的环境创建配置以及使用简单的命令行参数选择应使用哪些属性组变得很容易。

YAML 数据库配置

作为另一个 YAML 配置示例,我们将向你展示如何使用 YAML 配置文件配置数据源。让我们看看:

示例参考:第四章/catalog-service-database-ymlconfig

这个例子与 XML 配置示例非常相似。我们必须用其 YAML 等价物交换配置文件:

swarm:
  datasources:
    data-sources:
      CatalogDS:
        driver-name: h2
        connection-url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
        user-name: sa
        password: sa
    jdbc-drivers:
          h2:
            driver-class-name: org.h2.Driver
            xa-datasource-name: org.h2.jdbcx.JdbcDataSource
            driver-module-name: com.h2database.h2

还需要让 Main 类使用它(1):

package org.packt.swarm.petstore.catalog;

import org.wildfly.swarm.Swarm;

import java.net.URL;

public class Main {

    public static void main(String[] args) throws Exception {
        Swarm swarm = new Swarm();

 //1
 ClassLoader cl = Main.class.getClassLoader();
        URL ymlConfig = cl.getResource("datasources.yml");

        swarm.withConfig(ymlConfig);

        swarm.start().deploy();
    }
}

我们将在本书的整个示例中大量使用此类配置。

配置的混合使用

现在,关于配置的混合使用呢?你被允许这样做吗?是的。让我们看看以下代码:

package org.packt.swarm.petstore.catalog;

import org.jboss.shrinkwrap.api.Archive;
import org.wildfly.swarm.Swarm;
import org.wildfly.swarm.datasources.DatasourcesFraction;
import org.wildfly.swarm.undertow.UndertowFraction;

public class Main {

 public static void main(String[] args) throws Exception {
 Swarm swarm = new Swarm();

 //1
 ClassLoader cl = Main.class.getClassLoader();
 URL xmlConfig = cl.getResource("standalone.xml");
 swarm.withXMLFile(xmlFile);

 //2
 UndertowFraction undertowFraction = new UndertowFraction();
 undertowFraction.applyDefaults();
 undertowFraction.httpPort(12345);
 swarm.fraction(undertowFraction);

 //3
 Archive<?> deployment = swarm.createDefaultDeployment();
 deployment.addModule("com.h2database.h2");

 //4
 swarm.start().deploy();
 }

}

这是我们的数据库示例的另一种变体,你已经知道整个代码中发生了什么。只是为了回顾,我们加载了配置文件,并通知 Swarm 使用它(1),创建了 UndertowFraction 并将其配置为使用 12345 端口(2),将驱动模块添加到应用程序中(3),最后,启动了应用程序并在其上部署了创建的存档(4)。

这样的代码会产生什么结果?正如你可能已经猜到的,在运行应用程序后,我们将在 localhost:12345/hello 上看到随机消息。

注意,你可以混合使用 XML 和 Java API 配置。你也能使用属性吗?当然。让我们将 swarm.http.port12346 端口添加到命令行中,我们将在地址上看到我们的消息。是的,我们这里有一个冲突。这是一个错误吗?这不是错误。Swarm 对不同的配置方法赋予不同的优先级。优先级如下:

  1. Java API 覆盖了由 XML 指定的配置

  2. YAML 覆盖了 Java API 指定的配置

  3. 最后,系统属性覆盖了 YAML 配置

  4. 结果,在我们的最后一个例子中,我们将在 12346 端口看到我们的消息

摘要

在本章中,你学习了如何配置使用 Swarm 创建的服务。你学习了如何使用系统属性来修改 Swarm 的行为,提供自己的 main 方法并使用它来通过 Java API 或 XML 提供 Swarm 配置,最后,如何修改已部署应用程序的内容。

在阅读了前三章之后,你现在可以使用 WildFly Swarm 来构建微服务。在接下来的章节中,你将学习 OpenShift,这样你就可以将你的服务部署到云端。

进一步阅读

wildfly-swarm.io/documentation/

第五章:使用 Arquillian 测试你的服务

在本章中,你将学习如何测试你的微服务。为此,我们将使用 Arquillian,这是一个专为使用其专用运行时测试软件组件而设计的测试框架,而不是创建基于模拟的单元测试。这是开发人员无缝工作于 WildFly Swarm 的框架,实际上也是 WildFly Swarm 的首选框架。

我们将介绍 Arquillian,并展示项目目的及其主要特性。稍后,你将基于实际示例学习如何为你的服务开发、编写和配置测试。

介绍 Arquillian

我们都知道单元测试的好处。它们简单且可以立即运行。它们隔离了应用程序的组件,并允许你逐个测试它们,从而提供每个组件的使用场景覆盖率。

不幸的是,单元测试也有其不足之处。当你用单元测试覆盖你的应用程序时,它们将确认应用程序的每个组件都正常工作。显然,仅基于这些信息,你不能推断出整个应用程序都正常工作——这就是需要集成测试的原因。你必须测试你的组件在其将运行的环境中,以确保应用程序作为一个整体正常工作。

到目前为止,集成测试的问题在于它们往往难以配置,执行时间也很长。这就是 Arquillian 介入的地方。项目的目标是使集成测试与单元测试一样快且易于配置。

如你所回忆的,在第二章Getting Familiar with WildFly Swarm中,我们强调了现代运行时的速度有多快。Arquillian 利用这一点,让你能够轻松配置在应用程序将运行的相同运行时上运行的测试。例如,如果你正在开发一个 Java EE 应用程序,你可以配置 Arquillian 在所选的应用服务器上运行测试。由于现代应用服务器非常快,测试将立即运行。另一方面,你将能够在包含所有依赖项的真实环境中测试你的应用程序。

在我们的案例中,每个服务的运行时是由 WildFly Swarm 组装的(如第三章所述,Right-Sizing Your Applications)。Arquillian 还允许你为这类情况配置测试。让我们来看看如何操作。

使用 Arquillian 测试 Swarm 微服务

在本节中,你将学习如何使用 Arquillian 测试使用 Swarm 创建的微服务。正如你在前面的章节中学到的,Swarm 构建一个只包含特定服务所需部分的运行时,启动它,然后在上面部署一个存档,创建微服务。

正如我们刚刚学到的,Arquillian 在其专用运行时上测试应用程序。它启动运行时,在它上面部署测试代码,并执行测试。让我们为我们的 JAX-RS 和 CDI 目录服务示例配置这样的测试,并逐步解释我们在做什么。

例如:参考 第五章/catalog-service-simple-test

首先,我们必须提供所有必要的依赖:

(...)

    <dependencyManagement>
        <dependencies>
 <!-- 1 -->
 <dependency>
                <groupId>org.jboss.arquillian</groupId>
                <artifactId>arquillian-bom</artifactId>
                <version>${version.arquillian}</version>
                <type>pom</type>
                <scope>import</scope> </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>jaxrs</artifactId>
            <version>${version.wildfly.swarm}</version>
        </dependency>
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>cdi</artifactId>
            <version>${version.wildfly.swarm}</version>
        </dependency>
 <!-- 2 -->
 <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${version.junit}</version>
            <scope>test</scope> </dependency>
        <!-- 3 -->
 <dependency>
            <groupId>org.jboss.arquillian.junit</groupId>
            <artifactId>arquillian-junit-container</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 4 -->
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>arquillian</artifactId>
            <version>${version.wildfly.swarm}</version>
            <scope>test</scope> </dependency>

    </dependencies>

(...)

</project>
  1. 首先,我们将 Arquillian 添加到 dependencyManagement(1)。

  2. 其次,Arquillian 可以与各种测试库集成。由于我们将使用 JUnit,我们必须提供对其的依赖(2)。

  3. 要使用 JUnit 运行 Arquillian 测试,我们必须提供 JUnit 集成工件(3)。

  4. 第三,我们必须告诉 Arquillian 使用哪个运行时——我们通过提供一个适配器库的依赖来实现这一点。在我们的例子中,这显然是一个 Swarm 适配器(3)。

现在我们已经准备好查看代码了。为了回忆,这个例子中的服务只包含一个项目,该项目是手动添加的:

package org.packt.swarm.petstore.catalog;

import org.packt.swarm.petstore.catalog.model.Item;

import javax.enterprise.context.ApplicationScoped;
import java.util.HashMap;
import java.util.Map;

@ApplicationScoped
public class CatalogService {

    private Map<String, Item> catalog = new HashMap<>();

    public CatalogService(){
        Item turtle = new Item();
        turtle.setItemId("turtle");
        turtle.setName("turtle");
        turtle.setQuantity(5);
        turtle.setDescription("Slow, friendly reptile. Let your busy self see how it spends 100 years of his life laying on sand and swimming.");
        catalog.put("turtle", turtle);
    }

    public Item searchById(String itemId){
        return catalog.get(itemId);
    }

}

现在是时候编写一个 test 类了。基于 Arquillian 的测试以以下方式运行:Arquillian 寻找带有 org.jboss.arquillian.container.test.api.Deployment 注解的静态方法。该方法必须返回 ShrinkWrap 存档。

Arquillian 将启动容器并在其上部署返回的存档。之后,方法被 org.junit 注解。测试在容器内运行。让我们在我们的示例测试中查看所有这些:

package org.packt.swarm.petstore.catalog;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.packt.swarm.petstore.catalog.model.Item;

import javax.inject.Inject;

//1
@RunWith(Arquillian.class)
public class CatalogServiceTest {

 //2
    @Deployment
    public static JavaArchive createDeployment() {
 return ShrinkWrap.create(JavaArchive.class)
 .addClasses(Item.class,CatalogService.class)
 .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
    }

 //3
    @Inject
    CatalogService catalogService; 
 //4
    @Test
    public void testSearchById() {
 Assert.assertEquals(catalogService.searchById("turtle").getName(),"turtle");
    }
}

在开始时,我们告诉 JUnit 使用 Arquillian 测试控制器来运行测试。为此,我们使用 @RunWith 注解来标记测试,指定 Arquillian.class 作为测试运行器(1)。

createDeployment(2)方法,正如其名称所暗示的,负责创建部署存档,该存档将在配置的容器上部署。为了通知 Arquillian,我们必须使用 @Deployment 注解来标记此方法。该方法为静态,并返回 ShrinkWrap 存档。由于测试方法是在容器内运行的,我们能够注入其资源。在我们的例子中,我们必须注入我们即将测试的 CatalogService 类以及它所依赖的 Item 类(3)。

最后,Test 方法检查 searchById 方法是否正确工作(4)。

现在让我们运行测试:

mvn clean wildfly-swarm:run

你会注意到测试已部署在 Swarm 容器内:

图片

它成功完成:

图片

最后,Swarm 微服务启动(因为我们使用了 wildfly-swarm:run 命令):

图片

注意,Swarm,如前几章的示例中所示,使用了 when-missing 发现机制,并创建了一个包含所有必要分片的容器。该容器用于测试和运行生成的微服务。

正如您在前面的屏幕截图中所注意到的,我们唯一更改的文件是pom.xml文件,因此从 AS 切换到 Swarm 的过程再次非常简单。然而,这也存在一些缺点:没有更改CatalogTest类意味着我们再次手动创建存档——当服务创建时 Swarm 可以为我们创建它,那么它为什么不能创建部署测试呢?它可以——让我们来学习如何做。

默认部署

正如我们刚刚暗示的,Swarm 可以创建默认的测试部署。

例如,请参考chapter 5/catalog-service-test-default-deployment

我们将修改Test类,以便自动创建存档:

package org.packt.swarm.petstore.catalog;

import org.jboss.arquillian.junit.Arquillian;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.wildfly.swarm.arquillian.DefaultDeployment;

import javax.inject.Inject;

@RunWith(Arquillian.class)
//1 @DefaultDeployment
public class CatalogServiceTest {

    @Inject
    CatalogService catalogService;

    @Test
    public void testSearchById() {
        Assert.assertEquals(catalogService.searchById("turtle").getName(),"turtle");
    }
}

为了告诉 Swarm 自动创建测试部署,我们必须使用org.wildfly.swarm.arquillian.DefaultDeployment注解来注解类(1)。就是这样。如果你现在运行测试,你将看到与上一段相同的输出。请注意,我们没有像上一个例子那样使用带有@Deployment注解的静态方法。

Swarm 配置

在上一章中,我们向您展示了如何修改 Swarm 配置。我们用来展示的例子是一个数据库配置。在本节中,我们将向您展示如何使用相同的例子为 Swarm 测试提供类似的配置。

例如,请参考chapter 5/catalog-service-database-test

如果你想要手动创建 Swarm 容器,你必须实现一个带有org.wildfly.swarm.arquillian.CreateSwarm注解的静态方法,并从其中返回org.wildfly.swarm.Swarm类的实例。你可能还记得,我们在第四章“调整服务配置”中创建的main函数内部已经创建了很多 Swarm 容器。我们将要使用的测试中的 Swarm 创建方法也是同样的工作方式。让我们看看代码:

package org.packt.swarm.petstore.catalog;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.packt.swarm.petstore.catalog.model.Item;
import org.wildfly.swarm.Swarm;
import org.wildfly.swarm.arquillian.CreateSwarm;

import javax.inject.Inject;
import java.net.URL;

//1
@RunWith(Arquillian.class)
public class CatalogServiceTest {

    @Deployment
    public static JavaArchive createDeployment() {
        return ShrinkWrap.create(JavaArchive.class)
                .addClasses(Item.class, CatalogService.class)
 //1
 .addAsResource("datasources.yml")
 .addAsResource("META-INF/persistence.xml")
 .addAsResource("META-INF/load.sql")
                .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
    }

 //2
    @CreateSwarm
    public static Swarm createSwarm() throws Exception {
 Swarm swarm = new Swarm();
        //3
        ClassLoader cl = CatalogServiceTest.class.getClassLoader();
        URL dataSourcesConfig = cl.getResource("datasources.yml");
        //4
        swarm.withConfig(dataSourcesConfig);
        return swarm;
    }

    //4
    @Inject
    CatalogService catalogService;

    //5
    @Test
    public void testSearchById() {
        Assert.assertEquals(catalogService.searchById("turtle").getName(),"turtle");
    }
}

在开始时,我们创建了包含所有必要类和配置的部署。

我们必须添加数据源配置、持久化配置和加载文件(1),以便它们可以在测试中读取。

关键部分是之前提到的createSwarm方法(2)。它创建 Swarm 实例,读取数据源配置(3),并使用它配置 Swarm(4)。

当容器和部署就绪时,我们可以开始编写测试逻辑。我们首先将CatalogService注入到测试中(4)。回想一下,这个测试是在 Swarm 容器中运行的,因此服务可以被注入其中。最后,为了确保我们的服务确实工作正确,我们检查返回的数据是否正确(5)。

如果你现在运行测试,你会看到它正确通过。

然而,目前我们正在创建没有端点的微服务,并在容器内部对其进行测试。这没问题,但我们还想测试整个微服务,使用其外部接口。让我们看看如何做到这一点。

从独立客户端进行测试

这次,我们希望从独立客户端测试应用程序。让我们学习如何做到这一点。

例如,请参考第五章/catalog-service-database-test-standalone

首先,我们必须向pom.xml文件中添加一些依赖项:

(...)

    <dependencies>
    (...)
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>arquillian</artifactId>
            <version>${version.wildfly.swarm}</version>
            <scope>test</scope>
        </dependency>

 <!-- 1 -->
 <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-client</artifactId>
            <version>${resteasy.version}</version>
            <scope>test</scope> </dependency>

 <!-- 2 -->
 <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-jackson-provider</artifactId>
            <version>${resteasy.version}</version>
            <scope>test</scope> </dependency>

    </dependencies>
(...)

我们必须添加一个依赖项到我们将用于对服务进行 REST 调用的 JAX-RS 客户端。由于我们将使用resteasy实现,我们将添加其客户端(1)。我们还需要一个库来解析 JSON 响应,因此添加了resteasy-jackson-provider(2)。

让我们看看实现这一功能的代码:

package org.packt.swarm.petstore.catalog;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.packt.swarm.petstore.catalog.model.Item;
import org.wildfly.swarm.Swarm;
import org.wildfly.swarm.arquillian.CreateSwarm;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import java.net.URL;

@RunWith(Arquillian.class)
public class CatalogServiceTest {

    @Deployment
    public static WebArchive createDeployment() {
        return ShrinkWrap.create(WebArchive.class)
 //1
 .addClasses(Item.class, CatalogService.class, CatalogResource.class, CatalogApplication.class)
                .addAsResource("datasources.yml")
                .addAsResource("META-INF/persistence.xml")
                .addAsResource("META-INF/load.sql")
                .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
    }

    @CreateSwarm
    public static Swarm createSwarm() throws Exception {
        Swarm swarm = new Swarm();
        ClassLoader cl = CatalogServiceTest.class.getClassLoader();
        URL dataSourcesConfig = cl.getResource("datasources.yml");
        swarm.withConfig(dataSourcesConfig);
        return swarm;
    }

 //2
    private static Client client;

 //3
    @BeforeClass
    public static void setUpClient() {
 client = ClientBuilder.newClient();
    }

 //4
    @ArquillianResource
    private URL url;

 //5
    private Item testEndpoint(String itemId) {
 WebTarget target = client.target(url + "item/"+itemId);
        return target.request("application/json").get(Item.class);
    }

    @Test
 //6 @RunAsClient
    public void testSearchById() {
 //7
 Assert.assertEquals(testEndpoint("turtle").getName(),"turtle");
        Assert.assertEquals(testEndpoint("hamster").getName(),"hamster");
    }
}

我们不得不实现很多东西。让我们逐一分析。

由于我们将测试 REST 端点,我们必须添加将暴露它的类,即CatalogResourceCatalogApplication(1)。

Arquillian 能够找出创建的服务 URL 并将其注入到测试中。为了获取这样的对象,我们必须使用org.jboss.arquillian.test.api.ArquillianResource注解来注解 URL 字段(4)。

我们实现了便利的测试方法,该方法根据 ID 调用服务并获取项目实例(5)。

测试中最重要的新增是使用org.jboss.arquillian.container.test.api.RunAsClient注解来注解test方法。结果,测试将在 Maven 的 JVM 中以独立客户端的方式运行。我们使用这种方式注解的测试是为了创建一个测试,该测试将基于从测试 JVM 的调用来断言我们服务的正确行为(6)。

摘要

在本章中,你学习了 Arquillian 是什么以及如何使用它来测试 Swarm 微服务。你还学习了如何配置 Arquillian 以自动创建 Swarm 容器,如何修改容器配置,以及如何从容器内部和从独立客户端测试创建的微服务。

进一步阅读

arquillian.org/

第六章:使用 OpenShift 在云上部署应用程序

在前几章中,我们向您展示了如何使用 WildFly Swarm 开发微服务。在本章中,您将学习如何将这些服务部署到云端,并且您将使用 OpenShift 来实现这一点。然而,为什么我们要费这个功夫呢?云计算有哪些特性和好处?让我们先简单谈谈。

在我们进入下一节之前,您需要了解一些重要信息。本章描述了许多理论概念,解释了 OpenShift 的内部机制。如果有些概念听起来太复杂而难以配置,请不要担心,因为(剧透警告!),最终 OpenShift 会为您做大部分工作。本章的目标是提供知识,让您能够理解 OpenShift 能做的所有魔法,并在后面的章节中修改和重新配置这种行为。那么,让我们开始吧。

云计算

好的。让我们从头开始。那么,云计算到底是什么呢?

云计算是一种 IT 范式,它提倡使用通过互联网提供的可配置资源和服务共享池。这些服务按需、快速且管理成本最小化地提供。因此,云计算允许灵活的架构、优化的资源使用,以及将基础设施提供商与消费者分离的可能性,从而实现关注点的分离。让我们更详细地检查这些声明。

按需提供的资源使您作为开发者或架构师在配置技术基础设施方面具有灵活性。开始项目成本低,因为您不需要从基础设施投资开始。此外,当您的项目投入生产时,计算资源可以自动扩展以满足应用程序的需求。在每种情况下,您只需为使用的资源付费。

此外,云计算引入了关注点的分离。云服务提供商变得专业于提供基础设施。开发者被提供了云基础设施的接口,并将用它来进行开发。因此,只要他们遵守云提供的接口,他们就不需要关心基础设施配置的细节。另一方面,云服务提供商必须竞争以提供最方便和最稳健的云基础设施,同时尽量降低成本。

在本章中,我们将描述 OpenShift 提供的云接口。在此之前,我们将描述以下两个云基础设施特性,这将使在接下来的章节中使用一致的概念集成为可能:部署和服务模型。让我们从第一个开始。

云基础设施部署模型

云部署模型指定了云基础设施的组织方式。让我们看看常用的模型。

公共云

公共云是一种在网络上提供服务的模型,供公众使用。

公共云计算有时被描述为计算资源向类似电力的公用事业演变。使用电力供应商的例子可能会给你对这个段落中描述的主题提供另一种见解:电力分配公司负责确保网络能够处理需求高峰,或者在牵引中断的情况下提供网络冗余。然而,最终,我们并不会过多地考虑这些——当我们需要时,我们会把插头插入插座,并且只为我们使用的电量付费。

我们不应该过分夸大这个类比。计算资源显然不是像电力那样的商品,由于客户需求的不同,它们不能以统一的方式提供给每个人。

首先,由于各种原因,并非所有客户都能将他们的工作负载迁移到公共云。其中一个原因可能是需要更高层次的安全或对架构有更大的控制。这样的客户可以利用我们讨论的私有云。

私有云

私有云中,用于创建云的计算机资源仅专用于单个客户。这些资源可能位于现场或异地,由客户拥有,或由第三方管理。所有资源都为单个客户配置,不会与其他客户共享。这种配置允许有更大的控制和安全,但要求公司投资并管理云资源,从而消除了公共云解耦的好处。

混合云

混合云是一种将公共云和私有云连接在一起的模式。因此,你可以利用这两种解决方案的优势。例如,你可以在私有云上运行处理敏感数据的业务,但使用公共云来扩展其他服务。

我们希望我们的云服务提供商提供云抽象,使用户能够看到一个统一的云视图,抽象出混合云的基础设施细节。我们将在描述 OpenShift 架构时回到这个想法。

现在,让我们关注另一个云特性,那就是服务模型

服务模型

正如你在本章开头所学的,云服务提供商负责按需提供计算资源。计算资源可以通过不同的方式配置,并具有不同级别的抽象。云计算基础设施的这个特性被称为服务模型。让我们描述一下常用的模型。

基础设施即服务

基础设施即服务IaaS)中,客户能够在提供的资源上安装任意应用程序(包括操作系统)。客户不控制云提供商提供的基础设施。例如,客户可能能够访问一个远程虚拟机,他们可以完全操作。

平台即服务

平台即服务PaaS)中,正如其名所示,云提供商负责向客户提供现成的平台,客户可以在该平台上部署和运行他们的应用程序。假设你想使用数据库。在这种情况下,平台提供商负责为你提供一个最新且配置好的数据库,你可以在上面立即开始工作。你不必处理整个配置,可以直接与数据库(或任何其他技术;例如 WildFly Swarm)开始工作。

软件即服务

最后,还有软件即服务SaaS)。在这个模型中,客户能够使用云提供商提供的应用程序。SaaS 的一个例子可能是通过互联网提供的磁盘存储、电子邮件或办公套件。

好的,现在我们已经明确了术语,我们最终可以深入研究 OpenShift 架构。

OpenShift 架构

到目前为止,我们一直在用抽象的术语进行讨论。在本节中,我们将为您概述 OpenShift 架构。结果,您将获得对功能云 PaaS 基础设施的实际理解。

让我们从一张俯瞰的架构图开始:

图片

上述图表概述了 OpenShift 架构的层级。Docker运行在操作系统之上,提供容器层。容器是轻量级、独立且可执行的软件组件(进一步阅读,链接 2),可以在云的任何地方运行。这些容器由Kubernetes编排,它提供了异构计算资源的统一视图。最后,OpenShift建立在Kubernetes之上,提供开发者工具,自动化大多数配置任务。如果这个简短的描述让你感到困惑,不要担心。到本章结束时,一切都会变得清晰。让我们从容器开始。

容器化

如我们之前所说,在云计算中,云提供商负责根据用户需求提供服务器资源。然而,这实际上意味着什么?这些资源是如何提供的?如何创建带有所有必要配置的服务器,以及它是如何与其他用户隔离的?为了理解这一点,我们必须了解容器是如何工作的。

容器基本上是一种虚拟化。让我们讨论这个概念。

虚拟化

虚拟化是一种技术,它允许在一台服务器上运行多个隔离的虚拟机。虚拟机是服务器操作系统上运行的虚拟化应用程序所控制的计算机系统的模拟。虚拟机分配给用户的用户可以访问完全运行的宿主机。宿主机不是物理的,并且与其他虚拟机共享物理服务器的资源,这一点对用户来说是抽象的。

虚拟化的另一个关键特性是将虚拟机序列化为镜像的能力。这一特性使得虚拟机具有可移植性。镜像可以被移动到不同的服务器上,从而确保虚拟机的状态得以保留。

那么,为什么虚拟化对云服务提供商来说很重要呢?

首先,云服务提供商能够向用户提供虚拟机的访问权限。因此,用户将获得服务器资源的一部分。从用户的角度来看,他们将能够访问一个隔离的服务器

第二,用户可以使用他们想要使用的平台的预配置镜像。你需要带有 WildFly AS 的 Fedora 吗?这里就是你的配置好的镜像。我们将在我们的服务器上运行它,你就可以开始了。

第三,云服务提供商能够优化资源使用。一个服务器上可以运行许多虚拟机,从而最小化空闲时间。

第四,当需要时,虚拟机可以在不同的服务器之间自由移动。如果需要更多资源,可以将一些虚拟机转移到另一个服务器。再次从用户的角度来看,您将能够访问一个隔离的预配置服务器,而无需担心细节。

那么,关于实现方面呢?

你们中的一些人可能将虚拟化与硬件(全)虚拟化联系起来。在这种架构中,虚拟化应用程序负责模拟整个操作系统,包括所有必要的进程和库。

这种解决方案存在一些性能问题,其中大部分是由于操作系统最初是为在物理主机上运行而设计的。首先,要启动虚拟机,整个操作系统都需要启动,因此启动时间可能会很长(几分钟)。其次,操作系统进程和库必须在每个虚拟机中重复,这导致资源使用非最优。

让我们从云服务提供商的角度来考虑这个问题,特别考虑我们在第一章中描述的微服务架构,Java EE 和现代架构方法。我们希望有一个解决方案,使我们能够提供大量短暂的虚拟机。我们希望确保它们可以立即启动和停止,优化资源使用,并有效地存储镜像数据。结果是我们需要另一个工具。让我们来讨论容器。

容器

容器是系统级虚拟化(或准虚拟化)的实现。在这种虚拟化中,操作系统不是在每个虚拟机上模拟。相反,虚拟机共享相同的操作系统实例,使用它提供的工具来实现隔离。因此,在这种模型中,我们可以将虚拟机视为在相同操作系统之上运行的隔离用户空间实例。这样的实例被称为容器。

上述图表突出了全虚拟化和容器之间的主要区别。容器共享相同的操作系统,正如您将在下一节中学习的,能够有效地共享公共库。在上述图表中,水平线代表在创建新的虚拟机/容器时必须创建的层。

在我们描述 Docker 容器的实现之前,让我们先谈谈 Linux 内核提供的隔离工具。

内核隔离工具

Linux 内核提供了一系列工具,可以实现对进程的隔离,并对这些组设置资源使用限制。实现这一功能的主要工具(并被 Docker 容器使用)是命名空间(隔离)和cgroups(限制)。让我们更深入地了解它们。

命名空间

每个内核进程都可以分配到一个命名空间——具有相同命名空间的进程共享对一些系统资源的相同视图。例如,PID 命名空间提供了隔离进程的能力——同一 PID 命名空间中的进程可以看到彼此,但不能看到来自不同命名空间的进程。

Linux 内核中有一组命名空间,提供 PID、网络、挂载点和用户名隔离。

cgroups

cgroups负责限制进程组的资源使用。cgroups允许您将进程分配到多个组中,并为这些组配置资源配额。可以控制的资源包括 CPU、内存使用、网络和磁盘带宽。在资源拥堵的情况下,cgroups机制将确保该组不会超过该资源的配额。

在容器的情况下,可以为每个容器创建一个组。因此,我们可以为所有容器提供配额。例如,我们可以将 CPU 的 1/5 分配给其中一个容器。这将保证在拥堵的情况下,该容器可以访问相应数量的 CPU 周期。因此,我们能够保证资源访问。cgroups限制仅在拥堵期间生效。在我们的例子中,如果其他所有容器都处于空闲状态,分配了 CPU 配额分数的容器可能使用更多的 CPU 周期。

Docker 容器实现

你刚刚了解到 Linux 内核提供了工具,这些工具能够实现资源的隔离,为以下系统级虚拟化的实现奠定了基础。但我们必须问自己一些问题:哪些应用将在容器内运行?哪些库和文件将在各个容器中可见?

为了回答这些问题,我们将向您介绍 Docker 镜像。正如我们之前所暗示的那样,在硬件虚拟化中,虚拟机可以存储为镜像,这使得存储虚拟机的状态成为可能,从而允许创建可重用的预配置虚拟机,并实现可移植性。

同样的特性也适用于容器及其实现方式:Docker。正如你将在接下来的章节中了解到的那样,这一理念已经被发展到全新的高度,为我们提供了一个高效便捷的镜像生态系统,因此它为我们的云基础设施提供了一个基础环境。然而,让我们从基础开始讲起。

镜像和容器

在 Docker 术语中,镜像和容器之间有一个区别。镜像是一个不可变、明确可识别的文件和元数据集合。另一方面,容器是镜像的运行时实例。同一个镜像可以有多个容器实例,每个实例都是可变的,并且具有自己的状态。

让我们用一个例子来说明这一点。你可以在容器中启动 Fedora 发行版。为此,你必须下载并构建一个 Fedora 镜像。构建完成后,镜像将位于你的机器上,并包含 Fedora 发行版。正如我们在上一段中提到的,这个镜像是一个不可变的模板,可以用来启动容器。当你基于 Fedora 镜像启动容器并登录时,你会看到你能够访问裸露的 Fedora 发行版。你可以在那里安装软件和创建文件。当你这样做时,你只修改了那个特定的容器。如果你从同一个 Fedora 镜像运行另一个容器,你将再次能够访问裸露的 Fedora 发行版。

上述例子为你提供了容器和镜像行为的鸟瞰图。现在,让我们更深入地了解两者的架构。

镜像由 Dockerfile 描述。Dockerfile 是一个文本文件,其中包含一系列命令,指导您如何组装镜像。

镜像具有分层结构。在 Dockerfile 中执行的命令会导致创建额外的层——每个后续层都与前一个层不同。

每个镜像都必须派生自另一个镜像(可能是显式为空的 scratch 镜像),并在其之上添加其层。镜像层直接建立在内核代码之上。

让我们通过查看一个实际示例来明确所有这些概念。我们将创建一些简单的镜像。

首先,我们将从本地目录创建多个文件,然后我们将基于这些文件构建镜像:

现在让我们创建第一个镜像(注意,我们旨在描述架构;因此,尽管我们将简要解释所使用的命令,但如果您对细节感兴趣,请参阅(进一步阅读,链接 1)):

FROM centos:7
RUN useradd -ms /bin/bash tomek 
USER tomek 
WORKDIR /home/tomek

上述 Dockerfile 表示基础镜像。它派生自 centos:7 镜像,这是一个裸露的 Centos 发行版,添加了 tomek 用户(#2),并将之前的用户切换到 tomek,以便所有后续命令都将以此用户身份运行(#3),因此任何后续命令的目录都将执行到 tomekhomedir

为了从 Dockerfile 构建镜像,我们必须在镜像所在目录中执行以下命令:

docker build -t base 

在上述命令中,我们已将镜像标记为 base。因此,我们可以通过其基础名称来引用它。让我们继续到第二个 Dockerfile:

#1
FROM base 
#2
COPY A A 
#3
COPY B B

上述镜像派生自之前创建的基础镜像。它从我们的本地文件系统复制目录 A#2)和 B#3)到镜像中。同样,让我们构建并标记这个镜像:

docker build -t middle .

最后,看一下最后一个镜像:

#1
FROM middle 
#2
COPY C C

它从中间镜像(#1)派生,并将本地文件系统的 C 目录(#2)复制到镜像中。

以下图展示了顶层镜像的层。我们包括了图像层次结构中的所有命令,以便您可以查看顶层镜像是如何从头开始组装的:

正如您在前面的图中将注意到的,所有 Dockerfile 中的每个命令都被转换为一个额外的层。例如,基础镜像派生自 centos:7 镜像,因此其第一层被添加到 centos 层之上。同样,顶层镜像基于中间镜像,因此执行 COPY C 命令产生的层被添加到中间镜像的层之上。

我们现在理解了 Dockerfile 如何转换为镜像层结构。然而,为什么这种结构很重要?它很重要,因为它使得镜像能够共享层。让我们找出如何做到这一点。

当我们构建镜像时,Dockerfile中的命令将被执行,并创建所有层。正如你在前面的例子中所看到的,镜像之间是相互连接的,并且可以共享层。被多个镜像使用的层只需要创建一次。

在前面的例子中,中间和顶部的镜像共享了中间镜像的所有层。如果我们决定构建中间镜像,所有这些层也将被创建。如果我们稍后构建顶部镜像,中间的层不需要再次创建。

要理解为什么可以共享层,我们必须了解层如何影响容器启动后的文件系统行为。

当我们基于镜像启动容器时,我们是在启动容器的镜像之上插入另一层。容器将把其更改写入这一层,但结果不会改变镜像,因为镜像是不可变的。要理解为什么是这样,我们必须了解层如何影响运行时的文件系统行为。让我们来看看。

当 Docker 启动容器时,它创建一个文件系统,该文件系统将被挂载为容器的根文件系统。这样的文件系统覆盖了镜像和可写层中的所有层,创建了一个看起来结合了构成镜像的所有层的文件和目录的文件系统。让我们回到我们的例子来看看它是如何工作的。

让我们基于我们之前的例子中的镜像创建三个容器:

在前面的图中,顶部的矩形代表我们创建的镜像。中间的镜像添加了两个层,对应于目录AB。顶部的镜像添加了一个包含C目录的层。有三个容器。两个容器(23)基于顶部的镜像,而CONTAINER1基于中间的镜像。底部的矩形代表每个容器的可写层。

由于所有层都已合并成一个文件系统,所有容器都将看到整个操作系统分布。家目录的内容对于CONTAINER1CONTAINER2CONTAINER3将是不同的:CONTAINER1将只在其home文件夹中看到AB目录,而CONTAINER2CONTAINER3将看到ABC目录。

让我们解释这种覆盖是如何实现的。当容器读取一个文件时,存储驱动程序(负责联合文件系统实现的组件)从顶部层开始查找该文件。如果找不到文件,它将移动到下一层。这个过程会重复,直到找到文件或没有更多层。如果在任何层中都找不到文件,我们将得到一个File not found错误。

假设CONTAINER1想要读取~/C/c.txt文件。存储驱动器从CONTAINER3的可写层开始搜索。由于文件不在那里,它移动到来自顶层镜像的COPY C层。文件在那里被找到并读取。

如果CONTAINER1想要读取相同的文件会发生什么?

存储驱动器从CONTAINER1的可写层开始。再次,它找不到文件,但这次它移动到顶层的COPY B层,这是容器创建时从中创建的中间镜像的顶层。文件在那里找不到,在其下任何层中也找不到。我们最终会收到一条“文件未找到”的消息。

如果CONTAINER1CONTAINER2想要读取~/B/b.txt会怎样?

在阅读上一段中的假设之后,你会知道两个文件都可以读取。然而,请注意,两个容器都在读取相同的文件。"COPY B"层被中间和顶层镜像重用,b.txt文件从相同的镜像读取,两个容器都是如此。多亏了层,容器能够重用数据。

那么,写入文件会怎样呢?

存储控制器在将文件写入文件系统时使用写时复制策略。驱动器会从顶层到底层搜索所有层中的文件。如果文件存在于容器的可写层中,则可以直接打开进行写入。如果它存在于镜像的某个层中,则将其复制到可写层并打开进行写入。

让我们回到我们的例子。假设CONTAINER1想要写入~/A/a.txt文件:

图片

存储驱动器在COPY A层中找到了~/A/A.txt文件,并将其复制到CONTAINER1的可写层。从CONTAINER1的可写层进行的后续读取和写入操作将读取/写入文件。

假设CONTAINER3想要写入~/A/A.txt文件:

图片

情况类似;文件被复制到CONTAINER3的可写层。让我们看看当前的情况。每个容器都曾访问过 Fedora 发行版并修改了其部分。

CONTAINER1CONTAINER3仍然共享大部分数据,因为只有被给定容器修改的文件才会被复制到其可写层。

正如您在前面的图中注意到的,Docker 镜像的实现提供了一种在相同主机上存储多个镜像的有效方法。那么,容器启动时间如何呢?容器使用底层 Linux 内核的资源,并重用镜像层(如果存在的话;如果不存在,则只下载一次)。正因为如此,启动容器意味着创建一个可写层,并使用内核隔离特性运行容器进程。如您所见,与硬件虚拟化相比,这些进程非常轻量级。因此,容器可以立即启动和停止。

到目前为止,我们已经描述了 Docker 镜像架构对容器虚拟化性能的影响。您可能对在其他情况下这种实现的性能有所怀疑。当然,如果我们决定在分层文件系统上运行数据库,分层文件系统会有性能损失。这是一个需要澄清的好点。Docker 分层文件系统用于有效地与 Docker 容器一起工作。分层文件系统不是为了存储需要高性能的数据;这是卷的作用,我们将在下一章中学习。

存储驱动可能有不同的实现方式。例如,写时复制策略可能实现在文件或页面缓存级别。Docker 提供了一系列实现,选择正确的实现取决于您的使用场景。

如果您对特定存储驱动程序的架构感兴趣,或者您正在研究哪种驱动程序最适合您的使用场景,请参考 Docker 文档。

Docker 注册表

让我们回到我们示例容器的构建过程。当我们第一次构建基本镜像时,构建日志中可以看到以下内容:

Step 1 : FROM fedora:26 
Trying to pull repository docker.io/library/fedora ...  
sha256:b27b4c551b1d06be25a3c76c1a9ceefd7ff189f6f8b1711d3e4b230c2081bff3: Pulling from docker.io/library/fedora 
Digest: sha256:b27b4c551b1d06be25a3c76c1a9ceefd7ff189f6f8b1711d3e4b230c2081bff3 
Status: Downloaded newer image for docker.io/fedora:26
(...)

结果表明,fedora:26镜像是从docker.io服务器下载的。哪个服务允许用户下载镜像?

Docker 镜像,就像 Maven 工件或操作系统包一样,创建了一个相互关联的可重用实体生态系统。就像在 Maven 或操作系统场景中一样,我们需要一个服务来存储和分发这些镜像。这种服务被称为 Docker 注册表。

Docker 提供了一个默认的注册表,称为 DockerHub。DockerHub 是一个公开可用的免费注册表。如果没有配置,Docker 将使用 DockerHub 作为默认注册表。

Docker 总结

如您所见,Docker 是一个提供许多能力的工具,这些能力是我们云架构的基本构建块,如下所示:

  • 在操作系统级别实现的隔离,结合分层文件系统实现,使得有效地共享服务器资源成为可能,并允许容器立即启动。

  • 镜像生态系统提供了大量的镜像,可以立即下载和使用。

  • 容器使用相同的镜像运行,并在不同的 Docker 环境中操作,确保了所有环境的一致性。正如你将在本章的其余部分学到的那样,这是动态云环境的关键特性。

所有这些特性使 Docker 容器成为云基础设施的绝佳构建块。然而,我们需要的不仅仅是这些。首先,在非平凡环境中将会有大量的容器。我们追求的是一个动态环境,它能够实现自动扩展、高可用性和持续集成,因为我们预计这些容器将在短时间内被频繁启动和停止。最后,无论 Docker 镜像多么酷和高效,我们更愿意在构建我们的应用程序时自动生成它们。所有这些问题都由 OpenShift 为你解决。让我们继续学习 OpenShift 堆栈。接下来你需要学习的是编排。

编排 Docker

我们刚刚了解了一个工具,它使我们能够提供容器——轻量级的虚拟机,使用操作系统级别的虚拟化,为我们提供隔离、有效资源使用、即时创建时间和在不同环境中最可重复的行为。这是我们云环境的第一层,但不是我们的目标平台。在一个更复杂的生产系统中,我们不得不管理大量的容器,显然,我们不希望手动操作。我们需要一个能够以巧妙方式管理我们的容器的工具。让我们来认识一下 Kubernetes。

Kubernetes

为了向您展示 Kubernetes 提供的界面,让我们介绍其主要的架构概念。

节点和主服务

Kubernetes 从一组计算机中创建一个集群。构成集群的计算机可以是异构的。集群可以创建在你的笔记本电脑上、一组工作站或虚拟机上。此外,一个集群中可以混合使用所有类型的 worker 机器。

在 Kubernetes 术语中,每台工作机器是一个节点,整个集群由主节点管理。每个节点运行 Kubelet,这是一个守护进程,它使节点能够与主节点和 Docker 引擎通信,以便 Kubernetes 可以在其上部署容器。

另一方面,主节点是一组协调整个集群的服务。从用户的角度来看,主节点最重要的部分是其 REST API 服务,它提供了一个端点,允许用户与整个集群交互。

下面的图展示了我们将在后续描述中使用的示例 Kubernetes 集群。主服务由蓝色圆圈表示,每个节点由一个矩形表示。集群由两个工作站(青色)、两个虚拟机(绿色)和一台笔记本电脑组成。每个节点运行 Kubelet,以便它可以与主节点通信,并运行 Docker 引擎,以便它可以启动容器:

图片

在 Kubernetes 中,容器是短暂的,这意味着它们可能会频繁启动和停止。如果容器数据没有提交,当容器停止时,它将被删除。因此,我们需要另一个工具来存储数据。在 Kubernetes 中,这种功能由卷提供。卷是一种持久存储实现,它具有独立的生命周期,并且可以挂载到多个容器中。我们将在下一章详细讨论卷。

Pod

在 Kubernetes 中,Pod 是一组容器和卷,它们在集群中共享相同的 IP 地址。

Pod 的所有内容都保证在同一个主机上运行。因此,Pod 可以被视为部署和调度的原子单元。

Pod 概念是必需的,以便为我们提供实现解耦容器的能力。有了 Pod,我们能够将具有不同功能的一组容器放置在一起(并且可能共享数据)。这些不同的功能可以封装在每个容器中。另一方面,如果容器必须用作部署的原子单元,我们可能被迫将不同的功能放置在一个容器中,以确保它们将一起部署和扩展,这会破坏低耦合和高内聚的良好设计原则。

在大量场景中,一个 Pod 可能只包含一个容器,这完全没问题。Pod 不需要由许多容器组成,但提供了这样的可能性,以便在应用需要时使用。

部署

我们已经了解了 Kubernetes 集群的构建块。现在,是时候看看我们最感兴趣的事情了:将其部署应用程序。

当你想将应用程序部署到 Kubernetes 集群时,你必须创建包含该应用程序信息的部署对象。其中之一是,Kubernetes 必须知道哪些容器构成 Pod,以及需要创建多少个 Pod 副本。有了这些知识,Kubernetes 将决定在哪个节点的应用程序 Pod 上部署,并将它们部署在那里:

图片

在前面的图中,已经创建了一个需要 Pod 在三个主机上复制的部署。假设 Kubernetes 已经决定 Pod 将在WORKSTATION1和两个虚拟机上运行。部署对象已经成为主模型的一部分。我们实际上是什么意思呢?

必须强调的是,部署不是一个在执行后就会完成,不会对集群状态产生进一步影响的操作。相反,部署实际上向集群期望状态描述中添加了对象。确保这种状态得以维持是 Kubernetes 的角色。

我们已经暗示 Kubernetes 主节点提供了 REST API。此 API 允许创建一个描述集群期望状态的对象。部署就是那些对象之一。因此,将应用程序部署到集群相当于向集群描述中添加一个额外的部署对象。

Kubernetes 监控集群的状态,并负责确保它与描述等效。为了更清楚地说明这一点,让我们看看几个简单的例子。

假设集群中的一个节点已经关闭。结果,部署在该节点上的 Pod 组必须转移到不同的节点,以便部署的 Pod 数量与部署描述相匹配。为了在我们的例子中展示这一点,让我们假设虚拟机 1已经关闭:

在这种情况下,主节点会发现虚拟机 1节点已失败,为了保持 Pod 副本数量与描述一致,它将在另一台机器上部署一个额外的 Pod——在我们的例子中是笔记本电脑 1

那么,部署应用程序的新版本怎么办?我们必须在部署描述中更改版本。Kubernetes 会发现版本已更改。它将回滚具有先前版本应用程序的 Pod,并启动具有新应用程序的 Pod。让我们在我们的图中展示这个例子:

Kubernetes 已经卸载了构成前一应用程序的 Pod,用新的部署对象替换了旧的部署对象,选择了更新 Pod 需要部署的节点,最后在这些节点上执行了部署。

因为本章旨在解释 OpenShift 架构,所以我们使用一般理论示例。我们将在下一章的实践示例中向您展示如何进行扩展和部署。

现在需要理解的关键点是 Kubernetes 运行的原则:它负责使集群与用户描述的期望状态保持同步。

服务

你可能已经注意到,在前一节中描述的动态 Pod 部署存在问题。如果我们不知道应用程序的 Pod 位于何处,我们该如何连接到该应用程序?为了解决这个问题,Kubernetes 引入了服务概念。

服务是一个对象,它监控构成应用程序的 Pod 组。它包含搜索条件,用于定义哪些 Pod 是应用程序的一部分,并监控集群,以便知道这些 Pod 的位置。服务有自己的 IP 地址,可以从集群外部看到。因此,客户端只需要知道服务的地址。实际的集群状态(Pod 数量及其位置)被抽象化,从客户端隐藏。

让我们在我们的例子中再次介绍它:

图片

在前面的图中,对主服务的调用在主对象模型中创建了服务对象,这导致了服务的创建。创建的服务有一个 IP 地址,外部客户端可以访问。当调用服务时,它将对集群中的一个 pod 进行负载均衡。请注意,服务持续监控集群的状态。例如,如果一个节点发生故障,服务将学习到 pod 的新位置,并继续正确工作。

这个例子结合了我们介绍的所有概念。让我们回顾一下:Kubernetes 创建了一个异构工作机的集群。集群中的所有机器都必须能够运行 Docker 容器并与管理所有集群的 Kubernetes 主服务进行通信。部署的单位是 pod,它由一个或多个容器组成,并且可以有多个副本。用户使用主 API 创建此类部署的描述,Kubernetes 负责确保实际的集群状态与这个描述相匹配。为此,它必须执行节点和应用程序的健康检查,并在必要时重新部署它们。它还必须对所有模型更改做出反应,并相应地修改集群。由于本段中提到的所有原因,应用程序 pod 可以位于不同的节点上,并且这些位置可以动态变化。因此,Kubernetes 引入了服务概念,为外部客户端提供了连接到客户端想要连接的应用程序的 pod 的代理。

标签

在 OpenShift 集群模型中,每个对象都可以有任意数量的标签,这些标签基本上是键值属性。这是一个非常简单但功能强大的特性。

我们需要一种方法来对不同的对象类型进行分类。我们可以为这个目的构建一个目录结构,但这个解决方案的问题在于它不够灵活。目录结构提供了一个对象视图,但可能会有很多这样的视图,这取决于用户或当前的用法场景。

另一方面,标签提供了完全的灵活性。任何对象都可以应用任意数量的标签。这些标签可以用在查询中,根据广泛的特性来查找 OpenShift 对象。

例如,让我们再次查看服务对象。服务必须找到所有运行由服务表示的应用程序的 pod。服务通过查询标签来找到这些 pod:通常的做法是 pod 有一个 app 标签设置为在其上运行的应用程序的名称。因此,服务可以查询具有适当 app 标签的 pod。

优点

现在我们已经了解了关于 Kubernetes 架构的最重要概念,是时候看看它提供的接口的全貌了。Kubernetes 将异构工作机群组抽象化,为用户提供了一个同质化的容器执行环境视图。让我们思考一下。作为用户,您告诉 Kubernetes:“我想部署我的应用程序,它由 N 个 pod 组成。”Kubernetes 将这些 pod 放置在集群的某个位置。如果 pod 需要扩缩容,或者出现故障,Kubernetes 将负责在底层机器之间移动这些 pod,但机器的技术细节对用户来说是抽象化的。使用 Kubernetes,用户将集群视为容器执行资源池。此外,这种视图对于您可能希望在环境中运行的任何集群都是相同的。您笔记本电脑上的 Kubernetes 开发集群将具有与您的生产环境相同的接口。

我们还必须强调容器的作用——Docker 保证由同一镜像构建的两个容器将具有相同的行为。结合 Kubernetes 的作用,我们能够看到 Docker 和 Kubernetes 提供的云视图:一个容器执行计算资源池,这进一步保证了在所有部署环境中都能重复行为。

OpenShift

在前面的章节中,我们介绍了 Kubernetes 和 Docker 提供的强大云抽象。另一方面,我们在多个地方暗示,为了有效地使用 Kubernetes,您必须直接处理 Kubernetes 对象配置或 Docker 文件。正如我们之前所写的,我们更希望有一个工具可以抽象这些事情,使它们自动发生,或者使用易于使用的工具进行配置。正是在这里,OpenShift 出现了。OpenShift 在 Kubernetes 之上添加了另一层抽象,为其提供了额外的集群模型功能,如构建,或如网页控制台等工具。正如您将看到的,OpenShift 添加的这层使得所有云操作都非常简单,并且有效地让您能够专注于代码的开发。

回顾一下本章开头关于云计算类型的部分,我们可以这样说:Kubernetes 和 Docker 为您提供 IaaS,而 OpenShift 则将其转变为强大的面向程序员的 PaaS。

在接下来的段落中,您将了解 OpenShift 提供的最重要功能。现在,让我们快速概述 OpenShift 的最重要功能。我们将从构建基础设施开始。

构建基础设施

在前面的段落中,我们建议 OpenShift 将直接 Docker 镜像抽象化。现在,让我们更仔细地看看它。

OpenShift 提供构建和部署抽象。构建负责创建镜像,部署负责将这些镜像部署到集群中,提供高于 Kubernetes 对象(如副本控制器)的抽象。

现在脑海中浮现的第一个问题之一是这些镜像是如何构建的?有很多选项,但对我们来说最有趣的一个是源到镜像构建。正如其名所示,这种构建负责自动将您的代码转换为 Docker 镜像。

因此,您与 OpenShift 的交互可能配置如下:您编写应用程序并将更改推送到 GitHub 仓库。这触发了源到镜像的构建,创建了 Docker 镜像,创建 Docker 镜像可能还会触发自动部署。此外,所有这些步骤都可以根据持续交付管道集成到 Jenkins 中。正如您所看到的,OpenShift 构建工具允许您只专注于代码开发。与云的交互将由构建基础设施自动完成。

您将在第九章 配置使用 Jenkins 的持续集成中了解更多关于部署和构建(包括源到镜像和管道构建)的内容。

项目和用户管理

这个功能不如前一个新鲜,但仍然非常重要。为了使 OpenShift 能够在企业环境中工作,项目的概念是必要的。

Kubernetes 提供了命名空间的概念,这使得它能够将集群划分为一组虚拟集群。这个命名空间的概念虽然没有实现访问控制,但 OpenShift 创建了项目的概念,这是一个由特定注释标识的 Kubernetes 命名空间,并基于用户和用户组实现访问控制策略。

您将在第十章 使用 Keycloak 提供安全中了解更多关于安全微服务应用的内容。

Minishift

我们已经对 OpenShift 说了很多好话。现在是时候看看它在实际中是如何工作的了。然而,您实际上是如何做到这一点的呢?作为一个开发者,您有权访问 OpenShift Online。这是一个公开可用的 OpenShift 云,任何人都可以开设账户并测试 OpenShift 本身。

另外还有一个选项:Minishift。Minishift 是一个在您的本地计算机上启动虚拟机并在其中创建 OpenShift 集群的工具。因此,它使您能够在本地机器上尝试和测试一个功能齐全的 OpenShift 集群。这是我们将在本书中使用的选项。让我们先安装它。

安装

您可以从 GitHub 页面下载其最新版本。您还必须安装您将使用的虚拟机,并相应地配置环境变量。这个过程非常简单,只需几分钟即可完成。不同操作系统的特定安装步骤略有不同,它们在附带的安装指南中有详细描述。

启动集群

在您安装了集群后,您可以使用 minishift start 命令启动它。将默认参数提升以提供足够的内存和磁盘空间供我们开发和使用的服务是一个好习惯:

minishift start --memory=4096 --disk-size=30gb

在您运行上述命令后,您需要等待几分钟,以便集群启动:

在 minishift 启动后,我们可以使用启动日志中提供的地址访问它。第一个屏幕是登录屏幕。在这个屏幕上,您可以使用任何凭证(因为 Minishift 是一个测试工具)并点击登录按钮。完成此操作后,您将看到web console,这是管理 OpenShift 集群的一种方式。让我们更深入地了解它。

Web 控制台

web console是一个图形工具,它允许您查看和管理 OpenShift 项目的内 容。从技术角度来看,控制台是一个图形界面,它提供了对 OpenShift REST API 的方便抽象,它使用该 API 根据用户操作修改集群模型。

让我们看看主控制台窗口:

正如您在前面的屏幕截图中看到的,控制台允许您管理项目,查看其内容,并对其进行修改。概述(如前所述的屏幕截图所示)包含在 petstore 命名空间中部署的应用程序。左侧的菜单允许您查看和修改集群的不同方面,例如构建、部署或持久性资源。

在接下来的章节中,我们将广泛使用web console,在那里您将能够查看其大多数功能和能力。

YAML 表示法

虽然大多数配置都可以使用图形界面完成,但有时将需要编辑 OpenShift 对象的内部表示:YAML。

OpenShift 模型中的每个对象都可以使用这种类型的表示。如果您点击应用程序 | 部署,选择其中一个,点击右上角的操作,您将能够选择编辑 YAML 选项。这适用于控制台中的所有对象。

我们将在必要时进行此类编辑时,不时执行此操作,并通知您所执行操作的含义。

CLI

有时,使用命令行工具而不是图形界面会更方便。OpenShift 也提供了这个功能。OpenShift CLI 实现了oc命令行工具,它允许从终端管理集群。

您可以使用提供的说明安装 CLI。

为了使用 oc,您必须登录到集群,如下所示:

oc login

您将被要求提供凭证,并将必须提供在 Web 控制台中创建您的项目时使用的相同凭证。

oc 工具提供了许多操作。我们将广泛使用它来获取和描述操作。现在让我们介绍它们。

get 操作允许您获取有关给定类型对象可用性的信息;让我们调用该命令:

oc get

工具将建议一种您可以检查的对象类型;让我们看看:

图片

哇!这有很多,但别担心,您将在下一章中学到关于这些大多数内容的很多知识。让我们使用 oc get 命令检查集群中可用的服务:

图片

您还可以利用标签。如果您写下:

oc get all -l app=catalog-service

然后,您将能够看到与该服务相关联的所有类型的对象。

如您在前面的代码中所见,我们能够使用 get 命令列出我们感兴趣的对象。如果我们想获取更多关于它们的信息,我们需要使用 oc describe 命令,如下所示:

图片

describe 命令允许您读取有关给定类型对象的全部信息。

您现在已经学到了理解 OpenShift 所需的所有基本信息。现在,是时候尝试了。

OpenShift 示例中的目录服务

在本章中,我们涵盖了大量的理论,并介绍了许多将帮助您更好地理解 OpenShift 内部工作原理的概念。现在是时候在实践中尝试了。

我们将部署一个使用 h2 数据库的 catalog-service。在这个示例中,我们只会使用 Web 控制台并从书籍的代码仓库部署应用程序。

示例参考:chapter6/catalog-service-openshift-h2

让我们开始。让我们输入主机地址。您可以在 minishift 启动命令的日志中找到它。在将其输入到网页浏览器后,您将看到用户登录屏幕。让我们输入我们的用户名和密码。

我们将被引导到欢迎屏幕,如下所示:

图片

将项目名称输入为 petstore。为了将目录服务部署到 OpenShift,我们将使用 CLI 的源到镜像构建。首先,确保您已登录到集群,如下所示:

oc login

然后,您需要执行以下命令:

oc create -f https://raw.githubusercontent.com/wildfly-swarm/sti-wildflyswarm/master/1.0/wildflyswarm-sti-all.json

上述命令创建了一组必要的 OpenShift 对象,以启动 OpenShift 构建。

最后,是时候启动应用程序了:

oc new-app wildflyswarm-10-centos7~https://github.com/PacktPublishing/Hands-On-Cloud-Development-with-WildFly.git --context-dir=chapter6/catalog-service-openshift-h2/ --name=catalog-service

我们完全清楚,到目前为止,这些命令看起来很晦涩。在一段时间内,我们将把它们当作一个神奇的服务部署咒语来使用。不过,不用担心,在第八章“扩展和连接你的服务”中,它们将得到全面解释,你将能够理解整个过程的每一个部分。现在先给你一个快速概述:我们正在让 OpenShift 直接从源代码创建我们的服务构建。为了做到这一点,我们必须指定 GitHub 仓库。因为我们的书籍仓库包含许多子目录,我们必须指定包含此示例的子目录——我们使用--context-dir来指定。我们还提供了名称,使用--name命令。

现在,让我们使用 Web 控制台来检查应用程序是否已部署。

再次登录到 Web 控制台,导航到左侧的构建 | 构建。你会看到构建确实已经开始:

图片

等待构建完成,然后导航到应用程序 | 服务,并选择 catalog-service:

图片

如前一个截图所示,OpenShift 已经在一个 Pod 上部署了应用程序,并为它创建了一个服务。

在我们能够检查应用程序之前,我们必须做一件简单的事情。服务在集群外部是不可见的,因此,为了使用它们,我们必须在从外部网络可见的地址上公开它们。OpenShift 提供了一个工具,使我们能够使用这些路由。在上一个视图中,点击创建路由链接,不要做任何更改,然后点击创建按钮。之后,你将能够看到服务的对外地址:

图片

最后,我们准备好检查我们服务的运行情况。复制主机的外部地址,并添加 REST 路径:

图片

它工作了。恭喜!你已经在 OpenShift 中部署了你的第一个服务。

摘要

在本章中,你学习了关于 OpenShift 架构的很多知识。你被介绍了云计算,并提供了关于它的最基本信息。后来,你学习了 OpenShift 的架构:一个使用 Docker 镜像的 Kubernetes 集群,加上 OpenShift 层,使得集群易于使用,并允许开发者专注于编码。

在本章的后面部分,你使用 Minishift 工具启动了自己的本地 OpenShift 实例,并在其上部署了你的第一个服务。

在下一章中,你将学习如何为在云中部署的微服务配置持久存储。

进一步阅读

  1. docs.docker.com/engine/reference/builder/

  2. www.docker.com/

  3. openshift.io/

  4. github.com/minishift/minishift

  5. docs.openshift.org/latest/minishift/getting-started/installing.html

  6. docs.openshift.com/enterprise/3.1/cli_reference/get_started_cli.html

第七章:为您的应用程序配置存储

在本章中,我们将首先学习 OpenShift 存储配置的理论基础。稍后,我们将向您展示如何在云端部署数据库以及如何配置您的云应用程序以使用它。

在上一章的最后部分,我们使用 OpenShift 在云端部署了 CatalogService 的简单版本。我们还对 Web 控制台和 OpenShift cli 有了一个初步的了解。现在,是时候更进一步了。我们将重新配置我们的应用程序以使用数据库。

在做那之前,我们将介绍 OpenShift 持久化存储的概念。

OpenShift 存储概念

在上一章中,我们提到了卷的概念——OpenShift 用于实现存储的工具。让我们先更深入地了解一下它。

正如我们在上一章中提到的,OpenShift 的部署和扩展单元是 Pod,它可以包含许多容器。Pod 中的容器是短暂的——Kubernetes 可以在任何时候停止和启动它们。当容器关闭时,容器中的数据将会丢失,因为在重启过程中,新的容器是从镜像中重新创建的。

因此,我们需要另一个工具来实现存储。这个工具就是卷。

那么,什么是卷呢?从技术角度来看,卷基本上是运行 Pod 的节点上的目录,这些目录映射到容器文件系统中。此外,卷有一个明确定义的生命周期,与 Pod 生命周期相等。每当 Pod 停止时,卷就会被销毁。另一方面,当 Pod 内的容器重启时,卷保持不变;它只需在容器内重新挂载即可。

Linux 目录也可以是另一个目录或远程文件系统的链接,例如 网络文件系统NFS)。因此,当 Pod 停止时删除目录并不一定意味着删除所有数据。因此,卷的行为方式取决于其类型——我们将在下一节中描述它。

OpenShift 允许你配置多种卷类型。让我们来看看其中最常见的一些。

空目录

空目录,正如其名所示,是在节点的文件系统中创建的一个空目录。当 Pod 实例化时创建该目录,并且只要 Pod 在该节点上运行,目录就存在。当 Pod 因任何原因从节点上移除时,目录将被删除。

空目录可以挂载在 Pod 内运行的任何容器中。这种 Pod 的一个示例用途可能是用于收集公共数据的容器之间的共享目录。

正如我们在 部分中提到的,重新启动任何容器都不会导致目录的删除。目录将一直存在,直到 Pod 存在,并在容器重启后重新挂载到依赖于它的容器中。

主机路径

主机路径是另一种持久卷类型,它从节点的文件系统中挂载目录。与空目录不同,这种持久卷不会创建或销毁任何新的目录。例如,当容器需要访问主机配置的某些部分时,管理员可以将包含此配置的目录挂载到容器文件系统中。

远程文件系统

正如我们之前所暗示的,卷的目录不必指向本地文件系统。它也可以指向远程文件系统目录。从这里开始,有趣的事情开始发生。让我们更仔细地看看。

快速回顾一下,当你使用远程文件系统时,你必须在服务器上创建存储,导出它,然后将其挂载到客户端。在客户端,挂载目录将作为远程文件系统客户端实现。因此,对该目录的操作将通过给定的远程文件系统协议传播到服务器。

OpenShift 支持多种远程文件系统协议,例如 NFS 或光纤通道。此外,如果你了解你的集群架构,你可以使用专有远程文件系统,如 gcePersistentDisk(Google Cloud)或 awsElasticBlockStore(Amazon Web Services)。

根据我们所拥有的知识,让我们分析 OpenShift 中远程文件系统的行为。当启动具有远程文件系统卷的 pod 时,OpenShift 在节点上创建一个客户端目录,并根据配置将其挂载到适当的容器中。像往常一样,当容器停止时,目录不会受到影响,当容器重新启动时,它会被重新挂载。

当 pod 正在被删除或节点崩溃时,更有趣的事情发生了。在这种情况下,客户端目录正在被删除。与空目录场景相反,这并不意味着数据丢失。删除客户端目录意味着远程文件系统的一个客户端已被销毁。文件系统中的数据保持不变。

如你所见,远程卷使我们能够创建持久存储,可以将其挂载到我们的应用程序中。此外,其生命周期与 pod 独立。

好的,我们已经知道了卷是如何工作的,并且很乐意将它们添加到我们的应用程序中。然而,有一个问题,那就是配置。希望使用这些卷之一的开发者必须对集群配置有大量信息:配置了哪种远程文件系统,或者它在哪个节点上运行。更重要的是,即使你从管理员那里收集了这些信息,你也需要确保它在你的每个环境中都配置正确。

这不是很好,有多个原因。首先,我们希望将开发者与集群管理员解耦。理想情况下,开发者将指定他们需要的卷类型,而不需要了解集群配置的细节。其次,在前一章中,我们强调了统一云视图的重要性。由于这个统一视图现在受到了损害,您将不得不重新配置您的 pods,以便在测试环境中使用 NFS 而不是 Google 磁盘。我们显然不希望这样。

我们需要一个工具来解决这些问题。让我们讨论持久卷

持久卷和持久卷声明

持久卷,类似于常规卷,允许您定义不同类型的持久存储。实际上,您可以使用持久卷来定义类似于常规卷的存储类型,例如节点目录或远程文件系统。那么,区别在哪里?

持久卷是 Kubernetes 对象,就像 pods 和服务一样。它们的生命周期与任何 pods 无关。在这种情况下,我们可以将持久模块视为类似于节点——它们是集群基础设施的一部分。让我们看看以下示例PersistentVolume

apiVersion: v1
kind: PersistentVolume
metadata:
//1 
name: nfs
labels: 
  zone: 5
//2 
spec:
   capacity:
      storage: 100Mi
   accessModes:
   - ReadWriteMany
   nfs:
   server: 10.244.1.4
   path: "/exports"

如您在前面的代码中看到的,PersistentVolume指定了它将要连接的 NFS 服务器,就像卷一样。与常规卷相比,它有几个额外的字段:容量、访问模式和元数据标签。您很快就会了解到为什么它们是必需的。

最后,我们希望将这种持久存储挂载到我们的容器上。我们如何实现这一点?我们可以使用PersistentVolumeClaimsPersistentVolumeClaim是一个指定我们所需的PersistentVolume特性的对象。

让我们再次看看这个例子:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: myclaim
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 10Mi
  selector:
    matchLabels:
      zone: 5

如您在前面的代码中看到的,PersistentVolume声明指定了我们需要的存储量以及访问类型。在 OpenShift 中,您还可以使用标签机制来提供额外的匹配特性。在我们的例子中,我们使用这些来指定PersistentVolume应从哪个区域获取。

PersistentVolume为我们提供了声明持久存储的能力,这是集群基础设施的一部分,而PersistentVolumeClaim是一组标准,允许我们指定所需的PersistentStorage

然而,匹配过程看起来如何,它实际上是如何与我们创建的 pods 相关的?

技术上,PersistentVolumeClaim是一种卷,因此,您可以在您的 pods 配置中引用给定的PersistentVolumeClaim。当具有这种声明的 pods 启动时,系统中的可用PersistentStorage对象将被评估。如果可以找到匹配的PersistentVolume,它将被挂载到依赖于该声明的容器中。

让我们看看以下示例:

apiVersion: v1
metadata:
  name: mypod
spec:
  containers:
    - name: my-container
      image: tadamski/container
      volumeMounts:
      - mountPath: "/mount/storage1"
        name: pod
  volumes:
    - name: mypod
      persistentVolumeClaim:
        claimName: myclaim

正如您所注意到的,我们能够在我们的 pod 配置中引用之前创建的PersistentVolumeClaim。使用此配置,当mypod启动时,myclaim将被评估,如果找到匹配的PersistentVolume,它将被挂载到my-container内部。

是时候看看PersistentVolumePersistentVolumeClaims架构的更大图景了;这些对象将存储配置与 pod 配置解耦。因此,开发者能够指定他们需要的存储特性。他们不需要配置这些卷,也不需要了解给定集群的架构。此外,PersistentVolume是集群配置的一部分,而PersistentVolumeClaim是应用程序配置的一部分;两者都可以独立创建。包含PersistentVolumeClaim对象的应用程序可以由开发者创建并在许多 OpenShift 集群中部署而无需更改。另一方面,这些集群可能包含由集群管理员创建的不同持久存储配置。这些配置的详细信息被抽象化,从开发者那里隐藏起来。

带有数据库的目录服务

您已经学到了理解如何在 OpenShift 云中与持久存储工作的基本知识。现在,让我们通过一个实际例子来看看这一点。让我们更新我们的 catalog-service 部署,使其连接到数据库。

配置 PostgreSQL 数据库

首先,让我们确保我们已经删除了之前的catalog-service版本。为了做到这一点,我们需要使用oc delete 命令。命令界面与 get 操作的界面相同。您可以直接指定对象名称来删除对象,或者使用标签来指示要删除的对象。与单个应用程序相关的对象有很多,我们显然不想逐个删除它们。因此,我们将使用delete命令的标签版本:

oc delete all -l app=catalog-service

现在,我们准备部署数据库。打开您的网页控制台,点击“添加到项目”按钮。我们将搜索 PostgreSQL 项目:

图片

搜索结果会显示多个选项;我们需要选择数据存储选项:

图片

当您点击“选择”按钮时,以下表单会打开:

图片

我们将数据库服务器名称和数据库实例名称更改为catalogdb。为了方便起见,我们将用户名和密码都设置为 catalog。

我们还将覆盖标签:

图片

我们将为我们的不同服务使用一系列数据库容器。因此,我们不能使用标准的应用程序和模板标签。我们将它们更改为 catalogdb-template 和catalogdb

在我们完成之后,我们就准备好创建一个应用程序了;让我们点击页面底部的“创建”按钮。

我们必须等待 Pod 启动。让我们点击“概览”页面按钮,然后点击 postgresql 部署;我们必须等待有一个副本处于活动状态:

图片

应用程序正在运行。让我们用一些宠物填充我们的数据库,以便我们可以测试我们的服务行为。为此,我们需要访问运行数据库的容器控制台。为了实现这一点,我们必须转到“应用程序/Pods”菜单,选择运行 PostgreSQL 的 Pod,然后点击终端按钮:

图片

现在让我们填充数据库。让我们登录到用户目录并创建 SQL 脚本:

cd
vi items.sql

脚本与我们之前在应用程序中创建的加载脚本非常相似:

DROP TABLE IF EXISTS ITEM;

CREATE TABLE ITEM (id serial PRIMARY KEY, item_id varchar, name varchar, description varchar, quantity smallint);

INSERT INTO ITEM(item_id, name, description, quantity) VALUES ('dbf67f4d-f1c9-4fd4-96a8-65ee1a22b9ff',  'turtle', 'Slow friendly reptile. Let your busy self see how it spends 100 years of his life laying on sand and swimming.', 5);
INSERT INTO ITEM(item_id, name, description, quantity) VALUES ('fc7ee3ea-8f82-4144-bcc8-9a71f4d871bd', 'hamster', 'Energetic rodent - great as a first pet. Will be your only inmate that takes his fitness training serviously.', 10);
INSERT INTO ITEM(item_id, name, description, quantity) VALUES ('725dfad2-0b4d-455c-9385-b46c9f356e9b','goldfish', 'With its beauty it will be the decoration of you aquarium. Likes gourmet fish feed and postmodern poetry.', 3);
INSERT INTO ITEM(item_id, name, description, quantity) VALUES ('a2aa1ca7-add8-4aae-b361-b7f92d82c3f5', 'lion', 'Loves playing the tag and cuddling with other animals and people.', 9);

请注意,我们在这里改变了惯例。我们不再使用名称作为项目标识符,而是开始使用 UIDs,它将成为整个应用程序中宠物的唯一标识符。

最后,我们将执行脚本:

psql -U catalog catalogdb < catalog.sql

上述命令运行 PostgreSQL 命令行客户端。-U 参数指定用户(在我们的例子中是 catalog)和 catalogdb 参数指定客户端必须操作的架构。

我们的数据库现在准备好了,你可能会想到的问题可能是:我的持久卷在哪里?答案是:再次是 OpenShift 为你做了所有的事情。让我们进一步检查一下。

检查卷

为了查看数据库是如何配置的,让我们使用 cli

oc describe dc/catalogdb

您将能够看到,此部署配置已定义了一个 PersistentVolumeClaim 类型的卷:

图片

进一步来说,让我们分析 catalogdb 持久卷声明:

oc describe pvc/catalogdb

我们将能够看到,声明已经根据我们提供的数据库类型创建,并且它已经被绑定:

图片

如您在先前的屏幕截图中所见,OpenShift 已经根据您在从模板创建应用程序时提供的信息创建了一个 PersistentVolumeClaim。该声明已经绑定到集群中的一个 PersistentVolume。由于我们现在使用 Minishift,PersistentVolume 是通过虚拟机内部的磁盘实现的。但我们要再次强调,如果您决定在其他 OpenShift 集群上部署应用程序,您的应用程序配置不会发生任何变化。

让我们回到我们的例子。

更新目录服务

我们必须再次重新配置我们的目录服务,以便它与 PostgreSQL 数据库兼容。

示例参考:chapter7/catalog-service**-**openshift**-**postgresql

让我们从 pom.xml 的更改开始——我们必须向其中添加 Postgres 依赖项:

(...)

    <dependencies>
        (...)
        <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>jpa</artifactId>
            <version>${version.wildfly.swarm}</version>
        </dependency>
 <!-- 1 -->
 <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>${version.postgresql}</version>
        </dependency> 
    </dependencies>

(...)

我们已经将数据库驱动程序从 h2 更改为 PostgreSQL(1)。

让我们更改数据源配置:

swarm:
  datasources:
    data-sources:
      CatalogDS:
       driver-name: postgresql
       connection-url: jdbc:postgresql://catalogdb.petstore.svc/catalogdb
       user-name: catalog
      password: catalog
    jdbc-drivers:
      postgresql:
        driver-class-name: org.postgresql.Driver
        xa-datasource-name: org.postgresql.xa.PGXADataSource
        driver-module-name: org.postgresql.jdbc

我们必须重新配置 JDBC 驱动程序以使用postgresql类,并重新配置数据源,使其包含应用程序的数据。catalogdb.petstore.svc 地址的含义将在下一章中解释。

与之前的数据库示例一样,我们必须提供persistence文件:

<?xml version="1.0" encoding="UTF-8"?>
<persistence
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        version="2.1"
        xmlns="http://xmlns.jcp.org/xml/ns/persistence"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    <persistence-unit name="CatalogPU" transaction-type="JTA">
        <jta-data-source>java:jboss/datasources/CatalogDS</jta-data-source>
    </persistence-unit>
</persistence>

最后,我们必须将postgreSQL JDBC 模块添加到应用程序中...

图片

内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.5" name="org.postgresql.jdbc">

  <resources>
    <artifact name="org.postgresql:postgresql:${version.postgresql}"/>
  </resources>
  <dependencies>
    <module name="javax.api"/>
    <module name="javax.transaction.api"/>
  </dependencies>
</module>

好的,现在我们已经重新配置了我们的 catalog-service,是时候做一些有趣的事情了。让我们将我们的应用程序部署到 OpenShift。

我们将再次使用源到镜像构建,就像我们在上一章中做的那样:

oc new-app wildflyswarm-10-centos7~https://github.com/PacktPublishing/Hands-On-Cloud-Development-with-WildFly.git --context-dir=chapter7/catalog-service-openshift-postgresql/ --name=catalog-service

我们必须等待我们的 fat-JAR 启动。为了验证这一点,我们可以查看启动应用程序的 Pod 的日志:

图片

与先前的示例一样,我们必须创建一个路由。完成之后,让我们找出从集群外部可见的 catalog-service 地址:

图片

让我们复制路由名称并使用curl检查我们是否可以使用 catalog-service 获取宠物信息:

图片

它成功了。现在让我们扩展我们的服务,使其能够将数据持久化到数据库。

让我们扩展我们的CatalogService

package org.packt.swarm.petstore.catalog;

import org.packt.swarm.petstore.catalog.model.Item;

import javax.enterprise.context.ApplicationScoped;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;
import java.util.List;
import java.util.UUID;

@ApplicationScoped
public class CatalogService {

    @PersistenceContext(unitName = "CatalogPU")
    private EntityManager em;

    public Item searchById(String itemId) {
        return em.createNamedQuery("Item.findById", Item.class).setParameter("itemId", itemId).getSingleResult();
    }

 //1
 @Transactional
    public void add(Item item){
        //2
 item.setItemId(UUID.randomUUID().toString());
        em.persist(item);
    }

 //3
 public List<Item> getAll() {
 return em.createNamedQuery("Item.findAll", Item.class).getResultList();
    }

}

我们通过add方法(1)扩展了服务。请注意,该方法具有事务性,并为存储中的项目生成 UUID(2)。我们还添加了一个列出存储中所有项目的方法(3)。请注意,我们还需要为它添加 NamedQuery:

(...)

@Entity
@Table(name = "item")
@NamedQueries({
        @NamedQuery(name="Item.findById",
                query="SELECT i FROM Item i WHERE i.itemId = :itemId"),
 @NamedQuery(name="Item.findAll",
                query="SELECT i FROM Item i")
})
public class Item {
(...)

我们还必须向CatalogResource添加POST方法:

package org.packt.swarm.petstore.catalog;

import org.packt.swarm.petstore.catalog.model.Item;

import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;

@Path("/")
public class CatalogResource {

    @Inject
    private CatalogService catalogService;

    @GET
    @Path("item/{itemId}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response searchById(@PathParam("itemId") String itemId) {
        try {
            Item item = catalogService.searchById(itemId);
            return Response.ok(item).build();
        } catch (Exception e) {
            e.printStackTrace();
            return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
        }
    }

 //1
 @POST
    @Path("item")
    //2
 @Produces(MediaType.APPLICATION_JSON)
    //3
 @Consumes(MediaType.APPLICATION_JSON)
 public Response addNew(Item item) {
 try {
 catalogService.add(item);
            return Response.ok(item).build();
        } catch (Exception e) {
 return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
        }
 }

 //2
 @GET
    @Path("item")
 @Produces(MediaType.APPLICATION_JSON)
 @Consumes(MediaType.APPLICATION_JSON)
 public Response getAll() {
 try {
 List<Item> item = catalogService.getAll();
            return Response.ok(item).build();
        } catch (Exception e) {
 return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
        }
 }

}

我们实现了addNew方法,它使用CatalogService实例将Item对象(1)添加到数据库。如您在先前的代码中所注意到的,Request参数和Response都是 JSON 对象。它们由服务器自动解析;我们唯一需要做的是使用@Produces(2)和@Consumes(3)注解方法。在方法中,我们使用catalogService存储给定的 Item 对象。最后,我们根据存储操作的结果返回ok响应(5)或错误响应(6)。

我们还实现了getAll方法,这将允许我们请求商店中所有宠物的信息(2)。

应用程序准备就绪后,您必须提交更改的文件并将它们推送到 GitHub。当您这样做时,您可以进入 Web 控制台并触发更新服务的构建。为了做到这一点,您必须在 Web 控制台中点击构建 | 构建,选择 catalog-service,然后在右上角点击开始构建按钮:

图片

应用程序启动后,我们必须等待它被部署到云端。让我们使用curl向我们的存储中POST新的项目:

图片

一切似乎都很正常,所以让我们使用我们刚刚实现的请求来检查存储中可用的项目:

图片

我们数据库中有三只兔子。我们的服务在 OpenShift 集群内部运行正常。

我们现在可以检查存储是否确实持久。让我们进入网页控制台并终止 catalog-service 和数据库 Pod。为了做到这一点,进入网页控制台,点击应用程序 | Pods 并选择数据库 Pod。稍后点击右上角的操作,并选择删除。对 catalog-service Pod 重复这些操作。当这两个 Pod 都重新启动后(你可以在应用程序 | Pods 视图中监控这一点),你又可以列出所有项目。你应该能够看到提取的结果与前面的截图相同。

摘要

在本章中,你学习了如何在 OpenShift 中配置已部署服务的持久性。

本章从理论知识开始,为你提供了关于卷及其不同类型的更多细节。稍后,你学习了卷的一个特别有用的类型,即PersistentVolumeClaim。你还学习了为什么它是必要的,它与PersistentVolume的关系以及如何使用它。

最后,我们扩展了你的catalogService,使其使用postgresql数据库作为存储。

第八章:扩展和连接你的服务

在本章中,我们将更详细地探讨部署、扩展和连接应用程序的过程。在第六章,“使用 OpenShift 在云上部署应用程序”,你已经学习了将服务部署到 OpenShift 云的基本信息。现在,是时候扩展这些知识并学习如何在实践中应用它们了。

让我们从部署开始。

部署

让我们来检查在部署我们的服务时幕后发生了什么。我们将继续之前章节中的示例进行工作。

示例参考:chapter8/catalog-service-openshift-load-balancing

你需要打开 Web 控制台,并导航到应用程序|部署|catalog-service:

图片

现在,我们将能够看到部署配置。这是 OpenShift 的DeploymentConfiguration对象的图形表示。

正如你在第六章,“使用 OpenShift 在云上部署应用程序”中学到的,OpenShift 在 Kubernetes 之上添加了另一层,以提供更方便、更高效的编程体验。它通过扩展 Kubernetes 的对象模型来实现这一点。DeploymentConfiguration和 Deployments 是扩展 Kubernetes 对象模型的 OpenShift 对象。

DeploymentConfiguration对象管理 Deployments 对象的创建。它包含创建 Deployments 所需的所有必要信息,正如其名称所暗示的,它代表了一个部署实例。当一个 Deployments 触发器发生时,旧的部署对象将被新的对象替换。所有的部署对象都基于DeploymentConfiguration。Deployments,以及其他对象,封装了 Kubernetes 的ReplicationController对象。让我们更深入地理解它。

学习 ReplicationController 的基础知识

ReplicationController包含以下信息:Pod 模板、选择器和副本数量。让我们进一步探讨这些内容。

Pod 模板基本上是一个 Pod 定义。它包含有关容器、卷、端口和标签的信息。由这个复制控制器创建的每个 Pod 都将使用这个 Pod 模板启动。选择器用于确定哪些 Pod 受此ReplicationController管理。最后,副本数量是我们希望运行的 Pod 数量。

Kubernetes 的工作方式如下:它监控集群的当前状态,如果该状态与期望状态不同,它将采取行动以恢复期望状态。同样的事情也发生在ReplicationControllers上。ReplicationController持续监控与其关联的 Pod 数量。如果 Pod 的数量与期望的数量不同,它将启动或停止 Pod 以恢复期望状态。Pod 是通过 Pod 模板启动的。

让我们检查 Kubernetes 为我们的 catalog-service 创建的ReplicationController。为此,我们将使用 CLI:

图片

正如您在前面的屏幕截图中将注意到的,为 catalog-service 创建了三个复制控制器。这种情况是因为每次应用程序重新部署都会导致创建一个新的部署对象及其自己的复制控制器。请注意,只有 catalog-service-3 的期望实例数大于 0——当新的部署正在进行时,之前的部署已被设置为不活动状态。

让我们看看活动控制器的描述:

图片

选择器有三个标签:app、deployment 和 deployment-config。它明确地识别了与给定部署关联的 Pod。

Pod 模板中使用了完全相同的标签。Pod 模板的其他部分包含构建容器的镜像,以及我们在创建服务时提供的环境变量。最后,当前和期望副本的数量默认设置为 1。

好的。那么我们如何扩展我们的服务,使其在多个实例上运行?让我们再次转到 Web 控制台。我们需要再次导航到应用程序 | 部署,并输入 catalog-service 配置:

图片

要扩展 catalog-service 应用程序,我们必须将副本字段调整为我们想要的实例数量。就是这样。

当我们在oc中查看ReplicationControllers时,我们将看到以下信息:

图片

Pod 的数量已更改为 5。正如我们在oc输出中看到的,已经启动了额外的 Pod,我们现在有五个实例。让我们检查控制台(导航到应用程序 | Pods):

图片

OpenShift 确实根据我们的需求扩展了我们的应用程序。

在使用 OpenShift 一段时间后,你应该能够理解我们在第六章,“使用 OpenShift 在云上部署应用程序”中所说的意思,当我们写道 OpenShift 在 Kubernetes 之上构建了一个有效且易于使用的应用程序开发环境。前面的例子展示了它如何很好地工作:Kubernetes 负责确保集群的状态等于提供的描述。在前面的例子中,这个描述是由一个ReplicationController对象(它是 Kubernetes 对象模型的一部分)提供的。然而,请注意,OpenShift 已经为我们抽象掉了所有繁琐的细节。我们只提供了诸如代码仓库的地址或我们想要有多少副本等信息。OpenShift 层抽象掉了集群配置的技术细节,并为我们提供了方便、易于使用的工具,使程序员能够专注于开发。

让我们回到我们的主要话题。接下来我们将配置的是负载均衡

负载均衡

我们刚刚学习了如何扩展我们的服务。下一步自然的步骤是配置负载均衡器。好消息是 OpenShift 会为我们自动完成大部分工作。

在第六章,“使用 OpenShift 在云上部署应用程序”,我们介绍了服务,我们了解到服务是通过虚拟集群 IP 来访问的。为了理解负载均衡是如何工作的,让我们来了解集群 IP 是如何实现的。

正如我们在这里学到的那样,Kubernetes 集群中的每个节点都运行了一组服务,这些服务允许集群提供其功能。其中之一就是kube-proxy。kube-proxy 在每个节点上运行,并且负责服务实现。kube-proxy 持续监控描述集群的对象模型,并收集关于当前活动服务和运行这些服务的 Pod 的信息。当新的服务出现时,kube-proxy 修改 iptables 规则,以便虚拟集群的 IP 被路由到可用的 Pod 之一。iptables 规则被创建为随机选择 Pod。此外,请注意,这些 IP 规则必须不断重写以匹配集群的当前状态。

kube-proxy 在每个集群节点上运行。正因为如此,在每个节点上,都有一组 iptables 规则,这些规则将数据包转发到适当的 Pod。因此,服务可以通过集群的每个节点在其虚拟集群 IP 上访问。

从客户端服务的角度来看,这意味着什么呢?集群基础设施对服务客户端是隐藏的。客户端不需要了解任何关于节点、Pod 以及它们在集群内部的动态移动。他们只需使用其 IP 调用服务,就像它是一个物理主机一样。

让我们回到我们的示例,看看我们主机的负载均衡。让我们回到本章中我们正在工作的示例。我们静态地将目录服务扩展到五个实例。让我们进入网页控制台,查看当前运行应用程序的所有 pod:

图片

让我们追踪请求转发到哪些 pod。为了实现这一点,我们实现了一个简单的 REST 过滤器:

package org.packt.swarm.petstore.catalog;

import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.ext.Provider;
import java.io.IOException;

//1
@Provider
public class PodNameResponseFilter implements ContainerResponseFilter {
    public void filter(ContainerRequestContext req, ContainerResponseContext res)
            throws IOException
    {
        //2
 res.getHeaders().add("pod",System.getenv("HOSTNAME"));
    }
}

前面的过滤器会在响应处理完毕后(1)向响应头添加一个 "pod" 属性。该过滤器将在响应处理完毕后进行评估。在每一个 pod 上,都设置了一个 "HOSTNAME" 环境变量。我们可以使用这个变量并将其添加到响应元数据中(2)。

因此,我们准备好追踪负载均衡:

图片

在前面的屏幕截图中,请注意请求正在自动在可用的 pod 之间进行负载均衡。

服务发现

我们已经向您展示了如何配置应用程序的负载均衡。我们知道您现在可以访问 OpenShift 背后的虚拟集群 IP 地址,该地址正在对请求进行负载均衡。然而,我们实际上如何知道如何连接到我们的服务呢?我们将在下一个主题中学习这一点。在我们这样做之前,我们必须介绍我们将要相互通信的新服务。

新服务

在第一章中,我们简要介绍了宠物商店应用程序,并描述了构成它的服务。到目前为止,在我们的示例中我们只使用了目录服务。现在是时候实现定价服务和客户网关服务了。这些服务将作为本章节和未来章节的示例。让我们从定价服务开始。

定价服务

定价服务与目录服务非常相似。它可以用来通过宠物的名称获取价格。让我们直接进入实现。最初,我们必须创建数据库。像以前一样,我们将使用 PostgreSQL 模板:

图片

与目录服务的数据库一样,我们也想覆盖标签:

图片

为了填充数据库,我们必须创建以下脚本:

vi pets.sql

现在,输入示例数据:

DROP TABLE IF EXISTS PRICE;                                                                                                                                                                                                        

CREATE TABLE PRICE (id serial PRIMARY KEY, item_id varchar, price smallint);

INSERT INTO PRICE(item_id, price) VALUES ('dbf67f4d-f1c9-4fd4-96a8-65ee1a22b9ff', 50);
INSERT INTO PRICE(item_id, price) VALUES ('fc7ee3ea-8f82-4144-bcc8-9a71f4d871bd', 30);
INSERT INTO PRICE(item_id, price) VALUES ('725dfad2-0b4d-455c-9385-b46c9f356e9b', 15);
INSERT INTO PRICE(item_id, price) VALUES ('a2aa1ca7-add8-4aae-b361-b7f92d82c3f5', 3000);

为了填充数据库,我们将执行以下脚本:

psql -U pricing pricingdb < pets.sql

我们的定价数据库已经准备好了。我们现在可以开始编写代码了。

示例参考:chapter8/pricing-service

我们必须以与 catalog-service 相似的方式配置数据库。

swarm:
  datasources:
    data-sources:
 PricingDS:
       driver-name: postgresql
 connection-url: jdbc:postgresql://pricingdb.petstore.svc/pricingdb
 user-name: pricing
 password: pricing
    jdbc-drivers:
      postgresql:
        driver-class-name: org.postgresql.Driver
        xa-datasource-name: org.postgresql.xa.PGXADataSource
        driver-module-name: org.postgresql.jdbc

为了使数据库能够工作,我们必须提供 JDBC 驱动模块:

图片

如您所见,我们还需要 persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        version="2.1"
        xmlns="http://xmlns.jcp.org/xml/ns/persistence"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    <persistence-unit name="PricingPU" transaction-type="JTA">
        <jta-data-source>java:jboss/datasources/PricingDS</jta-data-source>
    </persistence-unit>
</persistence>

我们必须提供一个 Entity

package org.packt.swarm.petstore.pricing;

import com.fasterxml.jackson.annotation.JsonIgnore;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

//1
@Entity
//2
@Table(name = "Price")
//3
@NamedQueries({
 @NamedQuery(name="Price.findByName",
                query="SELECT p FROM Price p WHERE p.name = :name"),
})
public class Price {

 //4
 @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "price_sequence")
 @SequenceGenerator(name = "price_sequence", sequenceName = "price_id_seq")
 //5
 @JsonIgnore
    private int id;

 //6
 @Column(length = 30)
 private String name;
    @Column
    private int price;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}
 table that we have just created (2). We have provided NamedQueries, which will enable us to search the price of a pet by a name (3). An id, as in catalogdb, is generated using the Postgres sequence (4) and is not parsed in the JSON response (5). Finally, we have annotated the fields mapped to the price and name columns (6).

catalog-service 一样,我们还需要一个服务:

package org.packt.swarm.petstore.pricing;

import org.packt.swarm.petstore.pricing.model.Price;

import javax.enterprise.context.ApplicationScoped;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.ws.rs.WebApplicationException;
import java.util.List;

@ApplicationScoped
public class PricingService {

    @PersistenceContext(unitName = "PricingPU")
    private EntityManager em;

    public Price findByItemId(String itemId) {
        return em.createNamedQuery("Price.findByItemId", Price.class).setParameter("itemId", itemId).getSingleResult();
    }
}

我们还需要 REST 资源:

package org.packt.swarm.petstore.pricing;

import org.packt.swarm.petstore.pricing.model.Price;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;

@Path("/")
public class PricingResource {

    @Inject
    private PricingService pricingService;

    @GET
    @Path("price/{item_id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response priceByName(@PathParam("item_id") String itemId) {
        Price result = pricingService.findByItemId(itemId);
        return Response.ok(result).build();
}

我们还需要一个应用程序:

package org.packt.swarm.petstore.pricing;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/")
public class PricingApplication extends Application {
}

我们的第二个服务已经准备好了。现在是时候在 OpenShift 上部署它了。将你的应用程序推送到 GitHub 仓库并调用:

oc new-app wildflyswarm-10-centos7~https://github.com/PacktPublishing/Hands-On-Cloud-Development-with-WildFly.git --context-dir=chapter8/pricing-service --name=pricing-service

在你的应用程序部署后,你可以为其创建一个路由并验证它确实可以工作:

图片

的确如此。让我们转到第二个服务。

客户网关服务

在本节中,内容又变得更有趣了。客户网关服务是我们应用程序的网关,它将为 Web 客户端提供外部接口。我们将实现的第一项请求是获取宠物列表。让我们看一下以下图表:

图片

当执行/catalog/item请求时,服务会向CATALOG请求可用的项目。基于这些信息,宠物商店服务会向PRICE服务查询每只宠物的价格,合并结果后返回给客户端。然而,网关服务将如何知道这些服务的地址呢?我们很快就会找到答案。

示例参考:chapter8/customer-gateway-env

客户服务配置方式与之前的服务类似。如果你对配置的某些部分有疑问,请参考其描述。

让我们看看catalog/item请求的实现细节,从 REST 资源开始:

package org.packt.swarm.petstore;

import org.packt.swarm.petstore.api.CatalogItemView;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;

@Path("/")
public class GatewayResource {

    @Inject
    private GatewayService gatewayService;

 //1
 @GET
    @Path("/catalog/item")
 @Produces(MediaType.APPLICATION_JSON)
 public Response getItems() {
        //2
 List<CatalogItemView> result = gatewayService.getItems();
        return Response.ok(result).build();
    }

getItems方法从CatalogService(1)收集项目,为所有项目获取价格,并将获取的结果合并到商店中可用的宠物列表中。请注意,我们引入了CatalogItemView——这是一个传输对象,它是 Web 客户端 API 的一部分。

我们也已经实现了该服务:

package org.packt.swarm.petstore;

import org.packt.swarm.petstore.api.CatalogItemView;
import org.packt.swarm.petstore.catalog.api.CatalogItem;
import org.packt.swarm.petstore.pricing.api.Price;
import org.packt.swarm.petstore.proxy.CatalogProxy;
import org.packt.swarm.petstore.proxy.PricingProxy;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;

@ApplicationScoped
public class GatewayService {

 //2
 @Inject
    private CatalogProxy catalogProxy;

    @Inject
    private PricingProxy pricingProxy;

 //1
 public List<CatalogItemView> getItems() {
 List<CatalogItemView> views = new ArrayList<>();
        for(CatalogItem item: catalogProxy.getAllItems()) {
 Price price = pricingProxy.getPrice(item.getItemId());

            CatalogItemView view = new CatalogItemView();
            view.setItemId(item.getItemId());
            view.setName(item.getName());
            view.setPrice(price.getPrice());
            view.setQuantity(item.getQuantity());
            view.setDescription(item.getDescription()); 
 views.add(view);
        }
 return views;
    }

}

getItems方法实现(1)相当直接。我们正在合并目录和定价服务的数据,并返回结果对象的列表。这里最有趣的部分是代理,它使我们能够与这些服务通信(2)。让我们学习如何实现它们。

环境变量

当创建新的服务时,其坐标会被写入集群中每个 Pod 的环境变量中。

让我们登录到集群中的一个 Pod 并查看它。所有 OpenShift 环境变量名称都是大写的,我们需要有关定价服务的数据:

图片

在前面的屏幕截图中,请注意有许多变量描述了服务的坐标。我们感兴趣的是主机地址:

PRICING_SERVICE_SERVICE_HOST=172.30.104.212

注意,这又是虚拟集群 IP。因此,只要服务没有被移除,代理地址将保持不变。由部署、节点添加或故障引起的底层基础设施更改不会导致之前地址的改变。

让我们编写使用此变量连接到服务的代理。我们将从定价服务代理开始:

package org.packt.swarm.petstore.proxy;

import org.packt.swarm.petstore.pricing.api.Price;

import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;

@ApplicationScoped
public class PricingProxy {

    private String targetPath;

    PricingProxy(){
 //1
        targetPath = "http://" + System.getenv("PRICING_SERVICE_SERVICE_HOST")+":"+8080;
    }

    public Price getPrice(String name){
 //2
        Client client = ClientBuilder.newClient();
        WebTarget target = client.target(targetPath +"/price/" + name);
        return target.request(MediaType.APPLICATION_JSON).get(Price.class);
    }
}

就是这样。我们在创建代理时获得了clusterIP(1),并且用户直接使用 REST 客户端 API 为getPrice方法调用提供了一个适配器(2)。

catalogProxy的实现类似。

现在,我们已经准备好检查我们的应用程序是否工作。让我们为petstore服务创建一个路由并检查网页浏览器:

确实如此。然而,这个解决方案有一个主要的缺点——一个排序问题。如果 Pod 在服务之前创建,那么服务坐标将不会存在于 Pod 环境中。那么有没有更好的服务发现方法呢?是的,通过域名系统DNS)。

DNS 发现

每个 OpenShift 集群都包含一个 DNS 服务。这个服务允许您通过服务名称轻松地发现服务。每个服务在注册期间都会注册到 DNS 服务,并且随后定期向其发送实时消息。DNS 服务器使用以下模式创建记录:

${service name}.${application name}.svc

让我们以定价服务为例。我们已经创建了petstore应用程序。因此,使用前面模式创建的服务名称将是pricing-service.petstore.svc

我们可以在 Web 控制台中确认信息。让我们导航到应用程序 | 服务 | pricing-service:

注意hostname字段——这是我们之前创建的地址。另一个需要注意的重要事项是,那些服务名称只能在集群内部可见。

现在,我们已经准备好将我们的应用程序重构为使用优雅的 DNS 发现。

示例参考:chapter8/customer-gateway-dns

我们必须重写我们的代理。让我们从PricingProxy开始:

package org.packt.swarm.petstore.proxy;

import org.packt.swarm.petstore.pricing.api.Price;

import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;

@ApplicationScoped
public class PricingProxy {

 //1
    private final String targetPath = System.getProperty("proxy.pricing.url");

    public Price getPrice(String itemId){
        Client client = ClientBuilder.newClient();
        WebTarget target = client.target(targetPath + "/price/" + itemId);
        return target.request(MediaType.APPLICATION_JSON).get(Price.class);
    }
}

我们定义了一个targetPath,我们可以重复使用它来连接到服务(1)。我们将通过 YAML 配置将其作为参数提供:

proxy:
  catalog:
    url: "http://catalog-service.petstore.svc:8080"
  pricing:
    url: "http://pricing-service.petstore.svc:8080"

再次,CatalogProxy的实现类似。

现在,我们已经准备好再次重新部署 customer-gateway 服务。你可以再次检查它是否工作正确。

如您所忆,我们在创建数据库的环境文件时使用了服务的名称。集群中的每个服务都可以使用这种方法访问。

摘要

在本章中,你学习了如何在集群内部进行扩展和发现服务。正如你在本章中看到的那样,大部分工作都是由 OpenShift 完成的。负载均衡由服务自动实现,集成的 DNS 服务允许简单的服务发现。

在下一章中,你将了解更多的网络知识。你还将学习如何为服务调用提供弹性,以便底层网络故障不会导致你的应用程序停止工作。

第九章:使用 Jenkins 配置持续集成

在本章中,我们将教你如何将宠物商店应用程序与 Jenkins,一个持续集成CI)服务器集成。我们将介绍 CI 概念以及如何使用 Jenkins 实现它们。我们将配置一个示例pipeline,以便你可以看到应用程序代码中的更改是如何传播到已部署的应用程序的。

让我们从构建开始。

学习 OpenShift 构建

在前面的章节中,我们为了构建我们的应用程序做了一些真正的魔法。为了能够运行构建,我们执行了以下命令:

oc create -f https://raw.githubusercontent.com/wildfly-swarm/sti-wildflyswarm/master/1.0/wildflyswarm-sti-all.json

在前面的章节中,当我们想要构建我们的应用程序时,我们调用了以下命令:

oc new-app wildflyswarm-10-centos7~https://github.com/PacktPublishing/Hands-On-Cloud-Development-with-WildFly.git (...)

在经历了许多神秘事件(如日志增长所示)之后,我们终于看到了我们的应用程序正在运行。现在,是时候解释一下底层实际发生了什么。让我们来了解 OpenShift 构建。

通常,一个 OpenShift 构建是一个将输入参数转换成用于启动应用程序的结果对象的操作。在大多数情况下,构建会将源代码转换成一个将在集群上部署的镜像。

构建过程操作的细节取决于构建类型(我们将在稍后学习),但一般的算法如下:

  1. 构建容器从构建镜像开始

  2. 所有输入源都被注入到容器中

  3. 构建脚本正在运行

  4. 生成了输出 Docker 镜像

这里引入的新概念是构建容器。让我们更仔细地看看它。它实际上有什么目的?你构建应用程序的容器必须包含构建和运行应用程序所需的所有库、工具和运行时。例如,如果你使用 WildFly AS 构建镜像,它将包含 Java、Maven 和 WildFly 运行时等。应用程序构建完成后,相同的镜像将用作部署到 OpenShift 的 Docker 镜像的基础。更准确地说,你的应用程序将作为另一个层添加到构建镜像之上,从而生成一个包含你的应用程序的可运行镜像。好消息是,虽然你可以轻松地自己创建镜像,但在大多数情况下,这些镜像将由工具提供商创建。

输入类型可以从任何资源提供,例如 GitHub 仓库、现有镜像和 Dockerfile 配置。你提供的所有源都会在构建目录中解包并合并,该目录将由构建镜像在构建过程中处理。在这本书中我们将使用(实际上已经多次使用)的选项是 GitHub 仓库。

正如我们之前提到的,构建的工作方式取决于构建类型。您可以通过指定构建策略来定义构建类型。您可以使用 Docker、源到镜像或自定义构建来创建镜像。对我们来说最有兴趣的构建类型是源到镜像构建,我们将在下一节中解释。

还有另一种类型的构建——“管道”。pipeline 构建连接到 Jenkins CI 服务器,允许您创建一个功能齐全的 持续部署 (CD) 管道。我们将在本章的第二部分详细描述这种构建。

让我们现在转向源到镜像构建。

了解源到镜像构建

正如我们之前提到的,源到镜像构建需要一个构建器镜像,并且每次配置此类构建时都必须提供它。构建器镜像包含负责组装和运行应用程序的脚本。组装脚本将在构建算法的第 3 阶段运行,运行脚本将用作最终 Docker 镜像的启动命令。在构建过程中,包含可运行应用程序的层将被添加到构建器镜像之上,运行脚本将被设置为镜像启动命令,最终镜像将被提交。

我们已经了解了源到镜像构建的基础知识,因此现在我们可以解释在上一章部署我们的应用程序时我们做了什么。让我们从以下命令开始,这是我们在运行任何构建之前调用的命令:

oc create -f https://raw.githubusercontent.com/wildfly-swarm/sti-wildflyswarm/master/1.0/wildflyswarm-sti-all.json

上述命令负责将 YAML 对象文件包含到我们的集群中。此脚本创建的主要对象是 Docker 构建配置。如果我们使用命令行工具检查我们的集群,我们会发现已创建新的构建配置:

图片

这是我们的构建器镜像的构建配置。我们现在可以检查 Web 控制台中的构建。我们将能够看到基于 wildfyswarm-10-centos7 配置的构建已经执行:

图片

执行第一个命令后,构建器镜像被创建并存储在集群中。我们可以通过在 Web 控制台中导航到“构建 | 镜像”来确认这一点:

图片

正如您在前面的屏幕截图中所注意到的,我们在集群中有一个新的镜像,wildflyswarm-10-centos7。在这里需要注意的一个重要事项是,这些镜像已被描述为 ImageStreams。这实际上意味着什么?ImageStream,正如其名称所暗示的,是一个表示相关对象流的对象。在我们的场景中,ImageStream 包含了构建器镜像构建的所有结果镜像。

我们为构建器镜像创建了 BuildConfig。此镜像的源可能会更改;如果发生这种情况,OpenShift 将创建此镜像的新版本并将其添加到 ImageStream

流中的图片可以被标记,并且总是有最新的标记,它代表流中的最新图片。

现在我们来检查我们之前使用的 new-app 命令:

oc new-app wildflyswarm-10-centos7~https://github.com/PacktPublishing/Hands-On-Cloud-Development-with-WildFly.git (...)

我们现在准备好解释 new-app 语法意味着什么。它由两部分组成,由波浪号分隔。第一部分是构建镜像流的名称。第二部分是应用程序将从中构建的 GitHub 仓库。

在我们知道源到镜像构建的内部原理之后,我们可以再次运行构建并检查构建日志。

首先,我们必须删除我们之前部署的 pricing-service

oc delete all -l app=pricing-service

之后,我们准备好执行 new-app 命令,并使用网页控制台来检查日志:

图片

哎呀!我们必须下载所有依赖项。这个事实将导致构建需要花费大量时间:

图片

这只是一个第一次构建。那么,当我们第二次运行构建时会发生什么?

您可以使用网页控制台强制进行第二次构建,并检查日志以验证依赖项是否已重新下载。

这是一种严重的不便,因为它会导致构建类型的时间大大增加。我们能做些什么吗?是的,我们可以使用增量构建。

增量构建是源到镜像构建的一个特性,它从先前创建的镜像中提取构建艺术品,并使用它们来构建下一个镜像。

我们的构建镜像使用 Maven 插件来构建 Swarm 应用程序,因此正在下载的艺术品是 Maven 依赖 JAR。通常,不同的构建工具和不同类型的艺术品将使用。因此,特定的增量构建类型必须由镜像提供商实现。

在 Swarm 构建镜像的情况下,Maven 艺术品正从最后一个镜像中提取出来,并放置在新的 Maven 仓库中。因此,被多次使用的艺术品只需要下载一次。此外,为了减少下载 JAR 的时间,您可以使用 Maven 镜像。

好的。然而,我们如何开启增量构建?我们必须编辑我们的构建的 YAML 文件。

让我们使用网页控制台来做这件事。我们必须选择 pricing-service 构建,并导航到屏幕右上角的操作 | 编辑 YAML。YAML 必须按照以下方式编辑:

图片

正如您在前面的屏幕截图中所注意到的,我们在构建配置的 sourceStrategy 部分添加了一个增量属性,并将其值设置为 true。让我们再次运行我们的构建,看看会发生什么。

在我们新的构建日志中,我们可以看到两条乐观的行:

图片

第一条乐观的行在开始处,Maven 通知我们艺术品正在被恢复,第二条在结尾:

图片

构建只用了16.347秒,并没有比独立的 Maven 构建长多少。

配置环境变量

当我们部署我们的服务时,我们为目录和定价服务提供了环境变量脚本,这些脚本需要与我们的数据库交互。处理这个配置文件也是源到镜像构建的责任。如果用户想要向构建提供环境属性,他们必须在服务的 GitHub 仓库根目录下创建一个.s2i目录,并创建一个包含键值对的列表的环境文件。

例如,让我们回顾一下定价服务的配置文件:

POSTGRESQL_HOST=pricing-service.petstore.svc
POSTGRESQL_USER=pricing
POSTGRESQL_PASSWORD=pricing
POSTGRESQL_SCHEMA=pricingdb

在此文件中设置的属性将在镜像构建期间及其执行期间作为环境变量可用。

整个源到镜像算法

在详细介绍了源到镜像构建操作的具体内容后,让我们回顾一下 Swarm 的s2i构建步骤:

  1. 执行构建的容器是由构建镜像创建的。

  2. 应用程序的源代码将被注入到容器中。

  3. 如果启用了增量构建,Maven 工件将从之前的构建镜像中恢复。

  4. 如果提供了,环境变量将被设置。

  5. 由镜像创建者提供的组装脚本被执行。

  6. 镜像被提交,启动命令设置为镜像创建者提供的运行脚本。

想要使用源到镜像构建来构建他们的应用程序的开发者必须提供构建镜像的名称和应用程序的源代码。开发者可以启用增量构建并提供环境变量。

源到镜像概要

现在我们已经了解了源到镜像构建的内部工作原理,是时候从更广泛的角度来看待它了。

OpenShift 提供的源到镜像构建工具是另一个抽象化 Kubernetes 集群细节的工具,为开发者提供了一个简单的接口。开发者的角色是提供源代码和将要用于构建的镜像名称。创建镜像的责任在于组装将在集群上部署的 Docker 镜像。

再次强调,这导致了关注点的分离——构建镜像提供者负责以最佳方式组装源代码,而这些优化的细节不需要开发者知道。

由构建架构产生的构建的性能影响如下。执行构建和创建可运行容器所需的库位于创建一次(并且后来只在集群内更新)的构建镜像中。在构建过程中下载的工件如果启用了增量构建,可以从之前的构建中恢复。因此,应用程序的依赖项只需下载一次,以后可以重复使用。这导致构建时间非常快。正如你可能记得的那样,我们的定价服务的构建只花了大约 16 秒,这比现代工作站上的独立 Maven 构建只多了几秒钟。

此外,Docker 使用的恒定好处之一——可重复性,也适用于构建镜像。所有构建都是使用完全相同的镜像进行的。因此,可以保证构建结果在所有环境中都是相同的。

此外,由于构建镜像只是标准的 Docker 容器,并且明确的构建者合约允许工具创建者轻松编写构建镜像,因此有各种各样的 Docker 构建镜像可供使用。作为开发者,你已经可以访问到大量针对各种开发工具的专用构建镜像。

最后,源到镜像构建工具是代表 OpenShift 哲学核心的工具。它提供了一个简单的开发者界面,该界面抽象了集群内部结构,并且底层实现了优化的构建过程。

开发者视图

到目前为止,我们已经详细解释了源到镜像构建是如何根据你的代码构建镜像的。新应用命令不仅仅创建构建。正如你记得的那样,执行后,我们能够测试运行中的应用。显然,构建和镜像并不是命令的唯一产品。

除了BuildConfiguration之外,新应用命令还会创建DeploymentConfiguration(我们在第六章中描述过,使用 OpenShift 在云上部署应用程序)以及为我们应用创建的ImageStream

让我们看一下以下图中创建的对象:

图片

在前面的图中,与构建镜像相关的对象被涂成红色,与构建相关的对象被涂成蓝色,与部署相关的对象被涂成绿色。构建是由开发者通过将更改推送到 GitHub 触发的。它导致构建对象的创建。如果构建成功,镜像将被推送到镜像流。这进一步触发了应用的部署,如果部署成功,将导致应用服务的创建。

需要注意的重要一点是,在最简单的场景中,开发者可能只需负责将更改推送到仓库——换句话说,编程及其更改将被传播到集群中。

这听起来不错,但在某些场景中,我们希望得到更多:一个包含集成测试、检查已部署的应用程序或在不同环境中预演更改的完整 CD 管道。正如我们之前暗示的,我们可以将 OpenShift 集群与 Jenkins 集成,以充分利用其全部功能来为我们服务的实现 CD 管道。让我们学习如何做到这一点。

管道构建

在第一章中,当我们解释为什么你可能会考虑在你的应用程序中实施微服务架构时,我们提到了当前应用程序开发人员和架构师面临的一些挑战。

可能使我们能够以使我们能够应对那些挑战的方式提供软件的关键工具之一是自动化。正如我们在上一章中提到的,OpenShift 使我们能够自动化基础设施的提供。然而,我们需要的不仅仅是这些。

我们还希望自动化将软件部署到生产环境的过程。理想情况下,我们希望拥有能够使我们立即发布软件的工具。OpenShift 以构建管道的形式提供了这样的工具。让我们介绍这个概念背后的原理。

让我们从持续集成(CI)开始。

持续集成

作为一名开发者,你对项目的开发过程了如指掌。有许多开发者正在处理不同的功能,并将它们贡献给同一个仓库。所有开发者的贡献都必须集成到代码仓库中,以便创建稳定的代码。之后,代码可以发布到生产环境。

这听起来很简单,但如果你不创建一个执行此过程的有序顺序,你很快就会陷入混乱。如果开发者很少集成,他们就是在自找麻烦。他们的仓库将高度分歧,应用程序的功能将在他们的仓库之间分散。结果,在开发过程中,将没有当前状态的源仓库,我们将没有关于应用程序状态的信息。应用程序的新版本将在人们决定将他们的贡献推送到主代码(这可能会发生在发布的前一天)时出现。此时,集成过程将是痛苦的,不兼容的贡献将被发现,错误将出现。这种情况在过去被描述为集成地狱

由于前面提到的问题,很明显,频繁地集成代码是一个好主意。提倡这种行为并,更重要的是,提供如何操作的提示的方法论被称为持续集成(CI)。

显然,频繁地将代码推送到仓库并不能给我们带来太多帮助。在每次提交时,我们需要确保当前版本的代码至少能够编译,并且通过单元和集成测试。这绝对不是一个详尽的列表:为了正确声明你的代码,你可能还需要自动代码检查或代码审查等。

为了使此过程能够持续执行,它必须自动化,并在用户想要对代码进行更改时执行。此外,开发者应频繁地集成他们的代码,在每个逻辑功能开发完成后,应尽快修复出现的任何错误。

如果遵循此程序,这将带来许多好处:

  • 问题能够迅速被发现。因此,它们的源头可以迅速调试和修复。

  • 当前应用程序的版本始终存在——它是最后一次成功构建的结果。在每一个点上,我们都可以了解应用程序的状态、其工作原理以及目前实现了哪些功能。

  • 自动化过程作为质量控制的一个触发器。构建保证能够运行并且可重复。

持续部署

持续集成确保源代码的持续构建。它要求经常推送修复,并为开发者提供即时反馈。如果我们扩展这个概念,并配置我们的构建基础设施,以确保我们的服务将自动构建和部署,会怎样呢?

这种方法,作为持续集成的扩展,被称为持续部署。为了实施它,我们还需要自动化发布过程。这意味着我们必须保留所有需要发布软件到指定环境的资源,例如环境属性或配置脚本。

作为回报,我们将能够获得可靠且可重复的发布。首先,由于发布过程不再是手动的,发布过程中的所有魔法都被移除了。发布是通过使用环境属性(这些属性是版本化构建配置的一部分)的发布脚本来执行的。这些文件是关于构建过程的一个单一真相来源。因此,如果在构建过程中发生错误,这些脚本必须被修复。没有地方可以进行手动修补或临时修复。此外,构建经常发生,因此配置错误将有机会发生并被修复。另一方面,一旦构建和发布开始正常工作,每个后续的正确构建都会增加对发布过程的信心。因此,发布成为一个经过充分测试和自动化的活动。

这种方法通过改变开发功能的速度来改变团队的工作方式。使用持续部署(CD),你不是将软件的大块内容一次性发布给客户。相反,小的功能经常发布,并且立即对客户可见。

这是因为许多原因的预期行为。首先,客户会希望尽可能快地响应客户需求。拥有使他们能够做到这一点的工具将是客户的一个很大的市场优势。然而,这不仅仅是这样:因为新功能经常发布,它们会立即对客户可见。因此,客户可以立即评估实际实施的功能。这就在开发者和客户之间创建了一个有效的反馈循环,使他们能够更快地达到客户实际期望的功能。

部署管道

自动交付的过程是通过一个pipeline实现的。pipeline是一系列步骤,它以源代码作为输入,并在其输出上提供一个可工作的应用程序。

pipeline的目标是确保源代码准备好在生产环境中部署。因此,pipeline应该能够尽快捕获错误,并立即向开发者提供反馈。

此外,因为最终产品是发布的应用程序,pipeline应该自动化发布过程,以便它在所有环境中运行相同。

虽然pipeline是一个可配置的脚本,其直接操作取决于你的具体环境,但在部署pipeline中执行了许多常见的步骤:提交、构建、自动测试、手动测试、发布等。

在 OpenShift 环境中配置持续部署

在快速回顾理论之后,现在让我们回到我们的集群,并为我们的应用程序配置 CD。

在本章的开头,我们描述了源到镜像的构建,我们在前面的章节中使用了它。我们还暗示了有一个pipeline构建可用。正如你现在可能已经猜到的,这是我们用来实现服务 CD 的构建类型。

pipeline构建使用 Jenkins 服务器来配置pipeline配置。在继续之前,让我们快速介绍一下它。

介绍 Jenkins

Jenkins 是一个开源的软件自动化服务器。它允许创建pipeline并提供相关的语法。那么,我们如何在 OpenShift 集群中使用 Jenkins 并配置pipeline执行呢?让我们来看看。

我们的第一个pipeline

让我们从创建我们的第一个pipeline开始。我们必须登录到我们的网页控制台,并导航到“添加到项目”|“导入 YAML”。

为了做到这一点,我们必须进入网页控制台的主网页,并导航到“添加到项目”|“导入 YAML/Json”,并在那里输入以下脚本:

apiVersion: v1
kind: BuildConfig
metadata:
  name: pricing-service-pipeline
  labels:
    name: pricing-service-pipeline
spec:
  runPolicy: Serial
  strategy:
    type: JenkinsPipeline
    jenkinsPipelineStrategy:
      jenkinsfile:"pipeline { \n agent any\n stages {\n stage('Build') {\n steps {\n echo 'Pipeline is running'\n }\n }\n }\n }\n"

在脚本创建后,我们可以点击创建按钮:

图片

在我们进一步查看pipeline代码之前,让我们注意正在发生的事情。如果我们到达网页控制台的主视图,我们会注意到有一个新的资源:

图片

让我们看看当前可用的 Pods:

图片

的确,有一个新的 Jenkins 服务器正在运行部署,并且 Jenkins 服务器的容器正在创建中。OpenShift 使用 Jenkins 服务器运行 pipeline 构建。因此,每次你创建一个 pipeline 时,OpenShift 都必须检查集群中是否存在 Jenkins 服务器。如果没有,OpenShift 将自动启动一个。

Jenkins 服务器创建需要一些时间,所以我们必须等待它被部署。在我们能够在 Pods 视图中看到应用程序正在运行后,我们就准备好开始构建我们的第一个 pipeline

为了做到这一点,让我们导航到“构建 | Pipelines”。你将能够看到有一个新的 pipeline 存在:

图片

让我们点击“开始 Pipeline”按钮,看看会发生什么:

图片

注意在先前的截图中的构建已经运行。带有勾选标记的点表示一个阶段已经运行并且成功。我们将在稍后讨论 Jenkins pipeline 结构。现在,让我们通过点击“查看日志”按钮来查看更多关于当前构建的信息:

图片

正如你在前面的截图中所注意到的,我们已经被重定向到 Jenkins 控制台。构建已经创建,打印阶段已经执行,我们回显的打印消息确实已经写入日志。

如你所见,pipeline 构建配置已经自动转换为 Jenkins 构建,并在 Jenkins 控制台中运行。当我们点击屏幕左上角的 petstore/pricing-service-pipeline 时,我们将获得更多关于构建的信息:

图片

在这个窗口中,我们可以追踪构建历史,查看最新执行的日志和时间,或者编辑 pipeline 等。在这个时候,再次查看我们为创建 pipeline 编写的脚本是个好主意。你可能立刻就注意到 Jenkins pipeline 被压缩成一行,这使得阅读和编辑变得困难。在我们采取任何其他步骤之前,让我们找到一种人工的方式来编辑我们的 pipeline

为了做到这一点,让我们点击左侧菜单上的“配置”按钮并向下滚动:

图片

我们在这里有一个很好的 pipeline 编辑器。让我们对文件进行第一次编辑:

图片

然后,我们将对其进行测试以检查它是否工作。为了做到这一点,我们必须保存 pipeline 并在构建视图中点击“立即构建”按钮。之后,我们就可以通过点击刚刚执行的第二次构建来检查日志。

图片

我们将看到新的日志如下:

此外,让我们再次登录到 Web 控制台,并检查那里的pipeline

正如你所注意到的,pipeline构建配置已经根据我们在 Jenkins 中做出的更改进行了相应的修改。我们将使用 Jenkins 服务器执行未来的更改。

我们在构建中打印的新消息承诺,我们的构建将在某个时候做一些有用的事情。毕竟,我们想要为我们的服务创建一个 CD pipeline,而不是打印消息。不过,在我们能够做到这一点之前,我们还需要学习一些其他的东西。一开始,我们需要说更多关于我们用来定义pipeline的语言的话。

Pipeline 语法语言

当我们编写第一个pipeline时,我们使用了 Jenkins 声明式 pipeline 语言。我们将在下一节中描述声明式 Pipeline 语言DPL)的要点。

核心 pipeline 元素

为了做到这一点,让我们回到上一节中执行过的pipeline

//1
pipeline {
    //2
    agent any
    //3
    stages {
        //4
        stage('Print') {
            steps {
                echo 'This pipeline will build pricing-service one day'
            }
        }
    }
}

DPL 中的每个pipeline都必须用pipeline块(1)包围。

pipeline必须以agent指令(2)开始。这个指令指定了可以执行构建阶段(稍后会详细介绍)的 Jenkins 构建机器。这个设置可以在每个阶段中覆盖。在我们的例子中,我们将为所有阶段使用任何代理。

核心 pipeline 构建块是阶段。阶段旨在映射到 CD pipeline中的阶段。它们按顺序定义,并且每个阶段只能在之前的阶段成功后才能执行。

阶段必须用stages(3)块包围。每个阶段(至少需要有一个)都有自己的stage块,其名称作为参数指定。

每个阶段块可以包含一系列指令,后面跟着步骤块,该步骤块包围一个或多个将在pipeline中执行的步骤。

现在,我们来到了关键点。我们可以执行哪些可用的步骤?Jenkins 提供了由不同插件提供的非常多的不同步骤。我们将专注于一个特定的插件,该插件使得在 OpenShift 集群上开发和执行操作变得容易——让我们讨论 OpenShift,pipeline Jenkins 插件(进一步阅读,链接 1)。

标准 Maven 操作

我们将要实现的第一阶段是单元测试阶段。一开始,我们将像在第五章中那样,以相同的方式添加一个简单的单元测试,使用 Arquillian 测试你的服务。我们必须扩展pom.xml


(...)

    <dependencies>
        (...)
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>${version.postgresql}</version>
        </dependency>

 //1
 <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${version.junit}</version>
            <scope>test</scope> </dependency>

 //2
 <dependency>
            <groupId>org.jboss.arquillian.junit</groupId>
            <artifactId>arquillian-junit-container</artifactId>
            <scope>test</scope> </dependency>

 //3
 <dependency>
            <groupId>org.wildfly.swarm</groupId>
            <artifactId>arquillian</artifactId>
            <version>${version.wildfly.swarm}</version>
            <scope>test</scope> </dependency>

 //4
 <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>${version.h2}</version>
            <scope>test</scope> </dependency>

    </dependencies>

    (...)

</project>

回想一下,我们必须为 JUnit(1)、Arquillian(2)、Swarm 的 Arquillian 适配器(3)以及我们将使用的内存数据库添加依赖项(4)。

其次,我们必须提供测试资源,即persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        version="2.1"
        xmlns="http://xmlns.jcp.org/xml/ns/persistence"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    <!-- 1 -->
    <persistence-unit name="PricingPU" transaction-type="JTA">
        <!-- 2 -->
        <jta-data-source>java:jboss/datasources/PricingDS</jta-data-source>
        <properties>
            <!-- 3 -->
            <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
            <property name="javax.persistence.schema-generation.create-source" value="metadata"/>
            <property name="javax.persistence.schema-generation.drop-source" value="metadata"/>

            <property name="javax.persistence.sql-load-script-source" value="META-INF/load.sql"/>
        </properties>
    </persistence-unit>
</persistence>

以及我们将用来测试数据库的加载脚本:

DROP TABLE IF EXISTS PRICE;

CREATE TABLE PRICE (id serial PRIMARY KEY, name varchar, price smallint);

INSERT INTO PRICE(name, price) VALUES ('test-pet', 5);

确保我们也添加了h2驱动模块:

图片

我们现在准备好编写测试了:

package org.packt.swarm.petstore.pricing;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.wildfly.swarm.Swarm;
import org.wildfly.swarm.arquillian.CreateSwarm;
import org.wildfly.swarm.datasources.DatasourcesFraction;
import org.wildfly.swarm.jaxrs.JAXRSArchive;
import org.wildfly.swarm.spi.api.Module;

import javax.inject.Inject;

//1
@RunWith(Arquillian.class)
public class PricingServiceTest {

    //2
    @Deployment
    public static JavaArchive createDeployment() {
        return ShrinkWrap.create(JavaArchive.class)
                .addClasses(Price.class, PricingService.class)
                .addAsResource("META-INF/persistence.xml")
                .addAsResource("META-INF/load.sql")
                .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
    }

    //2
    @CreateSwarm
    public static Swarm createSwarm() throws Exception {
        DatasourcesFraction datasourcesFraction = new DatasourcesFraction()
                //3
                .jdbcDriver("h2", (d) -> {
                    d.driverClassName("org.h2.Driver");
                    d.xaDatasourceClass("org.h2.jdbcx.JdbcDataSource");
                    d.driverModuleName("com.h2database.h2");
                })
                .dataSource("PricingDS", (ds) -> {
                    ds.driverName("h2");
                    ds.connectionUrl("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
                    ds.userName("sa");
                    ds.password("sa");
                });

        Swarm swarm = new Swarm();
        swarm.fraction(datasourcesFraction);

        return swarm;
    }

    //3
    @Inject
    PricingService pricingService;

    //4
    @Test
    public void testSearchById() {
       Assert.assertEquals(pricingService.findByName("test-pet").getPrice(),5);
    }
}

现在,我们终于准备好编写测试阶段了。我们希望这个阶段运行得快,如果出现问题,立即失败,而不创建镜像或更改我们的 OpenShift 模型中的任何内容。为此,我们将使用命令行中的标准 Maven 和 git。

为了做到这一点,我们需要配置这些工具。为此,我们必须转到 Jenkins 主菜单中的 Jenkins 配置,点击管理 Jenkins 并选择 JDK 的工具配置:

图片

以及 Maven:

图片

我们最终准备好更新我们的pipeline了。让我们看看:

pipeline { 
//1
 agent any
//2 
tools {
    maven 'maven3.5.2'
    jdk 'jdk8u152'
    git 'Default'
 }
 stages {
//3
 stage('Unit tests') {
     steps {
      //4
      git url: 'https://github.com/PacktPublishing/Hands-On-Cloud-Development-with-WildFly.git'
 //5
 sh 'mvn clean test -Dswarm.build.modules=target/test-classes/modules'
 }
 }
}

我们已经为强制代理提供了任何(1)并配置了 Maven、JDK 和 git 工具,为它们都提供了版本。我们将我们的打印阶段替换为单元测试阶段(3),该阶段包括以下两个步骤:

  1. 第一步克隆了pricing-service的 git 仓库(4)

  2. 第二步运行 Maven 测试(5)

为了使测试能够工作,我们必须提供模块目录。

好的。所以,我们有了第一个阶段。接下来是什么?如果单元测试通过,我们希望构建并部署一个包含我们应用程序的镜像。为了做到这一点,我们必须在pipeline内部与我们的集群对象交互。将帮助我们轻松完成这项工作的工具是 OpenShift Pipeline Plugin。让我们更多地了解它。

OpenShift Pipeline Plugin

Jenkins 具有可插拔的架构,这允许插件开发。OpenShift 提供了它自己的插件,它允许以声明式方式直接在 OpenShift 集群对象上执行操作。该插件提供了一系列命令。我们将在pipeline开发过程中逐一介绍它们。

在开始时,我们将编写一个构建阶段,该阶段将组装镜像并确保应用程序能够正确运行。

我们将要使用的第一个命令是openShiftBuild命令。它允许运行 OpenShift 集群中定义的构建之一。这个命令接受一个强制参数buildCfg,它是将要执行的构建的名称。

我们将要使用的第二个命令是Build。这个命令也接受buildCfg参数,并检查这种类型的最后构建是否在合理的时间内成功完成。为了设置这个周期,我们将使用waitTime参数。

让我们看看我们的新pipeline

pipeline { 
 agent any
 tools {
    maven 'maven3.5.2'
    jdk 'jdk8u152'
    git 'Default'
 }
 stages {
 stage('Test') {
     steps {
      git url: 'https://github.com/PacktPublishing/Hands-On-Cloud-Development-with-WildFly.git'
      sh 'mvn clean install -Dswarm.build.modules=target/test-classes/modules'
     }
    }
 //1
 stage('Build') {
     steps {
        //2
        openshiftBuild(bldCfg: 'pricing-service', showBuildLogs: 'true')
        //3
        openshiftVerifyBuild(bldCfg: 'pricing-service', waitTime: '300000')
    }
 }
 }
}

正如前一段所述,我们已经介绍了Build阶段(1),并向其中添加了两个步骤。Build命令运行我们在本章开头配置的pricing-service s2i构建(2)。验证命令检查构建是否在 5 分钟内成功执行。

我们希望在这里只构建镜像,而不部署它。因此,我们需要修改我们的构建并移除镜像更改作为部署的触发器。

之后,我们就准备好在 Jenkins 中开始我们的 Build 操作。如果你这样做并点击控制台输出,你将能够看到执行日志。让我们来看看:

图片

哎呀!如果你再次查看测试,你会注意到有一个错误,因为测试宠物价格是 5 而不是 7。在我们修复它之前,让我们记一下 pipeline 的工作方式。我们的第一个单元测试阶段立即失败。结果,没有启动其他阶段。没有构建镜像,也没有部署应用程序。让我们也看看网页控制台上的 pipeline 视图:

图片

控制台以图形方式呈现 pipeline 执行情况,显示测试阶段失败。让我们修复我们的测试并再次运行应用程序。如果你这样做并查看控制台日志,你将能够看到测试已经通过,并且 Build 阶段已经执行:

图片

当你查看网页控制台时,你将能够看到 Build 已经完成,并且镜像已经创建:

图片

让我们看看当前可用的部署:

图片

现在,我们只有构建镜像,还没有触发部署。让我们在我们的构建中添加另一个阶段。我们将使用 openshiftDeployopenshiftScaleopenShiftVerifyDeploymentopenShiftVerifyService。在这样做之前,让我们介绍这些命令中的每一个。

openshiftDeploy 命令需要一个强制参数——dplCfg——这是部署的名称。它运行应用程序的部署。

openshiftScale,无论是否有强制 dplCfg 参数,都会使用 replicaCount 参数,该参数指定应用程序的副本数量。由于我们使用此命令来扩展应用程序,我们将更改 deploymentConfig 中的实例部署数量为零。结果,只有在 openshiftScale 操作执行后,pods 才会启动,而无需不必要的缩放。

openShiftVerifyDeployment 与前两个命令具有相同的强制参数——dplCfg。此命令有三个可选参数,我们将使用所有这些参数:

  • replicaCount: 此参数指定期望的副本数量

  • verifyReplicaCount: 这是一个布尔参数,指定是否应该检查副本数量

  • waitTime: 这表示我们应该等待验证的时间,以毫秒为单位

  • openshiftVerifyService: 此命令检查服务是否可用

openshiftVerifyService 有一个强制参数:

  • svcName

  • 一个可选参数retryCount指定在声明验证无效之前尝试连接的次数

在向您展示新脚本之前,我们将介绍一个额外的概念。正如我们在本章的理论部分所提到的,构建应该立即向其作者提供关于其状态的反馈。为了响应构建状态,DPL 提供了在管道完成后根据构建状态执行操作的能力。允许这样做的是后指令。

后指令使我们能够在构建完成后执行操作。它可以放置在管道的末尾或每个阶段的末尾。后指令提供了一组子目录:always、success、failure、unstable(如果构建不稳定,则运行——结果在构建过程中发生变化)、aborted 和 changed。

在我们的脚本中,为了简化,我们将构建状态输出到控制台,但我们可以使用可用的 Jenkins 插件来配置电子邮件、HipChat 或 Slack 通知。

让我们来看看构建:

pipeline { 
 agent any
 tools {
    maven 'maven3.5.2'
    jdk 'jdk8u152'
    git 'Default'
 }
 stages {
 stage('Test') {
     steps {
      git url: 'https://github.com/PacktPublishing/Hands-On-Cloud-Development-with-WildFly.git'
      sh 'mvn clean install -Dswarm.build.modules=target/test-classes/modules'
     }
    }
 stage('Build') {
     steps {
        openshiftBuild(bldCfg: 'pricing-service', showBuildLogs: 'true')
        openshiftVerifyBuild(bldCfg: 'pricing-service', waitTime: '300000')
    }
    }
//1
 stage('Deploy'){
     steps {
         //2
         openshiftDeploy(depCfg: 'pricing-service')
         //3
         openshiftScale(depCfg: 'pricing-service',replicaCount:'3')
         //4
         openshiftVerifyDeployment(depCfg: 'pricing-service',verifyReplicaCount:'true',replicaCount:'3', waitTime: '300000')
         //5
         openshiftVerifyService(svcName: 'pricing-service')
     }
 }
 }
 post {
    //6
    success {
        echo "Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' result: SUCCESS"
    }
    //7
    failure {
        echo "Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' result: FAILURE"
    }
 }
}

我们已经按照之前描述的方式扩展了我们的管道

  1. 我们添加了Deploy阶段(1),用于部署应用程序(2)

  2. 然后,它扩展了应用程序(3)

  3. 它验证了部署是否成功(4)以及服务是否可用(5)

  4. 在每次构建之后,测试的结果都会输出到输出中,具体取决于测试是否成功(6)或失败(7)

如果您查看控制台输出,您将能够看到我们已成功执行的所有步骤。

您也可以在 Web 控制台的管道视图中验证这一点:

图片

最后,您可以在 Web 控制台中验证服务确实已创建,并且相应的 Pod 正在运行。

摘要

在本章中,您学习了 OpenShift 提供的构建基础设施。然后您学习了如何使用源到镜像构建,它从开发者那里抽象出了 Kubernetes 内部细节,并让他们仅基于代码进行构建,配置最小化。

在本章的第二部分,您学习了管道构建,这实际上是一种将 Jenkins 管道与 OpenShift 基础设施集成的方式。您还学习了如何创建管道构建以及 DPL 语法的基础知识。因此,您能够为您的 petstore 的pricing-service创建一个 CD 管道

进一步阅读

jenkins.io/doc/book/pipeline/syntax/

第十章:使用 Keycloak 提供安全

在本章中,我们将学习基于令牌的分布式安全的基础知识。我们将介绍 Keycloak——一个可以用于保护分布式云应用程序的认证服务器。作为一个实际示例,我们将保护 Petstore 应用程序 API 的一部分。

基于令牌的安全

Keycloak 使用基于令牌的安全协议。为了理解它们是如何工作的,我们将介绍它们的基本信息。

理由

在使用客户端-服务器架构构建的应用程序中,服务器通常负责实现安全。客户端向服务器提供凭证,服务器负责验证和授权用户。

这种模型与需要在不同独立服务之间进行网络调用的分布式应用程序不太兼容。

首先,每个服务负责实现安全性的架构是不可扩展的。我们更愿意创建一个服务器,该服务器负责保存有关用户的数据并实现身份验证和授权。所有其他服务在需要做出任何安全决策时都必须依赖它。

这里是一个如何呈现的示例图:

图片

这种解决方案有一个基本的缺陷:凭证共享。

在云环境中,一个调用可能跨越多个异构服务,通常我们不能假设这些服务是可信赖的。如果至少有一个与用户共享了其凭证的客户端被破坏,那么所有系统都会被破坏。

然而,还有一个更微妙的问题。在先前的架构中,它无法区分用户发起的调用和代表用户发起的调用。例如,假设SERVICE B是一个磁盘存储服务,而SERVICE A需要代表用户从它那里获取文件。如果用户将他们的凭证传播到 A,那么 A 可以做用户能做的任何事情(例如,删除所有文件)。这显然是不希望的。

前述问题导致了基于安全令牌的协议的出现。现在让我们更详细地看看这些协议。

基本架构

让我们先介绍访问令牌的基本概念如下:

访问令牌是一个字符串,表示颁发给客户端的一组授权凭证。当客户端想要代表用户执行请求时,它需要获得这样做许可。令牌代表这样的许可。

访问令牌是为特定客户端创建的。因此,用户可以限制与访问令牌关联的权限,从而限制客户端。

访问令牌包含授权信息——基于访问令牌,服务可以决定执行调用的客户端是否被允许执行特定的操作。访问令牌不包含关于用户的信息,拥有访问令牌并不意味着请求是由用户执行的。

例如,使用访问令牌的分布式协议的典型流程如下:

图片

注意以下在先前的示例中的事项:

  1. 客户端想要在SERVICE A上执行请求,但需要用户权限才能这样做。因此,它请求用户给予这种权限。

  2. 如果用户同意,她将向身份验证服务器进行身份验证(2)。

  3. 如果身份验证成功完成,服务器将生成一个访问令牌,并将其发送到客户端(3)。

  4. 客户端可以使用此令牌代表用户访问SERVICE A(4)。

  5. 此外,令牌可以被传播到其他服务(5)。

让我们强调这种架构最重要的特性:

授权(无论使用哪种方法)仅发生在用户和身份验证服务器之间。没有其他组件可以访问任何类型的用户凭证。因此,授权服务器是唯一需要完全信任的组件。

访问令牌代表代表用户执行某事的权限,并为单个客户端生成。因此,向特定客户端颁发的访问令牌可以包含一组最小的权限,允许它执行其工作。此外,如果令牌传播到受损的客户端或被盗,它造成的损害比仅泄露用户凭证要小得多。尽管如此,为了最小化这种情况的影响,访问令牌的颁发时间很短。因此,即使令牌被盗,它也只有在很短的时间内才能被使用(具体的协议可能定义客户端刷新访问令牌的方法)。

总结来说,基于令牌的安全协议允许实现去中心化的身份验证和授权:用户向受信任的身份验证服务器进行身份验证并获取可用于授权访问服务的令牌。这在云架构中特别有用:基于受信任的身份验证服务器生成的令牌,我们可以访问网络中分布的多个异构服务,并为他们提供代表我们执行操作的能力,确保与令牌关联的权限集对服务执行其工作来说是尽可能小的。

有许多协议标准化了这种类型的分布式安全。其中之一是OpenID ConnectOIDC),这是 Keycloak 默认使用的现代协议;我们将在示例中使用它。让我们更详细地看看它。

OpenID Connect

OIDC 是建立在 Oauth2 协议之上的,这是一个开放标准,用于委托访问。Oauth2 直接指定了前面段落中概述的过程应该如何执行,哪些参与者将参与其中,以及他们应该如何以及按什么顺序合作以获取访问令牌并用于授权。问题是 Oauth2 只指定了这一点,为其实现留下了很大的空间。因此,它可以被认为是一个用于构建协议的框架,而不是一个协议本身。

OIDC 是一个使用此框架创建的协议。它填补了实现上的空白(例如令牌格式),并添加了 OAuth2 缺少的认证信息。OIDC 标准化了获取用户信息的方式。

协议的细节超出了本书的范围。如果您对此感兴趣,请参阅协议规范(进一步阅读,链接 1,2)。在本章中,我们将向您介绍您理解 Keycloak 配置以进行分布式安全基本使用所需的最少内容,我们将在以下示例中展示。

OIDC 指定了流的数量——详细描述获取和使用令牌的过程的程序。在下一节中,我们将查看认证码流程,这是 Keycloak 使用的默认流程。

认证码流程

在本节中,我们将描述认证码流程。这是一个精确的描述,正如您将看到的,它将直接影响认证服务器的配置。它假设客户端是一个运行在浏览器内的网络应用程序。因此,您应该字面地解释所使用的术语。例如,如果我们谈论客户端将用户重定向到认证服务器,我们字面意思是浏览器将 HTTP 重定向到认证服务器的地址。正如我们之前建议的,您将在本章的后面看到这些操作,当我们完成对宠物商店应用程序的安全保护时。

现在让我们看一下流程图:

图片

用户使用客户端,这是一个基于浏览器的应用程序。当用户在某个时刻执行登录操作时,他们会自动重定向到认证服务器。认证是在认证服务器和用户之间进行的。客户端与此无关:它的执行方式对它来说过于晦涩,客户端不会与用户提供的任何凭证进行交互,无论使用什么类型的认证方法。因此,凭证只提供给网络上的一个实体;如果它没有被破坏,那么它们也不会被破坏。

如果身份验证过程以成功结束,身份验证服务器将生成一个身份验证代码:一个非常短暂、一次性的代码,将用户重定向到客户端。客户端将使用代码和自己的凭据向身份验证服务器进行身份验证。如果客户端正确地进行了身份验证,身份验证服务器将生成一个访问令牌并将其返回给客户端。为什么这一步是必要的?正如我们在前面的部分中已经提到的,必须为资源所有者生成访问令牌,即客户端元组。用户和客户端都必须向身份验证服务器进行身份验证,以便生成令牌。身份验证服务器知道应该将哪些权限委派给客户端,并据此创建身份验证令牌。

除了访问令牌外,用户还获得一个 ID 令牌和可选的刷新令牌。

我们还提到,OIDC 也提供了有关身份验证的信息。这是真的。这些信息包含在 ID 令牌中。与可能对客户端不透明的访问令牌相比,ID 令牌包含提供给客户端的信息,并且不能用来访问资源。换句话说,ID 令牌是为客户端生成的令牌,允许他们读取有关用户的信息。我们将在我们的示例中使用这种功能。

我们还提到,一个协议可以指定刷新访问令牌的方式。OIDC 通过使用刷新令牌来实现这一点。刷新令牌允许客户端创建一个新的访问令牌,正如我们之前提到的,当旧的令牌过期时,新的令牌是短暂的。客户端可以使用刷新令牌来保持其授权在所需的时间内有效,而无需委派新的访问代码(使用户再次进行身份验证)。客户端应保持刷新令牌的机密性——即使访问令牌被泄露,也只能在短时间内使用,并且只有客户端才能获取新的令牌。

正如您将在 WildFly Swarm Keycloak 适配器 部分中看到的那样,我们将直接处理访问令牌(我们必须在我们的服务之间传播它们),但不会处理其他令牌,因为它们的函数被封装在 API 之后。让我们继续流程。

客户端获得访问令牌后,将其与请求一起发送到资源服务器。资源服务器必须验证令牌是否正确,提取其中的授权数据,并决定是否允许请求。

在示例应用程序中我们将使用的访问令牌是携带令牌。这意味着任何拥有这些令牌的实体都可以像使用它们一样使用它们。因此,这些令牌可以传播到我们应用程序中的所有微服务(我们很快就会利用这一点)。另一方面,这也意味着令牌的泄露是危险的,因此,携带令牌不能通过不受信任的网络发送(我们将在本章末尾讨论这一点)。

我们已经介绍了足够多的理论,并且,像往常一样,在艰难的技术介绍之后,我们将转向实践部分:一个充满牛奶和蜂蜜的地方,那里的工具为我们做所有事情。让我们立刻跳进去!

介绍 Keycloak

为了确保我们的 Petstore 应用程序的安全,我们将使用 Keycloak。Keycloak 是一个开源的、单点登录SSO)身份管理服务器,它支持基于 OIDC 的安全,以及其他功能。

Keycloak 配备了一个方便的、基于网页的用户界面,它使我们能够使用图形界面配置其行为的各个方面。此外,我们将编写的服务也必须与 Keycloak 集成。为了使这种集成变得简单,Keycloak 提供了一组适配器,这些适配器是可以安装到任何给定类型服务中的组件。在下面的示例中,我们将讨论如何使用这两个工具。

在我们开始之前,让我们概述我们将添加到宠物商店应用程序中的功能。

购物车服务

到目前为止,我们实现的所有服务都可以被匿名用户访问。在本章中,我们将实现购物车功能。显然,为了向购物车添加东西,你必须在该应用程序中进行身份验证:

我们将在我们的 OpenShift 集群中部署 Keycloak 服务,并配置客户网关和 购物车服务,以便它们只允许可以授权为客户的用户使用该 API 的这部分。让我们开始吧。

安装 Keycloak

为了使用 Keycloak,我们必须首先安装它。Keycloak 主要是基于 WildFly AS 的 Java 应用程序。为了云的使用,提供了一个 OpenShift Docker 镜像:

jboss/keycloak-openshift 

服务器已经配置好了,可以直接部署到 OpenShift 集群。让我们使用 OpenShift 网络控制台来部署它:

正如我们将很快看到的,所有配置都将使用管理员控制台进行。在配置中,我们必须使用环境变量提供初始管理员凭据:

由于服务器将在 OpenShift 代理后面可用,我们必须将 PROXY_ADDRESS_FORWARDING 参数设置为 true

设置这些参数后,我们就可以开始将镜像部署到我们的 OpenShift 集群中了。我们需要点击创建按钮,等待 Keycloak pod 启动。

Keycloak 服务器,就像网关服务器一样,必须从集群外部(由我们,管理员和petstore-ui)访问。那么让我们创建一个路由吧。我们将像之前章节中做的那样进行:我们必须点击网页控制台的服务菜单中的创建路由。我们应该使用默认参数。路由创建后,我们将能够看到其 IP 地址,如下所示:

图片

记下这个 IP,因为我们将在配置中大量使用它。

好的,我们已经将 Keycloak 部署到了我们的集群内部。我们终于准备好前往 Keycloak 网页控制台(我们刚刚创建的那个)了。

创建领域

如果你遵循了创建的路径,你会看到 Keycloak 欢迎页面。点击Authentication console链接,并输入我们在将 Keycloak 服务器部署到集群时定义的凭据(admin/admin)。你将看到管理控制台。

管理控制台是一个 UI,允许你轻松配置分布式安全性的所有方面:

图片

在前面的屏幕截图中,看看左上角——菜单的标题是 Master。这意味着什么?Keycloak 负责管理一组用户、他们的凭据、角色和用户可以委托访问权限的客户端。Keycloak 提供命名空间,允许对这些对象进行分组。这些命名空间被称为领域。领域是隔离的,每个领域只能管理它包含的对象。因此,所有与 Keycloak 服务器通信的服务都必须指定它们引用的是哪个领域。

为了我们的目的,我们将创建自己的 petstore 领域。为了做到这一点,我们必须点击左上角的 Master,并添加新的域名,称为 petstore。

创建客户端

如理论部分所述,认证服务器必须知道所有用户可以委托访问权限的客户端。在我们的应用程序中,我们需要配置一个客户端:petstore-ui。为了做到这一点,点击左侧菜单中的客户端,然后点击创建客户端按钮:

图片

我们必须将新的客户端设置 Client ID 设置为 petstore-ui。记住,我们的 Web 应用程序将在尝试登录时被重定向到 Keycloak。操作完成后,身份验证服务器必须使用客户端请求中发送的重定向 URI 将用户重定向到客户端。Keycloak 验证 URI 是否可信。因此,我们必须提供一个客户端可能运行的 URI 列表。我们将添加运行具有 UI 的浏览器的本地主机地址。此外,一些浏览器执行 跨源资源共享CORS)策略检查。将 Web Origins 参数设置为 + 将使 Keycloak 在重定向 URI 被正确验证时返回适当的 CORS 标头。

我们现在已经创建了我们的领域,并告诉 Keycloak 关于将使用它的客户端。现在让我们配置用户。

用户和角色

为了这个示例,我们将配置一个具有客户和管理员角色的用户。让我们从创建角色本身开始。

在 Keycloak 控制台中,我们需要在左侧菜单中点击“角色”,然后在右侧点击“添加角色”按钮,并输入客户角色:

图片

接下来,我们需要为管理员角色重复前面的步骤。

之后,我们需要以类似的方式添加用户:点击“用户”菜单,然后点击“添加用户”按钮,并输入我们用户的名称:

图片

这次,我们需要进行更多的配置。我们必须在凭据部分创建密码,如下所示:

图片

然后,将客户角色映射到用户:

图片

我们已经创建了用户 tomek,并赋予他们客户和管理员角色。如果 tomek 使用 petstore-ui,他们应该能够调用允许管理员角色的请求吗?不。petstore-ui 的目的是供商店客户使用。我们在理论部分提到,身份验证服务器应该创建一个包含客户端执行其工作所需的最小权限数量的访问令牌。在这个具体的例子中,petstore-ui 应该只允许代表 tomek 执行允许客户进行的请求。为了配置这一点,我们需要引入范围。

范围

Keycloak 范围是一个工具,允许您指定哪些角色将与为特定客户端生成的访问令牌相关联。让我们为 pestore-ui 客户端创建一个范围。

为了做到这一点,您必须在左侧菜单中点击“客户端”,选择 petstore-ui 客户端,然后在客户端名称下点击“范围”标签:

图片

正如您在前面的屏幕截图中所注意到的,每个客户端默认将允许的完整范围参数设置为 true。这意味着认证用户拥有的每个角色都将与为该客户端创建的访问令牌相关联。为了限制给定客户端的角色,我们必须关闭此选项并手动选择允许的角色。在我们的例子中,我们需要选择客户角色并将其移动到分配的角色。

管理员角色尚未移动。因此,如果为 tomek 代表petstore-ui生成访问令牌,它将只包含客户角色。因此,tomek 将无法从petstore-ui客户端执行任何管理员操作。

我们已经基本配置了 Keycloak。

那么,您如何配置 Java 服务?在找到答案之前,让我们介绍下一个需要用户身份验证的功能——购物车服务。

购物车服务

让我们介绍购物车服务的实现。首先,我们必须将新资源添加到客户网关 API 中。

示例参考:chapter10/cart-service

package org.packt.swarm.petstore;

import org.packt.swarm.petstore.api.CartItemView;
import org.packt.swarm.petstore.api.CatalogItemView;
import org.packt.swarm.petstore.cart.api.CartItem;

import javax.inject.Inject;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import java.util.List;

@Path("/")
public class GatewayResource {

    @Inject
    private GatewayService gatewayService;

    @GET
    @Path("/catalog/item")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getItems() {
        List<CatalogItemView> result = gatewayService.getItems();
        return Response.ok(result).build();
    }

 @GET
    @Path("/cart/{customerId}")
 @Produces(MediaType.APPLICATION_JSON)
 public Response getCart(@PathParam("customerId") String customerId) {
 List<CartItemView> cart = gatewayService.getCart(customerId);
        return Response.ok(cart).build();
    }
 @POST
    @Path("/cart/{customerId}")
 @Produces(MediaType.APPLICATION_JSON)
 public Response addToCart(@PathParam("customerId") String customerId, CartItem item, @QueryParam("additive") boolean additive) {
 gatewayService.addToCart(customerId, item, additive);
        return Response.ok().build();
    }
 @DELETE
    @Path("/cart/{customerId}/{itemId}")
 @Produces(MediaType.APPLICATION_JSON)
 public Response deleteFromCart(@PathParam("customerId") String customerId, @PathParam("itemId") String itemId) {
 gatewayService.deleteFromCart(customerId, itemId);
        return Response.ok().build();
    }

    @POST
    @Path("payment")
    @Produces(MediaType.APPLICATION_JSON)
    public Response payment(@QueryParam("customerId") int customerId, @Context  SecurityContext securityContext){
        try {
            String paymentUUID = gatewayService.buy(customerId);
            return Response.ok(paymentUUID).build();
        } catch (Exception e) {
            return Response.status(Response.Status.BAD_REQUEST).build();
        }
    }

}

正如您在前面的代码中所注意到的,我们添加了三个购物车方法。所有这些方法都通过customerId(我们将在几分钟内向您展示如何获取它)和应用程序的itemId来识别购物车。正如前几章所做的那样,所有方法都将实现委托给网关服务,该服务反过来使用代理将调用传播到后端服务。

为了对购物车执行操作,用户必须对系统进行身份验证。让我们确保GatewayResource的安全性,以确保未经授权的用户将无法访问这些方法。

WildFly Swarm Keycloak 适配器

如您从理论部分所知,具有安全 API 的服务必须根据身份验证令牌对其用户进行授权。为了做到这一点,它们必须与 Keycloak 服务器合作,在我们的例子中,使用 OIDC 协议。显然,我们不会自己实现此功能。正如我们建议的那样,Keycloak 为不同的工具提供了一系列适配器。WildFly Swarm 也有自己的适配器。那么,我们将如何安装它?

让我们扩展客户网关的pom.xml

(...)
<dependency>
    <groupId>org.wildfly.swarm</groupId>
    <artifactId>keycloak</artifactId>
    <version>${version.wildfly.swarm}</version> </dependency>
(...)

就这样——适配器已安装。

我们还有一件事要做。我们必须配置适配器。为了做到这一点,我们必须在 Maven 的资源目录内添加keycloak.json文件。该文件包含一些适配器配置属性。在我们的例子中,这相当简单:

{
  "realm": "petstore",
  "auth-server-url": "${env.KEYCLOAK_URL}/auth",
  "resource": "petstore-ui"
}

基本上,这些文件告诉适配器auth-server的位置、域和资源名称。如您所回忆的那样,我们都在这些后面:我们创建了指向部署在 OpenShift 上的 Keycloak 服务器的路由,为我们的宠物商店应用程序创建了域,并告诉 Keycloak 我们将使用配置的petstore-ui客户端进行身份验证。

请注意,我们需要提供 KEYCLOAK_URL 作为环境变量。这是我们所创建的 keycloak 代理的 URL。您可以在 web 控制台中配置此变量:

在提供此信息之后,Keycloak 适配器将能够根据 UI 提供的访问令牌授权用户。这是好的,但我们还没有保护我们的资源。

为了做到这一点,我们必须对客户网关的主类进行一些修改:

package org.packt.swarm.petstore;

import org.jboss.shrinkwrap.api.Archive;
import org.wildfly.swarm.Swarm;
import org.wildfly.swarm.keycloak.Secured;

public class Main {

    public static void main(String[] args) throws Exception {

       (...)    

        Archive<?> deployment = swarm.createDefaultDeployment();
        secureDeployment(deployment);

        swarm.deploy(deployment);
    }

    private static void secureDeployment(final Archive<?> deployment){
        deployment.as(Secured.class)
 .protect("/cart/*")
 .withMethod("POST","GET","DELETE")
 .withRole("customer");
    }
}

正如您在前面的代码中所注意到的,在将 deployment 标记为 Secured 之后,我们能够使用链式 API,我们使用它来指定哪些资源上的哪些请求需要保护以及哪些角色被允许。

通过前面的代码,我们确保只有具有customer角色的用户能够使用客户 API 中的与购物车相关的方法。请注意,然而,后端购物车服务也需要得到保护。

我们将以类似的方式将其安全地连接到网关服务,这确保了它依赖于 swarm 的 Keycloak 适配器,将 keycloak.json 添加到类路径中(注意,文件的上下文将保持相同,因为所有属性都保持有效),并在主函数中确保部署安全。

我们有一个问题:在网关服务中,我们依赖于 UI 在每次认证用户执行请求时发送给我们访问令牌。如您从前面的章节中回忆起来,我们使用 Rest Client 在后端服务上执行调用,并负责附加所有必要的信息。因此,我们还需要在调用期间将访问令牌传播到后端服务。如果我们不这样做,后端服务中的 Keycloak 适配器将识别请求为匿名请求,这显然是不正确的。

为了传播上下文,我们将实现一个简单的 JAX-RS 客户端请求过滤器。在我们的场景中,我们将检查访问令牌是否存在,如果存在,则将其进一步传播到调用中:

package org.packt.swarm.petstore.security;

import org.keycloak.KeycloakPrincipal;

import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.ext.Provider;
import java.io.IOException;

//1
public class AuthTokenPropagationFilter implements ClientRequestFilter {

    private static final String BEARER = "Bearer";

    //2
    @Context
    SecurityContext securityContext;

    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        //3
        KeycloakPrincipal keycloakPrincipal = (KeycloakPrincipal) securityContext.getUserPrincipal();
        //4
        if(keycloakPrincipal != null && keycloakPrincipal.getKeycloakSecurityContext()!=null) {
            //5
            String token = keycloakPrincipal.getKeycloakSecurityContext().getTokenString();
            if(token != null) {
            //6
                requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER + " " + token);
            }
        }
    }
}

让我们逐步分析这段代码:

  1. 如您所回忆的那样,JAX-RS ClientRequestFilter (1) 在将其传播到服务器之前,会过滤与它关联的客户端执行的每个调用。在我们的场景中,我们将检查访问令牌是否存在,如果存在,则将其附加到每个请求。

  2. 在 (2) 中,我们正在注入 SecurityContext。如果 Keycloak 使用访问令牌授权了用户,它将创建 SecurityContext 并将其附加到请求上。因此,我们将将其注入到调用线程上的对象中。

  3. Keycloak 创建的 SecurityContext 包含主体接口的 KeycloakPrincipal 实现 (3)。

  4. 如果存在主体,我们将能够从它那里获取进一步的 KeycloakSecurityContext (5)。

  5. 最后,可以从上下文中获取令牌并将其进一步传播为 BEARER 令牌。

让我们来看看 CartProxy

package org.packt.swarm.petstore.proxy;

import org.packt.swarm.petstore.cart.api.CartItem;
import org.packt.swarm.petstore.security.AuthTokenPropagationFilter;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import java.util.Arrays;
import java.util.List;

@ApplicationScoped
public class CartProxy {

    (...)

    public List<org.packt.swarm.petstore.cart.api.CartItem> getCart(String customerId){
            Client client = ClientBuilder.newClient();
            client.register(new AuthTokenPropagationFilter());
            WebTarget target = client.target(targetPath + "/cart/" + customerId);
            return Arrays.asList(target.request(MediaType.APPLICATION_JSON).get(org.packt.swarm.petstore.cart.api.CartItem[].class));
    }

    (...)

}

代理与我们在前几章中创建的代理类似,并使用标准的 JAX-RS 客户端 API 创建适当的请求到后端购物车服务。然而,我们必须为客户端注册我们刚刚创建的TokenPropagationFilter

让我们总结一下我们已经做的事情:我们扩展了 customer-gateway 以包含购物车方法,向项目中添加了 Keycloak 适配器及其配置,并将部署标记为Secured,指定了有权使用给定方法的角色。我们对 customer-gateway 和后端购物车服务都做了这件事。为了确保访问令牌传播到后端服务,我们实现了一个适当的过滤器并将其注册在我们的代理内部。看起来认证应该现在可以工作了。让我们试试!

您将在附带的代码中找到 UI 应用程序:

第十章/petstore-ui.

让我们启动 petstore 应用程序并打开 UI:

图片

在前面的屏幕截图中,没有用户登录到应用程序。为了获取所有必要的数据,客户端必须在项目资源上执行调用。由于资源未受保护,调用成功。为了登录购物车,我们必须进行身份验证。让我们来做这件事:

图片

点击登录按钮将我们重定向到 Keycloak 服务器。在提供您的凭据后,Keycloak 将验证您并将您重定向到商店:

图片

正如您在前面的屏幕截图中注意到的,我们已经成功登录到 UI。现在您可以向购物车添加项目并查看购物车视图:

图片

我们已经向您展示了具有有效安全配置的商店操作,但请随意玩转这个示例应用程序,以确认安全功能是否正常工作。您可以在 Keycloak 控制台中为用户创建和删除角色,或者使服务要求不同的角色进行授权。您将看到 Keycloak 确实能够正确授权用户;具有有效角色的用户将被允许执行请求,而缺乏有效角色的用户将被禁止。

SSL 配置

正如我们在本章开头提到的,当我们描述携带令牌时,在生产环境中使用这种类型的认证,您必须使用加密连接。在本节中,我们将描述 WildFly Swarm 和 OpenShift 如何支持使用安全连接。

我们将根据我们是否认为我们的云环境是安全的,将这一部分分为两个不同的案例。

安全的云

在您使用的云环境安全的情况下,您需要配置外部客户端和路由器之间的加密连接:

图片

在这种情况下,我们必须配置一个边缘路由器。边缘路由器有自己的证书,能够与外部客户端建立安全连接。在代理流量到目的地之前,路由器会加密连接。因此,在 OpenShift 集群中,通信是通过不安全连接进行的。

您可以为每个创建的路由配置边缘路由。为了这样做,您必须以标准方式创建路由(选择您想要建立路由的服务并点击创建路由)。之后,您必须选择一个安全路由,因此,从 TLS 终止下拉菜单中选择边缘,并在表单中输入您的 PEM 格式的证书,如下所示:

不安全的云

在某些情况下,您可能需要为所有通信配置安全连接,无论是与外部服务还是云内部:

为了这样做,您必须配置一个具有透传 TLS 终止的路由(以与前面段落相同的方式创建路由并选择透传终止)。

使用透传终止,路由器不会终止 TLS 连接,加密流量将被传播到目标。其影响之一是目标(以及所有其他服务)需要配置其安全设置。

Swarm 通过提供 HTTP 配置使您能够轻松地做到这一点。让我们看看示例配置:

swarm:
  https:
    only: true
    port: 8443
  http:
    keystore:
      path: keystore.jks
      password: password
    truststore:
      path: truststore.jks
      password: password

上述配置指定服务器将仅在8443端口上使用安全连接。它指定了trustorekeystore(类路径)的位置以及它们的password

摘要

在本章中,我们展示了您如何使用分布式安全协议通过我们的实际示例来保护您的云应用。

我们本章开始时介绍了基于分布式身份验证和授权的概念:令牌的理由以及它们如何获取和使用。后来,我们介绍了关于具体分布式安全协议的基本信息:OpenID Connect。

在实际部分,我们使用了 Keycloak SSO 服务器来保护 Petstore 应用程序中的购物车服务。

在此,我们将讨论如何使用断路器模式处理不可靠的网络问题;具体来说,是 Hystrix 库。

进一步阅读

  1. oauth.net/2/

  2. openid.net/developers/specs/

  3. www.keycloak.org/

第十一章:使用 Hystrix 增加弹性

在本章中,我们将学习如何处理分布式环境中不可避免的网络故障。为了做到这一点,我们将介绍断路器架构模式,并讨论何时应该使用它以及它的好处。我们将查看其 Netflix 实现,Hystrix。我们还将介绍其实现方式和如何使用它。作为一个例子,我们将使用 Hystrix 为我们的示例应用程序添加弹性和容错性。

不可靠的网络

当你在分布式环境中开发你的服务时,你必须考虑到服务的调用将通过网络进行。因此,应用程序必须准备好处理网络故障,这肯定会发生。

由于单个表现不佳的服务可以毒害大量服务,这个问题进一步加剧。让我们看看可能使这种情况成为可能的场景数量。

依赖服务

在大型分布式系统中,每个服务都会对其他服务有大量的依赖。只需要一个依赖失败,就会使服务变得不负责任。此外,服务也会崩溃,使其对依赖它的其他服务不可用。这种情况被称为级联故障(进一步阅读,链接 1)。

然而,这还不是全部。在一个生产就绪的环境中,当有很多调用正在进行时,一个有延迟问题的服务会迅速阻塞所有可用的线程,并使所有其他服务不可达。

显然,如果我们想设计一个健壮的分布式系统,我们需要一个工具,该工具将使用户能够处理之前描述的问题。我们将使用的工具是 Hystrix 库。

Hystrix 是由 Netflix 开发的一个库,用于处理服务故障并为复杂的分布式架构提供鲁棒性。让我们来看看 Hystrix 是如何处理之前描述的问题的。

断路器模式

为了处理之前描述的问题,创建的架构设计模式是断路器模式。其背后的主要思想很简单:将调用代码封装在命令中,该命令将执行调用并计算远程服务的状态。如果服务被命令使用的指标声明为不可达,则立即拒绝后续调用。在给定时间后,将再次尝试建立新的连接,如果成功,命令将再次开始对服务进行调用。

该模式的名称来源于电路断路器,这是一种用于保护电路免受过电流可能造成的损害的设备。如果电路中的电流过高,则断路器打开,阻止电流流动。为了使电路再次运行,必须关闭断路器。

由于其原型,软件断路器继承了电气术语。如果目标服务是健康的,并且调用直接转发到它,我们将谈论闭合断路器。如果健康指标超过,调用将不会执行,断路器将打开。

显然,负责实现断路器的库必须提供算法来决定远程服务是否健康,何时以及如何打开电路,以及断路器关闭时应该做什么。让我们讨论一下 Hystrix 是如何做到这一点的。

Hystrix 断路器

以下图表展示了 Hystrix 断路器的行为:

在调用远程服务期间,Hystrix 检查断路器是否打开。这个决定是基于从最近调用中收集的统计数据做出的。如果最后时间窗口中的失败百分比低于配置的阈值,那么电路是打开的,调用将被执行。

在执行调用之后,断路器将它的结果(成功/失败)存储在统计数据中。这些统计数据是在配置的时间窗口内收集的,这个窗口进一步被分成多个桶,每次只丢弃一个桶,这样就不会一次性丢弃给定窗口内的所有数据。

当电路打开时会发生什么?首先,算法检查配置的睡眠时间是否已经过去。如果是这样,那么只允许执行一个请求。这个断路器阶段被称为半开,其目的是检查被调用的服务是否再次健康。如果调用成功,那么断路器再次打开,并重置指标。另一方面,如果睡眠时间没有超过,或者半开状态中的一个调用失败了,那么断路器再次打开,并重置睡眠时间。

因此,我们现在知道了 Hystrix 断路器算法以及它是如何对成功和失败的调用统计做出反应的。然而,我们实际上是如何定义失败的?当调用被标记为失败时,有三种情况。首先,配置的调用超时已经超过。其次,客户端库抛出了异常。第三,给定依赖项可用的线程数已超过。

最后一点是大头板算法的实现。让我们更深入地了解它。

大头板

为了防止依赖项之一使用应用程序的整个线程池的情况,Hystrix 为每个依赖项保留线程池。如果其中一个依赖项变得迟钝,它将保持所有线程忙碌,并拒绝进一步的调用,从而导致失败计数增加。这种策略被称为大头板

这次,术语来自船舶工程:船体被分成隔离的舱壁,以便一个地方的船体损坏只会导致一个舱壁被水填满。同样,为每个依赖项提供一个线程池,如果其中一个服务表现不佳,只会使用一个专门的线程池。

在复杂的分布式环境中,通常应用程序有许多依赖项,每个依赖项都依赖于其他客户端库。通常,这些库是由第三方公司提供的黑盒库,这使得它们难以调试。此外,增加这些库的数量会增加其中一个库“毒害”整个应用程序的风险。通过隔离舱壁(bulkheading),你可以轻松地减轻这种风险。

每个客户端的状态可以通过其线程池的状态轻松跟踪。如果监控显示某个线程池已满,这表明应该对其进行检查。如果底层问题得到解决,线程池将清理并继续服务的操作。

共享相同线程池的依赖项是可配置的。因此,你可以根据你的架构调整隔离舱壁的行为。这种配置是通过 Hystrix 分组机制完成的,我们将在本章后面的示例中向你展示。

因此,我们已经知道调用可能会失败或被 Hystrix 强制失败。但在那种情况下会发生什么?处理调用失败机制的机制称为备用。现在让我们更深入地了解它。

备用

Hystrix 实现了一个备用机制,允许你在调用失败时执行你的代码。命令允许你实现一个备用方法,该方法将在失败时执行。该方法无论失败的原因如何都会执行——在超时或线程池溢出时都会执行相同的方法。

备用方法不必实现。如果未实现备用,Hystrix 抛出的异常将被传播到堆栈跟踪中。

如果,另一方面,你决定实现备用,你有一系列策略可以这样做。让我们看看几个例子。

如果你正在使用用于读取数据的服务的,在调用失败的情况下可以返回一个空答案。在这种情况下,如果服务失败,将没有数据可用。这种解决方案隐藏了底层失败并立即返回响应。问题是,显然,请求的数据不可用。你可以通过实现本地缓存并在失败的情况下返回最新的响应来处理这种情况。在这种情况下,失败将被隐藏,数据将可用。它可能不会在失败时是最新的,但它将允许你的系统继续其操作。

假设现在你正在使用授权服务来决定用户是否有权执行一些进一步的操作。在这种情况下,你可以实现回退机制,该机制将始终返回相同的响应。然而,这个响应应该是什么?像往常一样,这取决于你的用例。在某些场景中,你可能想避免用户已经付费但无法使用服务的情况。在这种情况下,你将每次都返回成功的授权。缺点是,在授权服务失败时,许多用户将能够使用他们当时未付费的内容。在其他场景中,你可能需要拒绝所有用户的授权。当然,临时的允许所有策略并不适合银行应用程序。在这种情况下,你必须拒绝所有用户的授权。

最后,在某些场景中,不编写回退是一种好的策略。假设你正在实现一个调用,该调用应作为事务操作的一部分修改某些数据。在这种情况下,传播的异常是我们想要的策略:整个操作将停止,异常将被传播到事务管理器,这将回滚事务。

在本节中,我们只简要介绍了许多可能的回退实现策略。正如你可能已经注意到的,具体的实现(或缺乏)直接取决于你的服务业务需求。要记住的关键点是,Hystrix 不会允许网络故障损害你应用程序的行为,如果发生故障,它将允许你使用回退机制来处理它。

整个算法

最后,我们准备总结 Hystrix 库的行为:

图片

在开始时,用户构建命令并启动其执行。Hystrix 检查与此命令关联的断路器是否关闭。如果断路器是打开的,那么调用将立即被拒绝,并执行回退(如果已实现)。如果断路器是关闭的,那么将检查线程池。如果没有可用的线程在线程池中,那么调用将失败;可选地执行回退并将失败报告给断路器。另一方面,如果有可用的线程,那么调用开始。如果调用错过了超时,那么错误会被报告给断路器,并可选地执行回退。

在这种场景中,线程可能会被阻塞。Hystrix 会超时,但必须等待客户端库返回线程。如果调用完成并失败,那么错误会被报告给断路器,并且可以选择执行回退。

最后,如果执行成功,则将成功报告给断路器,并将响应作为命令执行的结果返回。

您已经学习了 Hystrix 断路器实现的基础知识。现在是时候学习其基本 API 了,我们将在本章后面使用它。让我们现在就来做。

使用 Hystrix

为了在实践中学习 Hystrix 的行为,我们将扩展客户网关服务,使其调用时使用 Hystrix。稍后,我们将使我们的其中一个服务人工无响应,并观察 Hystrix 的行为。让我们开始吧。

示例参考:chapter11/customer-gateway-hystrix

首先,我们将向 pom.xml 中添加 Hystrix 依赖项:

(...)

<dependency>
    <groupId>com.netflix.hystrix</groupId>
    <artifactId>hystrix-core</artifactId>
    <version>${version.hystrix}</version> </dependency>

(...)

断路器命令是通过扩展 com.netflix.hystrix.HystrixCommand 类实现的。让我们看看我们的 PricingProxy 的具体示例中的使用情况:

(...)

@ApplicationScoped
public class PricingProxy {

    (...)

 //1
 private class GetPriceCommand extends HystrixCommand<Response> {

        private final String itemId;

 //2
 public GetPriceCommand(String itemId) {
            //3
 super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("pricing-service")); this.itemId = itemId;
        }

 @Override
        //4
        protected Response run() {
            //5
 Client client = ClientBuilder.newClient();
            WebTarget target = client.target(targetPath + "/price/" + itemId);
            return target.request(MediaType.APPLICATION_JSON).get();
        }
    }
}

命令必须扩展抽象类 HystrixCommand(1)。该类必须使用一个类型参数化,该类型将作为命令结果返回。在我们的例子中,它将是 JAX-RS 响应类——与我们用于原始调用的相同。

该类将 itemId 参数作为参数(2),该参数将在调用中使用。

如您在构造函数代码中所见,我们向其中提供了 HystrixCommandGroupKey 参数(3)。HystrixCommand 构造函数允许您提供三个参数的组合:HystrixCommandGroupKeyHystrixThreadPoolKey 和超时。这两个枚举参数用于命令分组:具有相同组键的命令将属于同一组,并将为了报告、警报和监控的目的分组在一起。线程池键指定了属于同一 Hystrix 线程池的命令。当线程池键未启用时,组键用作线程池标识符。

因此,在我们的例子中,所有 pricingService 调用命令都将属于同一组,并将使用它们自己的线程池。HystrixCommand 构造函数中的第三个参数是调用的超时时间。如果没有提供,则使用默认超时。

我们必须扩展 HystrixCommand 类的 run 方法(4)。当命令执行时,将调用此方法。如您所见(5),方法的内容与我们的原始调用中的代理代码相同。

现在,让我们看看如何执行命令:

(...)

@ApplicationScoped
public class PricingProxy {

    private final String targetPath = System.getProperty("proxy.pricing.url"); //1
    public Price getPrice(String itemId){
        //2
 return new GetPriceCommand(itemId).execute().readEntity(Price.class);
    }

    (...)
}

代理的 getPrice 方法(1)创建命令对象(2),并在其上调用 execute() 方法。这导致执行理论部分中描述的整个断路器算法。现在,让我们调用 catalog/item 方法并测量其调用时间:

没有区别;调用立即执行,没有任何错误。现在,让我们使 pricingService 人工无响应。

示例参考:chapter11/pricing-service-misbehave

为了做到这一点,我们将在返回结果之前让服务等待指定的时间:

package org.packt.swarm.petstore.pricing;

import org.packt.swarm.petstore.pricing.model.Price;

import javax.enterprise.context.ApplicationScoped;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

@ApplicationScoped
public class PricingService {

    @PersistenceContext(unitName = "PricingPU")
    private EntityManager em;

    public Price findByItemId(String itemId) {
 //1
 LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5));
        return em.createNamedQuery("Price.findByItemId", Price.class).setParameter("itemId", itemId).getSingleResult();
    }
}

让我们将新服务部署到云中并重试一次调用。结果如下:

图片

正如您在前面的屏幕截图中所注意到的,调用导致了失败。断路器处于开启状态,线程池中有一个可用的线程。因此,调用被执行,但超出了默认的 Hystrix 超时时间,该时间等于 1 秒。

为了确认这一点,让我们查看日志:

图片

Hystrix 在这里没有撒谎:超时已超过,我们尚未实现回退。我们将在下一秒做到这一点,但在那之前,让我们学习如何修改 Hystrix 属性。

如果您想修改HystrixCommand的配置,您必须使用带有Setter参数的构造函数。这个类允许您配置之前描述的所有构造函数参数。除此之外,该类还允许您为断路器行为的不同方面提供配置属性。此类属性的详尽列表在 Hystrix 文档中描述。在这里,我们将展示一些示例修改。让我们从断路器超时开始:

(...)
private class GetPriceCommand extends HystrixCommand<Response> {

    private final String itemId;

    public GetPriceCommand(String itemId) {
 //1
 super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("pricing-service"))
 .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
        //2
 .withExecutionTimeoutInMilliseconds(100)));
        this.itemId = itemId;
    }

    @Override
    protected Response run() {
        Client client = ClientBuilder.newClient();
        WebTarget target = client.target(targetPath + "/price/" + itemId);

        return target.request(MediaType.APPLICATION_JSON).get();
    }
}
(...)

上述代码修改了我们的命令类,以将调用超时缩短到 500 毫秒。使用Setter类,并且CommandGroupKey的设置方式与之前的示例相同(1)。为了修改配置,我们添加了带有适当配置的HystrixCommandProperites.Setter(2)。现在,让我们看一下以下结果:

图片

让我们重新配置应用程序以记录舱壁算法的行为;我们将增加超时时间并减少线程数量:

(...)private class GetPriceCommand extends HystrixCommand<Response> {

    private final String itemId;

    public GetPriceCommand(String itemId) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("pricing-service"))
//1           .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(3));
        this.itemId = itemId;
    }

    @Override
    protected Response run() {
        Client client = ClientBuilder.newClient();
        WebTarget target = client.target(targetPath + "/price/" + itemId);
        return target.request(MediaType.APPLICATION_JSON).get();
    }
}
(...)

为了做到这一点,必须创建另一个 setter(这次是HystrixThreadPoolProperties setter)(1)。

结果如下:

图片

正如您在前面的屏幕截图中所注意到的,前三个调用已经获取了它们的线程并且被阻塞。第四个线程立即返回,因为线程池中没有更多的线程了。

最后,让我们打开电路。如果我们在一个 bash 循环中运行代码并查看日志,我们将注意到以下结果:

图片

最后,让我们实现回退:

(...)
private class CreatePaymentCommand extends HystrixCommand<Response> {

    private final Payment payment;

    public CreatePaymentCommand(Payment payment) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(SERVICE_NAME))
                              .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                                      .withExecutionTimeoutInMilliseconds(100)));
        this.payment = payment;
    }

    @Override
    protected Response run() {
        Client client = ClientBuilder.newClient();
        WebTarget target = client.target(targetPath + "/payment");
        return target.request(MediaType.APPLICATION_JSON).post(Entity.json(payment));
    }

 @Override
    //1
    protected Response getFallback() {        //2
 return Response.status(Response.Status.SERVICE_UNAVAILABLE).build();
    }
}

(...)

为了实现回退,您必须重写getFallback方法(1)。在我们的例子中,每当paymentService不可达时,我们都返回SERVICE_UNAVAILABLE异常(2)。

现在,我们可以重新实现PetstoreService,使其在发生此类情况时创建有意义的异常:

public String buy(int customerId){
    Cart cart = cartProxy.getCart(customerId);

    Order order = createOrderFromCart(customerId, cart);
    int orderId  = orderProxy.createOrder(order);

    Payment payment = new Payment();
    payment.setMerchantId(Constants.MERCHANT_ID);
    payment.setDescription(String.format("ORDER_ID: %s", orderId));
    payment.setAmount(order.getPrice());

    Response response =  paymentProxy.createPayment(payment);

 if(response.getStatus() == Response.Status.SERVICE_UNAVAILABLE.getStatusCode()){
 throw new RuntimeException("Payment service unreachable");
    }

    return (String) response.readEntity(String.class);
}

这将是调用的结果:

摘要

在本章中,我们介绍了断路器模式背后的基本理论。

在本章的实践部分,我们将我们的宠物商店应用扩展以提供购买功能。然后,我们介绍了 Hystrix API 的基础知识,并使用它来实现与外部支付服务的弹性连接。

之后,我们使用我们自己的模拟支付服务异常实现的示例来展示断路器算法的行为。

进一步阅读

第十二章:未来方向

在本章中,我们将简要描述 Java EE 开发的未来可能的样子——平台演变的计划以及书中描述的应用程序提供的概念如何在将来实现标准化。我们还将探讨 MicroProfile 和 Jakarta EE 项目——描述它们的目的,并强调它们如何帮助您以更快的速度推进平台。

在第一章中,我们概述了 Java EE 标准创建的过程,强调了它提供的优势:可移植性和互操作性。似乎为了跟上 IT 的步伐,我们不得不放弃这些优势。让我们更深入地探讨这个问题。

不再需要标准了吗?

拥有一系列工具,使我们能够立即利用现代软件架构,使我们的生活变得更加容易。这些工具在近年来出现,是为了解决构建由大量分布在网络中的短暂服务组成的系统时必须解决的问题。需要注意的是,尽管我们选择了经过验证的解决方案,如 Hystrix 或 Keycloak,但我们已经失去了 Java EE 所提到的可移植性和互操作性优势。

问题在于创建 Java EE 标准的过程无法跟上新兴技术的快速发展。提供解决与云架构相关问题的共同标准的规范(例如,分布式安全或网络弹性)尚未成为 Java EE 的一部分。这是为什么?

标准最近版本创建的速度太慢,无法跟上所有最新的创新:Java EE 7 于 2013 年发布,而 Java EE 8 于 2017 年发布。但这并不是唯一的问题。规范是根据 Java 社区进程设计的。这个过程非常细致,包含许多步骤,旨在确保最终的标准准备好发布。

当您确实在标准化时,这个过程运作得很好——提取行业积累的知识,以提供通用的 API,以经过验证的方式解决问题。另一方面,它对创新并不那么有利。如果针对给定问题的解决方案出现,规范制定者必须有效地猜测正确的解决方案。无论标准过程多么细致,这都是非常困难的。

那么,我们最终是否应该放弃所有标准,以跟上创新?换句话说;失去标准带来的可移植性、互操作性和长期支持优势是当前 IT 世界移动速度的必然结果吗?也许我们可以做得更好。让我们讨论 Eclipse MicroProfile。

Eclipse MicroProfile

Eclipse MicroProfile 是一个定义了开发 Java 微服务编程模型的项目(进一步阅读,链接 1)。类似于 Java EE 标准,它包含了一系列规范,定义了提供微服务所需功能的标准方式。

让我们看看项目当前的内容(版本 2.0):

图片

正如你将在前面的图中注意到的那样,有一些规范直接来自 Java EE,我们在整本书的例子中广泛使用了它们(例如,JAX-RS 或 CDI)。然而,也有一些新的规范旨在处理微服务特有的问题。例如,JWT Propagation 规范处理基于令牌的安全性,而容错处理网络故障。

正如你所见,MicroProfile 是一个新兴项目,它将允许你以类似于 Java EE 的可移植性优势来构建微服务。我们在上一章中提到,Java EE 标准的开发方式使其不太适合引入创新。那么 MicroProfile 有何不同呢?

微服务范围并不是 MicroProfile 项目的唯一重要特征。另一个特征是规范的开发方式。构成 MicroProfile 的规范是在快速、基于社区的过程中开发的;如果有人有一个想法并需要向项目引入某些内容,他们可以向社区提出(进一步阅读,链接 2)。如果这个想法被接受,它就可以成为项目的一部分,发布,并向社区展示。

根据社区反馈,规范可以在下一个版本中进行修改。重要的是要注意,该项目将快速发布周期作为其基础之一。结合这两个方面,我们可以看到为什么过程是引入创新的好工具:轻量级的社区接受过程,以及响应式的反馈循环,允许快速引入新想法,并使它们快速演变。这在理论上听起来很棒,但在实践中是否可行呢?

MicroProfile 项目获得动力的方式似乎证实了这一点。项目的初始版本仅包括 CDI、JAX-RS 和 JSON-P 规范。从那时起,正如你在当前版本的图中所见,已经做了大量工作,并出现了一系列新的规范。

如果新的规范开发方式证明了自己,你可能会避免创新/可移植性的妥协。共同规范的快速演进将允许你以较快的速度提供创新,同时保持 Java EE 的优势:多厂商竞争性实现、可移植性和不同实现之间的互操作性。

另有一点需要提及的是,MicroProfile 并不认为标准化过程已经过时,也没有它的位置。相反,当 MicroProfile 的某个规范达到成熟并在社区中得到验证时,它将被委托给一个标准机构,并遵循标准化流程。

Jakarta EE

已宣布 Java EE 品牌将更名为 Jakarta EE,类似于 MicroProfile,它将成为在 Eclipse 基金会治理下的一个项目。标准的转型目前正在发生,但鉴于 MicroProfile 初始倡议的成功,可以预期新的标准创建方式将向其较小的兄弟学习很多,例如创新与标准化的分离,以及具有快速反馈的开放社区流程,在不牺牲可移植性的情况下提供最新的创新。

如果提到的努力证明是成功的,我们可以期待企业 Java 作为源自 Java EE 技术的产品家族拥有光明的未来。我们将基于经过验证的技术处理产品,这些技术基于多年的经验,同时通过允许快速创新来缓解其主要缺点。

摘要

在阅读这本书之后,你可能会对云计算和微服务领域新兴的企业软件架构有更广泛的理解。此外,你将熟悉许多工具,你可以使用这些工具来实现利用这两者的系统。我们已经向你展示了如何使用 WildFly Swarm 构建 microservices,并使用 OpenShift 在云中部署它们。在本书的后期部分,我们还向你展示了如何使用 Jenkins 配置持续部署,使用 Keycloak 配置安全,以及如何使用 Hystrix 使你的应用程序能够抵御网络故障。MicroProfile 和 Jakarta EE 的新兴解决方案有望使企业 Java 能够以更快的速度进行创新。因此,在不久的将来,你将能够利用工具来解决基于快速发展的规范描述的问题,从而保留 Java EE 的可移植性优势。

进一步阅读

  1. microprofile.io/

  2. wiki.eclipse.org/MicroProfile/FeatureInit

  3. projects.eclipse.org/projects/ee4j/charter

posted @ 2025-09-12 13:56  绝不原创的飞龙  阅读(25)  评论(0)    收藏  举报