Camel-云原生集成教程-全-
Camel 云原生集成教程(全)
一、欢迎来到 Apache Camel
作为一名解决方案架构师,系统集成是我在工作中面临的最有趣的挑战之一,因此它绝对是我热衷于讨论和撰写的东西。我觉得大多数书都太技术性了,涵盖了特定工具所做的一切,或者只是理论性的,对模式和标准进行了大量的讨论,但没有向您展示如何用任何工具解决问题。我对这两种方法的问题是,有时你读了一本书,学习了一种新工具,但不知道如何将它应用到不同的用例中,或者你非常了解理论,但不知道如何在现实世界中应用它。虽然这类阅读有足够的空间,比如当你想要一本技术手册作为参考,或者你只是想扩展你在某个主题上的知识,但我的目标是创建一个从入门视角到现实世界的实践体验的材料。我希望您能够很好地了解 Apache Camel,加深对集成实践的理解,并学习可以在不同用例中使用的其他补充工具。最重要的是,我希望你对自己作为架构师或开发人员的选择充满信心。
这本书里有很多内容。这个想法是有一个现实世界的方法,在那里你处理许多不同的技术,就像你通常在这个领域所做的那样。我假设您对 Java、Maven、容器和 Kubernetes 有一点了解,但是如果您不觉得自己是这些技术的专家,也不用担心。我将用一种对每个人都有意义的方式来介绍它们,从需要将应用部署到 Kubernetes 的 Java 初学者,到已经掌握了扎实的 Java 知识但可能不了解 Camel 或需要学习用 Java 开发容器的方法的人。
在这第一章,我将为你在这本书里将要做的一切奠定基础。您将学习所选工具的基本概念,随着您的进步,我们将讨论它们背后的模式和标准。我们正从理论内容转向运行应用。
本章的三个主题是系统集成、Apache Camel 和带有 Quarkus 的 Java 应用。我们开始吧!
什么是系统集成?
虽然这个名字很容易理解,但是我想清楚我所说的系统集成是什么意思。让我们看一些例子并讨论与这个概念相关的方面。
首先,让我们以下面的场景为例:
A 公司购买了一个 ERP(企业资源计划 )系统,该系统除了许多其他事情之外,还负责公司的财务记录。该公司还收购了一个系统,该系统可以根据财务信息,创建关于公司财务状况、投资效率、产品销售情况等的完整图形报告。问题是 ERP 系统没有一种本地方式来将其信息输入到 BI(商业智能)系统中,并且 BI 系统没有一种本地方式来消费来自 ERP 系统的信息。
上面的场景是一种非常常见的情况,两个专有软件程序需要相互“交谈”,但它们不是为这种特定的集成而构建的。这就是我说的“原生方式”的意思,即产品中已经开发的东西。我们需要在这两个系统之间创建一个集成层来实现这一点。幸运的是,这两个系统都是面向 web API 的(应用编程接口),允许我们使用 REST APIs 提取和输入数据。通过这种方式,我们可以创建一个集成层,它可以使用来自 ERP 系统的信息,将其数据转换成 BI 系统可以接受的格式,然后将这些信息发送到 BI 系统。如图 1-1 所示。
图 1-1
两个系统之间的集成层
尽管这是一个非常简单的例子,我没有向您展示这一层是如何构建的,但它很好地说明了这本书在我谈到系统集成时的含义。从这个意义上说,系统集成不仅仅是一个应用访问另一个应用。它是一个系统层,可以由许多应用组成,位于两个或多个应用之间,其唯一目的是集成系统,而不是直接负责业务逻辑。
让我们来看看这些概念之间的区别。
业务还是集成逻辑?
业务逻辑和集成逻辑是两个不同的概念。虽然可能不清楚如何将它们分开,但知道如何这样做是非常重要的。没有人想重写应用或集成,因为你创造了一个耦合的情况,我说的对吗?我们来定义一下,分析一些例子。
我在结束最后一节时说,集成层不应该包含业务逻辑,但是“这是什么意思?”。好吧,让我解释一下。
以我们的第一个例子为例。有些事情集成层必须知道,比如
-
从哪些 ERP 端点消费以及如何消费
-
如何以商业智能能够接受的方式转换来自 ERP 的数据
-
向哪些 BI 端点产生数据以及如何产生数据
这些信息与处理财务记录或提供业务洞察力没有关系,而这些能力应该由集成的各个系统来处理。该信息仅与使两个系统之间的集成工作相关。我们姑且称之为整合逻辑。让我们看另一个例子来阐明我所说的集成逻辑的含义:
想象一下,系统 A 负责识别与我们假想的公司有债务的客户。这家公司有一个单独的通信服务,当客户欠债时,可以发送电子邮件、短信,甚至打电话给客户,但如果客户欠债超过两个月,必须通知法律服务部门。
如果我们认为这种情况是由集成层处理的,那么看起来我们的集成层中有业务逻辑。这就是为什么这是一个展示业务逻辑和集成逻辑之间差异的好例子。
尽管对该客户负债时间的分析结果将最终影响流程或业务决策,但此处插入此逻辑的唯一目的是指示这三个服务之间的集成将如何发生。我们可以称之为路由,因为正在做的事情是决定将通知发送到哪里。看看图 1-2 。
图 1-2
基于接收数据的集成逻辑
移除集成层并不意味着业务信息或数据会丢失;这只会影响这些服务的集成。如果我们在这一层中有逻辑来决定如何计算费用或如何协商债务,它就不仅仅是一个集成层;这将是一个实际的服务,我们将从系统 a 输入信息。
这些都是非常简短的例子,只是为了让我们对本书的内容有一个清晰的认识。随着我们的深入,我将提供更复杂和有趣的案例进行分析。这个想法只是为了说明这个概念,正如我接下来要为一个云原生应用做的那样。
云原生应用
现在我已经阐明了我所说的集成的意思,为了充分理解本书的方法,还有一个术语你必须知道:云原生。
这本书的主要目标之一是给出一个关于如何设计和开发集成的现代方法,在这一点上,不谈论容器和 Kubernetes 是不可能的。这些技术是如此具有破坏性,以至于它们完全改变了人们设计企业系统的方式,使得世界各地的技术供应商投入大量资金来创建在这种环境中运行或支持这些平台的解决方案。
全面解释容器和 Kubernetes 是如何工作的,或者深入探究它们的架构、具体配置或用法,这超出了本书的目标。我希望您已经对这些技术有所了解,但是如果没有,也不用担心。我将以一种任何人都能理解我们在做什么以及为什么要做的方式来使用这些技术。
为了让所有人都站在同一立场上,让我们来定义这些技术。
容器 : “一种打包和分发应用及其依赖项的方式,从库到运行时。从执行的角度来看,这也是一种隔离 OS(操作系统)进程的方法,为每个容器创建一个沙箱,类似于虚拟机的想法。”
理解容器的一个好方法是将它们与更常见的技术虚拟化进行比较。看一下图 1-3 。
图 1-3
容器表示
虚拟化是一种隔离物理机器资源以模拟真实机器的方法。虚拟机管理程序是管理虚拟机并在一个托管操作系统上创建硬件抽象的软件。
我们进行虚拟化有许多不同的原因:隔离应用,使它们不会相互影响;为具有不同操作系统要求或不同运行时的应用创建不同的环境;隔离每个应用的物理资源,等等。出于某些原因,容器化可能是实现相同目的的一种更简单的方式,因为它不需要管理程序层或硬件抽象。它只是重用宿主 Linux 内核,并为每个容器分配资源。
Kubernetes 怎么样?
Kubernetes 是一个专注于大规模容器编排的开源项目。它提供了允许容器通信和管理的机制和接口。”
由于我们需要软件来管理大量的虚拟机,或者仅仅是为了创建高可用性机制,容器也不例外。如果我们想大规模运行容器,我们需要补充软件来提供所需的自动化水平。这就是 Kubernetes 的重要性。它允许我们创建集群来大规模地管理和编排容器。
这是对容器和 Kubernetes 的高度描述。这些描述给出了我们为什么需要这些技术的想法,但是为了理解术语云原生你需要知道一些关于这些项目的历史。
2014 年,谷歌启动了 Kubernetes 项目。一年后,谷歌与 Linux 基金会合作创建了云本地计算基金会(CNCF)。CNCF 的目标是维持 Kubernetes 项目,并作为 Kubernetes 所基于的或构成生态系统的其他项目的保护伞。在这种情况下, cloud native 的意思是“为 Kubernetes 生态系统制造”
除了 CNCF 的起源,“云”这个名字非常合适还有其他原因。如今,Kubernetes 很容易被认为是一个行业标准。在考虑大型公共云提供商(例如 AWS、Azure 和 GCP)时尤其如此。它们都有 Kubernetes 服务或基于容器的解决方案,并且都是 Kubernetes 项目的贡献者。该项目也存在于提供私有云解决方案的公司的解决方案中,如 IBM、Oracle 或 VMWare。即使是为特定用途(如日志记录、监控和 NoSQL 数据库)创建解决方案的利基参与者也已经为容器准备好了他们的产品,或者正在专门为容器和 Kubernetes 创建解决方案。这表明 Kubernetes 和容器已经变得多么重要。
在本书的大部分时间里,我将重点关注集成案例和解决这些案例的技术,但所有决策都将考虑云原生应用的最佳实践。在您对集成技术和模式有了坚实的理解之后,在最后一章中,您将深入了解如何在 Kubernetes 中部署和配置开发的应用。
那么让我们来谈谈我们的主要集成工具。
什么是 ApacheCamel?
首先,在开始编写代码和研究集成案例之前,您必须理解什么是 Apache Camel,什么不是 Apache Camel。
Apache Camel 是一个用 Java 编写的框架,它允许开发人员使用成熟的集成模式的概念,以简单和标准化的方式创建集成。Camel 有一个超级有趣的结构叫做组件,其中每个组件都封装了访问不同端点所必需的逻辑,比如数据库、消息代理、HTTP 应用、文件系统等等。它还有用于与特定服务集成的组件,如 Twitter、Azure 和 AWS,总共超过 300 个组件,这使它成为集成的完美瑞士刀。
有一些低代码/无代码的解决方案来创建集成。其中一些工具甚至是用 Camel 编写的,比如开源项目 Syndesis。在这里,您将学习如何使用 Camel 作为集成专用框架来编写与 Java 的集成。
让我们学习基础知识。
集成逻辑,集成路由
您将从分析清单 1-1 中显示的以下“Hello World”示例开始。
package com.appress.integration;
import org.apache.camel.builder.RouteBuilder;
public class HelloWorldRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
from("timer:example?period=2000")
.setBody(constant("Hello World"))
.to("log:" + HelloWorldRoute.class.getName() );
}
}
Listing 1-1HelloWorldRoute.java File
这个类创建了一个定时器应用,它每 2 秒钟在控制台中打印一次Hello World
。虽然这不是一个真正的集成案例,但它可以帮助您更容易地理解 Camel,因为最好从小处着手,一次一点。
这里只有几行代码,但是有很多事情要做。
首先要注意的是,HelloWorldRoute
类扩展了一个名为RouteBuilder
的 Camel 类。用 Camel 构建的每个集成都使用一个叫做路线的概念。其思想是集成总是从from
一个端点开始,然后到to
一个或多个端点。这正是这个Hello World
例子所发生的事情。
路线从计时器组件 ( from
)开始,最终到达最终目的地,也就是日志组件 ( to
)。另一件值得一提的事情是,您只有一行代码来创建您的路线,尽管它是缩进的,以使它更具可读性。这是因为 Camel 使用了一种流畅的方式来编写路线,您可以在其中附加关于您的路线应该如何表现的定义,或者简单地为您的路线设置属性。
路线建造者,比如HelloWorldRoute
类,只是蓝图。这意味着这种类型的类只在应用启动时执行。通过执行configure()
方法,这些栈调用的结果是一个路由定义,它用于实例化内存中的许多对象。内存中的这些对象将对由from
(消费者)端点触发的事件做出反应。在这种特殊情况下,组件将自动生成其事件,这些事件将通过路由逻辑,直到到达其最终端点。这种输入事件的执行被称为交换。
交流和信息
在上一节中,您看到了集成逻辑是如何创建和执行的,但是在这个逐步执行的过程中,数据将如何处理呢?为了使集成工作,路由携带其他结构。让我想想。
在最后一个例子中,还有一行代码需要注释。setBody(constant("Hello World"))
是您实际设置路线数据的唯一线路。让我们看看数据在路由中是如何处理的。
在上一节中,我说过:“内存中的那些对象将对由 from()端点触发的事件做出反应。”。在这种情况下,当我谈到一个事件时,我的意思是计时器触发了,但它可能是一个传入的 HTTP 请求、一个文件访问一个目录,或者来自不同端点的另一个触发的操作。重要的是,当这种情况发生时,会创建一个名为Exchange
的对象。该对象是路由执行的数据表示。这意味着每次定时器触发时,将创建一个新的Exchange
,并且该对象将一直可用,直到该执行完成。请看图 1-4 上的Exchange
表示。
图 1-4
交换表示
上图显示了一个Exchange
对象中可用的主要属性。所有这些都很重要,但是如果你想理解setBody(constant("Hello World"))
做什么,你必须把注意力集中在信息上。
您在路由链中遇到的每个端点都有可能改变Exchange
状态,大多数情况下,它们会通过与Message
属性交互来实现。
message
对象表示来往于路线中不同端点的数据。请看图 1-5 中message
物体的表示。
图 1-5
消息对象表示
message
对象是一个抽象概念,有助于以标准化的方式处理不同类型的数据。例如,如果您接收一个 HTTP 请求,它有头和 URL 参数,它们是描述通信特征的元数据,或者只是以键/值格式添加信息,但是它还有一个主体,可以是文件、文本、JSON 或许多其他格式。message
对象具有非常相似的结构,但是它足够灵活,可以将其他类型的数据表示为二进制文件、JMS 消息、数据库返回等等。
继续 HTTP 请求的例子,头和 URL 参数将被解析为消息头属性,HTTP 主体将成为一个message
主体。
当您使用setBody(constant("Hello World"))
时,您更改了exchange
中的消息对象,将字符串“Hello World
”设置为 body 属性。
还有一件事要解释。constant("Hello World")
是什么意思?
表达式语言
route 类只是一个蓝图,所以它不会执行一次以上。那么我们如何动态地处理数据呢?一个可能的答案是表达式语言。
setBody()
方法接收一个类型为表达式的对象作为参数。发生这种情况是因为该路由步骤可以是静态的,也可以根据通过该路由的数据而变化,这需要在路由创建期间进行评估。在这种情况下,您希望每次计时器触发新事件时,主体消息都应该设置为“Hello World
”。为此,你使用了方法constant()
。此方法允许您将静态值设置为常量,在本例中为字符串值,或者在运行时获取一个值并将其用作常量。无论执行什么,值总是相同的。
constant()
方法并不是处理交换数据的唯一方法。还有其他适合不同用途的表达式语言。表 1-1 中列出了 Camel 中所有可用的 ELs。
表 1-1
支持 Camel 的表达式语言
|豆子法
|
常数
|
很简单
|
DataSonnet
|
交换财产
|
| --- | --- | --- | --- | --- |
| 查询语言 | 路径语言 | XML 标记化 | 标记化 | 拼写 |
| exchange 属性 | 文件 | 绝妙的 | 页眉 | HL7 Terser |
| 乔尔!乔尔 | JsonPath | 拉维尔 | 每一个 | 裁判员 |
| 简单的 | | | | |
以后你会看到其他表达式语言的例子。
现在您已经完全理解了Hello World
示例是如何工作的,您需要运行这段代码。但是有一个缺失的部分。你是如何打包并运行这段代码的?为此,你需要先了解夸库斯。
第四的
您将使用云原生原则,并将应用作为容器映像进行分发,但这是该过程的最后一步。你如何处理应用的依赖性?如何编译 Java 类?如何运行 Java 代码?要回答这些问题,你需要夸库。
您几乎已经可以运行 Camel 应用了。你对 Camel 的工作原理有了基本的了解。现在,您将处理基本框架。
Quarkus 是 2018 年发布的开源项目。它是专门为 Kubernetes 世界开发的,创建了一种机制,使 Kubernetes 的 Java 开发变得更加容易,同时也处理了 Java 的“老”问题。
要理解 Quarkus 为什么重要,您需要理解 Java 的“老”问题。我们来谈谈历史。
Java 进化
让我们后退一步,理解在 Quarkus 出现之前,Java 是如何用于企业应用的。
Java 最早发布于 1995 年,差不多 26 年前。可以肯定地说,从那时起,世界发生了很大的变化,更不用说 It 行业了。让虚拟机能够执行字节码,让开发人员有可能编写可以在任何操作系统中运行的代码,这个想法非常棒。它围绕 Java 语言创建了一个巨大的社区,使它成为最流行的编程语言之一,或者可能是最流行的编程语言。对其受欢迎程度产生巨大影响的另一个特性是自我管理内存分配的能力,使开发人员不必处理分配内存空间的指针。但是一切美好的事物都是有代价的。JVM (Java 虚拟机)是负责将 Java 字节码翻译成机器码的“本地”程序,它需要计算机资源,在历史上,在虚拟机结构上花费几兆字节是可以的。
编写的大部分企业应用,这里我指的是 2000 年到 2010 年之间,都部署在应用服务器上。Websphere、JBoss 和 Web Logic 在当时是巨大的,我敢说它们现在仍然是,但没有它们辉煌时期那么大了。应用服务器提供了集中的功能,如安全性、资源共享、配置管理、可伸缩性、日志记录等等,而付出的代价非常小:除了额外的 CPU 使用之外,几兆字节用于虚拟机,几兆字节用于应用服务器代码本身。如果您可以在同一台服务器上部署大量应用,这个价格就会被稀释。
为了使它们高度可用,系统管理员将为该特定的应用服务器创建一个集群,将每个应用至少部署两次,集群的每个节点一次。
即使您可以实现高可用性,可伸缩性也不一定容易,而且肯定不便宜。您可以选择通过添加与集群中相同的另一个节点来扩展一切,这有时会扩展不需要扩展的应用,因此会不必要地消耗资源。您还可以为特定的应用创建不同的配置文件和不同的集群,因为有些应用无法扩展,需要自己的配置文件。在这种情况下,您可能会遇到这样的情况:每个应用服务器需要一个应用,因为应用的特征彼此差异太大,使得更难一起规划它们的生命周期。
在这一点上,拥有一个应用服务器的价格开始变得越来越高,即使供应商试图使他们的平台尽可能模块化以避免这些问题。应用开始以不同的方式发展来解决这些情况。
微服务
云原生方式通常构建在微服务架构之上。在这里,我将描述它是什么,以及它与 Java 发展的关系,最重要的是,它与 Java 框架生态系统的关系。
正如您在上一节中看到的,扩展应用服务器不是一件容易的事情。这需要根据您的应用和大量计算资源采取特定的策略。另一个问题是处理应用库的依赖性。
Java 社区如此强大的原因之一是分发和获得可重用的库是多么容易。使用像 Gradle、Maven 甚至 Maven 的哥哥 Ant 这样的工具,可以将您的代码打包到 jar/war/ear 中,并合并您的应用需要的依赖项,或者您可以将您的依赖项直接部署到您的应用服务器,并与该服务器上的每个应用共享它们。许多 Java 项目都使用这种机制。没有什么是刚刚创造出来的。尽可能重复使用所有东西。这很好,除非您必须将不同的应用放在同一个应用服务器上。
在应用服务器时代,处理依赖冲突是混乱的。您可以并且仍然可以让应用使用同一个库,但是使用不同的版本,并且版本可能完全不兼容。这是一个真正的阶级加载地狱。当时谁没有收到过NoSuchMethodError
异常?当然,应用服务器已经发展到可以处理这些问题。他们创建了隔离机制,这样每个应用都可以有自己的类加载过程,或者可以使用例如 OSGi 框架来准确指定它将使用哪些依赖项,但这并没有解决将所有鸡蛋放在同一个篮子中的风险。例如,如果一个应用出现了内存泄漏问题,就会影响到运行在同一个 JVM 上的每个应用。
大约在 2013~2014 年,多个项目开始以创建独立的 runnable jar 应用的想法发布。Spring Boot、Dropwizards 和 Thorntail 等项目开发了使开发更容易、更快速的框架。采用标准化优先于配置这样的原则,这些框架将允许开发人员通过编写更少的代码来更快地创建应用,并且仍然可以获得 JAVA EE 规范的大部分好处,而不依赖于应用服务器。您的源代码、依赖项和框架本身将被打包在一个单独的、隔离的、可运行的 jar 文件中,也称为 fat jar。在同一时期,REST 变得非常流行。
有了打包应用的方法和提供服务间通信的可靠协议,开发人员可以采用更具可伸缩性和模块化的架构风格:微服务。
微服务的对话很长。为了真正定义微服务,我们可以讨论服务粒度、领域定义、技术专业化、服务生命周期以及其他可能干扰我们如何设计应用的方面,但为了避免偏离我们的主题太多,让我们同意这样的理解,即微服务是旨在更加简洁/专业化的服务,将系统复杂性分散到多个服务中。
现在离 2018 年更近了。fat jar 框架是真正的交易,通过少量的自动化,它们占据了应用服务器所占据的空间。这个模型非常适合容器,因为我们只需要 jar 和运行时(JRE)来运行它。很容易创建一个容器映像来打包应用和运行时依赖项。这是将 Java 引入容器和 Kubernetes 的最简单的方法。现在,不是将十个 war 文件部署到一个应用服务器,而是将这十个新服务打包到十个不同的容器映像中,创建十个作为容器运行的 JVM 进程,这些进程由您的 Kubernetes 集群编排。
开发和部署用 Java 编写的服务比以往任何时候都更加容易和快捷,但现在的问题是:您在这方面花费了多少资源?
你还记得我说过花很小的代价就能拥有一个 Java 虚拟机吗?现在这个价格乘以十。你还记得我说过库的极端重用,以及在共享环境中有多混乱吗?嗯,现在我们没有依赖冲突,因为服务是隔离的,但我们在那些框架中仍然有大量的依赖,我们正在复制这一点。胖罐子真的变得越来越胖,使得类加载过程越来越慢和沉重,有时在启动时比应用真正运行时消耗更多的 CPU。我们也在消耗更多的内存。让许多微服务在 Java 中运行是非常耗费资源的。
我想回顾一下这段历史,这样你们就能理解为什么我们要用 Quarkus 进行整合。Quarkus 就是在所有这些问题出现的时候被创造出来的。所以它是为了解决这些问题而产生的。它的库是从头开始编码的,这使得它的类加载过程更快,内存占用更少。它也是为 Kubernetes 世界设计的,所以在容器中部署它并与 Kubernetes 环境交互要容易得多。我们可以将 Camel 与另一个框架一起使用,但我们的重点是构建云原生集成。这就是我选择夸库斯的原因。
别说了,开始编码吧。
开发要求
运行本书中的代码示例需要一些工具。它们将是用于运行所有章节中的示例的相同工具。
这本书的源代码可以在 GitHub 上通过这本书的产品页面获得,位于 www.apress.com/ISBN
。在那里你会发现第一个示例代码,名为camel-hello-world
,,我们现在就来解决这个问题。
以下是使用的工具列表:
-
安装了 JAVA_HOME 并进行适当配置的 JDK 11
-
配置了 M2_HOME 的 Maven 3.6.3
-
夸尔库斯
-
Camel 3.9.0
-
CE 20.10.5 Docker
-
运行命令的终端或提示符
由于不同操作系统之间的指令可能会有所不同,所以我不会介绍如何安装和配置 Java、Maven 和 Docker。你可以在每个项目的网站上找到这些信息。
这本书是 IDE 不可知论者。使用你最熟悉的 IDE。您将需要一个终端或提示符来运行 Maven 和 Docker 命令,所以要正确设置一个。您将使用的唯一插件是 Maven 插件,它应该与所有主流操作系统兼容。
让我们从下载这本书的代码开始。完成后,转到项目camel-hello-world
目录。它应该看起来像图 1-6 。
图 1-6
quartus 目录结构
如图 1-6 所示,这个 Maven 项目中只有三个文件:你已经知道的 route 类、application.properties
文件和pom.xml
文件。
教 Maven 超出了本书的范围。我希望您已经对该工具有所了解,但是如果没有,也不要担心。我会给你所有需要的命令,你将使用源代码提供的 pom 文件。你只需要在你的机器上配置 Maven。有关如何安装和配置 Maven 的信息,请访问 https://maven.apache.org/
。
让我们看看来自pom.xml
文件的清单 1-2 中的代码片段。
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-universe-bom</artifactId>
<version>1.13.0.Final</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
...
Listing 1-2Camel-hello-world pom.xml Snippet
这是 pom 的一个非常重要的部分。本节描述了您将从中检索本书中使用的所有依赖项的版本的参考。Quarkus 提供了一个名为quarkus-universe-bom
、的“bill of materials
”依赖项,在这里声明了框架的每个组件。这样你就不需要担心每个依赖版本以及它们之间的兼容性。
清单 1-3 显示了项目的依赖关系。
...
<dependencies>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-log</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-timer</artifactId>
</dependency>
</dependencies>
...
Listing 1-3Camel-hello-world pom.xml snippet
Quarkus 物料清单中的依赖项被称为扩展。在第一个例子中,只有三个,这很好。这样代码会更简单、更轻便。这是可能的,因为除了作为一个全新的框架,Quarkus 还实现了 MicroProfile 规范。让我们稍微谈一谈。
微文件规范
技术总是在发展,有时它们对生态系统变得如此重要,以至于我们可能需要为它们制定一个规范。这有助于生态系统的发展,因为它提供了不同项目之间更多的互操作性。微文件规范就是其中的一种。
这在过去发生过。我们可以用 Hibernate 作为例子。它对 Java 社区变得如此流行和重要,以至于这个 ORM(对象关系映射)项目驱动了后来成为 JPA (Java 持久性 API)规范的许多方面,这影响了 Java 语言本身。
微文件规范和微服务框架(Spring Boot、Quarkus、Thorntail 等等)重复了历史。随着它们越来越受欢迎,越来越多的项目为这个生态系统提供了新的功能,需要一个规范来保证它们之间最小的互操作性,并为这些框架设置需求和良好的实践。
MicroProfile 规范相当于微服务框架的 Jakarta EE(以前称为 Java Platform,Enterprise Edition–Java EE)。它将 Jakarta EE 中存在的(应用编程接口)API 规范的子集翻译到微服务领域。这只是一个子集,因为有些组件对这种不同的方法没有意义,这里主要关注的是微小而有效。
以下是规范中的 API 列表:
-
记录
-
配置
-
容错
-
健康检查
-
韵律学
-
开放 API
-
应用接口
-
JWT 认证
-
OpenTracing
-
依赖注入
-
JSON-P(解析)
-
JSON-B(绑定)
尽管这些 API 中的大部分是每个微服务所必需的,但每一个都是独立的。这种模块化有助于我们尽可能地维护我们的服务,因为我们只导入将要使用的依赖项。
MicroProfile 当前版本为 4.0。
通过选择 Quarkus 作为我们的 Camel 基础框架,我们也获得了 MicroProfile 规范的能力。所以让我们回到我们的代码。
运行代码
现在您已经了解了这些工具是如何工作的以及它们是如何产生的,让我们开始运行示例代码。
关于pom.xml
文件还有一点值得一提的是:quarkus-maven-plugin
。看看清单 1-4 。
...
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>1.13.0.Final</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
</goals>
</execution>
</executions>
</plugin>
...
Listing 1-4Camel-hello-world pom.xml Snippet
这个插件对 Quarkus 来说极其重要。它负责构建、打包和调试代码。
为了实现更快的启动时间,Quarkus 插件不仅仅是编译。它预测了大多数框架在运行时执行的任务,比如加载库和配置文件、扫描应用的类路径、配置依赖注入、设置对象关系映射、实例化 REST 控制器等等。这种策略减轻了云本地应用的两种不良行为:
-
应用需要更长时间准备接收请求或启动
-
应用在启动时比实际运行时消耗更多的 CPU 和内存
没有这个插件你就不能运行你的代码,所以在创建你的 Quarkus 应用的时候记得配置它。让我们运行camel-hello-world
代码。
在您的终端中,转到camel-hello-world
目录并运行以下命令:
camel-hello-world $ mvn quarkus:dev
如果您是第一次在这个版本中运行 Quarkus 应用,可能需要几分钟来下载所有的依赖项。之后,您将看到如清单 1-5 所示的应用日志。
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-04-04 19:43:20,118 INFO [org.apa.cam.qua.cor.CamelBootstrapRecorder] (main) bootstrap runtime: org.apache.camel.quarkus.main.CamelMainRuntime
2021-04-04 19:43:20,363 INFO [org.apa.cam.imp.eng.AbstractCamelContext] (main) Routes startup summary (total:1 started:1)
2021-04-04 19:43:20,363 INFO [org.apa.cam.imp.eng.AbstractCamelContext] (main) Started route1 (timer://example)
2021-04-04 19:43:20,363 INFO [org.apa.cam.imp.eng.AbstractCamelContext] (main) Apache Camel 3.9.0 (camel-1) started in 86ms (build:0ms init:68ms start:18ms)
2021-04-04 19:43:20,368 INFO [io.quarkus] (main) camel-hello-world 1.0.0 on JVM (powered by Quarkus 1.13.0.Final) started in 1.165s.
2021-04-04 19:43:20,369 INFO [io.quarkus] (main) Profile prod activated.
2021-04-04 19:43:20,370 INFO [io.quarkus] (main) Installed features: [camel-core, camel-log, camel-support-common, camel-timer, cdi]
2021-04-04 19:43:21,369 INFO [com.app.int.HelloWorldRoute] (Camel (camel-1) thread #0 - timer://example) Exchange[ExchangePattern: InOnly, BodyType: String, Body: Hello World]
2021-04-04 19:43:23,367 INFO [com.app.int.HelloWorldRoute] (Camel (camel-1) thread #0 - timer://example) Exchange[ExchangePattern: InOnly, BodyType: String, Body: Hello World]
2021-04-04 19:43:25,370 INFO [com.app.int.HelloWorldRoute] (Camel (camel-1) thread #0 - timer://example) Exchange[ExchangePattern: InOnly, BodyType: String, Body: Hello World]
Listing 1-5Application Output
这是您通过调用quarkus:dev
与插件的第一次交互。这里你用的是 Quarkus 开发模式。该模式将在本地运行您的应用,并允许您测试它。它还允许您远程调试代码。默认情况下,它将侦听端口 5005 上的调试器。
好了,你终于可以运行一些代码了,但是你如何打包应用来发布呢?接下来看看。
包装应用
要运行 Java 应用,至少需要一个 jar 文件。你如何用夸库斯提供这些?
集成代码使用 Quarkus 打包,quar kus 是一个云原生微服务框架,因此,您知道您将在容器中运行它,但在您可以创建容器映像之前,您需要了解如何创建可执行文件。在 Quarkus 中,有两种方法:传统的 JVM 或本地编译。
我们已经讨论了 JVM、类加载,以及 Quarkus 如何通过预测构建过程中的一些运行时步骤来优化过程,但是有一种方法可以进一步优化应用性能:原生编译。
原生映像是 GraalVM 的一种运行模式,GraalVM 是 Oracle 开发的一个 JDK 项目,旨在改善 Java 和其他 JVM 语言的代码执行。在这种模式下,编译器创建本机可执行文件。所谓本机,我指的是“不需要 JVM 的代码,它可以在编译到的每个操作系统上本机运行。”生成的可执行文件具有更快的启动时间和更小的内存占用。如果我正在运行数百个服务,这是非常可取的。
发生这种情况是因为代码是预编译的,一些类是预先初始化的。所以不需要字节码解释。一些 JVM 功能,比如垃圾收集器(一种处理内存分配的方法),内置在生成的二进制文件中。这样,没有 JVM 也不会损失太多。
可以想象,使用这种编译方法有一些注意事项。由于提前编译,反射、动态类加载和序列化在本机方法中的工作方式不同,这使得一些常用的 Java 库不兼容。
Quarkus 是为这个新世界而生的,它与 GraalVM 兼容,但在本书中,我们将重点关注传统的 JVM 字节码编译。我的想法是保持对集成和 Camel 的关注,但是本书示例中的每个pom.xml
都将配置原生概要文件,所以你可以在喜欢的时候尝试原生编译。请记住,在本机编译期间有大量的处理,这使得编译过程稍微长一点,并且消耗更多的内存和 CPU。
好了,现在您已经知道了原生编译和 GraalVM 的存在,让我们回到 runnable jar 方法。Quarkus 提供了两种包装 jar 的方法:快速 jar 或超级 jar。
快速汽车
快速 jar 是创建可运行 jar 的另一种方式。这是 Quarkus 1.13 的默认打包选项。接下来你将看到它是如何工作的。
打开终端,在camel-hello-world
文件夹下运行以下命令,开始打包应用:
camel-hello-world $ mvn package
这将生成一个名为target
的文件夹,Maven 将构建结果文件放在这里。看目录结构;应该是像图 1-7 。
图 1-7
Maven 的目标生成文件夹
进入quarkus-app
文件夹,列出其内容,如图 1-8
图 1-8
quarkus-app 文件夹结构
正如您所看到的,这里的结构与您通常使用 Maven 打包 runnable jars 时得到的略有不同。尽管在target
文件夹中有一个 jar 文件,但是camel-hello-world-1.0.0.jar
并不包含运行这个 jar 所需的MANIFEST.MF
信息。它只包含编译后的代码和资源文件。Quarkus-maven-plugin
将生成quarkus-app
文件夹,其中的结构将用于运行应用。
让我们试一试。在/camel-hello-world/target/quarkus-app
文件夹下运行以下命令:
quarkus-app $ java -jar quarkus-run.jar
此后,Hello World
应用应该开始运行。查找如下所示的日志条目:
2021-04-10 15:13:10,314 INFO [org.apa.cam.imp.eng.AbstractCamelContext] (main) Apache Camel 3.9.0 (camel-1) started in 88ms (build:0ms init:63ms start:25ms)
该日志条目显示了启动应用所用的时间。在我的例子中,它是 88 毫秒,非常快。你的结果可能会和我的不同,因为这取决于机器的整体性能。磁盘、CPU 和 RAM 的速度会影响机器的速度。您可能会得到更快或更慢的结果,但您可以看到 Quarkus 与更传统的 Java 框架相比速度更快。
在快速 jar 方法中,类加载过程被分解为引导依赖项和主依赖项,正如您通过检查文件所看到的。解压缩quarkus-run.jar
(记住,jar 是 zip 文件)并查看清单文件。它应该看起来像清单 1-6 。
Manifest-Version: 1.0
Class-Path: lib/boot/org.jboss.logging.jboss-logging-3.4.1.Final.jar li
b/boot/org.jboss.logmanager.jboss-logmanager-embedded-1.0.9.jar lib/boo
t/org.graalvm.sdk.graal-sdk-21.0.0.jar lib/boot/org.wildfly.common.wild
fly-common-1.5.4.Final-format-001.jar lib/boot/io.smallrye.common.small
rye-common-io-1.5.0.jar lib/boot/io.quarkus.quarkus-bootstrap-runner-1.
13.0.Final.jar lib/boot/io.quarkus.quarkus-development-mode-spi-1.13.0.
Final.jar
Main-Class: io.quarkus.bootstrap.runner.QuarkusEntryPoint
Implementation-Title: camel-hello-world
Implementation-Version: 1.0.0
Listing 1-6Manifest File
如您所见,这个 jar 中没有类。class-path
只指向 Quarkus 的依赖项,而main-class
属性指向一个 Quarkus 类。代码将被打包在quarkus-app/app
目录中,而您用来处理 Camel 的依赖项将在quarkus-app/lib/main
目录中。这个过程保证首先加载基础类,在本例中是 Quarkus 类,然后加载您的代码,这使得启动过程更智能,因此也更快。
让我们看看另一种方法。
优步罐
这是在其他面向微服务的框架中常见的更传统的打包方式。让我们看看如何使用它。
优步罐子,或者说胖罐子,是一个非常简单的概念:从源代码的角度来看,把你需要的所有东西放在一个地方,然后运行这个罐子。将所有内容放在一个文件中会使事情变得更容易,比如分发应用,尽管有时会创建大文件。因为 fast jar 是默认选项,所以您需要告诉quarkus-maven-plugin
您想要覆盖默认行为。有不同的方法来告诉插件你希望你的打包方式是什么。让我们看看第一个。
在camel-hello-world
文件夹中运行以下命令:
camel-hello-world $ mvn clean package \
-Dquarkus.package.type=uber-jar
通过传递值为uber-jar
的quarkus.package.type
参数,插件将修改其行为并创建一个uber-jar
。
Quarkus 框架和quarkus-maven-plugin
都对作为环境变量传递的配置、JVM 属性或存在于application.properties
文件中的配置做出反应。在以后的章节中你会学到更多。
检查在camel-hello-world/target/
文件夹中创建的uber-jar
,如图 1-9 所示。
图 1-9
优步-jar 构建结果
要运行应用,在camel-hello-world/target/
文件夹中执行以下命令:
target $ java -jar camel-hello-world-1.0.0-runner.jar
应用开始运行后,等待几秒钟并停止运行。找到日志条目以确定启动应用所用的时间。这是我的结果:
2021-04-10 17:37:36,875 INFO [org.apa.cam.imp.eng.AbstractCamelContext] (main) Apache Camel 3.9.0 (camel-1) started in 115ms (build:0ms init:89ms start:26ms)
正如你所看到的,在我的电脑上启动应用花了 115 毫秒,这仍然是一个非常好的启动时间。与 fast jar 构建的结果(88 毫秒)相比,相差 27 毫秒。从绝对值来看,这似乎不算多,但它代表启动时间增加了大约 32%。
好了,现在您已经了解了如何使用quarkus-maven-plugin
打包 Java 代码。这将有助于您分发您的代码,尤其是在独立的应用中,您可以在操作系统中将其配置为服务。你可能会问,容器和 Kubernetes 呢?接下来看看。
容器映像
实现云原生状态的一个重要步骤是能够在容器中运行,为了在容器中运行,您首先需要一个容器映像。让我们看看 Quarkus 如何帮助完成这项任务。
您差不多完成了打包应用以供分发和安装的重要任务。因为您的目标是云原生方法,所以您需要知道如何创建符合 OCI(开放容器倡议)的映像。顺便说一下,我以前没有和你谈过 OCI 组织。我想现在是个好时机。
OCI 成立于 2015 年 6 月,是一个 Linux 基金会项目,CNCF 也是如此,旨在围绕容器格式和运行时创建开放的行业标准。因此,当我说“我们需要知道如何创建一个符合 OCI 标准的映像”时,我正在寻找一种方法,将我的应用作为一个容器映像进行分发,该映像可以在多个运行时中运行,并且也符合 OCI 标准。
说到这里,是时候使用 Quarkus 创建您的第一个映像了。
您需要做的第一件事是向您的 Maven 项目添加一个新的 Quarkus 扩展。为此,在camel-hello-world
文件夹下运行以下命令,如下所示:
camel-hello-world $ mvn quarkus:add-extension \
-Dextensions="container-image-jib"
这是你的新把戏。有了插件目标quarkus:add-extension
,你可以用一种简化的方式操作你的 pom 结构。该命令将使用您在 Quarkus 物料清单中映射的版本添加您需要的依赖项,因此您不需要担心兼容性。
Quarkus 有一个非常广泛的扩展列表。你可以使用相同的插件来搜索它们。运行以下命令:
camel-hello-world $ mvn quarkus:list-extensions
您将获得您正在使用的特定bom
版本中的扩展列表。您还可以通过运行如下命令获得更详细的信息:
camel-hello-world $ mvn quarkus:list-extensions \
-Dquarkus.extension.format=full
这将向您显示可用的扩展并指出扩展文档。您可以使用此命令来查找更多关于您正在使用的扩展container-image-jib
的信息,例如如何在生成的映像中更改标签、名称或注册表。现在,您将只设置组名以保持一致性,因为默认情况下,该配置将使用运行用户用户名的操作系统。这样我就可以展示一个每个人都可以不用改编就能使用的命令。
回到最初的目的,即生成一个容器映像,您已经有了扩展集。让我们打包应用。运行以下命令:
camel-hello-world $ mvn clean package \
-Dquarkus.container-image.build=true \
-Dquarkus.container-image.group=localhost
您可能会看到 Maven 构建的一部分是创建一个容器映像。它应该类似于清单 1-7。
[INFO] --- quarkus-maven-plugin:1.13.0.Final:build (default) @ camel-hello-world ---
[INFO] [org.jboss.threads] JBoss Threads version 3.2.0.Final
[INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] Starting container image build
[WARNING] [io.quarkus.container.image.jib.deployment.JibProcessor] Base image 'fabric8/java-alpine-openjdk11-jre' does not use a specific image digest - build may not be reproducible
[INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] The base image requires auth. Trying again for fabric8/java-alpine-openjdk11-jre...
[INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] Using base image with digest: sha256:b459cc59d6c7ddc9fd52f981fc4c187f44a401f2433a1b4110810d2dd9e98a07
[INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] Container entrypoint set to [java, -Djava.util.logging.manager=org.jboss.logmanager.LogManager, -jar, quarkus-run.jar]
[INFO] [io.quarkus.container.image.jib.deployment.JibProcessor] Created container image localhost/camel-hello-world:1.0.0 (sha256:fe4697492c2e9a19030e6e557832e8a75b5459be08cd86a0cf9a636acd225871)
Listing 1-7Maven Output
该扩展使用'fabric8/java-alpine-openjdk11-jre'
作为您的基本映像(您将在其上创建您的映像)。这个映像将提供您需要的操作系统文件和运行时,在本例中是 JDK 11。创建的映像使用 localhost 作为映像组名,Maven 工件 id ( camel-hello-world
)作为映像名,Maven 项目版本(1.0.0
)作为映像标签。生成的映像将保存到您的本地映像注册表中。您可以通过运行以下命令来检查这一点
$ docker image ls
您应该会看到类似图 1-10 的内容。
图 1-10
生成的容器映像
为了检查是否一切都按计划进行了配置,让我们运行生成的容器映像:
$ docker run -it --name hello-world localhost/camel-hello-world:1.0.0
您正在使用选项-it
( -i
用于交互,-t
用于伪终端),这样您就可以像在本地运行应用时一样查看应用日志,并且可以使用 Control + c 来停止它。您使用--name
来设置容器名称,以便将来更容易识别容器。
让我们从内部检查这个映像。打开一个新的终端/提示窗口,让hello-world
容器运行起来。执行以下命令打开容器内的终端:
$ docker exec -it hello-world sh
将打开一个终端,您将被定向到容器工作区。通过列出如图 1-11 所示的目录来检查其内容。
图 1-11
容器内容
如您所见,生成的映像使用了快速 jar 方法。这样你可以利用这种方法更快的优势,你不需要担心如何打包或者如何配置映像,因为插件为你做了一切。
摘要
在这一章里,我为我们将要在这本书里做的每件事设定了基础。您了解了以下内容:
-
什么是系统集成,你将如何实现它
-
云原生应用及其背后的历史相关项目和组织
-
介绍 Apache Camel,它是什么,以及它的基本概念
-
Java 语言的演变
-
模式和规范设定了您将在实现中遵循的标准
-
关于 Quarkus,您需要了解什么,以便能够为集成交付 Camel 应用
现在,您已经对 Camel 有了基本的了解,并且知道了如何打包和运行您的集成,随着我们一路讨论模式和标准,您将对 Camel 以及如何解决集成挑战有更多的了解。
在下一章中,您将开始把 HTTP 通信作为您的主要案例,但是您也将从 Camel 中学到许多新的技巧。
二、开发 REST 集成
在上一章中,向您介绍了 Apache Camel 和 Quarkus,您开始了系统集成讨论,并且了解了一点本书所涉及的技术的发展。这些基础知识对你深入更具体的对话非常重要。现在,我将讨论使用 REST 的同步通信。
对于大多数开发人员来说,进程间通信曾经是一个挑战。如果我们以 Java 语言为例,在它的开发过程中,创建了许多机制来允许不同的应用(不同的 JVM)相互通信。我们曾经使用 RMI(远程方法调用)、直接套接字通信或 EJB 远程调用来执行这种通信。当然,在这一演变过程中,我们使用了 HTTP 实现。JAX-RPC 规范是使用 SOAP(简单对象访问协议,一种基于 XML 的消息传递协议)标准化基于 HTTP 的通信的一大步。JAX-RPC 最终被 JAX-WS (Web 服务)规范所取代。
在接下来的几年里,SOAP 是构建 web 服务的主要选择。SOAP 是一个开源的、描述性很强的、与编程语言无关的协议,这使得它成为当时 web (HTTP)服务实现的一个非常好的选择。即使在今天,您也会发现 SOAP 服务部署在传统的应用服务中,或者有时微服务实现 SOAP 来与遗留系统通信。
SOAP 推出几年后,Roy Fielding 在他的博士论文中定义了 REST(表述性状态转移)架构。REST 不是一种消息传递协议,而是一种使用 HTTP 特性子集的软件架构风格。这意味着我们没有增加 HTTP 通信的复杂性,而是定义了一种使用 HTTP 进行 web 服务通信的方式。这使得 REST 实现起来比 SOAP 更轻更简单,而且更适用于更多的用例,比如 web 应用、移动应用和嵌入式系统。
我们将在接下来的章节中讨论更多关于 REST 和 HTTP 的内容,但是现在,让我们开始用 Camel 编码。
Camel DSLs
Camel 是一个非常灵活的框架。它的灵活性的一个例子是可以用不同的方法编写 Camel 代码,以满足不同的目的。这可以通过 Camel 的不同领域特定语言(DSL)实现来实现。
Camel 实现了以下类型的 DSL:
-
Spring XML:基于 Spring XML 文件的 XML 实现
-
蓝图 XML:基于 OSGi 蓝图 XML 文件的 XML 实现
-
Java DSL:创建路由的 Java 方式。您在第一个示例中使用了这种方法。
-
Rest DSL:一种定义 Rest 路由的特殊方式。可以用 XML 或者 Java 来完成。
-
注释 DSL:一种使用 Java 注释与 Camel 对象交互和创建 Camel 对象的方法。
我没有在这本书里涵盖所有的 DSL。我将重点介绍 Java DSL 及其补充,比如用于 REST 集成的 REST DSL。
让我们从学习其余的 DSL 开始。
检查清单 2-1 中的代码,它摘自第二个例子camel-hello-world-restdsl
。
package com.appress.integration;
import org.apache.camel.builder.RouteBuilder;
public class RestHelloWorldRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
rest("/helloWorld")
.get()
.route()
.routeId("rest-hello-world")
.setBody(constant("Hello World \n"))
.log("Request responded with body: ${body}")
.endRest();
}
}
Listing 2-1RestHelloWorldRoute.java File
在评论这段代码之前,我们先测试一下。您可以使用以下命令运行此代码:
camel-hello-world-restdsl $ mvn quarkus:dev
在这个命令之后,您应该会看到 Quarkus 日志,并且有一个如下所示的日志条目:
2021-04-21 12:58:33,758 INFO [io.quarkus] (Quarkus Main Thread) camel-rest-hello-world 1.0.0 on JVM (powered by Quarkus 1.13.0.Final) started in 2.658s. Listening on: http://localhost:8080
这意味着您有一个在本地运行并监听端口 8080 的 web 服务器。您可以通过运行以下命令来测试此应用:
$ curl -w "\n" http://localhost:8080/helloWorld
结果应该如图 2-1 所示。
图 2-1
应用响应
我使用 cURL 作为我的命令行 HTTP 客户端,这是一个在类 Unix 系统中常见的工具。您可以使用任何您喜欢的工具来执行这些测试。只要让 cURL 作为你应该指向的 URL 和应该设置的参数的参考。
在您最喜欢的 IDE 中打开此项目。检查一下。您可能会注意到第一个示例和这个示例之间的一些差异。第一个区别是在 POM 文件中只声明了一个扩展/依赖项,即camel-quarkus-rest
。这是因为 Camel 扩展已经声明了它们所依赖的其他扩展。例如,camel-quarkus-core
依赖项已经被camel-quarkus-rest
声明了。
在本书的第一个例子中,我想让你知道camel-quarkus-core
是你使用 Camel 的基础库。从现在开始,我们不需要显式声明它。
您可以通过运行以下命令来检查我所说的内容
camel-hello-world-restdsl $ mvn dependency:tree
上面的命令显示了项目依赖关系树。除了 Camel 核心依赖,我想让你注意另一个依赖,camel-quarkus-platform-http
。这种依赖性将允许您使用 Quarkus 中的 web 服务器实现。你可能会问,哪个 web 服务器实现,因为我们没有声明任何东西。嗯,如果你看一下quarkus-platform-http
的依赖关系,你会看到quarkus-vertx-web
。这种依赖是 Quarkus 使用的 web 服务器实现之一。通过这样声明,您通知 Quarkus 您想要实现这个特定的 web 服务器模块。
与第一个例子不同的另一点是你记录的方式。您没有使用camel-quarkus-log
来提供日志端点。相反,您正在使用内置的 fluent builder log()
。虽然log()
不如使用日志端点灵活,但它将很好地满足您记录每次交换的消息的目的。在第一个例子中,我希望您知道路由结构是如何工作的,并且我需要一个简单的端点用于我的to()
调用。这就是我选择日志端点的原因。在幕后,两个实现都使用来自 Quarkus 的日志实现,在这种情况下就是jboss-logging
。
在这个例子中,我正在传递一个包含我想要显示的消息的字符串,但是在这个字符串中有一些动态的东西,即${body}
标记。你还记得我说过 ELs 吗?${body}
是 EL 的一个例子,在这个例子中是简单的 EL。因此,根据正文内容,信息会发生变化。
我们将在以后更多地讨论日志和简单的 EL,但是让我们继续 REST DSL 的解释。
通过分析RestHelloWorldRoute
类,您可能注意到的第一件事是没有from()
调用。发生这种情况是因为 REST DSL 通过创建基于 HTTP 方法的条目(如post()
、put()
和delete().
)来代替from()
调用。如果它们有不同的路径,您甚至可以拥有同一个方法的多个条目。
您也可以不使用 REST DSL 来创建 REST 服务。看看camel-hello-world-rest
项目中的代码。它做的事情和hello-world-restdsl
完全一样,但是没有其余的 DSL 。我们来分析一下它的RouteBuilder
;见清单 2-2 。
package com.appress.integration;
import org.apache.camel.builder.RouteBuilder;
public class RestHelloWorldRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
from("platform-http:/helloWorld?httpMethodRestrict=GET")
.routeId("rest-hello-world")
.setBody(constant("Hello World"))
.log("Request responded with body: ${body}");
}
}
Listing 2-2RestHelloWorldRoute.java File
您可以像在第一个示例中一样运行并测试这段代码,您将获得相同的结果。
当您构建 REST 服务时,通常您必须创建一个具有不同路径并使用不同 HTTP 方法的资源。看看清单 2-3 中更复杂的例子。
public class TwoPathsRestRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
from("platform-http:/twoPaths/helloWorld?httpMethodRestrict=GET")
.routeId("two-paths-hello")
.setBody(constant("Hello World"))
.log("Request responded with body: ${body}");
from("platform-http:/twoPaths/sayHi?httpMethodRestrict=GET")
.routeId("two-paths-hi")
.setBody(constant("Hi"))
.log("Request responded with body: ${body}");
}
}
Listing 2-3TwoPathsRestRoute.java File
为了公开两条不同的路径,您必须创建两条不同的路由。这样你可以独立地定义每条路径的每个方面。这也是你第一次看到一辆RouteBuilder
生产多条路线。RouteBuilders
可以创建多条路线定义。这是一个可读性和语义的问题,你需要多少RouteBuilders
来创建你需要的路线。
这是一个简单的例子,向您展示了为什么要使用 REST DSL 以及如何使用 Camel 公开 HTTP 端点。REST DSL 使复杂的 REST 实现变得更容易,可读性更强。从现在起,对于 REST 资源声明,您将只使用 REST DSL,并且您将学习如何正确地配置您的接口。
REST 和 OpenAPI
当我开始谈论 web 服务并提到 SOAP 时,我说过的关于该协议的一件有趣的事情是它的描述性。该规范允许软件和开发人员了解如何进行 web 服务调用,预期的数据模型是什么,如何处理身份验证,以及在错误情况下会发生什么。这些都是 HTTP 协议没有提供的,所以社区开发了一种方法来使 REST 接口更具描述性,更易于交互。
在 REST 架构风格普及期间,有不同的尝试来创建一种接口描述语言来描述 RESTful 服务(RESTful 意味着服务实现了 REST 架构风格的所有原则)。可以肯定地说,最成功的尝试是大摇大摆。
Swagger 创建于 2011 年,是一个开源项目,它为 RESTful 应用创建了 JSON/YAML 表示,采用了许多为 SOAP 协议构建的功能。除了接口描述语言,Swagger 还提供了开发工具来促进 API 的创建、测试和可视化,从基于文档的代码生成到基于应用代码的带有 Swagger 文档的显示网页的库。
2016 年工具和规范拆分,规范更名为 OpenAPI。
OpenAPI 或 OpenAPI 规范(OAS)是您将在本书中采用的另一个开放标准。这有助于如何使用许多开源或专有软件,因为 OpenAPI 是一种广泛使用的标准,并且是一种公共语言。
既然介绍已经完成,您可以开始开发一些 RESTful 应用了。
第一个应用:REST 文件服务器
受够了 Hello World 应用。是时候看看更复杂的应用来展示 Apache Camel 的更多功能了。有一些重要的 Camel 概念需要讨论,但是我们将在分析一个功能性的和可测试的代码时进行讨论。
作为第一个应用,我想要一些能够显示 REST DSL 配置和需求的东西。一些更深入 Camel 概念的东西,但也是一些容易理解和测试的东西。我想到了一个解决方案,在我们不得不与使用操作系统文件系统输入和输出数据的应用进行交互的时候,我们曾经这样做过。
看图 2-2 。
图 2-2
REST 文件服务器
这种集成通过 REST 接口抽象了文件系统。这样,不在同一服务器上的其他应用可以使用更合适的通信协议向遗留系统发送文件。该集成公开了一个 REST 接口来保存文件系统中的文件,这样遗留系统就可以读取它们。它还允许客户端列出哪些文件已经保存在服务器中。我们只关心集成层,更具体地说是camel-file-rest
项目。所以不要担心在你的机器上运行一个遗留系统。
当我说“回到过去”时,我的意思是这种情况现在并不常见。这并不是因为通过文件进行通信已经过时了,而是因为我们通常不会使用简单的操作系统文件系统来进行通信。然而,我认为有些应用仍然是这样工作的。
文件通信的一种更加云本地的方法是使用更加可靠和可伸缩的机制来发送这些文件。它可以使用对象存储解决方案,如 AWS s3,面向文档的 NoSQL 数据库,如 MongoDB 或 Elasticsearch,或者消息代理,如 Apache Kafka。将来你会看到这些项目中的一些与 Camel 互动。
这些机制将创建一个接口,其中集成应用部署不会绑定到服务器来访问文件系统,也不会依赖于非原子的可靠协议(我指的是 NFS 或 FTP),或者不是为处理并发场景、文件索引或文件重复数据删除而定制的。
Camel 为我上面提到的每个产品和协议都提供了组件,但是对于第一个例子,我决定使用 file 组件,因为它非常容易在本地测试和设置。
考虑到这一点,我们来分析一下camel-file-rest
项目。
REST 接口和 OpenAPI
我讨论了为你的界面准备一个描述性文档的重要性。您将看到如何使用 Camel 生成 OAS 文档。
有两种方法可以使用 OpenAPI 和 Camel,就像我们以前使用 SOAP web 服务一样。第一种方法是自顶向下的,首先使用 OpenAPI 规范设计接口,然后在代码中使用它来生成部分实现。你不会在这里遵循这种方法。我的目标是教你如何解决集成问题,以及如何编写 Camel 代码。我想让你知道 OpenAPI 的存在,它的重要性,以及如何使用它与 Camel。深入研究 OpenAPI 规范不是我的目标。话虽如此,您将采用第二种方法,即自底向上的方法,使用您的代码生成您的 OpenAPI 文档。
首先,让我们从分析camel-file-rest
项目中使用的依赖项开始。参见清单 2-4 。
...
<dependencies>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-file</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-openapi-java</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-direct</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-bean</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-jsonb</artifactId>
</dependency>
</dependencies>
...
Listing 2-4Camel-file-rest pom.xml Snippet
camel-quarkus-rest
依赖并不是新的。您在 Hello World REST 示例中使用了它。您使用它来为您的路由提供其余的 DSL 功能。您将使用camel-quarkus-file
来使用file:
端点。它将允许你用最少的努力保存文件。camel-quarkus-openapi-java
将为您生成 OpenAPI 文档。还有另外三个依赖项需要评论,但是我会在稍后讨论它们如何影响代码时再做评论。首先,让我们关注一下接口声明。
看看FileServerRoute
类及其configure()
方法,如清单 2-5 所示。
@Override
public void configure() throws Exception {
createFileServerApiDefinition();
createFileServerRoutes();
}
Listing 2-5FileServerRoute.class Configure Method
这里,我将定义 REST 应用接口的代码与实现与文件系统集成的代码分开。这样,您就可以专注于代码的各个部分,并分别讨论其内容。看清单中的createFileServerApiDefinition()
2-6。
private void createFileServerApiDefinition(){
restConfiguration()
.apiContextPath("/fileServer/doc")
.apiProperty("api.title", "File Server API")
.apiProperty("api.version","1.0.0")
.apiProperty("api.description", "REST API to save files");
rest("/fileServer")
.get("/file")
.id("get-files")
.description("Generates a list of saved files")
.produces(MEDIA_TYPE_APP_JSON)
.responseMessage().code(200).endResponseMessage()
.responseMessage().code(204)
.message(CODE_204_MESSAGE).endResponseMessage()
.responseMessage().code(500)
.message(CODE_500_MESSAGE).endResponseMessage()
.to(DIRECT_GET_FILES)
.post("/file")
.id("save-file")
.description("Saves the HTTP Request body into a File, using the fileName header to set the file name. ")
.consumes(MEDIA_TYPE_TEXT_PLAIN)
.produces(MEDIA_TYPE_TEXT_PLAIN)
.responseMessage().code(201)
.message(CODE_201_MESSAGE).endResponseMessage()
.responseMessage().code(500)
.message(CODE_500_MESSAGE).endResponseMessage()
.to(DIRECT_SAVE_FILE);
}
Listing 2-6createFileServerApiDefinition Method
restConfiguration()
方法负责与 Camel 如何连接底层 web 服务器(Quarkus Web 服务器)相关的配置。因为您依赖于默认配置,所以您并没有做太多事情,但是您通过调用apiContextPath()
设置了希望 OAS 文档显示的路径,并通过调用apiProperty()
为生成的文档添加了信息。
从rest()
调用开始,你就在描述你的资源方法和路径,声明期望什么样的数据,将给出什么样的响应,以及这个接口应该如何工作。
在深入实现之前,让我们看看代码的第一部分是做什么的。按如下方式运行应用:
camel-file-rest $ mvn quarkus:dev
要检索生成的文档,可以使用以下命令:
$ curl http://localhost:8080/fileServer/doc
你也可以使用你最喜欢的网络浏览器,访问相同的网址。无论哪种方式,您都应该收到清单 2-7 中所示的 JSON 文档。
{
"openapi" : "3.0.2",
"info" : {
"title" : "File Server API",
"version" : "1.0.0",
"description" : "REST API to save files"
},
"servers" : [ {
"url" : ""
} ],
"paths" : {
"/fileServer/file" : {
"get" : {
"tags" : [ "fileServer" ],
"responses" : {
"200" : {
"description" : "success"
},
"204" : {
"description" : "No files found on the server."
},
"500" : {
"description" : "Something went wrong on the server side."
}
},
"operationId" : "get-files",
"summary" : "Generates a list of files present in the server"
},
"post" : {
"tags" : [ "fileServer" ],
"responses" : {
"201" : {
"description" : "File created on the server."
},
"500" : {
"description" : "Something went wrong on the server side."
}
},
"operationId" : "save-file",
"summary" : "Saves the HTTP Request body into a File, using the fileName header to set the file name. "
}
}
},
"tags" : [ {
"name" : "fileServer"
} ]
}
Listing 2-7OAS-Generated Document
请注意“openapi
”属性。其值为“3.0.2
”。这意味着你使用的是 2017 年发布的 OpenAPI 规范版本 3。因为这是一个相当新的版本,所以您仍然可以在版本 2 中找到文档。顺便说一下,这是该规范第一次从 Swagger 规范改名为开放 API 规范(OAS)。
本节的目的是向您介绍 OAS,并教您如何编写 Camel RESTful 集成,但是如果您想了解 OAS 的更多信息,请访问 OpenAPI Initiative 网站 www.openapis.org/
。
可读性和逻辑重用
在这一点上,您没有处理调整两个不同端点之间的通信的复杂性。你在重点学习 Camel 的原理,以及如何写整合路线。现在,您将开始添加更多的复杂性,因为示例开始有更多的端点,并且您将为其集成添加更多的逻辑。为了处理这种复杂性,您可以使用一些技术来使您的路线易于阅读和维护。
您通过使用两个非常简单的端点开始了这本书:计时器和日志组件。与这些组件相关的配置选项很少,但是它们被用来以一种简化的方式说明 Camel 是如何工作的。现在你有一个更复杂的情况要处理。您需要将一个 HTTP 请求转换成一个文件,并返回一个 HTTP 响应。
让我们通过查看如何列出文件来检查这是如何做到的。参见清单 2-8 。
...
.post("/file")
.id("save-file")
.description("Saves the HTTP Request body into a File, using the fileName header to set the file name. ")
.consumes(MEDIA_TYPE_TEXT_PLAIN)
.produces(MEDIA_TYPE_TEXT_PLAIN)
.responseMessage().code(201)
.message(CODE_201_MESSAGE).endResponseMessage()
.responseMessage().code(500).message(CODE_500_MESSAGE)
.endResponseMessage()
.to(DIRECT_SAVE_FILE);
...
Listing 2-8createFileServerApiDefinition Method Snippet
关注上面的 POST 方法声明,您可以看到这里没有完整的路由定义,但是您确实有一个使用静态变量的to()
调用。让我们看看变量声明:
public static final String DIRECT_SAVE_FILE = "direct:save-file";
尽管每个 HTTP 方法声明,以及各自的路径,总是会生成一个路由,但是您并没有在这个单一的 fluent builder 结构中声明整个路由。为了提高代码的可读性并帮助我完成任务,我决定使用直接组件。
direct 组件允许您在同一个 Camel 上下文中同步链接不同的路线。Camel context 是 Camel 架构中的一个新概念,您现在将要探索它。
再次运行应用。查找如下所示的日志条目:
(Quarkus Main Thread) Apache Camel 3.9.0 (camel-1) started in 128ms (build:0ms init:86ms start:42ms)
你可能想知道camel-1
是什么意思。这就是你的 Camel 语境。在应用运行时启动过程中,Camel 将创建一个对象结构,以便集成可以运行。在这个过程中,将创建 Java beans,加载您的路由和配置,所有内容都与特定的上下文相关联,因此这些对象可以在彼此之间共享数据并继承相同的配置。
现在,您不需要对 Camel 上下文进行任何特定的配置。我只是想让你知道这个概念是存在的,你需要你的路由在相同的上下文中使用直接组件。按照您的工作方式,每条路线都将在相同的上下文中创建。
回到直接组件分析,您看到了它从生产者的角度看起来是什么样子(调用to()
)。我们来看看它在消费者端是怎么走的(from()
)。请看清单 2-9 中的createSaveFileRoute()
方法。
private void createSaveFileRoute() throws URISyntaxException{
from(DIRECT_SAVE_FILE)
.routeId("save-file")
.setHeader(Exchange.FILE_NAME,simple("${header.fileName}"))
.to("file:"+ FileReaderBean.getServerDirURI())
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(201))
.setHeader(Exchange.CONTENT_TYPE,
constant(MEDIA_TYPE_TEXT_PLAIN))
.setBody(constant(CODE_201_MESSAGE)) ;
}
Listing 2-9createSaveFileRoute Method
同一个静态变量定义了生产者和消费者,只是为了便于解释。请记住,生产者和消费者有不同的选择,但是您现在没有使用它们。除了直接调用,您还使用简单的 EL 从 POST 请求头中动态检索文件名,并使用静态方法检索保存文件的目录名。看看清单 2-10 中的FileReaderBean
类、getServerDirURI()
方法。
public static String getServerDirURI() throws URISyntaxException{
return Paths.get(FileReaderBean.class.getResource("/")
.toURI()).getParent()+ "/camel-file-rest-dir";
}
Listing 2-10getServerDirURI Method
您将使用 project Maven 生成的目标文件夹来保存文件。通过这种方式,您不需要在您的系统中配置任何东西,并且您也可以通过简单地运行"mvn clean"
来清理您的测试。
注意到我在这里使用了一种非常“乐观”的开发方法是很重要的。我不考虑任何可能的例外。在这一点上,想法是让事情尽可能简单。这样我们可以专注于一个特定的研究课题。在以后的章节中,您将学习如何使用 Camel 和其他模式处理替代执行流的异常。
为了进一步解释直接组件,让我们分析一个使用 direct 进行代码重用的不同代码。在您的 IDE 中打开camel-direct-log
项目。看看DirectTestRoute
类的configure()
方法,如清单 2-11 所示。
public void configure() throws Exception {
rest("/directTest")
.post("/path1")
.id("path1")
.route()
.to("log:path1-logger")
.to("direct:logger")
.setBody(constant("path1"))
.endRest()
.post("/path2")
.id("path2")
.route()
.to("log:path2-logger")
.to("direct:logger")
.setBody(constant("path2"))
.endRest();
from("direct:logger")
.routeId("logger-route")
.to("log:logger-route?showAll=true");
}
Listing 2-11DirectTest Route Configure Method
上面的代码做的不多。它是一个 REST 接口,记录输入的数据并返回一个常量响应。这里的重点是两个不同的路由如何链接到第三个路由以重用其逻辑。
像这样运行代码:
camel-direct-log $ mvn quarkus:dev
您可以通过运行以下命令来测试应用
$ curl http://localhost:8080/directTest/path1 -X POST -H 'Content-Type: text/plain' --data-raw 'Test!'
查看应用日志。对于每个交换,您会发现两个日志条目,如清单 2-12 。
2021-05-01 17:03:36,858 INFO [path1-logger] (vert.x-worker-thread-2) Exchange[ExchangePattern: InOut, BodyType: io.vertx.core.buffer.impl.BufferImpl, Body: Test!]
2021-05-01 17:03:36,859 INFO [logger-route] (vert.x-worker-thread-2) Exchange[Id: B23C7938FE44124-0000000000000005, ExchangePattern: InOut, Properties: {}, Headers: {Accept=*/*, CamelHttpMethod=POST, CamelHttpPath=/directTest/path1, CamelHttpQuery=null, CamelHttpRawQuery=null, CamelHttpUri=/directTest/path1, CamelHttpUrl=http://localhost:8080/directTest/path1, Content-Length=5, Content-Type=text/plain, Host=localhost:8080, User-Agent=curl/7.54.0}, BodyType: io.vertx.core.buffer.impl.BufferImpl, Body: Test!]
Listing 2-12camel-direct-log Logs
每次完成对path1,
的请求时,都会创建两个日志条目,一个用于path1-route
路线,另一个用于logger-route
。示例中使用的默认日志格式化程序使用以下格式:
${date-time} ${log level} ${logger name} ${thread name} ${log content}
日志是不同的,因为logger-route
路径将showAll
参数设置为真,这意味着整个exchange
对象将被打印。你也可以测试一下path2
,你会得到相似的结果。
$ curl http://localhost:8080/directTest/path2 -X POST \
-H 'Content-Type: text/plain' --data-raw 'Test!'
path1-route
和path2-route
实现了相同的逻辑,并且都使用了logger-route
,但是我想让您看到的是,尽管每个交换在不同的路由中生成日志,但是path-router
和logger- router
,由于直接组件,它们在相同的线程中执行。查看日志条目;两者都将(vert.x-worker-thread-2)
打印为线程名,因为它们是在同一个线程中执行的。
消费者通常有一个线程池,以便一次处理多个交换。在本例中,您使用 Vertx web 库来实现 HTTP web 服务器。Vertx 使用反应式方法,通过遵循异步非阻塞 IO 执行模型来更好地利用计算资源,即使它为事件循环和工作线程分配了多个线程。
直接允许您将一个路由逻辑聚合到另一个路由。这样,您可以在多条路由中重用路由逻辑。这是一个非常简单的例子,说明了如何使用 direct 来重用逻辑或提高代码可读性,但它的目的是解释它是如何工作的,并扩展其他 Camel 概念。
让我们回到camel-file-rest
项目。
Beans 和处理器
组件是抽象实现复杂性的巨大工具,但是有些情况下它们可能还不够。你可能找不到适合你的必需品的组件,你可能想做一个简单的处理,或者你可能想做一个非常复杂的处理,但在路线上做是不可能的。Camel 提供了不同的方法来处理这些情况。让我们看看一些可能性。
回头看看camel-file-rest
项目中的FileServerRoute
类,更准确地说,是在createGetFilesRoute()
方法中,如清单 2-13 所示。
private void createGetFilesRoute(){
from(DIRECT_GET_FILES)
.routeId("get-files")
.log("getting files list")
.bean(FileReaderBean.class, "listFile")
.choice()
.when(simple("${body} != null"))
.marshal().json(JsonLibrary.Jsonb);
}
Listing 2-13createGetFilesRoute Method
这条路线中有一些新的概念需要探索,但首先让我们分析一下为什么需要FileReaderBean
类。
您可能还记得另一条路径中的这个类,在这条路径中,您使用了它的静态方法getServerDirURI()
来检索服务器目录 URI,并在文件组件配置中设置它的值。您还可以使用这个类来列出服务器中存在的文件。您需要这样做,因为文件组件以一种特定的方式工作,不适合这种情况。
组件可能充当消费者或生产者,在某些情况下,同时充当两者。需要明确的是,Camel 世界中的消费者意味着您连接到一个资源,比如数据库、文件系统、消息代理等等。这也可能意味着您正在公开一个服务并使用传入的数据,就像您在 REST 集成中使用camel-quarkus-platform-http
组件一样。它允许您公开 REST 服务并接收请求。
另一方面,生产者是将数据发送或保存到另一个端点的组件。生产者还可以向数据库、文件系统、消息代理等发送数据。这实际上取决于组件如何工作。在您的情况下,文件组件确实作为消费者和生产者工作,但并不完全符合您的需要。
您的路由从一个 REST 接口(消费者)开始,它通过直接组件调用另一个路由。从那里你需要找到一个列出服务器目录的方法,但是文件组件作为一个生产者(to()
),并没有提供一个查询目录的方法。它只保存文件。您可以通过在一个名为FileReaderBean
的 Java bean 类中实现逻辑来解决这个问题,您可以使用 fluent builder bean()
调用它,传递您想要使用的类和您需要的方法。
看看清单 2-14 中listFile()
方法的FileReaderBean
实现。
public void listFile(Exchange exchange) throws URISyntaxException {
File serverDir = new File(getServerDirURI());
if (serverDir.exists()){
List<String> fileList = new ArrayList<>();
for (File file : serverDir.listFiles()){
fileList.add(file.getName());
}
if(!fileList.isEmpty()){
LOG.info("setting list of files");
exchange.getMessage().setBody(fileList);
}else{
LOG.info("no files found");
}
}else{
LOG.info("no files created yet");
}
}
Listing 2-14FileReader Bean listFile Method
这个方法使用 Java IO 库与文件系统交互,并列出给定目录的文件。这对 Java 开发人员来说并不新鲜。新的是这种逻辑如何与 Camel 路线相互作用。
要注意的主要事情是,这个方法接收一个 Camel 交换对象作为参数。如果您查看路由,您没有为 bean 调用设置参数,也不需要这样做。Camel 的绑定过程可以根据方法的声明方式或 bean 调用的设置方式来确定调用哪个方法。例如,您可以从bean(FileReaderBean.class, "listFile")
调用中删除参数"listFiles"
,它仍然可以工作,因为FileReaderBean
的实现方式,只有listFile()
方法适合这个调用。
交换对象不是 bean 调用中唯一自动绑定的对象。你也可以使用
-
org.apache.camel.Message
-
org.apache.camel.CamelContext
-
org.apache.camel.TypeConverter
-
org.apache.camel.spi.Registry
-
java.lang.Exception
我选择交换加“void return”模式,因为我的意图是改变交换的消息对象并影响路由响应。对于这个特殊的例子,我也可以使用消息对象绑定,因为我没有访问任何其他 Exchange 的属性,但是我只想给你一个更广泛的例子,供你将来参考。
另一件值得一提的事情是我们如何访问 bean 对象。bean 可以通过它们的名称来调用,因为它们是在 bean 注册表中注册的。我可以传递一个带有 bean 名称的字符串,如果我使用 CDI 规范注册了它,Camel 就会找到正确的对象。我也可以传递一个对象实例供路由使用。因为我的代码非常简单,所以我选择简化我的方法,传递我需要的类,让 Camel 为我实例化和处理对象。
可以使用beans()
fluent builder 或使用 bean 组件来访问 bean,在这种情况下引用bean:
端点。如您所见,有许多方法可以在 Camel 中重用和封装您的代码逻辑,但也有一些方法可以插入更多本地化的处理逻辑。一种方法是使用Processors
。
清单 2-15 显示了如果你使用Processor
来代替的话get-files
路线会是什么样子。
private void createGetFilesRoute(){
from(DIRECT_GET_FILES)
.routeId("get-files")
.log("getting files list")
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
File serverDir = new File(FileReaderBean.getServerDirURI());
if (serverDir.exists()){
List<String> fileList = new ArrayList<>();
for (File file : serverDir.listFiles()){
fileList.add(file.getName());
}
if(!fileList.isEmpty()){
exchange.getMessage().setBody(fileList);
}
}
}
})
.choice()
.when(simple("${body} != null"))
.marshal().json(JsonLibrary.Jsonb);
}
Listing 2-15createGetFilesRoute Method with Processor
Processor
是声明单个方法process(Exchange exchange)
的接口。在本例中,您将使用一个匿名内部类来实现它。这是在您的路线中输入一些处理逻辑的最简单的方法,但是它不可重用。如果您想重用这段代码,您也可以在一个单独的类中实现这个接口,并将一个实例传递给 route process()
调用。
我一般用豆子做加工。Beans 不需要特定的接口,是 Java 语言中一个更广泛的概念。通过这种方式,我可以让我的代码对于不熟悉 Camel 的其他 Java 开发人员来说更具可移植性和可读性。
我想让你知道这两种方法,和豆子,你可能会在未来与 Camel 的冒险中发现这两种方法。让我们继续代码解构。
述语
当考虑路由逻辑时,有些情况下可能会有条件步骤。由于传入的数据,您可能需要选择特定的路线。您可以使用谓词将条件步骤合并到路由逻辑中。
回到get-files
路线,您有一个只有在满足特定条件时才会执行的步骤。您通过调用choice()
方法开始构建,该方法可以使用when()
方法指定许多不同的选项,甚至可以使用otherwise()
设置一个只有在其他选项都失败时才会遇到的选项。通过运行一个必须返回Boolean
结果的表达式语言谓词来分析每个选项。
在get-files
路径中给出的例子中,只有当消息体不是null
时,最后一行代码才会被执行。如果您还记得FileReaderBean
类中的listFile()
方法,那么只有在目录中有文件时才会设置一个主体。对于您的 REST 组件,一个包含空主体且没有异常的响应意味着请求是成功的,但是没有找到任何内容,因此 HTTP 状态代码应该是204
。
让我们试试这个场景。运行以下命令清理目录并启动应用:
camel-file-rest $ mvn clean quarkus:dev
要测试get-files
路线,您可以运行以下命令:
$ curl -v http://localhost:8080/fileServer/file
使用-v 获得带有 cURL 的详细响应。这样您可以清楚地看到响应中的 HTTP 状态代码是什么,如图 2-3 所示。
图 2-3
没有内容响应
在您分析get-file
路径中的最后一行代码之前,让我们看另一个如何使用谓词的例子。在您喜欢的 IDE 中打开camel-rest-choice
项目。这个项目创建了一个基于 HTTP 请求参数返回 salute 的 REST 路由。看看清单 2-16 中的RestChoiceRoute
类中创建的路线。
public void configure() throws Exception {
rest("/RestChoice")
.get()
.id("rest-choice")
.produces("text/plain")
.route()
.choice()
.when(header("preferred_title").isEqualToIgnoreCase("mrs"))
.setBody(simple("Hi Mrs. ${header.name}"))
.when(header("preferred_title").isEqualToIgnoreCase("mr"))
.setBody(simple("Hi Mr. ${header.name}"))
.when(header("preferred_title").isEqualToIgnoreCase("ms"))
.setBody(simple("Hi Ms. ${header.name}"))
.otherwise()
.setBody(simple("Hey ${header.name}"));
}
Listing 2-16RestChoiceRoute Configure Method
下面是一个如何使用选择结构创建多选项方案的示例。您还使用了otherwise()
,它允许您设置一个默认选项,以防前面的任何选项不满足。您正在使用 Header EL 根据 HTTP 请求中的报头内容来评估决策。
让我们测试一下这段代码。使用以下命令运行应用:
camel-rest-choice $ mvn quarkus:dev
在另一个终端中,您可以使用以下命令测试应用:
$ curl -w "\n" http://localhost:8080/RestChoice?name=John \
-H "preferred_title: mr"
此呼叫的响应将是“Hi Mr. John
”,因为您使用“mr
”作为首选标题。如果您没有发送“preferred_title
”头,或者您使用了一个意外的值来设置它,那么响应将是“Hey John
”,因为会遇到otherwise()
选项。
如您所见,您可以使用 EL 谓词来评估选择结构中的条件。尽管表达式语言非常灵活,但在某些情况下,您可能需要计算更复杂的变量。在这些情况下,您可以实现一个接口来创建一个可定制的谓词。
数据格式
对于这个特殊的 REST 接口,您正在使用两种不同的媒体类型:text/plain 和 application/json。使用 text/plain 很方便,因为当您将它翻译成 Java 语言时,您将处理字符串,这是一种易于使用且非常完整的数据结构,但通常您需要处理表示您的数据结构的更高级的对象。
回到camel-file-rest
项目中的get-files
路线。留下下面一行代码来解释:
marshal().json(JsonLibrary.Jsonb);
如果您还记得的话,您的 REST 接口应该返回一个 JSON 对象作为对get-files
方法的响应,但是FileReaderBean
方法listFile()
只返回一个 Java 格式的名称列表。这就是为什么您需要将消息体转换成 JSON 格式。
通常当你需要在 Java 中处理数据结构时,你倾向于用 POJO (plain old Java object)类来表示那些结构,将二进制数据简化成字符串或字节数组,并将这些数据转换成编程时容易引用的东西。当您考虑 JSON 或 XML 并希望操作其内容时,将内容解析为 POJO 是一种常见的方法。使用像 JAXB 或 JSON-B 这样的库,您可以将 Java 对象转换成 XML/JSON,或者将 Java 对象转换成 XML/JSON 文档。
XML 和 JSON 并不是唯一常用的数据格式。Camel 提供了大量的格式,包括
-
亚姆
-
战斗支援车
-
欧罗欧欧欧罗欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧欧
-
压缩文件
-
Base64
-
以及其他等等
在本例中,您正在编组数据,这意味着将 Java 对象结构转换成二进制或文本格式。您也可以反过来将二进制或文本格式转换成 Java 对象。
为了处理将在 REST 集成中大量使用的 JSON 数据格式,您将使用 JSON-B 作为解析器,因为它是 MicroProfile 规范的标准实现。
在以后的章节中,你会看到不同上下文中的数据格式。现在,让我们看看更低级的数据转换。
类型转换器
示例应用接收一个 HTTP 请求,并将其保存到服务器文件系统的一个文件中。你只写了几行代码,描述了你想要使用的界面类型,并指出你想要保存文件的位置。在这些步骤之间发生了很多事情,这就是 Camel 的魅力所在:您可以用几行代码做很多事情。我希望您了解幕后发生的事情,以便在规划路线时做出正确的选择。
让我们回头看看在文件系统中保存文件的那部分代码。参见清单 2-17 。
private void createSaveFileRoute() throws URISyntaxException{
from(DIRECT_SAVE_FILE)
.routeId("save-file")
.setHeader(Exchange.FILE_NAME, simple("${header.fileName}"))
.to("file:"+ FileReaderBean.getServerDirURI())
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(201))
.setHeader(Exchange.CONTENT_TYPE,
constant(MEDIA_TYPE_TEXT_PLAIN))
.setBody(constant(CODE_201_MESSAGE)) ;
}
Listing 2-17createSaveFileRoute Method
将请求保存为文件唯一需要做的事情是从 header 参数中提取文件名。让我们运行应用并保存一个文件。首先启动应用:
Camel 文件架$ mvn clean quarkus:dev
您可以创建这样的文件:
$ curl -X POST http://localhost:8080/fileServer/file -H 'fileName: test.txt' -H 'Content-Type: text/plain' --data-raw 'this is my file content'
您可能需要检查文件是否在那里。你可以查看项目的目标文件夹(camel-file-rest/target/camel-file-rest-dir
)或者只是运行列表文件调用:
$ curl http://localhost:8080/fileServer/file
数据转换或变换是 Camel 路线中经常发生的事情。这通常是因为每个组件或端点都处理特定类型的数据。
以camel-rest-file
应用为例。Java web 服务器将传入的数据视为通过网络顺序发送的字节流。为了抽象这个过程,Java 利用库来执行 IO 操作,具体说到读取数据,它通常使用InputStream
类来读取文件系统中的数据或来自网络的数据。当您发送数据或将数据写入文件系统时,也会发生同样的情况。Java 也有写字节流的表示法,即OutputStream
。
Java 中还使用了其他对象来表示更低级的数据结构。Camel 识别这些对象或原语,并通过一个叫做类型转换器的结构来处理它们的操作。以下是 Camel 中默认处理的类型列表:
-
文件
-
线
-
字节[]和字节缓冲区
-
输入流和输出流
-
读者和作者
-
文档和来源
除了已经存在的类型,您还可以使用TypeConverters
接口实现自己的转换器。让我们分析一下转换器的工作原理。打开camel-type-converter
项目。查看TypeConverterTimerRoute
类并查看创建的路线。参见清单 2-18 。
public void configure() throws Exception {
from("timer:type-converter-timer?period=2000")
.routeId("type-converter-route")
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
MyObject object = new MyObject();
object.setValue(UUID.randomUUID().toString());
exchange.getMessage().setBody(object);
}
})
.convertBodyTo(AnotherObject.class)
.log("${body}");
}
Listing 2-18TypeConverterTimerRoute Configure Method
这是一条非常简单的路线,只是为了展示转换是如何工作的。对于这个例子,有两个 POJO 类,MyObject
和AnotherObject
,它们有一个名为value
的属性。这两个类的主要区别在于,AnotherObject
实现了toString()
方法,使结果字符串显示对象属性值。这条路线最重要的部分是当你明确地请求一个调用convertBodyTo()
的转换,并作为一个参数传递你希望对象被转换成的类AnotherObject.class
。正如您所看到的,路由中没有明确声明必须如何完成这种转换,但这是在运行时发现的。
看看清单 2-19 中的MyObjectConverter
类。
@Singleton
@Unremovable
public class MyObjectConverter implements TypeConverters {
@Converter
public static AnotherObject toAnotherObject(MyObject object){
AnotherObject anotherObject = new AnotherObject();
anotherObject.setValue(object.getValue());
return anotherObject;
}
}
Listing 2-19MyObjectConverter.java File
MyObjectConverter
是一个 bean,因为它用@Singleton
进行了注释,这意味着这个对象的单个实例将由 bean registry 创建和维护。该类还实现了TypeConverters
接口,该接口没有任何方法声明,但用于使该对象可被发现。这个类有一个注释为@Converter
的静态方法,带有一个特定的返回类和一个特定的对象作为参数,这使得这个方法足以在转换过程中被发现。
您可能也注意到了@Unremovable
注释。这与 Camel 实现没有直接联系,但与 Quarkus 插件在编译时如何准备代码有关。你还记得 Quarkus 预测了一些运行时进程吗?其中之一是验证代码如何使用 CDI。由于没有任何注入这个 bean 的类的显式引用,Quarkus 从加载过程中删除了这个类。为了避免这种行为,一个可能的解决方案是用@Unremovable
注释这个类。您可以尝试删除此注释,看看在尝试执行应用时会发生什么。有时候犯错是一种很好的学习方式。
与其他对象结构一样,转换器也在 Camel 上下文中维护,更具体地说,是在类型转换器注册表中。由于 bean 实例是由框架创建的,Camel 可以发现它,因为它使用了接口,并将其添加到类型转换器注册表中。因此,当需要转换身体时,Camel 可以在注册表中查找合适的转换器。
要测试这个应用,只需运行以下命令:
camel-type-converter $ mvn quarkus:dev
此时,您应该开始看到类似清单 2-20 的日志条目。
2021-05-10 08:53:04,915 INFO [type-converter-route] (Camel (camel-1) thread #0 - timer://type-converter-timer) AnotherObject{value='2090187e-0df7-4126-b610-fa7f92790cde'}
2021-05-10 08:53:06,900 INFO [type-converter-route] (Camel (camel-1) thread #0 - timer://type-converter-timer) AnotherObject{value='deab51df-75b1-4437-a605-bda2f7f21708'}
Listing 2-20camel-type-converter Logs
因为你用随机的 UUIDs 设置了MyObject
,每个日志条目都会有所不同,但是这样很明显你实际上是在调用AnotherObject
类的toString()
方法。
摘要
本章介绍了如何用 Camel 公开 REST web 服务,但也深入探讨了 Camel 的概念。
您了解了以下内容:
-
写 Camel 路线的不同方法
-
多年来 Web 服务的演变
-
休息是什么
-
带有 OpenAPI 的新开放标准
-
如何提高代码可读性,重用代码
-
如何在您的路线中包含编程逻辑
-
Camel 如何处理不同的数据结构和格式
现在您对 Camel 的工作原理有了更全面的了解。您已经看到了很多代码,但是现在您已经准备好查看更复杂的代码示例,尤其是同时处理多个应用。
在下一章中,您将探索应用与 REST 和 Web 服务安全性的通信。
三、使用 Keycloak 保护 Web 服务
我们一直在谈论 web 服务,方法是如何发展的,以及如何编写 web 服务,但是当我们谈论在互联网上公开服务时,有一个非常重要的话题我没有提到:安全性。
当然,安全性是 web 服务主题中的一个主题,但它本身也是一个主题。我将讨论一些总体的安全性概念,但是请记住,本章的目的不是广泛地解释安全性,而是教您在使用 Apache Camel 编写集成时如何处理一些常见的安全性需求或协议。
当我们谈论 web 上的安全性时,我们谈论的是如何使对 web 服务或 Web 应用的访问安全。为了涵盖应用的安全方面,我们必须考虑不同的事情。首先,我们需要确保通信渠道的安全。当我们谈论我们自己组织中的合作伙伴或消费者时,我们可能会创建 VPN(虚拟专用网络),这将掩盖我们的数据和 IP 路由。对于我们确切知道谁在访问我们的应用的场景,这种机制是非常安全和最佳的,但对于面向互联网上开放受众的服务来说,这种机制并不适用。对于第二个场景,我们通常依靠 TLS 来加密 HTTP 连接,以保护通过互联网传输的数据。HTTPS(TLS+HTTP 的组合)保护我们免受中间人攻击,在中间人攻击中,黑客可以窃听连接以窃取数据或篡改数据,从而攻击服务器或客户端。
还有其他可能的攻击,如注入、跨站点脚本、目录遍历、DDoS 等。保护您的服务免受列出的攻击是极其重要的。其中大部分不会由您的服务来处理,而是由专门的软件来处理,这些软件在您的客户端和您的服务之间充当连接的媒介,比如 web 应用防火墙(WAF)。
在本章中,您将探索访问控制,这也是服务安全性的一个基本方面。您将看到如何公开 REST APIs 并使用完善的开放标准身份验证和授权协议保护它们,以及如何使用相同的协议消费 API。
访问控制
web 服务中一个常见的需求是根据谁在请求数据来提供服务响应。这可能意味着这些数据是私有的,应该只由它的所有者访问,因此服务必须能够识别是谁发出的请求,并检查该实体是否有权访问所请求的数据。在其他场景中,我们可能拥有供公共消费的数据,或者与某个组相关或由某个组拥有的数据,但是无论哪种情况,为了提供这些功能,我们都需要工具来允许我们执行访问控制。
想想您生活中必须与之交互的大多数应用或系统。如果您的用户体验是从在登录屏幕上输入用户名和密码开始的,我不会感到惊讶。拥有用户名和密码或密钥和密码对是认证用户或系统的最常见方式。为了明确这个概念,身份验证意味着“识别所提供的用户及其凭证(在这个例子中是密码)对于特定的系统是否有效。”
被识别通常是访问控制探索的一个步骤。大多数系统或应用都有基于用户属性、他们可能所属的组或应用于他们的角色的专有内容。一旦用户试图访问特定内容,访问控制必须检查用户是否被授权访问该内容。从这个意义上说,授权意味着“验证给定实体是否有权访问数据或在系统中执行某个操作的行为。”
正如你所看到的,认证和授权是两个不同的概念,它们有着内在的联系。它们构成了我们如何构建应用访问控制的基础。对用户进行身份验证有不同的方式,处理授权也有不同的方式。让我们讨论一些协议。
OAuth 2.0
如果我们有一个开放的行业标准协议来描述我们的授权流程应该如何工作,会怎么样?OAuth 2.0 就是这种情况。让我们看看这个协议是关于什么的。
我们比以往任何时候都更加紧密地联系在一起,而所有这些联系都依赖于网络应用和网络服务。当我说 web 应用时,我指的是为 web 浏览器开发的应用。Web 服务是非可视化的、非面向浏览器的 API,为其他 web 服务、移动应用、桌面应用等提供支持。要访问所有这些服务和应用,我们需要对用户进行身份验证和授权。如果我们回到几年前,对于给定的 web 应用,我们会有以下场景:
一旦用户登录到应用,就会为他创建一个会话来维护他在服务器端的信息,并跟踪他在使用系统时生成的数据。如果用户无意中丢弃了浏览器并丢失了该会话的本地引用,他将不得不重新输入用户名和密码才能重新进入应用。服务器端可以尝试检索会话信息或创建一个新的会话信息,并最终从内存中删除“孤立的”会话信息。
现在很少有这样设计的应用,因为这种方法有很多问题。主要问题是它的扩展性有多差。当我们想到有数百万用户访问我们的应用时,在服务器端维护内存中的会话信息对服务器资源来说是极其繁重的。当然,仍然有必要在内存中保存一些关于连接或整体服务器端状态的信息,但是现在的策略是尽可能地与客户端共享这一负载。Web 应用现在严重依赖 cookies 来存储和持久化客户端的用户会话状态,以及 REST APIss 的使用,REST API 本质上是无状态服务。很自然,我们的访问控制机制也会进化得更适合这种场景。
此时,我们需要将身份和认证的概念与授权和访问授权的概念分开。你马上就会明白为什么了。
OAuth 是一个开放的标准协议,旨在指定访问授权流(授权)应该如何发生。它的开发始于 2006 年,由 Twitter 和 Gnolia 联合开发,当时 Gnolia 正在为其网站实现 OpenID 认证。
OpenID 也是一个开放标准,但它专注于身份验证。它旨在解决当今世界的一个普遍问题:必须为不同的系统处理许多用户和密码。人们在互联网上消费许多不同的服务。从视频流媒体平台到社交媒体,我们连接到许多不同的网站,每个网站都有自己的身份数据库,我们需要在其中创建我们的用户。如果我们可以在一个单独的系统中拥有我们的身份信息,并使用它在不同的系统中进行身份验证,生活会简单得多。这就是 OpenID 的意义所在。这是可用于各种系统的单点身份认证。
我说过我们需要将认证和授权概念分开,因为在我们的例子中,它们是由不同的协议规范处理的。OpenID 负责认证,OAuth 负责授权。现在,让我们关注 OAuth 和授权。
为了理解 OAuth 如何帮助访问授权,您需要理解 OAuth 的流程,但是在此之前,您需要理解规范定义的角色。看图 3-1 。
图 3-1
OAuth 角色交互
OAuth 在我们与互联网服务的关系中非常常见。以社交登录为例,你可以从社交媒体,如脸书、GitHub、谷歌等,使用你的用户帐户登录到一个给定的网站。它们都使用 OAuth 作为协议。以这个例子来理解角色:
您刚刚发现了一个非常有趣的网站,可以让您编辑脸书相册中的照片。它要求您使用您的脸书帐户登录,因此您必须允许该网站访问您的一些脸书数据。获得权限后,您就可以编辑照片并将其保存到脸书相册中。
在这种情况下,您是资源所有者,因为您拥有数据(照片、个人资料信息等等)。您正在访问的网站是将您重定向到脸书进行身份验证和授权的客户端。一旦您登录到脸书并拥有正确的权限集,客户端将代表您访问您的数据。脸书是授权服务器和资源服务器。它是一个授权服务器,因为它负责识别您的身份并签发一个签名令牌,该令牌包含足够的信息来识别您的身份并允许客户端代表您进行操作。它也是一个资源服务器,因为它提供了访问和操作相册的 API,并根据客户机传递的令牌信息检查客户机是否得到了适当的授权。这个流程可以描述如下 1 :
-
客户端通过将资源所有者的用户代理定向到授权端点来启动流。客户端包括其客户端标识符、请求的范围、本地状态和重定向 URI,一旦授权(或拒绝)访问,授权服务器就会将用户代理发送回该重定向。
-
授权服务器对资源所有者进行身份验证(通过用户代理),并确定资源所有者是同意还是拒绝客户端的访问请求。
-
假设资源所有者授权访问,授权服务器使用之前提供的重定向 URI(在请求中或在客户端注册期间)将用户代理重定向回客户端。重定向 URI 包括授权码和客户端先前提供的任何本地状态。
-
客户端通过包含上一步中接收到的授权代码,向授权服务器的令牌端点请求访问令牌。当发出请求时,客户端向授权服务器进行身份验证。客户端包括重定向 URI,用于获得验证的授权码。
-
授权服务器对客户端进行身份验证,验证授权代码,并确保 URI 收到的重定向与步骤 3 中用于重定向客户端的 URI 相匹配。如果有效,授权服务器用访问令牌和可选的刷新令牌进行响应。
基于这一描述,我们可以突出图 3-2 中说明的特征。
图 3-2
OAuth 角色
上面描述的场景是 OAuth 建议应该如何进行访问授权的一个例子。当前版本的 OAuth 2.0 规范描述了六种不同的授权类型:
-
授权代码
-
客户端凭据
-
设备码
-
刷新令牌
-
隐式流
-
密码授权
每种授权类型针对不同的使用案例。我们的示例使用了授权代码授权类型,这更适合基于 web 浏览器的应用和移动应用,在这些应用中,我们通常希望授权第三方站点或应用。
除了定义角色和流程,OAuth 规范还定义了客户端类型、如何使用令牌、威胁模型以及在实现 OAuth 时应该考虑的安全问题。我的目标不是全面讨论这个规范,而是给你足够的信息,这样你就能理解你在本章后面要做什么。
OpenID 连接
OAuth 是一个授权协议,但是为了完全实现访问控制,我们还需要定义如何使用身份验证。用 OpenID 基金会的话说,“OpenID Connect 1.0 是 OAuth 2.0 协议之上的一个简单的身份层。” 2 这是您在实施访问控制时将要使用的协议。
关于 OpenID Connect,我不需要你了解太多,除了它是 OAuth 2.0 之上的一个身份层。当我们讨论授权类型时,我们将讨论 OAuth 定义。当我们谈论令牌时,我们将谈论 JWT (JSON Web Tokens)实现。因此,为了配置您将使用的 IAM(身份和访问管理)工具,我不需要您理解 OpenID Connect 中的任何特定概念。
OIDC 规范是广泛的。它从核心定义(指定了建立在 OAuth 2.0 之上的身份验证)到如何为客户端提供动态注册、如何管理会话、OpenID 提供者发现等等。如果你对此感到好奇,并想更深入地研究这个主题,我推荐你访问 OpenID 基金会网站, https://openid.net/
。在那里,您可以找到完整的规范,以及关于该协议和社区如何发展的其他信息。
一个旁注,我想补充的是,在 OpenID Connect 之前就有 OpenID 规范。OpenID 是我在讲述 OAuth 的创建历史时提到的认证规范。OpenID 规范现在被认为是过时的,这就是为什么经常听到 OpenID,事实上,人们指的是 OpenID Connect,因为 OIDC 取代了第一个 OpenID 规范。
凯克洛克
我们讨论了协议,但现在我们需要开始使用一个解决方案,它实际上实现了协议,并提供了其他功能,这些功能将与这些协议相结合,提供一个完整的访问控制解决方案。这是奇洛克。
除了允许我们遵循标准并保证我们的应用和其他解决方案之间的互操作性的协议之外,我们还需要担心其他事情。也许我想到的第一个问题是,我要在哪里以及如何坚持/管理我的用户群?毕竟,如果我没有用户列表,我将如何对某人进行身份验证和授权?这就是为什么你要用奇洛克。
Keycloak 实现了两个认证和授权标准: SAML 2.0 和 OpenID Connect 。作为一个身份管理解决方案,它提供了一个完整的用户管理系统,并能够联合其他用户群,如 Kerberos 或 LDAP 实现。您还可以实现一个提供者来使其他用户群适应它,例如,一个存在于 SQL 数据库中的用户群。另一种可能性是代理另一个基于 SAML 2.0 和 OpenID Connect 的身份提供者。这样,即使与不同的身份提供商合作,您也可以实现单点访问控制。
您将只关注 OpenID Connect 标准,但是如果您想知道,SAML 2.0 是为传统 web 服务世界(SOAP)构建的基于 XML 的标准,因此它是一个更老的协议。给你一个概念,v2.0 版本发布于 2005 年。
开始试用 Keycloak 的最好和最快的方法是使用项目社区提供的容器映像,这也是您将要做的事情。从您的终端运行以下命令:
$ docker run --name keycloak -e KEYCLOAK_USER=admin \
-e KEYCLOAK_PASSWORD=admin -p 8180:8080 -p 8543:8443 jboss/keycloak:13.0.0
您使用的是 Keycloak 版本13.0.0
,这是目前最新的版本。您正在将管理员用户设置为admin
,并使用admin
作为密码。您正在将其 HTTP 端口映射到8180
,将其 HTTPS 端口映射到8543
。
在您最喜爱的网络浏览器中,访问http://localhost:8180
。您应该会看到类似图 3-3 的页面。
图 3-3
Keycloak 主页
点击Administration Console
链接。您将被重定向到登录页面,如图 3-4 所示。
图 3-4
键盘锁登录页面
输入用户名admin
和密码admin
,然后点击登录按钮。你将被重定向到键盘锁控制台页面,如图 3-5 所示。
图 3-5
键盘锁管理控制台
此时,您将停止使用 Keycloak 配置。您知道如何运行它以及如何访问它,但是要开始配置 Keycloak 并开始讨论它的概念,您需要看到它的一个用例。
用 Keycloak 保护 REST APIs
在讨论了协议和 Keycloak 必须提供什么之后,下一步是理解如何配置 Camel 应用来使用 Keycloak。首先,您应该分析一个示例场景。
真正的集成案例需要至少两个不同的应用或端点以及一个应用来组成集成层。这对于代码来说可能有点太复杂了。因为我的目标是教你如何使用 Camel 解决常见的集成问题,所以我将遵循一种方法,在这种方法中,你编写更多的 Camel 代码来解决不同的情况,即使这种情况不一定是集成,而你实际上是在实现一个服务。这就是你现在要做的。您将使用 Camel 实现一个 REST 服务,并使用 Keycloak 保护它。因此,当您需要保护一个需要 REST 接口的集成时,您将知道该怎么做。
公开联系人列表 API
你将处理一个非常简单的案例。您将实现一个能够处理联系人列表的服务。您首先要学习如何公开您的服务 API 并保护它。
让我们开始检查出现在contact-list-api
项目中的代码。在 IDE 中打开它。看清单 3-1 。
public class ContactListRoute extends RouteBuilder {
public static final String MEDIA_TYPE_APP_JSON = "application/json";
@Override
public void configure() throws Exception {
rest("/contact")
.bindingMode(RestBindingMode.json)
.post()
.consumes(MEDIA_TYPE_APP_JSON)
.produces(MEDIA_TYPE_APP_JSON)
.type(Contact.class)
.route()
.routeId("save-contact-route")
.log("saving contacts")
.bean("contactsBean", "addContact")
.endRest()
.get()
.produces(MEDIA_TYPE_APP_JSON)
.route()
.routeId("list-contact-route")
.log("listing contacts")
.bean("contactsBean", "listContacts")
.endRest();
}
}
Listing 3-1ContactListRoute.java File
这个服务只有两个操作:一个是在列表中保存联系人,另一个是列出保存的联系人。选择的媒体类型是 JSON,您已经看到了如何使用它,但是这里还有一些新的东西需要您学习:如何自动转换 REST 接口的输入和输出数据。
通过设置bindingMode(RestBindingMode.json)
,您告诉 Camel 您希望传入的 JSON 数据被转换成 POJO 对象,在本例中是用于post()
操作的type(Contact.class)
,并且响应必须被自动转换成 JSON。
对于这个自动绑定,您使用的是camel-quarkus-jackson
数据格式,这是 JSON REST 绑定模式的默认数据格式。这就是为什么你不需要声明一个数据格式。
除了接口声明之外,清单 3-2 真的很神奇。看一看它。
@Singleton
@Unremovable
@Named("contactsBean")
public class ContactsBean {
private Set<Contact> contacts = Collections.newSetFromMap(Collections.synchronizedMap(new LinkedHashMap<>()));
private static final Logger LOG = Logger.getLogger(ContactsBean.class);
public ContactsBean() {}
@PostConstruct
public void init(){
contacts.add(new Contact("Bill","bill@email.com","99999999"));
contacts.add(new Contact("Joe", "joe@email.com","00000000"));
}
public void listContacts(Message message) {
message.setBody(contacts);
}
public void addContact(Message message) {
if( message.getBody() != null){
contacts.add(message.getBody(Contact.class)) ;
}else{
LOG.info("Empty body");
}
}
}
Listing 3-2ContactBean.java File
首先要注意的是,您正在使用 CDI 规范创建一个带有包含联系人列表的Linked Hash Map
的Singleton
bean。在 bean 创建后,您还可以使用@PostConstruct
为散列表设置一些默认条目。listContacts()
和addContact()
方法非常简单,其中addContact()
从主体中取出 POJO 并将其放入 hashmap 中,而listContacts()
将 hashmap 放入消息主体中以作为 HTTP 响应返回集合。
Contact POJO 类也很简单。看一下清单 3-3 。
public class Contact {
public String name;
public String email;
public String phone;
public Contact() {}
public Contact(String name, String email, String phone) {
this.name = name;
this.email = email;
this.phone = phone;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Contact)) return false;
Contact contact = (Contact) o;
return Objects.equals(name, contact.name) && Objects.equals(email, contact.email) && Objects.equals(phone, contact.phone);
}
@Override
public int hashCode() {
return Objects.hash(name, email, phone);
}
}
Listing 3-3Contact.java File
如您所见,POJO 中不需要注释。由于这个类没有特殊要求,Jackson 会知道如何在 JSON 之间进行转换。唯一有点不同的是实现了equals()
和hashCode()
方法。这样做是为了避免联系人列表中出现重复条目。
也就是说,您可以测试应用。在您的终端上,使用 Quarkus 插件运行应用:
contact-list-api $ mvn quarkus:dev
您可以通过以下方式保存联系人:
$ curl -X POST 'http://localhost:8080/contact' \
-H 'Content-Type: application/json' \
--data-raw '{ "phone": "333333333", "name": "Tester Tester", "email": "tester@email.com"}'
您可以像这样检索列表:
$ curl http://localhost:8080/contact
该服务正在工作,但不受保护。在对应用进行任何修改之前,您必须了解如何正确配置 Keycloak。
配置键盘锁
您看到了如何在本地运行 Keycloak。现在您将学习如何配置它以及如何配置应用。
当您访问管理控制台时,您停止了 Keycloak 简介。回到控制台。有几个概念我们必须先讨论一下。
登录后访问的第一个页面是“领域设置”页面。您可以看到您正在使用主领域,它的显示名称是 Keycloak,并且它公开了两个端点:一个用于 OpenID,一个用于 SAML 2.0。
领域是属于同一域的用户基础和客户端应用的集合。主领域代表 Keycloak 服务器域,其他域源自该域。因此,让我们为您的用例创建一个领域。请遵循以下步骤:
-
左上角是带向下箭头的领域名称。单击箭头。
-
将出现“添加领域”按钮。点击它。
-
您将被重定向到领域创建页面。输入
contact-list
作为名称,并点击创建按钮。 -
此时,领域已经创建,但是您可以添加附加信息作为显示名称。将其设置为
Contact List Hit
并按下保存按钮。
现在您已经配置了该领域,您需要配置应用来使用该领域。Keycloak 将应用视为客户端。请遵循以下步骤:
-
在左侧菜单中,单击客户端。
-
现在你在客户列表页面。如您所见,已经定义了其他客户端。这些客户端是 Keycloak 用来管理这个领域各个方面的应用。你现在不需要担心他们。
-
单击创建按钮。
-
输入
contact-list-api
作为客户端 ID。将客户端协议保留为 openid-connect。将根 URL 设置为http://localhost:8080/contact
。 -
单击保存按钮。
创建客户端后,您将被重定向到客户端设置页面,如图 3-6 所示。
图 3-6
键盘锁客户端设置页面
剩下要做的唯一配置是将客户端访问类型设置为机密。这样,Keycloak 将创建一个客户端和一个用于客户端标识的密码。进行更改,然后在页面底部按下Save
按钮。
一旦您配置了领域和客户机,您就需要一个经过身份验证的用户基础来使用服务。在这种情况下,您将使用 Keycloak 作为您的身份提供者。按照以下步骤创建用户:
图 3-7
键盘锁用户设置页面
-
在左侧面板中,单击用户菜单。您将被重定向到用户列表页面。
-
在屏幕右侧,单击添加用户按钮。
-
将用户名设置为
viewer
。 -
单击保存按钮。您将被重定向到用户设置页面,如图 3-7 所示。
图 3-8
“锁定用户身份证明”页
-
将电子邮件验证属性设置为开。
-
现在单击凭证选项卡。将密码设置为
viewer
。确保将临时属性设置为关,如图 3-8 。 -
单击设置密码按钮。现在您已经有了一个准备测试的用户。
-
按照前面的步骤,使用密码
editor
创建另一个名为editor
的用户。
现在,您已经配置了领域、客户机和用户。唯一需要配置的是角色。您将在 API 中采用基于角色的访问控制(RBAC ),这意味着您需要为用户分配角色。基于这些角色,应用将确定给定用户有权执行哪些操作。
要创建角色,请执行以下步骤:
图 3-9
键盘锁角色页面
-
在左侧面板中,单击角色菜单选项。这将带您进入角色页面,如图 3-9 所示。
-
您将创建一个对整个领域都有效的角色。单击屏幕右侧的添加角色按钮。
-
将角色名称设置为
view
。将描述保留为空,然后单击保存按钮。 -
按照相同的步骤创建一个名为
edit
的角色。
创建角色后,您需要按照以下步骤将它们分配给用户:
图 3-10
键盘锁用户列表
-
在左侧面板菜单中,单击用户。
-
单击查看所有用户。它将列出之前创建的用户,如图 3-10 所示。
图 3-11
键盘锁用户角色映射
-
单击编辑按钮。这将引导您进入用户设置页面。
-
单击角色映射选项卡。您将看到您创建的角色,如图 3-11 所示。
-
因为您正在编辑
editor
用户,所以单击edit
角色。 -
“添加所选内容”按钮将被解锁。点击它。
-
您应该会收到以下消息:“成功!角色映射已更新。
-
现在编辑
viewer
用户,按照相同的步骤向其添加view
角色。
现在,您已经为您的应用示例配置了 Keycloak。您还有机会对它的工作原理以及如何导航其配置有了更多的了解。
配置资源服务器
您的授权服务器和身份提供者 Keycloak 已配置完毕。现在,您需要配置应用来与它通信,并将您的服务映射到基于用户角色的受限访问。
让我们回到contact-list-api
项目。看看清单 3-4 中描述的依赖关系。
...
<dependencies>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-bean</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-jackson</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>io.quarkus</groupId>-->
<!--<artifactId>quarkus-keycloak-authorization</artifactId>-->
<!-- </dependency>-->
</dependencies>
...
Listing 3-4contact-list-api pom File Snippet
有一个被注释的依赖项,quarkus-keycloak-authorization
。此扩展提供了一个策略实施器,它根据权限实施对受保护资源的访问。这是对 JAX-RS 实现的一个补充,它使用由 OpenID Connect 和 OAuth 2.0 兼容的授权服务器(如 Keycloak)发布的令牌的无记名令牌认证。取消对此依赖关系的注释。现在看看清单 3-5 。
### Client Configuration
#quarkus.oidc.auth-server-url=http://localhost:8180/auth/realms/contact-list
#quarkus.oidc.client-id=contact-list-api
#quarkus.oidc.credentials.secret=
### Path Policies Mapping
## only authenticated access will be allowed
#quarkus.http.auth.permission.authenticated.paths=/*
#quarkus.http.auth.permission.authenticated.policy=authenticated
#
#quarkus.http.auth.policy.role-edit.roles-allowed=edit
#quarkus.http.auth.permission.edit.paths=/contact
#quarkus.http.auth.permission.edit.methods=POST
#quarkus.http.auth.permission.edit.policy=role-edit
#
#quarkus.http.auth.policy.role-view.roles-allowed=view,edit
#quarkus.http.auth.permission.view.paths=/contact
#quarkus.http.auth.permission.view.methods=GET
#quarkus.http.auth.permission.view.policy=role-view
Listing 3-5contact-list-api project, application.properties File
应用已经准备好接受 Keycloak 的保护。我只对配置进行了注释,这样您就可以测试应用,而不必先配置 Keycloak。取消属性的注释。先稍微说一下这个配置。
第一个属性条目与该应用如何连接到 OIDC 授权服务器有关。这就是为什么 Keycloak URL 指向为这个例子配置的领域。您还需要关于客户机的信息,在本例中是客户机 id 及其秘密。你一定注意到了秘密属性是空白的。由于这个值是在你设置客户端为“机密”时由 Keycloak 自动生成的,所以你会有和我不一样的结果。
按照以下步骤获取您的秘密值:
图 3-12
键控锁定客户机身份证明页
-
重新登录到 Keycloak。
-
在左侧面板菜单中,单击客户端。
-
单击联系人列表 api 客户端 ID。
-
在“客户端设置”页面上,单击“凭据”选项卡。您应该会看到如图 3-12 所示的页面。
-
复制灰色输入框中的秘密值并粘贴到空白的
quarkus.oidc.credentials.secret
属性中。
属性文件的第二部分是关于如何映射受保护的资源以及对它们应用什么策略。
以quarkus.http.auth.permission.authenticated.paths
房产为例。它使用通配符来标记quarkus.http.auth.permission.authenticated.policy
中指出的策略的每一条路径,这是经过验证的。这意味着只有具有有效承载令牌的请求才会被接受。由于这是一个非常通用的规则,在接下来的属性中,您将描述更具体的路径,并将它们与 HTTP 方法相结合,以创建更细粒度的访问控制。观察最后一部分,如清单 3-6 所示。
...
quarkus.http.auth.policy.role-view.roles-allowed=view,edit
quarkus.http.auth.permission.view.paths=/contact
quarkus.http.auth.permission.view.methods=GET
quarkus.http.auth.permission.view.policy=role-view
...
Listing 3-6Path Mapping in the application.properties File
在这里,您创建了一个策略,向任何用户授予角色view
和edit
的权限,并将这个策略映射到/contact
路径和GET
HTTP 方法。
让我们现在启动应用,看看会发生什么。运行以下命令:
contact-list-api $ mvn quarkus:dev
让我们通过列出可用的联系人来测试它,但是添加了-v 开关,以便在出现错误时获得更多信息。
$ curl -v http://localhost:8080/contact
您将收到类似于图 3-13 的内容。
图 3-13
未经授权的响应
由于您没有进行身份验证,也没有传递有效的令牌,因此不允许您访问此内容,因此您会收到一个401
HTTP 代码作为响应。
为了成功地进行这个调用,首先您需要从属性文件(quarkus.oidc.credentials.secret
)中获取客户端密码,并将其设置为一个环境变量,如下所示:
$ export SECRET=[SECRET VALUE]
要获取有效令牌,请运行以下命令:
$ curl -X POST http://localhost:8180/auth/realms/contact-list/protocol/openid-connect/token \
--user contact-list-api:$SECRET \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=viewer&password=viewer&grant_type=password'
在这个命令中有一些东西需要分解。首先是你访问的网址。它是您创建的领域的 OpenID 连接令牌生成端点。第二个是您正在使用基本身份验证来验证客户端,使用客户端 id 作为用户名,使用密码作为密码。最后,您有了用户凭证和授权类型,在本例中是 password。您在这里使用密码授权,因为这不是一个可以将用户重定向到授权服务器的 web 或移动应用。这甚至不是一个涉及人类互动的过程。因此,您需要应用知道用户的凭证,这没有任何问题,因为您处于一方应用场景中。
运行该命令后,您应该会收到类似清单 3-7 的内容。
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiA...",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5w...",
"token_type": "Bearer",
"not-before-policy": 0,
"session_state": "ff9a1588-863a-4745-9a28-87c8584b22cd",
"scope": "email profile"
}
Listing 3-7JSON Web Token Snippet
我剪切了这里表示的标记,以便更好地适应页面。您可以使用提供的命令获得完整的表示。
该响应包含您需要获得授权的令牌,还包含关于令牌的其他信息,比如它的有效期、类型和范围。您还将获得一个刷新令牌,该令牌允许客户端获得新的令牌并继续访问资源服务器,而无需新的身份验证过程。
让我们运行一个脚本来访问 API。我使用 jq,一个 JSON 命令行处理器,只提取访问令牌值。您可能需要在终端中安装 jq 工具。如果您没有访问它或其他类似工具的权限,您可以手动提取该值,并将其设置为ACCESS_TOKEN
变量,如以下命令所示:
$ export ACCESS_TOKEN=$( curl -s -X POST http://localhost:8180/auth/realms/contact-list/protocol/openid-connect/token \
--user contact-list-api:$SECRET \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=viewer&password=viewer&grant_type=password' | jq --raw-output '.access_token' )
$ curl -X GET http://localhost:8080/contact -H "Authorization: Bearer $ACCESS_TOKEN"
您可以尝试使用相同的令牌添加新联系人。使用以下命令:
$ curl -X POST 'http://localhost:8080/contact' -H 'Content-Type: application/json' \
-H "Authorization: Bearer $ACCESS_TOKEN" \
--data-raw '{"phone": "333333333","name": "Tester Tester", "email": "tester@email.com"}'
没用,对吧?该用户无权拨打此电话。通过编辑器用户为自己获取一个有效的令牌,如下所示:
$ export ACCESS_TOKEN=$( curl -s -X POST http://localhost:8180/auth/realms/contact-list/protocol/openid-connect/token \
--user contact-list-api:cb4b7d21-e8f4-4223-9923-5cb98f00209a \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=editor&password=editor&grant_type=password' | jq --raw-output '.access_token' )
您现在可以尝试插入新联系人。之后可以用GET
的方法列出联系人,看看新的有没有。
使用 Camel 消费 API
您已经创建了 REST APIs 并使用命令行测试了它们,但是您还需要学习如何在您的 Camel routes 中使用 API。在本例中,您将了解如何做到这一点,以及如何使用 OpenID Connect 保护 API。
首先在您最喜欢的 IDE 中加载contact-list-client
项目。先检查一下RouteBuilder
吧。参见清单 3-8 。
public class OIDClientRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
from("timer:OIDC-client-timer?period=3000")
.routeId("OIDC-client-route")
.bean("tokenHandlerBean")
.to("vertx-http:http://localhost:8080/contact")
.log("${body}");
}
}
Listing 3-8OIDClientRoute.java File
这是一个简单的路由,每三秒钟从 API 获取一次联系人列表。这里唯一的新东西是您正在使用 vertx-http 客户端调用 web 服务。
Camel 提供了各种各样的 HTTP 客户端可供选择。对于这种情况,我选择 vertx-http 有两个主要原因:vertx 组件具有高性能,并且它与本例中使用的 OIDC 客户端是依赖兼容的。
看看清单 3-9 中pom.xml
声明的依赖项。
...
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-timer</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-bean</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-vertx-http</artifactId>
</dependency>
...
Listing 3-9contacts-list-client pom.xml Snippet
您已经熟悉了camel-quarkus-timer
和camel-quarkus-bean
扩展。在整个示例中,您一直在使用它们。新的是quarkus-oidc-client
和camel-quarkus-vertx-http
。HTTP 客户端负责将 HTTP 请求发送到受保护的资源。OIDC 客户端负责获取和管理您的令牌。
让我们检查一下负责处理清单 3-10 中令牌的 bean。
@Singleton
@Unremovable
@Named("tokenHandlerBean")
public class TokenHandlerBean {
@Inject
OidcClient client;
volatile Tokens currentTokens;
@PostConstruct
public void init() {
currentTokens = client.getTokens().await().indefinitely();
}
public void insertToken(Message message){
Tokens tokens = currentTokens;
if (tokens.isAccessTokenExpired()) {
tokens = client.refreshTokens(tokens.getRefreshToken())
.await().indefinitely();
currentTokens = tokens;
}
message.setHeader("Authorization", "Bearer " + tokens.getAccessToken() );
}
}
Listing 3-10TokenHandlerBean.java File
需要注意的主要事情是,您在这个Singleton
中注入了一个OidcClient
,在 bean 创建之后,您将从授权服务器获得一个令牌。只有一种方法可以绑定到您的路线,即insertToken()
。此方法将消息作为参数,并检查当前令牌是否未过期。如果是,insertToken()
将使用刷新令牌生成一个新的有效访问令牌,然后将其值作为头传递给消息对象。因为 HTTP 客户端将消息头转换为 HTTP 头,所以您将它作为头进行传递。
正如您所想象的,OidcClient
配置是在application.properties
文件中完成的。看看清单 3-11 。
...
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/contact-list/
quarkus.oidc-client.client-id=contact-list-client
quarkus.oidc-client.credentials.secret=
quarkus.oidc-client.grant.type=password
quarkus.oidc-client.grant-options.password.username=viewer
quarkus.oidc-client.grant-options.password.password=viewer
Listing 3-11contact-list-client application.properties Snippet
这里所做的配置与您使用 cURL 测试应用时所做的非常相似。您继续使用 password grant 作为您的授权类型,使用相同的用户设置相同的认证服务器,但是您需要使用不同的客户端,因为这是不同的应用。正如您可能注意到的,这个秘密是空白的,所以您需要在 Keycloak 实例中为这个应用创建一个客户机。如果您忘记了如何做,请遵循以下步骤:
-
登录到钥匙锁控制台。
-
在左侧面板菜单中,单击客户端。
-
在“客户端列表”页面中,单击“创建”按钮。
-
将客户端 ID 设置为联系人列表客户端。将客户端协议设置为 openid-connect。不要为根 URL 设置任何内容。
-
单击保存按钮。
-
将访问类型设置为机密,并输入
http://localhost:8080
作为有效的重定向 URI。 -
在页面底部,单击“保存”按钮。
-
保存更改后,将出现凭据选项卡。点击它。
-
复制秘密值并将其粘贴到项目的
application.properties
文件中。
你的客户列表页面应该如图 3-14 所示。
图 3-14
客户列表页面
现在您已经准备好一起测试这两个应用了。打开两个终端窗口或标签。第一个,启动contact-list-api
项目。在第二个示例中,使用以下命令启动contact-list-client
项目:
contact-list-client $ mvn clean quarkus:dev -Ddebug=5006
因为您在调试模式下运行两个应用,所以需要更改第二个应用的调试端口,以避免端口冲突。
此时,您应该开始在contact-list-client
终端中看到日志条目,每三秒钟显示一次请求结果。如您所见,属于同一领域的客户机可以共享用户和角色定义,因为在这种情况下,重要的是它们共有的授权服务器,即http://localhost:8180/auth/realms/contact-list
。
你还可以做另一个测试。在这两个应用运行的情况下,停止 Keycloak 服务器。您将看到不会出现任何错误。发生这种情况是因为资源服务器正在自己验证令牌。令牌经过数字签名,客户端可以检查该签名以确保令牌有效且未被篡改。只要令牌没有过期,在我们的例子中,过期时间是 300 秒(5 分钟),客户端应用就能够访问资源服务器。
当然,用 OIDC 和奇克洛还可以做更多的事情。我们只是触及了表面。我的想法是向你介绍协议和奇克洛教你如何在你的 Camel 路线上处理它们。
摘要
在本章中,您了解了关于 web 应用和服务安全性的开放标准,以及使用 IAM 和开发的开源工具来实现它们。您学到了以下内容:
-
关于 OAuth 2.0 协议
-
关于 OpenID 连接协议
-
如何运行和配置 Keycloak
-
如何用 OIDC 保护 Camel 蜜蜂
-
如何使用 Camel 消费 API
随着您对 Camel 的概念和集成模式的了解越来越多,您将通过讨论持久性来继续您的旅程。
四、使用 Apache Camel 访问数据库
我们在实现 API 或集成时所做的大部分工作是移动数据。我们提供数据,消费数据,转换数据,复制数据,等等。这样,我们可以让前端系统或自动化系统完成它们的任务。为了在我们的路由中提供这种能力,在某些时候,我们将需要持久化数据,或者从专门从事持久化和数据搜索的系统中读取数据。通常它会涉及到数据库。
现在有大量的各种各样的数据库。我们有传统的 SQL 和表格数据库,我们有面向文档的数据库,我们有图形数据库,等等。它们中的每一个都适合特定的用例,对于每一个用例,都有不同的解决问题的可能性。这是我在本章中不打算越过的界限。Camel 提供了各种各样的组件来访问大多数类型的数据库。我无法在这一章中涵盖所有的内容。所以这里我们将关注关系数据库,因为它们有更标准化的访问方法,并且仍然是最常见的用例之一。
从集成的角度来看,我希望您知道如何使用 Camel 访问数据库,以及您需要在 Quarkus 上进行的配置,这样当您遇到数据库集成案例时,您将知道如何使用这些工具访问它。您将看到维护数据一致性的机制以及如何处理应用异常。
像往常一样,当我们讨论本章的主要主题时,我们将参与关于集成模式的讨论。你还会学到新的 Camel 概念。
关系数据库
我们从关系数据库开始,这是最常见的数据库使用案例。您将看到如何利用 Java 数据库连接规范(JDBC)和 Jakarta 持久性 API (JPA ),通过 Quarkus 和 Camel 与数据库进行交互。
这本书涵盖了很多不同的技术和用例,因为 Camel 是一个非常广泛的工具。我想让您了解 Camel 是如何工作的,它的概念是什么,同时也给你一些最常见用例的实际例子。连接到关系数据库出现在十大常见用例列表中。
因为我们讨论了许多不同的主题,所以方法总是去寻找最常见的需求和特性,这些需求和特性展示了特定组件是如何工作的,以及为什么应该使用它。我会给你一个基本的例子,展示如何解决它。从那以后,你可以继续你的研究,尝试实现更复杂的用例。这一章也不例外。
我希望您已经熟悉了关系数据库,尤其是 Java 如何处理它们,但是如果您是 Java 初学者,请不要担心。你仍然能够执行本书中的例子,并且理解正在做的事情。
不再拖延,让我们看看例子。
JPA 的持久性
您需要一个用例来演示如何在 Camel 中使用 JDBC 和 JPA,所以让我们使用一个您已经知道的用例:联系人列表 API。让我们用一个实际的数据库替换内存中的对象集。
首先在 IDE 中打开contact-list-api-jpa
项目。首先,您必须分析本例中使用的扩展。看清单 4-1 中的pom.xml
。
...
<dependencies>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-jackson</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
</dependencies>
...
Listing 4-1Project contact-list-api-jpa pom.xml Snippet
你已经知道的前两个依赖项。您使用了camel-quarkus-rest
来提供其余的 DSL 和camel-quarkus-jackson
,这样您就可以自动绑定传入和传出的 JSON。另外三个将帮助您进行关系数据库连接。
您可能已经注意到,quarkus-jdbc-h2
有一个不同于其他依赖项的组 id。这个库不是 Camel 组件,而是一个 Quarkus 扩展,为给定的数据库提供 JDBC 驱动程序,并允许配置 Camel 组件将使用的数据源,在本例中为camel-quarkus-jpa
。我们一会儿会谈到它。
让我们检查一下路线是如何变化的。看清单 4-2 。
public class ContactListRoute extends RouteBuilder {
public static final String MEDIA_TYPE_APP_JSON = "application/json";
@Override
public void configure() throws Exception {
rest("/contact")
.bindingMode(RestBindingMode.json)
.post()
.type(Contact.class)
.outType(Contact.class)
.consumes(MEDIA_TYPE_APP_JSON)
.produces(MEDIA_TYPE_APP_JSON)
.route()
.routeId("save-contact-route")
.log("saving contacts")
.to("jpa:" + Contact.class.getName())
.endRest()
.get()
.outType(List.class)
.produces(MEDIA_TYPE_APP_JSON)
.route()
.routeId("list-contact-route")
.log("listing contacts")
.to("jpa:" + Contact.class.getName()+"?query={{query}}")
.endRest();
}
}
Listing 4-2ContactListRoute.java File
这条路线与前一章中的路线基本相同,但是做了一些小的改动,使用关系数据库来“持久化”数据。我在引号中使用 persist 这个词,因为我不是将数据保存在文件中,而是将该文件保存在持久存储单元中。对于这个例子,我使用 H2 作为嵌入式内存数据库。这将允许您运行这个示例,而不必在您的机器上配置数据库实例,但是您不需要修改任何东西来使用它。因为这段代码使用 JDBC 和 JPA,所以对关系数据库的访问是标准化的。因此,如果它是 JDBC 兼容的,那么使用什么关系数据库并不重要。
由于 Quarkus 的工作方式,您需要使用 Quarkus 项目提供的扩展(依赖项)。单纯使用数据库社区或数据库提供商提供的 JDBC 驱动程序可能无法达到预期的效果。
从如何保存联系人开始,在post()
操作中,唯一改变的是to("jpa:" + Contact.class.getName())
调用。不是调用 bean 将 POJO 保存在内存集合中,而是调用由camel-quarkus-jpa
扩展提供的组件将 POJO 保存在数据库中。这个组件生成器只需要实体 bean 类名,以确保正确的主体类型。您已经发送了正确的类型,因为您正在使用 JSON 绑定将传入的 JSON 转换成您需要的实体 bean,即Contact
类。
看看现在Contact
类是怎么定义的。参见清单 4-3 。
@Entity
public class Contact {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer id;
@Column(unique = true)
public String name;
public String email;
public String phone;
public Contact() { }
}
Listing 4-3Contact.java File
你可以看到这个类的一些简化。不再有重载的构造函数,也没有hash()
或equals()
的实现。这个类现在表示关系数据库中的一个表,因此做了一些改变来反映关系表的特征。例如,一个表需要一个主键,所以我添加了一个顺序数字属性,当你在那个表中插入一个新行并把它命名为“id
”时,这个属性会自动生成。“id
”属性用@Id
和@GeneratedValue
进行了注释,表示这是主键,因此每个没有 id 值的新条目都将获得一个由数据库中的 identity 列生成的 id 值。
现在查看清单 4-4 中的application.properties
文件,并检查您是如何定义对数据库的访问的。
# datasource configuration
quarkus.datasource.db-kind = h2
quarkus.datasource.username = h2
quarkus.datasource.password = h2
quarkus.datasource.jdbc.url = jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
# drop and create the database at startup
quarkus.hibernate-orm.database.generation=drop-and-create
query=select c from Contact c
Listing 4-4contact-list-api-jpa application.properties
Quarkus 中的大多数应用配置都是通过在application.properties
文件中设置值来完成的,对于数据源也是如此。您只需要设置参数,Quarkus 就会为您创建连接工厂。我们来分析一下这些参数。
您首先用您将要使用的数据库设置quarkus.datasource.db-kind
,在您的例子中是H2
。
目前,除了H2
,Quarkus 还提供以下选项:
-
Apache 德比
-
IBM DB2
-
马里亚 DB
-
搜寻配置不当的
-
关系型数据库
-
甲骨文数据库
-
一种数据库系统
用户名和密码是通用值。因为您使用的是嵌入式模式,所以您是在应用启动期间使用提供的信息创建数据库的,所以可以使用任何值。quarkus.datasource.jdbc.url
有更多关于应该创建什么的信息。通过设置mem:db
,你对H2
说你想要创建一个名为db
的内存数据库。只要 JVM 存在,您就希望该数据库在 JVM 中可用,这就是为什么DB_CLOSE_DELAY
是负数的原因。剩下的配置与模式创建和用于搜索的 JPQL 查询相关。JPA 组件利用 Hibernate 作为 JPA 实现,您可以根据需要使用 Quarkus Hibernate 配置属性来设置 Hibernate。在这个例子中,您告诉 Hibernate,您希望在每次启动应用时重新创建数据库模式。这很有意义,因为每次应用启动时都会重新创建数据库本身。
搜索逻辑或 GET 操作也没有太大变化,但是现在您调用 JPA 组件来执行 JPQL 查询。通话中有新东西to("jpa:"+Contact.class.getName()+"?query={{query}}")
。大括号之间的值取自属性文件。通过使用双括号,您可以访问属性文件中的值,并更改在路由中声明元素的方式。在这个例子中,您只设置了一个参数,但是您可以做更多的事情。
在未来的主题中,我们将更深入地探讨如何使用属性来编写 Camel 路线。现在,我们继续讨论对关系数据库的访问。
使用查询“select c from Contact c
”可以返回 Contact 表中的每个条目,因为您使用的是 ORM,所以结果将是Contact.class
的List.class
。对于 HTTP 响应,该返回将自动转换为 JSON。
让我们试试这个应用。在您的终端上运行以下命令:
contact-list-api-jpa $ mvn quarkus:dev
您可以从搜索联系人条目开始。您知道那里什么也没有,但是您想检查应用的行为。你可以提出以下要求:
$ curl -v -w '\n' http://localhost:8080/contact
使用开关-v,这样您就知道 HTTP 响应代码是什么。您得到的响应应该类似于图 4-1 。
图 4-1
获取操作响应
如您所见,您从 API 获得了成功的响应。因为您还没有任何条目,所以您得到了一个空列表作为响应。不过,这可能是一种不受欢迎的行为。当您没有条目时,您可能希望用一个204
HTTP 代码来响应,这意味着“no content found.
”。在这种情况下,您需要在 JPA 组件响应之后添加验证逻辑,因为它总是会返回一个列表。
现在让我们使用 API 向数据库添加一个条目。在您的终端中运行以下命令:
$ curl -w '\n' -X POST 'http://localhost:8080/contact' \
-H 'Content-Type: application/json' \
--data-raw '{ "phone": "333333333", "name": "Tester Tester", "email": "tester@email.com"}'
您应该会收到类似于清单 4-5 的响应。
{
"id": 1,
"name": "Tester Tester",
"email": "tester@email.com",
"phone": "333333333"
}
Listing 4-5POST Operation Response
您没有发送 id,但是由于您的条目被保存,因此为其生成了一个新的 id。在这种情况下,JPA 组件的响应是数据库中的持久化对象。
在搜索它们之前,让我们再添加两个条目。在终端中运行以下命令:
$ curl -X POST 'http://localhost:8080/contact' \
-H 'Content-Type: application/json' \
--data-raw '{ "phone": "1111111", "name": "Jane Doe", "email": "jane.d@email.com"}'
$ curl -X POST 'http://localhost:8080/contact' \
-H 'Content-Type: application/json' \
--data-raw '{ "phone": "2222222", "name": "John Doe", "email": "john.d@email.com"}'
现在,您可以使用以下命令搜索联系人:
$ curl http://localhost:8080/contact
如果您只添加了这三个联系人,您的响应应该类似于清单 4-6 。
[{
"id": 1,
"name": "Tester Tester",
"email": "tester@email.com",
"phone": "333333333"
}, {
"id": 2,
"name": "Jane Doe",
"email": "jane.d@email.com",
"phone": "1111111"
}, {
"id": 3,
"name": "John Doe",
"email": "john.d@email.com",
"phone": "2222222"
}]
Listing 4-6GET Operation Response
使用 JPA 的参数化查询
在使用 JPA 组件的第一个示例中,您执行了一个简单的 find all 查询,但是通常您需要对查询进行参数化,以便只检索数据库中的特定条目。现在,您将学习如何使用 JPA 组件进行不同的搜索,以及如何将参数值动态传递给查询。
为了更多地了解 JPA 组件是如何工作的,您需要对contact-list-api-jpa
项目做一些修改。让我们用一个新项目来代替在它上面应用更改:这个项目就是contact-list-api-v2
。
在 IDE 中打开它。先来分析一下RouteBuilder
;参见清单 4-7 。
public class ContactListRoute extends RouteBuilder {
public static final String MEDIA_TYPE_APP_JSON = "application/json";
@Override
public void configure() throws Exception {
declareInterface();
declareGetListContactRoute();
declareSearchContactByIdRoute();
declareSaveContactRoute();
}
private void declareInterface(){
rest("/contact")
.bindingMode(RestBindingMode.json)
.post()
.type(Contact.class)
.outType(Contact.class)
.consumes(MEDIA_TYPE_APP_JSON)
.produces(MEDIA_TYPE_APP_JSON)
.to("direct:create-contact")
.get()
.outType(List.class)
.produces(MEDIA_TYPE_APP_JSON)
.to("direct:list-contacts")
.get("/{id}")
.outType(Contact.class)
.produces(MEDIA_TYPE_APP_JSON)
.to("direct:search-by-id");
}
...
Listing 4-7ContactListRoute.java Snippet
我决定改变我宣布路线的方式,因为现在路线开始变得有点复杂。我没有声明嵌套在 REST DSL 中的路由逻辑,而是决定将每个路由分开,并使用direct
调用它们。这样我可以提高代码的可读性,并且通过小的部分来解释事情会更容易。
declareInterface()
方法负责声明 REST API 接口。注意新的 GET 操作,但是这个操作使用了 path 参数。这个想法是,对于这种情况,您将总是检索单个结果,这就是为什么您将outType()
改为Contact.class
,而不是List.class
。
先从id
的搜索开始。看看清单 4-8 中的路由是如何实现的。
private void declareSearchContactByIdRoute(){
from("direct:search-by-id")
.routeId("search-by-id-route")
.log("Searching Contact by id = ${header.id}")
.setBody(header("id").convertTo(Integer.class))
.to("jpa:" + Contact.class.getName()+ "?findEntity=true");
}
Listing 4-8declareSearchByContactId Method
这里,您在 JPA 组件声明中使用了一个新的查询参数findEntity
。当findEntity
设置为真时,组件将试图寻找在组件 URI 路径参数中声明的类的单个实例,在本例中为Contact.class.getName()
。该组件将使用消息体作为选择操作的键。这就是你需要header()
fluent builder 的原因。由于Contact
类的键是一个Integer
对象,fluent 构建器从消息头中提取作为 URL 参数传递的值,将其解析为一个Integer
对象,并将其设置在消息体中。
让我们试试这部分代码。你不需要发送几个帖子请求就可以搜索到一些东西。看看资源目录中的import.sql
文件(清单 4-9 )。
INSERT INTO CONTACT(NAME,EMAIL,PHONE,COMPANY) VALUES
('John Doe','john.d@email.com','1111111','MyCompany');
INSERT INTO CONTACT(NAME,EMAIL,PHONE,COMPANY) VALUES
('Jane Doe','jane.d@email.com','2222222','MyCompany');
INSERT INTO CONTACT(NAME,EMAIL,PHONE,COMPANY) VALUES
('Tester Test','test@email.com','00000000','Another Company');
Listing 4-9import.sql File
这个文件用于在 Hibernate 创建模式后执行 DML 命令。这样,在开始测试之前,您可以运行几个 insert 命令来填充数据库。如果您想添加更多的条目,您仍然可以使用 POST 操作,但是现在您不需要这样做来测试搜索。
启动应用,并在终端中运行以下命令:
contact-list-api-v2 $ mvn quarkus:dev
您可以像这样通过id
开始搜索:
$ curl -w '\n' http://localhost:8080/contact/2
结果应该如图 4-2 所示。
图 4-2
按 Id 响应搜索
现在,让我们看看列表搜索路线是什么样子的;参见清单 4-10 。
private void declareGetListContactRoute(){
from("direct:list-contacts")
.routeId("list-contact-route")
.choice()
.when(simple("${header.company} != null"))
.log("Listing contacts by company = ${header.company}")
.to("jpa:" + Contact.class.getName()+ "?query={{query.company}}¶meters=#mapParameters")
.otherwise()
.to("jpa:"+Contact.class.getName()+"?query={{query.all}}");
}
Listing 4-10declareGetListContactRoute Method
现在,list contacts route 将能够进行两种不同的搜索,检索所有联系人,或者查找给定公司的联系人。您可能已经注意到,在 insert 语句中,一个新字段被添加到了实体Contact
中,即 company。此字段表示联系人列表中联系人的一些共同之处,有助于举例说明使用特定动态参数进行搜索的工作方式。
您必须使用带有选择的条件流,因为两个搜索具有相同的路径和方法。唯一的区别是,当您想要基于公司进行搜索时,您必须在 HTTP 请求中将公司名称作为查询参数进行传递。所以你检查header.company
是否为空,如果不是,你基于公司名称进行搜索。否则,您将检索所有条目。如果您查看query.company
属性值,您会发现现在您在 JPQL 查询中使用了“where
”子句。参见清单 4-11 。
...
query.all=select c from Contact c
query.company=select c from Contact c where c.company = :company
Listing 4-11application.properties File Snippet
该查询使用一个命名参数:company
作为where
子句的条件。如果您返回到路由并查看 JPA 组件声明,除了查询之外,您还将一个 bean 引用作为参数用“parameters=#mapParameters
”传递。此查询参数需要一个对Map<String,Object>
对象的引用。看看清单 4-12 所示的类中的最后一个方法。
@Produces()
@Named("mapParameters")
public Map createMapParameters(){
Map<String, Object> parameters = new HashMap<>();
parameters.put("company", "${header.company}" );
return parameters; }
Listing 4-12createMapParameters Method
您正在使用 CDI 注册一个包含查询所需参数的命名 bean。您使用 JPQL 命名参数作为映射的键,并且可以将任何对象设置为值。在这种情况下,您使用一个Simple
表达式来动态地从每个交换的消息头中检索值。由于头返回值将是一个String
对象,所以不需要担心对象解析。
我们来试试这些操作。在应用运行时,执行以下命令以返回所有条目:
$ curl http://localhost:8080/contact
要根据公司名称进行搜索,您可以使用以下命令:
$ curl http://localhost:8080/contact?company=Another%20Company
在本例中,将只返回一个结果,如图 4-3 所示,因为您只有一个公司名称等于“Another Company.
的条目
图 4-3
按公司名称搜索响应
您还可以使用头以更动态的方式传递参数。清单 4-13 显示路线。
private void declareGetListContactRoute(){
from("direct:list-contacts")
.routeId("list-contact-route")
.choice()
.when(simple("${header.company} != null"))
.log("Listing contacts by company = ${header.company}")
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
Map<String, Object> parameters = new HashMap<>();
parameters.put("company", "${header.company}" );
exchange.getMessage().setHeader(
JpaConstants.JPA_PARAMETERS_HEADER, parameters);
}
})
.to("jpa:"+Contact.class.getName()+"?query={{query.company}}")
.otherwise()
.to("jpa:"+Contact.class.getName()+"?query={{query.all}}");
}
Listing 4-13Alternative Way to Pass Parameters
正如您所看到的,在这个例子中,您不需要在组件配置中将 bean 引用作为查询参数传递。使用JPA_PARAMETERS_HEADER
头动态发送参数。
特别是对于这个例子,第一种方法更好,因为您没有真正改变参数值,因为您正在使用Simple
。在这里,当您需要使用仍然需要动态传递的不同对象类型时,您有了一个参考。
处理
在处理数据库时,一个非常重要的问题是如何使数据保持一致的状态。当然,数据库已经实现了许多保证数据一致性的机制,但是数据库不能控制访问它的应用是否正在执行正确的操作来改变数据库状态。我所说的正确操作是指改变数据库状态,并使其与使用它的系统或应用保持一致。
当您开发路由时,大多数情况下您将连接至少两个不同的端点。在前面的例子中,您对数据库使用了 REST 接口,并使用 Camel 实现了数据库中持久化的 REST 服务。尽管这是一个完全功能性的实现,但这不是一个集成。您只是使用 Camel 来实现一项服务。
让我们将持久性需求放在一个集成场景中。看图 4-4 。
图 4-4
集成期间保持数据
假设你在一家公司工作,该公司的应用需要访问合作伙伴提供的特定系统。不是直接访问系统,而是由架构团队决定,应用应该通过一个集成层来使用该服务,除了其他事情之外,该层应该通过将请求保存在数据库中来审核发送到合作伙伴系统的内容。
在一个粗略且过于简单的表示中,路线可能看起来像清单 4-14 。
from("{{rest.interface.definition}}")
.to("{{database.component.definition}}")
.to("{{partner.system.endpoint}}");
Listing 4-14Model Route
在一个理想的世界里,这每次都行得通,但是这个世界从来都不是我们所期望的那样,不是吗?如果合作伙伴的系统在交易过程中出现故障,会发生什么情况?数据库中可能有他们从未收到的请求条目。这将在您的数据库数据中产生不一致,它不能反映集成中真正发生的事情。
为了解决这一问题和许多其他同步情况,您可以在路由中实现事务。
让我们看看如何用 Camel 解决这个问题。在您的 IDE 中打开contact-list-api-transacted
项目。让我们从分析清单 4-15 中的RouteBuilder
开始。
public class ContactListRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
rest("/contact")
.bindingMode(RestBindingMode.json)
.post()
.type(Contact.class)
.outType(Contact.class)
.consumes(APP_JSON)
.produces(APP_JSON)
.route()
.routeId("save-contact-route")
.transacted()
.log("saving contacts")
.to("jpa:" + Contact.class.getName())
.log("Pausing the Transaction")
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
Thread.sleep(7000);
}
})
.log("Transaction Finished")
.endRest()
.get()
.outType(List.class)
.produces(APP_JSON)
.route()
.routeId("list-contact-route")
.to("jpa:"+Contact.class.getName()+ "?query={{query.all}}");
}
}
Listing 4-15ContactListRoute.java Snippet
我对这个例子做了一些修改。我简化了搜索,所以现在只有搜索。我还删除了import.sql
,因为我们在测试中不需要数据库中的任何条目,但是主要的变化是在保存联系路径上。transacted()
调用表明我们希望这些路由交换执行被处理,这意味着路由中的操作只有在交换完成时才会被提交。
事务行为取决于所使用的组件。并不是所有的都支持事务。它们必须能够在失败的情况下执行后期提交和回滚操作,例如,这对于 HTTP 客户端来说是不可能的。在这种情况下,我们使用与事务兼容的 JPA 组件,它将自动加入事务。
为了实现这一点,需要另一种配置。请看清单 4-16 中的pom.xml
。
...
<dependencies>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-jackson</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-jta</artifactId>
</dependency>
</dependencies>
Listing 4-16pom.xml File Dependencies Snippet
如果你查看camel-quarkus-jpa
的依赖项,你会看到quarkus-hibernate-orm
。如前所述,Hibernate 是 Quarkus 选择的 JPA 实现。该扩展使用quarkus-narayana
实现 JTA (Jakarta Transaction API)。因此,要使这个事务管理器对 Camel 可用,您需要camel-quarkus-jta
扩展。它使 Camel 能够使用 Narayana 事务管理器作为事务策略。
让我们试试这个代码。打开三个终端窗口或标签。其中一个将用于运行应用。第二个用于发送 POST 请求,将实体保存到数据库中,第三个用于将数据持久化到数据库中。
在第一个窗口或选项卡中,运行以下命令:
contact-list-api-transacted $ mvn quarkus:dev
一旦应用启动,您可以像这样发送帖子:
$ curl -X POST 'http://localhost:8080/contact' -H 'Content-Type: application/json' --data-raw '{ "phone": "2222222", "name": "John Doe", "email": "john.d@email.com"}'
JPA 调用完成后,线程会休眠 7 秒钟。这将证明,即使您已经“处理”了更改,数据也没有在数据库中持久化,因为事务没有被提交。
在第三个窗口中,您可以列出如下联系人:
$ curl http://localhost:8080/contact
您将看到,在这 7 秒钟内,window cURL 将只返回一个空列表,但是一旦 7 秒钟过去,您将在应用日志中看到“Transaction Finished
”,GET 请求将返回您在 POST 命令中发送的实体。
配置事务时要记住的一件重要事情是如何定义事务边界。我说的界限是指你的事务应该在哪里开始,在哪里结束。这可以通过为事务设置适当的传播行为来实现。
有六种传播策略可供选择。他们是
-
PROPAGATION_REQUIRED
:默认选项。如果没有启动事务,则启动事务,或者保持现有的事务。 -
PROPAGATION_REQUIRES_NEW
:如果没有启动,则启动一个事务。如果有一个已启动,它会挂起它并启动一个新的。 -
PROPAGATION_MANDATORY
:如果没有启动事务,它会启动一个异常。 -
PROPAGATION_SUPPORTS
:有事务就加入;否则,它不处理任何事务。 -
PROPAGATION_NOT_SUPPORTED
:如果有事务,它将被该边界挂起,该流程部分将在没有事务的情况下工作。 -
PROPAGATION_NEVER
:如果有交易,则发起异常。它不需要交易就可以工作。在示例中,我们将使用默认选项,因为这是最常见的情况。
第一个示例旨在展示提交过程是如何工作的,但是事务的另一个非常重要的功能是在失败的情况下回滚操作。我们来测试一下。在contact-list-api-transacted
项目的ContactListRoute.java
中执行清单 4-17 中的更改,这里不使用线程休眠,而是抛出一个异常。
...
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
throw new Exception("Testing Rollback Exception.")
}
})
...
Listing 4-17ContactListRoute.java Changes
如果已经停止,请再次运行该应用。尝试发出另一个发布请求。您将收到一个错误,一个包含栈跟踪的 HTML 响应页面。如果您尝试列出所有联系人,您将收到一个空列表作为响应。
该事务防止您将数据库置于不一致的状态。手术还没结束,所以你不可能在数据库里登记。
现在移除transacted()
调用。您可以这样注释该行://.transacted()
做同样的测试。您仍然会收到一条错误消息作为响应,但是现在您的数据库中有了一个不应该存在的条目。
为了正确处理事务,您还需要指定处理故障的方法。到目前为止,我一直以乐观的方式编码,不考虑某些操作在执行过程中可能会失败。很明显,这里的目的是让代码简单易懂,但我也想特别谈谈 Camel 提供异常处理的不同方式。接下来我们将深入探讨这个问题。
处理异常
预料到可能发生的异常并为它们做好准备是一个非常重要的编程实践,当我们谈论集成时更是如此。我们不能真正相信另一边的东西,即使是我们自己编写的应用。网络并不总是可靠的,应用可能会崩溃,硬件可能会出现故障,因此我们需要让应用准备好处理异常,以避免丢失数据或使系统处于不一致的状态。接下来,您将看到用 Camel.s 处理异常的不同方法
尝试-捕捉-最终
类似于我们使用 Java 处理异常时所做的,Camel 也有可以在路由声明中使用的try/catch/finally
子句。让我们看一些如何做的例子。
首先在 IDE 中打开camel-try-catch
项目。让我们分析清单 4-18 中显示的TryCatchRoute.java
文件。
public void configure() throws Exception {
rest("/tryCatch")
.bindingMode(RestBindingMode.json)
.post()
.type(Fruit.class)
.outType(String.class)
.consumes(APP_JSON)
.produces(TEXT_PLAIN)
.route()
.routeId("taste-fruit-route")
.doTry()
.choice()
.when(simple("${body.name} == 'apple' "))
.throwException(new Exception("I don't like this fruit"))
.otherwise()
.setBody(constant("I like this fruit!"))
.endDoTry()
.doCatch(Exception.class)
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(500))
.setBody(exceptionMessage())
.doFinally()
.setHeader(Exchange.CONTENT_TYPE,constant(TEXT_PLAIN))
.log("Exchange processing completed!")
.endRest();
}
Listing 4-18TryCatchRoute.java Snippet
这条路线有一个简单的逻辑来演示try/catch/finally
如何与 Camel 一起工作。这个 REST API 有一个单独的操作,它接收一个作为 JSON 的Fruit.class
对象,并验证水果名称是否为“apple
”。如果是“apple
,抛出异常说“I don't like this fruit
”;否则,它会用一个简单的短语“I like this fruit!
”来响应。所以我把这条路线叫做taste-fruit-route
。顺便说一下,我确实喜欢苹果:这只是一个例子。
这里的一个新东西是你如何使用Simple
语言来执行一个方法。通过设置“${
body.name
}
”你知道体内的对象有一个方法叫name
,返回的是一个你可以和字符串apple
比较的对象。让我们看看清单 4-19 中的Fruit.java
文件。
@Entity
public class Fruit {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(unique = true)
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
}
Listing 4-19Fruit.java File
在前面的例子中,我为类属性使用了公共访问修饰符,以使代码更小,但是在这个例子中,我需要一个方法来使Simple
表达式工作,这就是我选择使用 getters 和 setters 的原因。
为了使用Simple
进行方法调用,您需要将camel-quarkus-bean
扩展添加到项目中。
一旦满足条件,就使用 DSL throwException()
抛出一个异常。这个异常将被您的doCatch()
子句捕获,该子句将通过设置正确的响应头和适当的消息给客户端来处理这个异常。如果抛出异常,doFinally()
子句将被独立执行,因此您可以使用它为响应设置正确的Content-Type
,并记录交换处理已完成。
exceptionMessage()
调用是一个值构建器,它封装了一个像这样的Simple
表达式:"${exception.message}"
。
我们来测试一下这条路线。打开终端并运行应用:
camel-try-catch$ mvn quarkus:dev
您可以发送这样的请求:
$ curl -w '\n' -X POST http://localhost:8080/tryCatch \
-H 'Content-Type: application/json' -d '{"name":"grape"}'
由于这不是苹果,所以你不会落入例外,如图 4-5 。
图 4-5
TryCatch 路由响应
尝试一个将导致引发异常的请求:
$ curl -w '\n' -X POST http://localhost:8080/tryCatch \
-H 'Content-Type: application/json' -d '{"name":"apple"}'
你会得到如图 4-6 的答案。
图 4-6
会引发异常。
如您所见,try/catch/finally
的工作方式与 Java 语言非常相似。您可以有多个doCatch()
子句,您可以将一个try/catch/finally
嵌套在一个doTry()
中,等等。
让我们看看带有try/catch
的事务处理路由是什么样子的。在与之前相同的项目中,打开TryCatchTransactedRoute.java
文件,如清单 4-20 所示。
public class TryCatchTransactedRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
rest("/tryCatchTransacted")
.bindingMode(RestBindingMode.json)
.post()
.type(Fruit.class)
.outType(String.class)
.consumes(APP_JSON)
.produces(TEXT_PLAIN)
.route()
.transacted()
.routeId("save-fruit-route")
.doTry()
.to("jpa:" + Fruit.class.getName())
.choice()
.when(simple("${body.name} == 'apple' "))
.throwException(new Exception("I don't like this fruit"))
.otherwise()
.setHeader(Exchange.CONTENT_TYPE,constant(TEXT_PLAIN))
.setBody(constant("I like this fruit!"))
.endDoTry()
.doCatch(Exception.class)
.setHeader(Exchange.HTTP_RESPONSE_CODE,constant(500))
.setHeader(Exchange.CONTENT_TYPE,constant(TEXT_PLAIN))
.setBody(exceptionMessage())
.markRollbackOnly()
.endRest()
.get()
.outType(List.class)
.produces(APP_JSON)
.route()
.routeId("list-fruits")
.to("jpa:"+Fruit.class.getName()+"?query={{query.all}}");
}
}
Listing 4-20TryCatchTransactedRoute.java
这条路线与前一条不同。它不仅会分析水果名称,还会在数据库中保存一个水果。水果名称必须有唯一的值,正如您在Fruit
类声明中看到的,如果您试图用重复的名称保存水果,将会抛出一个违反约束的异常。这个路径还有一个 GET 操作,允许您从数据库中检索一个水果列表。
你可能已经注意到的一件事是没有doFinally()
子句。这是因为您必须处理事务回滚的方式。如果你分析doCatch()
块,最后一步是markRollbackOnly()
。这意味着您将在此时回滚事务,但不会抛出异常,这意味着异常得到了正确处理。在这之后,什么都不会执行,这就是为什么要在调用之前通过设置消息头和消息体来准备响应。这也是你在这里没有使用doFinally()
的原因。只有在没有异常发生的情况下,它才会被执行。
测试这条路线。在应用运行的情况下,运行以下命令两次:
$ curl -X POST http://localhost:8080/tryCatchTransacted \
-H 'Content-Type: application/json' -d '{"name":"pineapple"}' -w '\n' -v
在您第二次请求时,您将收到类似于图 4-7 的内容。
图 4-7
TryCatchTransacted 路由响应
内容类型和 HTTP 状态代码正是您在doCatch()
块中设置的,响应主体是异常消息。
现在试着发送“apple
”作为请求:
$ curl -X POST http://localhost:8080/tryCatchTransacted \
-H 'Content-Type: application/json' -d '{"name":"apple"}'
然后检查数据库中是否有“apple
”:
$ curl http://localhost:8080/tryCatchTransacted
使用 transactioned plustry/catch
允许您捕获异常,向客户端提供经过处理的响应,并回滚可能导致数据库不一致的操作,但这不是唯一的方法。让我们学习一个新的。
错误处理程序
使用try/catch/finally
是处理局部异常的一种简单方法,在这种情况下,您可以捕获路径内给定块的异常。我决定从它们开始,因为它们类似于我们在 Java 语言中所拥有的,但是除了路由中的映射块之外,还有其他方法来处理异常。让我们看看他们。
Camel 附带了处理异常的预定义策略,称为错误处理程序。有四种错误处理程序,它们分为两类,事务处理和非事务处理。
未交易的有
-
DefaultErrorHandler
:默认错误处理程序。它会将异常传播回调用者。 -
DeadLetterChannel
:它允许消息在发送到死信端点之前重新传递。 -
当你想使用任何一个提供的错误处理程序时,使用它。
对于事务处理的路由,我们有TransactionErrorHandler
,这是这种路由的默认错误处理程序。
尽管我以前没有提到过错误处理程序,但我们一直在使用它们。如果以您正在测试的事务处理路由为例,如果您在测试期间查看日志,您将会看到如下日志条目:
[org.apa.cam.jta.TransactionErrorHandler] (vert.x-worker-thread-3) Transaction rollback (0x2627d493) redelivered(false) for (MessageId: 0C577FF9615449E-0000000000000001 on ExchangeId: 0C577FF9615449E-0000000000000001) due exchange was marked for rollbackOnly
这是一个关于TransactionErrorHandler
如何工作的例子。您不必配置它。因为您的路由是事务性的,如果没有指定一个TransactionErrorHandler
,将会创建一个新的路由并分配给该路由。
不再拖延,让我们看看如何配置错误处理程序。
首先在 IDE 中打开camel-dead-letter
项目。这个项目有三个不同的RouteBuilders
,每个都有不同的目的。让我们从负责声明您将与之交互的接口的RouteBuilder
开始。打开清单 4-21 所示的RestRoute.java
文件。
public class RestRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
rest("/deadLetter")
.consumes(TEXT_PLAIN)
.produces(TEXT_PLAIN)
.post()
.route()
.routeId("rest-route")
.log("Redirecting message")
.wireTap("seda:process-route")
.setBody(constant("Thanks!"))
.endRest();
}
}
Listing 4-21RestRoute.java File
在本例中,您公开了一个 REST 接口,该接口接受带有text/plain
主体的 POST 请求。然后,接收到的消息被窃听到使用 SEDA 组件的另一个路由,之后,在向客户端返回响应之前,消息体被更改。
首先,我们先明确一下wiretap()
是什么。Wire Tap 是一种集成模式,它允许使用来自原始交换的数据复制交换或生成新的交换,并将其异步发送到另一个端点。这里的消息模式是inOnly
,因为主路由不会等待端点响应。
另一个对路线开发很有价值的新事物是 SEDA。该组件基于分阶段的事件驱动架构,将创建一个内存队列,以便您可以在不同的线程中处理消息。通过这种方式,您可以异步发送消息副本,由另一个线程进行处理。
看看清单 4-22 中显示的声明 SEDA 消费者的RouteBuilder
。
public class ProcessRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
errorHandler(deadLetterChannel("seda:dead-letter")
.maximumRedeliveries(1)
.useOriginalMessage()
.onExceptionOccurred(new Processor() {
@Override
public void process(Exchange exchange) throws Exception
{
log.error("Exception Message : " +
exchange.getException().getMessage()) ;
log.error("Current body:\"" +
exchange.getMessage().getBody() +"\"");
}
}));
from("seda:process-route")
.routeId("process-route")
.bean("exceptionBean")
.log("Message Processed with body : ${body}");
}
}
Listing 4-22ProcessRoute.java
这条路线背后的想法是向您展示使用错误处理程序的不同可能性。这里您声明了一个deadLetterChannel
错误处理程序,并设置了一些策略来定义这个错误处理程序应该如何工作。
有两种方法可以声明错误处理程序,即使用路径生成器范围或使用路径范围。在本例中,您使用的是路由生成器作用域,因此如果您在此路由生成器中添加另一个路由器,此新路由也将使用已定义的配置(除非它是事务处理路由)。由于这个错误处理程序的例子影响了一个单一的路由,您可以像清单 4-23 那样声明它。
@Override
public void configure() throws Exception {
from("seda:process-route")
.routeId("process-route")
.errorHandler(deadLetterChannel("seda:dead-letter")
.maximumRedeliveries(1)
.useOriginalMessage()
.onExceptionOccurred(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
log.error("Exception Message : " +
exchange.getException().getMessage());
log.error("Current body: \"" +
exchange.getMessage().getBody()+"\"");
}
}))
.bean("exceptionBean")
.log("Message Processed with body : ${body}");
}
}
Listing 4-23ProcessRoute with Route Scope
观察错误处理程序配置。第一个参数是设置在所有尝试之后消息应该发送到哪里。在这里,您将发送原始消息,即发送到 SEDA 队列的消息,在对“seda:dead-letter
”重试一次后,当重试期间出现异常时,将调用一个处理器来记录有关交换的一些信息。
让我们看看死信路线是什么样子的。打开清单 4-24 所示的DeadLetterRoute.java
文件。
public class DeadLetterRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
from("seda:dead-letter")
.routeId("dlq-route")
.log("Problem with request \"${body}\"");
}
}
Listing 4-24DeadLetterRoute.java File
这个路由唯一做的事情就是记录它收到的消息体。这将帮助我演示不同的配置如何影响路由逻辑。
关于流程路线,最后要评论的是被调用的 bean。该 bean 将根据处理尝试的次数生成异常。让我们检查一下。打开清单 4-25 所示的ExceptionBean.java
文件。
@Singleton
@Named("exceptionBean")
@Unremovable
public class ExceptionBean {
private static final Logger LOG = Logger.getLogger(ExceptionBean.class);
int counter;
public void analyze(Message message) throws Exception{
++counter;
LOG.info("Attempt Number " + counter);
message.setBody(UUID.randomUUID().toString());
if(counter < 3){
throw new Exception("Not Now!");
}
}
}
Listing 4-25ExceptionBean.java File
这个 singleton 有一个int
计数器来记录路由尝试的次数。它通过设置一个随机 UUID 来改变主体,如果尝试的次数少于三次,它将抛出一个异常,这将激活错误处理程序。
所以我们来试试吧。打开终端,运行以下命令启动应用:
camel-dead-letter $ mvn quarkus:dev
一旦应用启动并运行,您可以像这样向 REST API 发送一个请求:
$ curl -X POST http://localhost:8080/deadLetter -H 'Content-Type: text/plain' -d "testing dead letter"
查看应用日志。它将像清单 4-26 一样。
[rest-route] [INFO] Redirecting message
[co.ap.in.ExceptionBean] [INFO] Attempt Number 1
[co.ap.in.ro.ProcessRoute] [ERROR] Exception Message : Not Now!
[co.ap.in.ro.ProcessRoute] [ERROR] Current body: "7b3cc16c-d409-4166-b12d-b23299729999"
[co.ap.in.ExceptionBean] [INFO] Attempt Number 2
[co.ap.in.ro.ProcessRoute] [ERROR] Exception Message : Not Now!
[co.ap.in.ro.ProcessRoute] [ERROR] Current body: "2520a0db-a75c-4324-9f0c-722aa073c60d"
[dlq-route] [INFO] Problem with request "testing dead letter"
Listing 4-26Application Logs Snippet
正如您在日志中看到的,有两次尝试让 bean 调用工作。在每一次尝试中,在抛出异常之前都修改了消息体,但是当错误处理程序达到重试次数的限制时,“dlq-route
”就会收到原始消息并打印出来。
您可以进行第二次请求,现在不会抛出任何异常。您将会看到如下的日志条目:
[process-route] [INFO] Message Processed with body : cb65c846-993a-4835-9e90-f077961f90b2
这意味着process-route
能够进行到最后一步。
您还可以探索其他策略和参数,比如在每次重试之间添加一个延迟,使用修改后的消息并将其发送到死信地址,或者甚至根据表达式语言定义重试的截止时间。设置错误处理程序时有很多可能性。在考虑如何处理异常时,请记住这一点:
-
我故意放了一个异步
inOnly
例子。我对死信配置所做的不适合需要向客户端提供同步答案的消费者。在本例中,我们正在处理可以在以后处理的消息,拥有一个可以保存消息的死信通道有助于在将来重放它们,或者只是进行故障排除。 -
在这个例子中,我使用一个 bean 强制一个异常,但是它可以是任何可以抛出异常的组件。在这种情况下,您可以使用 redelivery 来重试对组件的调用。当我们访问一个可能暂时没有响应的外部端点时,这一点特别有趣。
让我们继续学习处理异常的新方法。
一个例外条款
您看到了如何使用try/catch/finally
处理异常来包围 route 块。在错误处理程序示例中,演示了如何以更通用的方式处理异常,在路线生成器级别或路线级别定义错误处理程序。使用这种新方法,您仍然可以用这种更广泛的方式来分配处理程序,并根据异常类型来专门化它们。让我们来学习一下onException()
子句。
在您的 IDE 中,打开camel-on-exception
项目。这个项目有三个路径构建器,演示了使用onException()
的不同方法。让我们从一条non-transacted
类型的路线开始,因此打开清单 4-27 中所示的OnExceptionRoute.java
文件。
public class OnExceptionRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
onException(Exception.class)
.handled(true)
.log(LoggingLevel.ERROR, "Exception Handled")
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(500))
.setBody(exceptionMessage());
onException(DontLikeException.class)
.handled(true)
.log(LoggingLevel.ERROR,"DontLikeException Handled")
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(500))
.setBody(constant("There is a problem with apples, try another fruit."));
rest("/onException")
.bindingMode(RestBindingMode.json)
.post()
.type(Fruit.class)
.outType(String.class)
.consumes(APP_JSON)
.produces(TEXT_PLAIN)
.route()
.routeId("taste-fruit-route")
.choice()
.when(simple("${body.name} == 'apple' "))
.throwException(new DontLikeException())
.otherwise()
.throwException(new Exception("Another Problem."))
.end()
.log("never get executed.")
.endRest();
}}
Listing 4-27OnExceptionRoute.java File
您又回到了水果的例子,但是这一次无论您选择哪个水果,都会抛出一个异常(但是如果您在请求中发送"apple"
,它会有不同的响应消息。).
这里的想法是向您展示您可以在相同的路径中处理不同种类的异常,也向您展示您可以有不同的onException()
声明范围。
这里我使用了 route builder 范围,因为只有一条路由,这使得路由可读性更好。
两个声明的异常是handle()
,这意味着这些异常不会传播回调用者,在本例中是 HTTP 客户端。通过说异常已被处理,如果路由是inOut
,执行将在抛出异常的地方停止,并且在将响应发送回客户端之前执行onException()
块。这就是为什么行log("never get executed.")
永远不会被执行。
试试这段代码。从终端启动应用:
camel-on-exception $ mvn quarkus:dev
一旦应用启动,发送如下请求:
$ curl -w '\n' -X POST http://localhost:8080/onException \
-H 'Content-Type: application/json' -d '{"name": "apple"}'
您将得到如图 4-8 所示的响应。
图 4-8
一个例外路由响应
当谈到事务路由时,使用onException()
并没有什么不同。你只需要记住你需要标记回滚的路径。请看清单 4-28 中的OnExceptionTransactedRoute.java
文件示例。
public class OnExceptionTransactedRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
rest("/onExceptionTransacted")
.bindingMode(RestBindingMode.json)
.post()
.type(Fruit.class)
.outType(String.class)
.consumes(APP_JSON)
.produces(TEXT_PLAIN)
.route()
.routeId("save-fruit-route")
.onException(Exception.class)
.handled(true)
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(500))
.setHeader(Exchange.CONTENT_TYPE, constant(TEXT_PLAIN))
.setBody(exceptionMessage())
.markRollbackOnly()
.end()
.transacted()
.to("jpa:" + Fruit.class.getName())
.choice()
.when(simple("${body.name} == 'apple' "))
.throwException(new Exception("I don't like this fruit"))
.otherwise()
.setHeader(Exchange.CONTENT_TYPE,constant(TEXT_PLAIN))
.setBody(constant("I like this fruit!"))
.endRest()
.get()
.outType(List.class)
.produces(APP_JSON)
.route()
.routeId("list-fruits")
.to("jpa:"+Fruit.class.getName()+"?query={{query.all}}" );
}
}
Listing 4-28OnExceptionTransactedRoute.java File
在本例中,您在一个路由范围中使用了onException()
,并且和前面的例子一样,将异常标记为handled(true)
。在onException()
块中,通过设置消息头和消息体来准备对客户端的响应。一旦完成所需的设置,就可以将交换设置为回滚。
这条路线就像try/catch
的例子一样。它将回滚任何带有“apple
”的条目,以及任何具有先前保存的名称的条目。它还有一个 GET 操作,因此您可以检查数据库中的值。
在应用仍在运行的情况下,发送一个将被 API 接受的请求:
$ curl -X POST http://localhost:8080/onExceptionTransacted \
-H 'Content-Type: application/json' -d '{"name": "grape"}'
重复相同的请求,然后尝试发送带有“apple
”的请求:
$ curl -X POST http://localhost:8080/onExceptionTransacted \
-H 'Content-Type: application/json' -d '{"name": "apple"}'
现在,您可以查看数据库中有多少水果:
$ curl http://localhost:8080/onExceptionTransacted
只有第一个非 apple 请求会被持久化,因此您将收到一个列表,其中只有一个项目,如下所示:
[{"id":1,"name":"grape"}]
还有一个例子可以探究如何用 Camel 处理异常。这一次,您将处理异常并继续原来的路由,因为异常没有发生。
在 IDE 中打开清单 4-29 中的OnExceptionContinuedRoute.java
文件。
public class OnExceptionContinuedRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
onException(Exception.class)
.continued(true)
.log(LoggingLevel.ERROR, "Exception Handled")
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(500))
.setBody(exceptionMessage());
rest("/onExceptionContinued")
.bindingMode(RestBindingMode.json)
.post()
.type(Fruit.class)
.outType(String.class)
.consumes(APP_JSON)
.produces(TEXT_PLAIN)
.route()
.routeId("continued-route")
.choice()
.when(simple("${body.name} == 'apple' "))
.throwException(new DontLikeException("Try another Fruit."))
.otherwise()
.setBody(constant("I like this fruit."))
.end()
.setHeader(Exchange.CONTENT_TYPE, constant(TEXT_PLAIN))
.log("Gets Executed.")
.endRest();
}
}
Listing 4-29OnExceptionContinuedRoute.java
这段代码是OnExceptionRoute
的简化版本,因为我只需要一个onException()
子句来展示continued()
是如何工作的。正如你所看到的,你没有使用handled()
,而是使用了continued()
,这意味着异常将被捕获,然后onException()
块将被执行,然后你将返回到抛出异常的原始路径。这就是为什么要在选择之后设置内容类型头。有没有得到异常并不重要;这最后一段代码将被执行。
随着应用的运行,尝试在请求中发送一个"apple"
:
curl -X POST http://localhost:8080/onExceptionContinued \
-H 'Content-Type: application/json' -d '{"name": "apple"}'
您将像前面的示例一样收到经过处理的响应,但是请查看应用日志。您会发现日志条目是路由的最后一部分,向您显示原始路由继续。日志条目将如下所示:
2021-05-31 08:56:55,224 INFO [continued-route] (vert.x-worker-thread-1) Gets Executed.
您探索了在 Camel 中执行错误处理的三种不同方式。当然,在每种机制中可以应用更多的配置,但是在这里,您探索了错误处理的主要方面和案例。一旦理解了它们中的每一个,您将能够为您的给定用例选择最好的一个。
摘要
在本章中,您关注了如何持久化和消费来自关系数据库的数据,以及在操作数据时保持数据一致性的技术。随着您的进步,您看到了使用 Camel 执行错误处理的不同方式。
在本章中,您学习了以下主题:
-
如何使用 quartus 配置数据源和 hibernate
-
如何使用
camel-quarkus-jpa
来保存和消费关系数据库中的数据 -
如何使用 quartus 和 camel 配置 jta 事务
-
事务处理和非事务处理路由的不同错误处理策略
在下一章中,我们将关注使用消息传递的异步通信以及这种方法带来的架构可能性。
五、使用 Apache Kafka 发送消息
在前面的章节中,我们主要关注跨应用通信的同步方法的使用,更具体地说是 REST。尽管 REST(及其生态系统)的特性非常丰富,可以支持许多不同的用例,但它不像某些异步通信模式那样具有可伸缩性和弹性。同步通信要求后端能够立即响应客户端的调用,但是一些服务由于其复杂性可能需要更多的时间来响应,或者甚至可能依赖于其他服务,这可能会产生超时链,其中链中的一个服务可能会成为瓶颈并使请求开始失败。
在有状态与无状态的讨论中,由于无状态架构的许多好处,我们理想地尝试无状态,我们知道不是每个应用都是无状态的。对于一些必须同步执行的流程和服务也是如此。这个想法是为了理解异步通信是如何工作的,我们拥有哪些模式,以及我们如何实现它们。我们可以将反应式编程或事件驱动编程作为异步处理如何有益于应用性能的例子。这意味着没有一个组件需要等待另一个组件来完成它的任务,所以它可以不等待,而是执行别的事情。
在这一章中,我们将深入研究使用消息传递系统的异步通信。选择的工具是另一个 Apache 基金会项目,Apache Kafka。您将学习何时以及如何使用消息传递来使用异步通信,同时学习如何使用 Kafka 和 Camel 来实现它。
面向消息的中间件
面向消息的中间件(MOMs)是专门接收和发送消息格式的数据的系统。他们充当中间人的角色,保证从生产者那里接收到消息,并且该消息将是可用的,并被正确地传递给消费者。让我们来讨论一下这种系统是如何工作的,我们能从中得到什么。
在谈论妈妈之前,我们首先需要定义消息传递。谈论消息传递可能听起来有些多余,因为这个概念在我们的生活中非常普遍。电子邮件、短信应用,甚至邮政服务都可以作为消息传递的一个很好的类比,即使用服务在实体之间传递消息。消息传递的另一个特征是,生产者或消息发送者不需要等待消费者或消息接收者来确认它收到了消息。消息生产和消息消费的中介由中间件层完成。正是中间件层向生产者确认消息的传递,一旦有了它,该层就有了保证消息到达目的地的机制。这使得生产者无需等待消费者就可以执行其他任务,而消费者可以按照自己的节奏使用消息。
除了异步通信的特性之外,由于其模式,mom 的使用还允许其他可能性。我们可以强调的一点是使用 mom 如何提高服务的可组合性。
可组合性是一种设计原则,它分析服务如何相互连接,以创建可以服务于新用例的组合。为了阐明这意味着什么,让我们把这个概念放到一个真实的生活例子中。想想社交媒体。这一切都是从人们联系的方式开始的。你想知道你的家人怎么样,你的朋友在做什么,甚至想认识新的人。现在将它与现在的情况进行比较。它现在是一个公众人物的平台,从政治家到流行艺术家,在这里你可以买卖任何东西,观看现场音乐表演或体育比赛等活动。它在服务和容量方面增长如此之快,以至于我们觉得我们生活的方方面面都在那里得到了处理。这是可能的,因为人们的数据处理方式。当然,这是一个非常有说服力的例子,因为它带来的不仅仅是可组合性的概念,而是为了举例说明它的含义。
妈妈们通常使用目的地的概念,也就是消息被发送的地址。这个目的地并不存在于生产者或消费者中,而是消息传递系统或消息代理中的一个寄存器,这使得它成为一种通过促进解耦来提高可组合性的方法。生产者不知道谁在消费来自目的地的消息,消费者也不知道谁在发送消息。这样,如果生产者能够产生预期的数据,我们可以向目的地添加更多的生产者,或者如果数据与更多的应用相关,我们可以添加更多的消费者。注意,这种通信更多的是关于被交换的数据,而不是组件的关系。一个好的领域设计对于实现更好的可组合性是必不可少的。
目的地通常分为两种类型:队列和主题。我们单独讨论一下。
队列是通信通道的目的地,接收方通常是单个应用。它们就像电子邮件收件箱。他们可以接收来自任何人的消息,但只有电子邮件所有者能够访问这些信息。消息会一直留在队列中,直到被相应的接收者使用,或者直到“生存时间”等规则(根据消息在队列中的时间来清除消息)生效。你可以在图 5-1 中看到一个队列的表示。
图 5-1
队列表示
另一方面,话题是传播信息的交流渠道。不同的用户可以订阅一个主题,一旦任何消息可用,所有用户(如果没有配置过滤)都会收到该消息的副本。您可以在图 5-2 中看到一个主题表示。
图 5-2
主题表征
这是消息传递中主要概念的总体表示。根据您使用的产品或语言,这些概念可能有不同的名称。这里我使用 Jakarta 消息传递(以前的 JMS,Java 消息传递)规范术语。规范本身在 Java 语言使用的公共 API 中抽象了这些概念,独立于供应商或实现,因此当我们使用 Java 创建与消息传递的集成时,使用这种命名法是公平的。
谈到实现,我们还必须考虑队列和主题的其他方面,而持久性可能是主要方面。
队列和主题可以是持久的,也可以不是。这完全取决于生产者和消费者的关系以及数据的性质。以负责监控卡车位置的设备为例。它会不时发送通知消息,告知卡车的当前坐标。根据卡车的移动方式,如果它在交通中停止或正在加油,您可能会收到许多具有相同坐标的消息。对于这种情况,并不是每条消息都很重要。真正重要的是消息排序。它可能会丢失一些消息,但仍然不会影响整体监控,但消息需要不断出现,消费者需要跟上生产者的节奏。在这种情况下,我们可以轻松地实现非持久队列。我们将获得中间件层的灵活性,并允许生产者向消费者提供更好的吞吐量。我们仍然需要注意中间件资源,以允许它接收生产者的负载,并在消费者比生产者慢时暂时保持它。
在这种情况下,排序也很重要。代理之间的消息负载均衡之类的策略将变得更加难以实现,因为这可能导致生产者向不同的代理发送消息,而消费者不按照发送顺序访问代理。不过,如果我们有不同的案子,这就不是问题了。
想象一下,我们有一个电子商务网站,将客户订单放在队列中进行处理。在这种情况下,排序不是问题,因为每个订单都是完全独立的事件。因此,我们可以通过负载均衡轻松地分配负载,但每个事件都很重要,不容错过。对于这种情况,我们需要一个持久的队列来保证我们有机制在代理失败时恢复消息,因为我们不能冒丢失消息的风险。
题目也有类似的要求。更传统的实现使主题成为订阅者的广播机制,订阅者在代理收到消息时可用,因此消息并没有真正持久化,但仍然存在我们需要广播消息并保证消费者稍后可以获得它们的情况。传统的消息传递是关于消费消息的。一旦消息被处理,它就会从目的地被删除。对于主题来说,完全采用这种机制会更加困难,因为很难期望所有的订阅者都阅读特定的条目,或者在任何给定的时间添加更多的订阅者,并且仍然保持一致。为了解决主题需要持久性的问题,不同的产品实现了不同的机制,从将消息从主题路由到专门为订阅者创建的持久性队列,到不基于读取而是基于时间或存储空间来删除消息,您将在后面看到。
高可用性、性能、数据复制、可伸缩性、监控和其他因素将引导您实现一个特定的消息代理。我们不会孤立地讨论这些特征;我们在描述 ApacheKafka 的时候会谈到他们。
ApacheKafka
因此,让我们通过在一个实现产品中具体化消息代理理论来实现它。您将学习 Kafka 的核心概念和特征,然后学习如何在 Camel 中使用它们。
用项目网页的话说,“Apache Kafka 是一个开源的分布式事件流平台”。让我们分解其中的一些单词来理解它们真正的意思。从“事件流”开始,从更广泛的意义上来说,事件是由一个源生成的数据,该数据由将在某种级别上处理该数据的消费者捕获或接收。事件源的一些例子是传感器、数据库改变、网络挂钩、应用调用等等。它们都生成可以触发其他应用的数据。流意味着这些事件持续发生,并且有高容量的可能性。
要理解分布式的部分,首先需要理解 Kafka 的架构。
概念和架构
围绕 Kafka 有很多炒作。大型科技公司正在使用它,他们说他们每天处理数十亿次交易,移动数万亿字节的数据,这显然会引起需要为其内部服务通信构建弹性和高性能解决方案的开发人员和架构师的注意。所以让我们了解 Kafka 到底是什么,然后你就可以得出你自己的结论。
Kafka 是 LinkedIn 在 2011 年成为开源之前创建的。它最初是为处理大数据流而设计的,比如跟踪页面浏览事件,从服务中收集聚合日志。你可能会问自己,为什么 LinkedIn 会创建一个市场上有这么多可用的消息系统?它需要一个强大且可扩展的平台,以非常低的延迟处理非常高的容量,因此必须做出一些重大的设计决策来实现这一点。
Kafka 只提供持久的话题。如前所述,您可能需要不同的策略来允许持久主题,因为很难同步消费者如何阅读主题中的消息,所以基于阅读来删除它们变得很复杂。在 Kafka 中,消息不会被消费,而是根据存储利用率或消息的持续时间进行轮换。
这个单一的设计选择开启了新的可能性。由于一切都是一个主题,我们可以根据需要向目的地添加更多的消费者,消费者将能够获得他们开始订阅之前就存在的消息。虽然这可能是某些情况下的期望行为,但对于消费者不希望接收旧消息的其他情况,这可能不是期望的行为。如果现在的消费者下线了呢?它将如何从它开始的地方恢复?Kafka 使用名为 offset 的结构保存消息索引,如图 5-3 所示。
图 5-3
偏移表示
偏移是 Kafka 用来标记消息位置的连续长值。它们用于识别哪个消息是为某个消费者群体阅读的(稍后我们将讨论消费者群体)。
Kafka 的信息处理有些不同。主题分为个分区。每个分区都是存储主题消息的独立结构。为了实现这个解释,想象一个有两个代理(Kafka 实例)的 Kafka 集群,如图 5-4 所示。
图 5-4
Kafka 分区分布
在这个例子中,有一个主题,两个分区分布在两个代理之间。每个分区都是独立的,接收来自生产者的隔离写入,并为消费者提供读取。每个分区都有自己的偏移量计数,该偏移量用于使用者组识别哪个消息在该分区中被读取。消费者组允许同一个消费者应用的不同实例并行读取,而没有重复。在本例中,使用者组中的每个使用者都被分配到一个分区,只有该使用者可以从该分区中读取数据。如果该消费者退出,并且为同一消费者组分配了新的消费者,则新的消费者可以从旧消费者停止的地方开始读取。
如你所见,Kafka 中的主题本质上是一组分区。分区是允许并行和分布式处理的很好的设计选择。我们在主题创建中设置我们需要的分区数量,但是我们也可以在以后添加更多的分区。分区的数量是根据预期的容量、预期的响应时间、集群安排、消费者行为、存储空间等决定的。这里的重要思想是调整这种配置的灵活性,以满足我们的系统需求。
有许多低层次的配置也会影响性能,但我们这里的重点是使 Kafka 成为一个非常有趣的消息传递解决方案的设计选择。
继续我们对 Kafka 描述中“分布式”含义的转换,你需要理解如何组装一个集群。看一下图 5-5 。
图 5-5
Kafka 集群
Kafka 实例在启动时做的第一件事就是在 Zookeeper 实例中注册自己。Zookeeper 是分布式应用的协调服务,这意味着它提供命名、配置管理、同步和组服务等公共服务。Kafka 利用 Zookeeper 来管理关于集群成员、主题、访问控制列表和一个非常重要的任务的信息,即 Kafka 控制器的选举,Kafka 实例将成为集群自动化背后的大脑。
2 . 8 . 0 版本提供了 KIP-500 的早期访问版本,允许你在没有 Apache ZooKeeper 的情况下运行 Kafka brokers,但这不是生产就绪,这就是为什么我在这里谈论 ZooKeeper。
Zookeeper 是这个架构中的一个重要部分,因为它通过提供协调来实现集群的高可用性。动物园管理员也是哈。它利用一个选举过程,一旦选出一个领导者,传入的写请求总是由该领导者处理,该领导者向所有可用节点请求相同的写。如果达到法定人数,则该请求被视为成功。
当我们谈论系统中的高可用性时,我们不仅仅是在谈论服务可用性,还包括数据可用性,尤其是对于一个行为类似于数据库的系统。Kafka 用一种叫做复制品的功能解决了这种必要性。看图 5-6 检查 Kafka 如何复制分区数据。
图 5-6
Kafka 复制品
在上面的例子中,有一个主题,它有三个分区,分布在三个代理之间。新的是每个分区都是复制的,所以是三个分区两个副本的题目。
我之前说过,每个分区都是独立的,并且将接收独立的写请求,这仍然是正确的,即使我们有多个副本。所发生的是控制器必须选择一个分区副本作为领导者。只有领导者接收写入和读取的请求,它负责向追随者发送写入请求以保持副本的一致性。这一过程受到高度监控,其状态通过 ISR (同步副本)值来表示。您将在几页中看到该值的显示。理想情况下,分区副本不会在同一个代理中,这样我们就有了数据冗余以防代理失败。
继续上面的例子,假设一个经纪人倒下了。我们会遇到如图 5-7 所示的情况。
图 5-7
代理失败
在这个例子中,partition 2
领导者和partition 0
跟随者在失败的代理中。假设最小 ISR 是 1,并且所有分区都是同步的,我们将有一个场景,在这个场景中,broker 1
中的partition 2
跟随者将成为领导者。partition 0
将有一个同步的副本,即broker 0
中的领导者。使用这种配置,不会丢失任何数据,并且该服务仍可用于所有分区。
还有更多的概念和可能的配置需要讨论,但这里的想法是在你开始使用它之前,对 Kafka 的架构以及它为什么有趣有一个基本的了解。这些是每个和 Kafka 一起工作的人需要理解的主要概念。
接下来,您将看到如何在本地安装和使用一个实例。
安装和运行
您已经了解了 Kafka 的基本概念,现在是时候尝试这个工具了。它可以被配置为创建巨大的集群,以支持大量生产者和消费者交换大量数据,但是,像往常一样,我们将从小处着手,使用一个可以在您的开发设置中轻松运行的示例。
您使用 Docker 来促进您对 Keycloak 的体验,您将对 Kafka 做同样的事情。不幸的是,该项目没有提供现成的容器映像,但我们可以指望社区为我们提供一个。您将使用wurstmeister/kafka
映像。在kafka-installation
文件夹下是本例中使用的映像的 git 项目,kafka-docker
和zookeeper-docker
,以防您想知道这些映像是如何构建的或者甚至想自己构建它们。在同一个文件夹中,你会发现一个docker-compose.yml
文件,你将使用它来启动应用。在 IDE 或文本编辑器中打开该文件。我们来看一下;见清单 5-1 。
version: '3'
services:
zookeeper:
image: wurstmeister/zookeeper:latest
container_name: zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
ports:
- 22181:2181
kafka:
image: wurstmeister/kafka:2.13-2.7.0
container_name: broker
depends_on:
- zookeeper
ports:
- 9092:9092
environment:
KAFKA_ADVERTISED_HOST_NAME: localhost
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
Listing 5-1Kafka docker-compose.yml
对于这个例子,您使用 Docker compose,因为 Kafka 需要一个 Zookeeper 实例来连接,所以您在文档中定义了两个服务,zookeeper
和kafka
。对于 Kafka 如何工作以及使用它在应用之间交换消息的基本体验,您不需要集群,这就是为什么您要通过在每个服务中固定一个容器名称来设置这个合成文件只允许单个实例。它还将帮助您使用需要执行的 docker 命令。
让我们从 Kafka 开始。在终端中,导航到kafka-installation
文件夹并运行以下命令:
kafka-installation $ docker compose up -d
一旦它完成下载映像,你应该得到一个类似图 5-8 的可视化效果。
图 5-8
坞站合成 up result
要检查 Kafka 是否正确启动,请使用以下命令查看容器日志:
$ docker logs broker
查找类似清单 5-2 的日志条目。如果你找到了他们,这意味着 Kafka 已经准备好了。
...
[2021-06-05 15:30:28,450] INFO [KafkaServer id=1001] started (kafka.server.KafkaServer)
[2021-06-05 15:30:28,506] INFO [broker-1001-to-controller-send-thread]: Recorded new controller, from now on will use broker 1001 (kafka.server.BrokerToControllerRequestThread)
Listing 5-2Kafka Container Log Snippet
Kafka 没有提供可视化工具或控制台,尽管有许多项目和供应商提供这种功能,但它提供了一组脚本,允许您管理它。
首先访问代理容器来可视化脚本集合。从终端运行以下命令:
$ docker exec -it broker /bin/sh
从这一点上,你可以导航到 Kafka 在这个映像中的安装位置。这里的安装路径是/opt/kafka
。使用命令转到该文件夹:
/ # cd /opt/kafka/bin/
一旦在目录中,列出它。你会得到一个如图 5-9 的结果。
图 5-9
Kafka 的剧本
使用这些脚本可以进行许多不同的配置,但是您将只执行一些基本的操作,这些操作将帮助您利用 Kafka 进行消息传递。
第一步是创建一个主题。运行以下命令创建主题:
/opt/kafka_2.13-2.7.0/bin # kafka-topics.sh \
--bootstrap-server localhost:9092 --create \
--replication-factor 1 --partitions 2 --topic myTestTopic
预期结果是“Created topic myTestTopic.
”消息。
您可以使用kafka-topics.sh
查找更多与主题管理相关的操作,例如,列出集群中存在的主题:
/opt/kafka_2.13-2.7.0/bin # kafka-topics.sh \
--bootstrap-server localhost:9092 --list
该操作仅显示主题名称,以便您知道它们的存在,但是如果您需要关于主题的更多信息,您可以描述它,如以下命令所示:
/opt/kafka_2.13-2.7.0/bin # kafka-topics.sh \
--bootstrap-server localhost:9092 --describe \
--topic myTestTopic
如果您运行这个命令,您会得到如图 5-10 所示的结果。
图 5-10
主题描述命令
这个命令显示了关于这个主题的许多重要信息,比如它的分区数量、复制因子以及它可能具有的特定配置,但是真正有趣的是第二行。它显示了分区在哪里,谁是领导者,副本在哪里,以及哪些副本是同步的。在本例中,有两个分区,但只有一个代理实例。在这种情况下,分区领导者位于 id 为 1001 的代理中(您可以在启动日志中看到这一点),因此每个分区都有一个副本,这就是分区领导者本身。
您也可以改变此实体配置。假设您想将主题的保持期设置为 10 秒。你可以这样做:
/opt/kafka_2.13-2.7.0/bin # kafka-configs.sh \
--bootstrap-server localhost:9092 \
--topic myTestTopic --alter --add-config retention.ms=10000
然后,您可以再次检查该主题,看看上面的命令是否有一些效果。再次运行describe
命令,如图 5-11 所示。
图 5-11
改变话题
已应用更改。您可以看到该主题保留期有了新的配置。
测试安装
现在您已经有了一个正在运行的 Kafka broker,您需要开始使用应用测试它。稍后您将使用 Camel 访问 Kafka,但是首先让我们利用 Kafka 安装提供的一些应用。
我希望您仍然打开了带有代理容器终端。如果没有,就按照上一节的步骤,因为您将在那里使用两个脚本:kafka-console-producer.sh
和kafka-console-consumer.sh.
先从设置消费 app 开始。在已经打开的终端的kafka/bin
目录中,运行以下命令:
/opt/kafka_2.13-2.7.0/bin # kafka-console-consumer.sh \
--bootstrap-server localhost:9092 --topic myTestTopic
此时命令行处于冻结状态,等待消息开始进入主题,因此让我们准备好生产者。打开一个新的终端,访问代理容器,并转到/opt/kafka/bin
目录。像这样运行生产者脚本:
/opt/kafka_2.13-2.7.0/bin # kafka-console-producer.sh--bootstrap-server localhost:9092 --topic myTestTopic
将为您打开一个光标。键入一条消息,然后按 Enter。然后检查你的消费者。它会收到你发送的准确信息。
通过按 Control + C 停止消费者和生产者。您现在将执行一个新的测试。首先,让我们增加保留期,将其设置回默认值,即 7 天。运行以下命令:
/opt/kafka_2.13-2.7.0/bin # kafka-configs.sh \
--bootstrap-server localhost:9092 --topic myTestTopic \
--alter --config retention.ms=60480000
再次打开生成器。发送三条这样的消息:
-
编译
-
生产者
-
消费者
现在像以前一样打开消费者。你不会得到任何消息。发生这种情况是因为该使用者被配置为仅读取新消息。添加选项-from-beginning
,你将得到这些信息,如图 5-12 。
图 5-12
读取旧偏移
本节展示了如何使用 Kafka 作为消息代理来连接应用。这些脚本可以帮助您进行调试,因为它们可以接收不同的配置,比如使用特定的分区或使用给定的组 id。
Camel 和 Kafka
您刚刚学习了 Kafka 的概念和架构,如何执行基本操作,以及如何运行基本测试。现在,您将连接一个 Camel 应用,并讨论这种实现的一些注意事项。
在第一个使用 Camel 访问 Kafka 主题的例子中,您将使用两个不同的应用:camel-kafka-producer
和camel-kafka-consumer
。生产者将公开一个 REST 接口,使您能够向消费者应用发送消息。通过这个简单的设置,您将探索使用 Kafka 组件的生产者和消费者的一些配置。
设置应用
让我们看看如何配置 Camel 应用来访问 Kafka 主题。
从分析camel-kafka-producer
代码开始。将它加载到您喜欢的 IDE 中。查看清单 5-3 中的 pom 文件。
...
<dependencies>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-rest</artifactId>
</dependency>
</dependencies>
...
Listing 5-3pom.xml Snippet
这个项目只有两个依赖项,camel-quarkus-rest
和camel-quarkus-kafka
。你已经知道的 REST 组件。它将负责添加 webserver 实现并启用 REST DSL。Kafka 成分是你的研究对象。让我们看看这个项目路线是什么样子的。打开清单 5-4 所示的RestKafkaRoute.java
文件。
public class RestKafkaRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
rest("/kafka")
.consumes(TEXT_PLAIN)
.produces(TEXT_PLAIN)
.post()
.route()
.routeId("rest-route")
.log("sending message.")
.removeHeaders("*")
.to("{{kafka.uri}}")
.removeHeaders("*")
.setBody(constant("message sent."))
.log("${body}")
.endRest();
}}
Listing 5-4RestKafkaRoute.java File
这个路由接收到一个text/plain
体,发送到一个 Kafka 主题,得到一个响应通知消息已经发送到主题,但是这个逻辑中间有一个你以前没有做过的事情。就在 Kafka 组件步骤之前,您正在使用一个无所不包的模式删除消息头。这是您以前没有做过的事情,因为您面临的案例没有受到标头传播的影响。
根据组件的不同,它可能会对一些特殊的头做出反应,例如 JPA 查询参数,但是对于这个组件,其他头会被完全忽略。在这种情况下,您正在使用一个对消息头更敏感的组件,因为被调用的应用在其数据模型中有消息头。
Kafka 消息(也称为记录)是带有一些元数据和消息头的键值对条目。该键是用于分区分配的可选字段。该值是您想要发送的实际消息。
在这个例子中,您删除了来自 HTTP 请求的所有 of 头,因为您不需要它们,也不想向 Kafka 发送无用的信息。在组件调用之后,您还要删除头,因为您不需要返回的信息,也不想将 Kafka 信息暴露给 HTTP 客户端。
您可能已经注意到了用于声明端点配置的属性键。因为这个组件比前面的例子需要更多的配置,所以使用application.properties
文件来提高代码的可读性和可配置性是有意义的。
在 IDE 中打开属性文件(清单 5-5 )。
topic=example1
brokers=localhost:9092
id=producer
kafka.uri=kafka:{{topic}}?brokers={{brokers}}&clientId={{id}}
Listing 5-5application.properties File
Kafka 组件需要一些参数才能正常工作。第一个是它将向其发送消息的topic
。在这里,您将设置“example1
”作为值。稍后您将创建这个主题。你还需要设置brokers
地址。如果要连接到集群,则必须在此参数中配置一个列表。Kafka 客户端需要知道集群的所有成员,以便根据分区分布或负载均衡来访问它们。这种情况下的clientId
会帮助 Kafka 和你追踪通话。
现在,让我们看一下消费者应用。在您的 IDE 中打开camel-kafka-consumer
项目。看清单 5-6 中的RouteBuilder
。
public class KafkaConsumerRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
from("{{kafka.uri}}")
.routeId("consumer-route")
.log("Headers : ${headers}")
.log("Body : ${body}");
}
}
Listing 5-6KafkaConsumerRoute.java File
这个路由使用来自一个主题的消息并记录消息内容,首先是消息头,然后是消息内容。
让我们看看清单 5-7 中该项目的application.properties
。
topic=example1
brokers=localhost:9092
kafka.uri=kafka:{{topic}}?brokers={{brokers}}&clientId=${kafka.clientid}&groupId=${kafka.groupid}
Listing 5-7camel-kafka-consumer Project’s application.properties File
这种配置与您之前看到的类似。唯一不同的是,对于消费者,建议设置一个groupId
。组 id 允许您持久化读取的偏移量,因此如果您重新启动消费者应用,它将能够从停止的地方重新启动。clientId
和groupId
使用属性标记作为值,因为您将把这些参数作为 JVM 变量传递给应用。
首次测试
您已经看到了示例应用是如何配置的。现在你需要看看它们在不同的场景下会有怎样的表现。
让我们测试代码。在运行应用之前,您需要创建“example1
”主题。您可以运行以下命令来完成此操作:
docker exec -it broker /opt/kafka/bin/kafka-topics.sh \
--create --bootstrap-server localhost:9092 \
--replication-factor 1 --partitions 2 --topic example1
创建主题后,您就可以启动消费者了。在终端的camel-kafka-consumer
目录下,运行以下命令:
camel-kafka-consumer/ $ mvn quarkus:dev -Dkafka.clientid=test -Dkafka.groupid=testGroup
消费者启动后,查看消费者日志。那里有一些有趣的信息。它们看起来会像这样:
[Consumer clientId=test, groupId=testGroup] Notifying assignor about the new Assignment(partitions=[example1-0, example1-1])
日志条目通过分配过程中的clientId
和groupId
来标识消费者,该过程确定消费者组的成员将从哪个分区获取消息。在这种情况下,由于您只有一个使用者和两个分区,因此使用者将从两个分区获得消息。
[Consumer clientId=test, groupId=testGroup] Found no committed offset for partition example1-0
[Consumer clientId=test, groupId=testGroup] Found no committed offset for partition example1-1
上面的消息告诉您,没有为两个分区保存偏移读数。
[Consumer clientId=test, groupId=testGroup] Resetting offset for partition example1-0 to position FetchPosition{offset=0, offsetEpoch=Optional.empty, currentLeader=LeaderAndEpoch{leader=Optional[localhost:9092 (id: 1001 rack: null)], epoch=0}}.
[Consumer clientId=test, groupId=testGroup] Resetting offset for partition example1-1 to position FetchPosition{offset=0, offsetEpoch=Optional.empty, currentLeader=LeaderAndEpoch{leader=Optional[localhost:9092 (id: 1001 rack: null)], epoch=0}}.
这些条目告诉您将从哪个偏移量开始读取消息,在本例中为offset=0
。它们还告诉给定分区的领导者是谁,在本例中是localhost:9092 (id: 1001)
。
你现在可以开始制作了。在新终端中,运行以下命令:
camel-kafka-producer/ $ mvn quarkus:dev -Ddebug=5006
启动生成器后,您可以向它发送请求。发送以下请求:
$ curl -X POST 'http://localhost:8080/kafka' \
-H 'Content-Type: text/plain' -d 'Testing Kafka'
看看消费者记录。你会得到这样的东西:
2021-06-06 08:47:42,524 INFO [consumer-route] (Camel (camel-1) thread #0 - KafkaConsumer[example1]) Headers : {kafka.HEADERS=RecordHeaders(headers = [], isReadOnly = false), kafka.OFFSET=0, kafka.PARTITION=1, kafka.TIMESTAMP=1622980062458, kafka.TOPIC=example1}
2021-06-06 08:47:42,528 INFO [consumer-route] (Camel (camel-1) thread #0 - KafkaConsumer[example1]) Body : Testing Kafka
第一个日志条目是消息头的值。正如您所看到的,消费者将返回一些关于读取的信息,比如偏移读取、哪个分区、哪个主题和消息时间戳,这将告诉您消息何时进入主题。第二个值是实际的消息。
让我们用不同的信息再试一次:
$ curl -X POST 'http://localhost:8080/kafka'\
-H 'Content-Type: text/plain' -d 'Learning Camel'
再看看消费者日志。您将看到类似这样的条目:
2021-06-06 08:49:22,988 INFO [consumer-route] (Camel (camel-1) thread #0 - KafkaConsumer[example1]) Headers : {kafka.HEADERS=RecordHeaders(headers = [], isReadOnly = false), kafka.OFFSET=0, kafka.PARTITION=0, kafka.TIMESTAMP=1622980162982, kafka.TOPIC=example1}
2021-06-06 08:49:22,989 INFO [consumer-route] (Camel (camel-1) thread #0 - KafkaConsumer[example1]) Body : Learning Camel
该分区与之前的测试不同,但是消费者仍然可以从两个分区获得消息。让我们在同一个消费者组中添加一个新的消费者。
打开新的终端。导航到camel-kafka-consumer
项目目录并运行以下命令:
camel-kafka-consumer/ $ mvn quarkus:dev -Dkafka.clientid=other -Dkafka.groupid=testGroup -Ddebug=5007
一旦应用启动,查看它的日志。以下是我的例子:
[Consumer clientId=other, groupId=testGroup] Adding newly assigned partitions: example1-0
[Consumer clientId=other, groupId=testGroup] Setting offset for partition example1-0 to the committed offset FetchPosition{offset=1, offsetEpoch=Optional.empty, currentLeader=LeaderAndEpoch{leader=Optional[localhost:9092 (id: 1001 rack: null)], epoch=0}}
分区 0 是为我的新用户分配的,它将从offset=1
开始读取。如果您查看第一个消费者的日志,您会看到现在只有一个分区被分配给它。
扩大消费者规模
我们讨论了 Kafka 架构的可伸缩性,但是在这种情况下,可伸缩性也意味着增加消费者端的处理能力。当考虑如何衡量消费者时,我们需要遵循一些规则。
如果我们在同一个组中添加一个新的消费者,会发生什么情况?让我们试试。在camel-kafka-consumer
目录中打开一个新的终端,并运行以下命令:
camel-kafka-consumer/ $ mvn quarkus:dev -Dkafka.clientid=third -Dkafka.groupid=testGroup -Ddebug=5008
应用启动后,我的新消费者就遇到了这种情况:
[Consumer clientId=third, groupId=testGroup] Notifying assignor about the new Assignment(partitions=[])
没有给它分配分区。让我们来看看当您向主题添加消息时会发生什么。运行下面的 bash 脚本,在主题中输入十条新消息。
$ i=0; while [ $i -lt 10 ]; do ((i++)); curl -w "\n" -X POST 'http://localhost:8080/kafka' -H 'Content-Type: text/plain' -d "Message number: $i" ; done
如果您查看消费者的日志,您将看到没有分配分区的消费者没有接收消息。这被称为饥饿的消费者。
请记住,活动消费者的数量将取决于该主题当前可用的分区数量。在代理失败的情况下,我们可能会遇到某个特定分区没有领导者的情况。
如果您需要提高整体性能,请记住,您可以稍后向一个主题添加更多的分区。
您可以使用其他客户端配置来提高消费者处理能力,比如consumersCount
和consumerStreams
。
consumerStreams
参数负责设置组件线程池的线程数量,而consumersCount
负责设置应用中 Kafka 消费者的数量。每个偏移量读取都将在一个线程中完成,这意味着您可以进行的并发读取的数量将取决于您拥有的消费者数量,以及您是否有可供该消费者使用的线程。
为了演示这个配置,在您的 IDE 上打开camel-kafka-consumer-v2
项目。让我们看看这个项目的路线,这样你就可以了解这个测试是如何工作的;看清单 5-8 。
public class KafkaConsumerRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
from("{{kafka.uri}}")
.routeId("consumer-route")
.log("Headers : ${headers}")
.log("Body : ${body}")
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
log.info("My thread is :"+Thread.currentThread().getName());
log.info("Going to sleep...");
Thread.sleep(10000);
}
});
}}
Listing 5-8KafkaConsumerRoute.java File
这条路线上唯一的新东西是,现在您有了一个处理器,它让正在执行的线程休眠十秒钟。这将有助于你想象执行过程。
让我们看看清单 5-9 中的属性文件。
topic=example1
brokers=localhost:9092
kafka.uri=kafka:{{topic}}?brokers={{brokers}}&clientId=${kafka.clientid}&groupId=${kafka.groupid}&consumersCount=${kafka.consumers.count}&consumerStreams=${kafka.consumers.stream}
Listing 5-9camel-kafka-consumer-v2 properties File
这里您添加了consumersCount
和consumerStreams
参数,但是您也将从 JVM 属性中获得它们。
要开始测试此代码,请停止任何正在运行的使用者。在终端中,像这样启动应用:
camel-kafka-consumer-v2/ $ mvn clean quarkus:dev \
-Ddebug=5006 -Dkafka.clientid=test -Dkafka.groupid=testGroup \ -Dkafka.consumers.count=1 -Dkafka.consumers.stream=10
在这里,您可以使用默认值设置参数。要测试应用如何使用消息,请在另一个终端中运行以下命令:
$ i=0; while [ $i -lt 3 ]; do ((i++)); curl -w "\n" -X POST 'http://localhost:8080/kafka' -H 'Content-Type: text/plain' -d "Message number: $i" ; done
三条消息足以向您显示每十秒钟将处理一条消息,即使您在池中有十个线程。
现在尝试使用两个 Kafka 消费者和池中相同数量的线程。停止消费者,然后像这样重新启动它:
camel-kafka-consumer-v2/ $ mvn clean quarkus:dev \ -Ddebug=5006 -Dkafka.clientid=test -Dkafka.groupid=testGroup \
-Dkafka.consumers.count=2 -Dkafka.consumers.stream=10
现在,不是只发送三条信息,而是发送八条。
$ i=0; while [ $i -lt 8 ]; do ((i++)); curl -w "\n" -X POST 'http://localhost:8080/kafka' -H 'Content-Type: text/plain' -d "Message number: $i" ; done
您将看到应用每十秒钟消耗两条消息。但是如果你的消费者比线程多会怎么样呢?停止消费应用,并像这样启动它:
camel-kafka-consumer-v2/ $ mvn clean quarkus:dev \
-Ddebug=5006 -Dkafka.clientid=test -Dkafka.groupid=testGroup \ -Dkafka.consumers.count=2 -Dkafka.consumers.stream=1
再发三条信息。您将看到每十秒钟只处理一条消息。
您在这里所做的是通过允许消费者消耗更多资源来纵向扩展消费者,在本例中是分配给分区。当您纵向扩展应用时,您还需要调整应用消耗计算资源(如内存和 CPU)的方式。
您正在对应用采用微服务方法,因此您不希望它们变得太大,以至于可能会损害其他重要的微服务特性,如正常关闭的敏捷性或水平扩展的能力(通过添加新实例)。这完全是了解您的数据和您的应用,然后适当调整配置的问题。测试是必须的。
偏移复位
您可以为已经包含消息的现有主题添加新的消费者。在这种情况下,你需要为你的新消费者设定正确的行为。
在讨论当一个主题已经有消息时你能做什么之前,我需要你看一看一些东西。在代理运行的情况下,在终端上运行以下命令:
$ docker exec -it broker /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
这个命令列出了代理中可用的主题。如果你没有删除任何主题,你的输出应该如图 5-13 所示。
图 5-13
主题列表
组 id 消耗的每个分区偏移量保存在__consumer_offsets
主题中。您可以在应用日志中看到,在每次启动过程中,客户端都会检查给定分区的可用偏移量,总是寻找最新的引用。
在开始这个新例子之前,您需要一个新的主题。来清理一下你的老话题吧。停止任何正在运行的应用,并运行以下命令来执行此操作:
$ docker exec -it broker /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic myTestTopic
$ docker exec -it broker /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic example1
要开始您的测试,您将需要一个主题,但是这次不是您自己创建一个,而是让主题自动创建来完成。默认情况下,它将创建一个具有单个副本和单个分区的主题。这对你的测试来说足够了。
如果这不是理想的配置,您可以通过将 Kafka 的 config 目录中的server.properties
文件中的auto.create.topics.enable
属性设置为 false 来禁用自动创建。这将需要重新启动代理。
再次运行camel-kafka-producer
应用。启动后,使用以下命令发送十条消息:
$ i=0; while [ $i -lt 10 ]; do ((i++)); curl -w "\n" -X POST 'http://localhost:8080/kafka' -H 'Content-Type: text/plain' -d "Message number: $i" ; done
您现在可以启动消费者,知道主题中有消息。像这样启动camel-kafka-consumer
应用:
camel-kafka-consumer/ $ mvn clean quarkus:dev -Ddebug=5006 -Dkafka.clientid=test -Dkafka.groupid=testGroup
你没有收到任何信息,是吗?这是意料之中的行为。默认情况下,自动偏移重置的组件属性设置为采用分区中的最新偏移。查看消费者应用中的日志,如清单 5-10 所示。
2021-06-06 20:52:09,669 INFO [org.apa.kaf.cli.con.int.ConsumerCoordinator] (Camel (camel-1) thread #0 - KafkaConsumer[example1]) [Consumer clientId=test, groupId=testGroup] Found no committed offset for partition example1-0
2021-06-06 20:52:09,693 INFO [org.apa.kaf.cli.con.int.SubscriptionState] (Camel (camel-1) thread #0 - KafkaConsumer[example1]) [Consumer clientId=test, groupId=testGroup] Resetting offset for partition example1-0 to position FetchPosition{offset=10, offsetEpoch=Optional.empty, currentLeader=LeaderAndEpoch{leader=Optional[localhost:9092 (id: 1001 rack: null)], epoch=0}}.
Listing 5-10camel-kafka-consumer Application Log Snippet
第一个日志显示,对于所提供的组 id,没有找到分区example1-0
的提交偏移量。第二个显示为FetchPosition offset=10
重置了偏移,这将是下一个生成的偏移。
发送一条消息来检查消费者日志中的记录标题:
$ curl -w "\n" -X POST 'http://localhost:8080/kafka'-H 'Content-Type: text/plain' -d "Single message"
在日志中,您会发现如下条目:
2021-06-06 20:57:16,338 INFO [consumer-route] (Camel (camel-1) thread #0 - KafkaConsumer[example1]) Headers : {kafka.HEADERS=RecordHeaders(headers = [], isReadOnly = false), kafka.OFFSET=10, kafka.PARTITION=0, kafka.TIMESTAMP=1623023836299, kafka.TOPIC=example1}
您可以看到消息偏移量是10
,这是在消费者启动中标记的位置,但是您仍然没有获得之前的消息。这可能是所希望的情况,因为新的应用可能不想要旧的消息。如果您想要重放主题中的每条消息,您只需要一个新的组 id 和以下配置。
首先,停止消费者应用。将kafka.uri
属性更改为如下所示:
kafka.uri=kafka:{{topic}}?brokers={{brokers}}&clientId=${kafka.clientid}&groupId=${kafka.groupid}&autoOffsetReset=earliest
现在,像这样启动应用:
camel-kafka-consumer/ $ mvn clean quarkus:dev-Ddebug=5006 -Dkafka.clientid=test -Dkafka.groupid=newGroup
这样,应用将获得主题中存在的所有消息,如果您使用相同的参数重新启动应用,它将不会获得相同的消息,因为提供的组 id 已经保存了一个偏移量,客户端不需要重置它。
单元测试应用
对于单元测试应用,保持代码质量并使维护更容易是很重要的。在处理集成时,您可能会遇到不容易模仿的应用,比如消息代理。Camel 提供的组件和功能可以帮助您完成这项任务。
对于这个新例子,有一个名为camel-kafka-tests
的新项目。这个项目融合了camel-kafka-producer
和camel-kafka-consumer
plus 单元测试。
它有一个RouteBuilder
来公开一个 REST 接口并将一条消息发布到一个主题中,还有一个RouteBuilder
来为该主题创建一个消费者。先说清单 5-11 中的生产商路线。
public class RestKafkaRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
rest("/kafka")
.consumes(TEXT_PLAIN)
.produces(TEXT_PLAIN)
.post()
.route()
.routeId("rest-route")
.log("sending message.")
.removeHeaders("*")
.to("{{kafka.uri.to}}")
.removeHeaders("*")
.setBody(constant("message sent."))
.log("${body}")
.endRest();
}
}
Listing 5-11RestKafkaRoute.java File
这条路线实际上就是您一直用来在主题中发布消息的路线。唯一的区别是现在属性名不那么通用了,因为现在有两条路由。让我们看看清单 5-12 中的消费者。
public class KafkaConsumerRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
from("{{kafka.uri.from}}")
.routeId("consumer-route")
.log("Headers : ${headers}")
.to("{{final.endpoint}}");
}
}
Listing 5-12KafkaConsumerRoute.java File
唯一改变的是最后一步。您将使用日志端点,而不是使用log()
DSL 进行日志记录。你马上就会明白为什么了。
为了测试这段代码,您需要首先创建主题。奔跑
$ docker exec -it broker /opt/kafka/bin/kafka-topics.sh \ --create --bootstrap-server localhost:9092 \ --replication-factor 1 --partitions 2 --topic example2
然后,您可以启动应用:
camel-kafka-tests/ $ mvn quarkus:dev
并发送消息进行测试:
$ curl -X POST 'http://localhost:8080/kafka' \
-H 'Content-Type: text/plain' -d "hi"
既然您已经看到了应用是如何工作的,那么您可以开始关注单元测试了。观察项目 pom 的新增内容,如清单 5-13 所示。
...
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-log</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-direct</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-mock</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
...
Listing 5-13camel-kafka-tests pom.xml File Snippet
我只强调这个例子中新增加的内容。
你已经知道camel-quarkus-log
。您之所以使用它,是因为您也在使用组件进行日志记录。您将使用camel-quarkus-direct
和camel-quarkus-mock
来替代和模仿一些端点定义。这将为您提供在没有运行 Kafka 代理的情况下执行单元测试的灵活性。JUnit 5 的 Quarkus 实现是测试的基础。考虑到夸尔库斯建筑模型,它将正确地设置环境。最后,您可以放心,这是一个用于测试 REST 应用的流行库。
所以让我们来看看第一个单元测试类,如清单 5-14 所示。
@QuarkusTest
public class RestKafkaRouteTest {
@Test
public void test() {
given()
.contentType(ContentType.TEXT)
.body("Hello")
.when()
.post("/kafka")
.then()
.statusCode(200)
.body(is("message sent."));
}
}
Listing 5-14RestKafkaRouteTest.java File
首先用@QuarkusTest
注释测试类。这将允许 JUnit 实现启动应用和其中的 Camel 上下文。测试非常简单。您使用 REST Assured 向 REST 端点发送一条消息,然后断言响应状态代码是否为 200,响应消息是否为"message sent.
"
你可能会问在路线中间的 Kafka 端点调用。这一呼吁遭到了嘲笑。看一下测试文件夹下的application.properties
文件,如清单 5-15 所示。
kafka.uri.to=mock:kafka-topic
kafka.uri.from=direct:topic
final.endpoint=mock:destination
Listing 5-15application.properties for Test
在本例中,您使用属性声明端点,因此可以在测试时替换属性值。这样,您仍然可以对路由逻辑进行单元测试,而不必在测试期间提供代理。在这个测试中,模拟组件用于传递交换,没有任何修改。然后路由结束,HTTP 客户端将得到一个响应。
现在让我们看看清单 5-16 中的消费者路线。
@QuarkusTest
public class KafkaConsumerRouteTest {
@Inject
CamelContext camelContext;
@Inject
ProducerTemplate producerTemplate;
@ConfigProperty(name = "kafka.uri.from")
String direct;
@ConfigProperty(name = "final.endpoint")
String mock;
private static final String MSG = "Hello";
@Test
public void test() throws InterruptedException {
producerTemplate.sendBody(direct, MSG);
MockEndpoint mockEndpoint =
camelContext.getEndpoint(mock, MockEndpoint.class);
mockEndpoint.expectedMessageCount(1);
mockEndpoint.assertIsSatisfied();
assertEquals(MSG,mockEndpoint.getExchanges().get(0)
.getMessage().getBody())
}
}
Listing 5-16KafkaConsumerRouteTest.java File
在这里,您使用不同的方法来测试路由,因为您的路由消费者必须被嘲笑。因为您没有运行代理,所以有必要用 Kafka 组件替换 direct 组件。这样,您可以使用ProducerTemplate
来调用路由,这实际上将创建一个交换并将其发送到直接端点。从那里,路由逻辑将继续。这条路线的问题是它做的不多,这就是为什么你把最后一步改成使用to()
。在单元测试中,您将替换模拟组件的日志定义,并使用它来检查路由状态。通过注入 Camel 上下文,可以获得端点引用。在这里,您可以获得对模拟端点的引用。这个端点将保留它收到的交换信息,使用这些信息,您可以断言路由是否按预期执行。您可以检查收到了多少条消息,以及消息正文是否与您发送的相同,因为不应该进行任何处理。
要执行这些测试,请运行以下命令:
camel-kafka-tests/ $ mvn clean test
预计会在日志中收到以下消息:
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] -------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] -------------------------------------------------------
这里展示的技术可以用来设置许多不同的测试场景。例如,您可以用 bean 调用替换端点定义,并使用这些方法在交换中注入您可以模仿的任何对象。这样,您可以虚拟地模仿您需要做的每一个集成。只要记住保持路由逻辑的整洁,并使用属性来声明端点,单元测试 Camel 将会很容易。
摘要
本章致力于服务之间的异步通信,最常见的方式是使用面向消息的中间件。在本章中,您学习了以下主题:
-
面向消息的中间件的特征
-
异步通信对健壮架构的重要性
-
Kafka 的概念和建筑
-
如何使用 Kafka 运行和执行基本配置
-
如何设置 Camel 访问 Kafka 主题
-
如何使用 Quarkus 和 Camel 执行单元测试
在下一章也是最后一章,我们将深入探讨如何在 Kubernetes 环境中运行这些所谓的云原生应用,以及这种方法对架构设计的影响。
六、将应用部署到 Kubernetes
欢迎来到云原生集成之旅的最后一部分。到目前为止,您一直关注如何进行集成,学习 Camel 的概念,以及为不同的用例应用组件,但是现在您将关注如何使用 Kubernetes 以云原生方式部署这些应用。
早先做出的决定将对 Kubernetes 的应用开发体验产生巨大影响,其中最大的影响是 Quarkus 的使用。因为您不需要运行 Camel 集成;是选择其他框架还是简单运行 Camel 的主类。Quarkus 是你的目标的完美选择,因为它是云原生的。这不是 Camel 本身最初的设计,因为这个概念在它被创造出来的时候并不存在,尽管由于它坚固的架构,它非常适合这种类型的环境。
我谈了很多 Java 规范,以及它们是如何演变成遵循云原生原则的。这些原则的开发和成型得益于无数人的努力,他们致力于为云或云中提供可靠、有弹性和可扩展的服务,并在开放论坛和开放社区上分享他们的实践。一项强有力地综合了云服务开发最佳实践的举措是 12 因素应用,这是一种开源方法,最初由平台即服务公司 Heroku 的开发人员开发。这 12 条规则定义了 Kubernetes 应用固有的方法以及 Quarkus 的设计方式,所以我强烈建议您(如果您还没有这样做的话)访问 https://12factor.net/
,从理论上了解您在这里执行的操作。
在这一章中,你将了解 Kubernetes 是什么以及它是如何工作的。您将学习如何使用 minikube,这是一个用于在本地试验 Kubernetes 的社区工具,以及如何使用 Quarkus 扩展来简化工作和部署容器的过程。
不再拖延,让我们开始新的话题吧。
KubernetesKubernetesKubernetesKubernetesKubernetesKubernetesKubernetesKubernetesKubernetesKubernetes
Kubernetes 已经成为大规模管理应用的标准。技术领域最大的参与者已经聚集在这个开源项目中,创造了一系列为这种平台提供支持或提供平台本身的产品和服务。让我们了解一下是什么让这个项目与当今的组织如此相关。
在我们生活的数字时代,可伸缩性已经成为组织采用的任何服务或解决方案的基本特征。我们必须预计到,在任何给定的时间,对我们服务的需求将会增加,要么是因为产品/服务在市场上的总体受欢迎程度、访问的选择,要么是因为我们开始处理比最初计划的更多的数据。规模是不够的;我们还必须保持相同的服务质量(响应时间、成功率等等)。
Kubernetes 非常适合这个高要求的世界,原因很简单。第一个是它与容器一起工作,这是一种与语言/产品无关的打包和分发应用的方式,也是一种利用机器资源托管应用的更有效的方式。第二,它提供了在各种场景下大规模管理容器所需的所有自动化,是一个高度可用的解决方案。
你一直在使用容器与 Keycloak 和 Kafka 等应用进行交互,但你也在第一章中使用了容器来运行 Camel 路线。至此,您已经很好地理解了容器的含义及其提供的好处。让我们从 Kubernetes 的高级架构开始,重点关注它的工作原理以及它提供的自动化类型。看图 6-1 。每个盒子代表一台主机。
图 6-1
库伯内特高层建筑
容器被抽象成称为pod的实体。pod 由一个或多个容器以及运行这些容器的必要配置组成。这种结构是 Kubernetes 真正精心安排的。pod 分布在集群中正在运行的节点之间,这就是为什么您可以实现高可用性。如果一个节点出现故障,有适当的控制器来保证集群中运行所需数量的 pod。控制器将尝试将故障节点中存在的 pod 分配到另一个可用节点中,以保持状态一致。
说到主机,控制面板是集群自动化背后的大脑。它负责协调 pod 和集群成员。在常见的高可用性设置中,您会在一组三个或更多主机中找到它。控制面板实例中有四个主要组件。让我们一个一个来看。
API 服务器,或kube-api-server
,是用于内部和外部通信的接口。组件向该 API 发出请求,以获取关于集群状态的信息或更改状态。同样,集群管理员使用这个 API 来管理集群。
Kubernetes 中的状态是通过文档表示的,文档也称为资源,这些文档需要保存在某个地方。ETCD
,分布式键值数据库,是 Kubernetes 的内存。
之前我提到过控制器,举了一个例子说明在失败的情况下会发生什么,但是控制器做的不仅仅是这些。控制器是 Kubernetes 内部的一种模式,其中一个组件负责跟踪至少一个 Kubernetes 的资源类型,将文档状态转化为现实。控制器管理器或kube-controller-manager
是默认控制器,其中有四个控制器:节点、作业、端点和服务帐户&令牌。
最后是调度器,该组件负责根据 pod 需求和可用的节点资源或特征,向节点分配新的 pod。
节点中也有重要的组件。第一个是库伯莱特经纪人。它负责处理和检查PodSpecs
中描述的容器的健康状况。如果控制面板是大脑,那么这个组件很容易被认为是手。
第二个组件是 kube-proxy。我还没有讨论的一件事是,这种程度的自动化是如何实现的。动态创建 pod 并移动它们听起来不错,但是这如何与网络一起工作呢?端口和 IP 分配以及可能的扩展服务负载均衡是需要自动化的任务。Kube-proxy 是解释其工作原理的一部分。它是运行在集群中每个节点上的网络代理。它维护允许从集群内部或外部的网络会话与 pod 进行网络通信的网络规则。
Kubernetes 文档中列出的另一个节点组件是容器运行时。Docker 是容器运行时的一个例子。我没有在这里列出它,因为有一些基于 Kubernetes 的产品在容器中运行控制面板中的组件。在这种情况下,节点和控制面板都将拥有容器运行时,从而将运行时变成一种公共需求。因此,我不把它描述为节点组件。
我们只是触及了 Kubernetes 的皮毛。在深入了解它的工作原理以及可以完成哪些配置之前,您需要从一些实际的东西开始。在下一节中,您将看到如何在一台个人计算机上使用 Kubernetes 进行实验。
迷你库比
我不期望您在家中拥有集群,也不期望您能够访问公共云中的集群。有更简单的方法来研究和试验 Kubernetes。让我们试试 minikube。
minikube 是一个工具,使您能够创建一个本地 Kubernetes“集群”。您将能够应用与集群中的应用相同的配置,但是使用单个节点来完成。通过这种方式,您将体验到配置一个应用是什么样的,以及它是如何工作的,而不需要真正拥有一个集群。
请访问 minikube 网站 https://minikube.sigs.k8s.io/
,了解如何在您使用的操作系统上安装它。我用的是 Minikube 版本 v1.20.0。
你还需要 kubectl 。这个命令行工具将允许您与 Kubernetes API 进行通信。您可以从 Kubernetes 项目网站上的 https://kubernetes.io/docs/tasks/tools/
获得它,或者您可以使用 minikube CLI 中的kubectl
,如下所示:
$ minikube kubectl help
正确安装二进制文件后,使用以下命令启动 minikube:
$ minikube start
您可能希望使用诸如--cpus
或--memory
之类的选项来增加实例容量。只要考虑到你拥有的可用资源的数量。我将在示例中使用默认值。
要测试 minikube 是否有响应,请运行以下命令:
$ kubectl get pods -A
该命令将返回正在运行的 pod,如图 6-2 所示。
图 6-2
获取 POD
你可能已经认出了一些 POD。它们是我前面提到的组件,但是因为您只有一台主机,所以在这台主机中有控制面板和节点的组件。
我之前没有提到 coredns 组件,这是一个默认组件,但被认为是一个附加组件。该组件负责提供一个 DNS 服务器,为 Kubernetes 服务提供 DNS 记录。
storage-provisioner 是 minikube 提供的一个组件,它允许我们在 pods 中使用持久性,但在示例中并不需要。
该图中另一个需要注意的重要事情是名为名称空间的第一列。它是 Kubernetes 用来创建资源之间的分离的抽象。这样,您可以隔离资源,就好像它们在不同的环境中一样,比如生产和开发。
在该命令中,您使用选项-A
,这意味着您想要从所有名称空间中检索 pod。在这种情况下,它帮助您识别是否有正在运行的 pod,因为您不知道您拥有哪个名称空间。
在进入下一节之前,删除 minikube 安装。下一节你需要一个不同的。通过运行以下命令停止它:
$ minikube stop
然后使用以下命令删除它:
$ minikube delete
首次应用部署
您了解了如何创建本地 Kubernetes,并了解了更多相关概念。要继续前进,您需要学习如何实际部署应用以及如何与之交互。
在前几章中使用容器时,您依赖 Docker Hub 和本地注册表来检索映像。对于 Kubernetes 来说,情况是一样的。它还需要一个集群可以访问的注册中心,以便能够获取映像和运行容器。在这种情况下,您需要为 minikube 提供一个容器注册表。
首先创建一个新的 minikube 实例,如下所示:
$ minikube start --insecure-registry "10.0.0.0/24"
因为您不会为此安装提供安全的注册表,所以您需要配置实例以允许来自 minikube 内部的不安全注册表。
实例启动后,您可以运行以下命令向其添加注册表:
$ minikube addons enable registry
minikube 将下载并运行注册表映像。完成后,您可以使用以下命令检查注册表是否正在运行:
$ kubectl get pods -n kube-system
您正在使用-n
来定义您想要在哪个名称空间中执行查询。您应该会得到类似于图 6-3 的响应。
图 6-3
注册表窗格
Kubernetes 有一个资源叫做服务。该资源创建一个 DNS 记录,以允许 pod 通过名称访问其他 pod。这很重要,因为 pods 可以扩展并移动到集群中的不同节点,改变其地址,但是客户端应用只需要服务名就可以到达应用。该资源除了创建一个可预测的名称供其他 pod 使用之外,还增加了对不同 pod 或具有多个实例的 pod 的负载均衡请求的可能性。安装注册表时,会创建一个服务。使用以下命令检查安装中可用的服务:
$ kubectl get service -n kube-system
这将给出在kube-system
名称空间中的服务列表,如图 6-4 。
图 6-4
获取服务库-系统
注册服务是一种ClusterIP
类型的服务,这意味着它将在 minikube 的网络中拥有一个可解析的 IP。如果您想获得关于此服务的更多详细信息,您可以像这样进行更精确的查询:
$ kubectl get service registry -o json -n kube-system
该命令将返回类似于清单 6-1 的内容。
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "..."
},
"creationTimestamp": "2021-06-13T11:00:49Z",
"labels": {
"addonmanager.kubernetes.io/mode": "Reconcile",
"kubernetes.io/minikube-addons": "registry"
},
"name": "registry",
"namespace": "kube-system",
"resourceVersion": "467",
"uid": "2f31047f-8ae5-4920-bc4d-9a84ad39288f"
},
"spec": {
"clusterIP": "10.103.159.51",
"clusterIPs": [
"10.103.159.51"
],
"ports": [
{
"name": "http",
"port": 80,
"protocol": "TCP",
"targetPort": 5000
},
{
"name": "https",
"port": 443,
"protocol": "TCP",
"targetPort": 443
}
],
"selector": {
"actual-registry": "true",
"kubernetes.io/minikube-addons": "registry"
},
"sessionAffinity": "None",
"type": "ClusterIP"
},
"status": {
"loadBalancer": {}
}
}
Listing 6-1Service Resource
如我所说,Kubernetes 中的资源是文档,这些文档可以用 YAML 或 JSON 文件表示。这里我使用 JSON,因为我希望您从它的规范中检索一个特定的值,并且您将使用jsonpath
来获取它。
minikube 的虚拟机不使用 Kubernetes DNS 配置,但是您需要 minikube 使用的 Docker 才能解析注册表主机来运行映像。因此,让我们为此创建一个变通解决方案。
首先,让我们检索服务 IP。您将使用jsonpath
来自定义查询结果,只带回ClusterIP
值。使用以下命令:
$ kubectl get service registry -o=jsonpath='{.spec.clusterIP}' -n kube-system
稍后您将使用该命令。现在您需要部署一个应用。让我们使用camel-hello-minikube
项目。
在 IDE 中打开项目。观察清单 6-2 中的项目路线。
public class HelloMinikubeRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
rest("/helloMinikube")
.get()
.route()
.routeId("k8s-hello-minikube")
.log("Request Received.")
.setBody(constant("Hello from minikube."))
.endRest();
}
}
Listing 6-2HelloMinikubeRoute.java
这是一个简单的“Hello World”类型的应用,只是为了演示如何将 Camel 应用部署到 minikube 中。在这个项目中,你将像在第一章中一样使用quarkus-container-image-jib
。您将使用这个扩展来构建容器映像,并将其推送到创建的注册表中。
为了能够将映像推送到注册表,您需要从 minikube 的网络外部访问它。您可以使用名为port-forward
的kubectl
命令来启用它。
在单独的终端窗口或选项卡中,运行以下命令:
$ kubectl port-forward service/registry -n kube-system 5000:80
这个命令将创建一个从您的本地网络到 Kubernetes 集群的隧道。运行之后,终端将挂起,等待连接。让它一直开着。
现在您可以连接到注册中心了,让我们构建应用并推送映像。在单独的终端窗口中,转到camel-hello-minikube
目录。从那里运行以下命令:
camel-hello-minikube $ mvn clean package \
-Dquarkus.container-image.push=true \
-Dquarkus.container-image.build=true \
-Dquarkus.container-image.group=apress \
-Dquarkus.container-image.registry=localhost:5000 \
-Dquarkus.container-image.insecure=true
这里使用了新的参数。首先,通过设置quarkus.container-image.push=true
并在quarkus.container-image.registry=localhost:5000
中通知注册表地址,通知扩展您想要将映像推送到注册表。这个注册表是不安全的,这就是为什么你需要设置quarkus.container-image.insecure=true
。
Maven 构建完成后,您可以通过运行以下命令来检查注册表中是否有该映像:
$ curl http://localhost:5000/v2/_catalog
响应应该如图 6-5 所示。
图 6-5
注册表目录
你现在可以停止port-forward
了。
回到项目,在k8s
文件夹中,打开清单 6-3 中所示的deployment.yml
文件。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: camel-hello-minikube
name: camel-hello-minikube
namespace: first-deploy
spec:
replicas: 1
selector:
matchLabels:
app: camel-hello-minikube
strategy:
type: RollingUpdate
template:
metadata:
labels:
app: camel-hello-minikube
spec:
containers:
- image: {REGISTRY}/apress/camel-hello-minikube:1.0.0
imagePullPolicy: IfNotPresent
name: camel-hello-minikube
ports:
- containerPort: 8080
protocol: TCP
Listing 6-3Camel-hello-minikube Deployment File
我说过,Kubernetes 中的资源是文档,部署描述符也是如此。这里有一个最小的deployment
配置文件。您可以设置容器映像、pod 副本的数量、部署应该如何展开,以及可以识别此部署资源的组件的标签。
你一定注意到了{REGISTRY}
标记。您需要替换这个值,以便能够在 minikube 中创建这个资源。
首先,让我们创建一个命名空间来托管部署。使用此命令:
$ kubectl create namespace first-deploy
使用sed
,一个流编辑器,动态替换标记并创建资源。在项目目录中,运行以下命令:
camel-hello-minikube $ sed "s/{REGISTRY}/$(kubectl get svc registry -o=jsonpath='{.spec.clusterIP}' -n kube-system)/" k8s/deployment.yml | kubectl create -f -
您不需要在create
命令中传递名称空间,因为名称空间已经在资源文件中声明了。
上面的命令使用资源名称的缩写形式。它用了svc
而不是service
。大多数 Kubernetes 基础资源都有一个缩写。
您可以通过使用以下命令查看命名空间中生成的事件来检查部署过程:
$ kubectl get events -n first-deploy
您还可以检查名称空间中的部署状态。运行此命令以查看 pod 是否准备就绪:
$ kubectl get deployment -n first-deploy
如果一切顺利,你的结果应该如图 6-6 所示。
图 6-6
获得部署
有一个1/1 ready
意味着所需的一个 pod 正在运行,没有可察觉的错误。我说“可察觉的错误”是因为应用可能正在运行,但不能正常工作。如果您想对这个状态行更加自信,您应该在应用中实现更好的健康检查。我们将在接下来的章节中详细讨论这一点。
我希望您知道部署资源是什么样子的,这就是为什么您使用文件来创建部署,但是还有另一种方法来使用kubectl
创建默认部署。您可以使用以下命令:
$ kubectl create deployment camel-hello-minikube--image=$(kubectl get svc registry -n kube-system-o=jsonpath='{.spec.clusterIP}')/apress/camel-hello-minikube:1.0.0 --port=8080 -n first-deploy
要访问和测试该部署,您需要再次运行port-forward
。首先,让我们为这个deployment
创建一个service
。在终端窗口中,运行
$ kubectl expose deployment camel-hello-minikube -n first-deploy
这将为您创建一个ClusterIP
类型的服务,目标是唯一声明的端口。您可以通过运行以下命令来检查结果
$ kubectl get services -n first-deploy
现在您可以使用这个服务来创建带有port-forward
的隧道:
$ kubectl port-forward service/camel-hello-minikube \
-n first-deploy 8080:8080
您不需要服务来port-forward
到 pod。我在这里只使用这种方法,所以您不需要寻找 pod 的名称,也不需要演示如何公开部署。
在另一个终端中,您可以使用 cURL 测试应用:
$ curl http://localhost:8080/helloMinikube
您应该会得到如图 6-7 所示的响应。
图 6-7
卷曲响应
另外,检查容器日志。它将帮助您理解应用在 minikube 中的行为。
$ kubectl logs deployment/camel-hello-minikube -c camel-hello-minikube -n first-deploy
一旦您完成了对示例的测试,您可能想要删除所创建的内容,以便为其他测试节省资源。您可以通过运行如下命令来删除命名空间和其中的所有资源:
$ kubectl delete namespace first-deploy
夸克-迷你立方
在上一节中,我想向您介绍一些使用 minikube 和 Quarkus 扩展时的概念和可能性。您了解了如何将映像推送到外部注册表,以及如何使用port-forward
来访问 minikube 内部的应用,但是根据您想要实现的目标,还有更简单的方法来实现这一点。在这一节中,您将看到 quarkus-minikube 扩展如何在使用 minikube 进行测试时帮助您。
在开始这个新例子之前,让我们获得一个全新的 minikube 实例。按顺序运行以下命令:
$ minikube stop
$ minikube delete
$ minikube start
首先要注意的是,您没有设置--insecure
,因为您不打算使用容器注册表。相反,你要保存映像直接在 minikube 的 Docker 注册表。您将使用同一个项目camel-hello-minikube
,但是您需要向它添加一个新的扩展。
在camel-hello-minikube
目录下,运行以下命令:
camel-hello-minikube $ mvn quarkus:add-extension \
-Dextensions="quarkus-minikube"
该扩展生成 Kubernetes 清单(minikube.yaml
和minikube.json
),将用于部署应用。当与quarkus-container-image-jib
一起使用时,jib
会将映像推送到 minikube 的 Docker,而quarkus-minikube
会将清单中的deployment
和service
资源应用到 Minikube。
在构建和运行这些扩展之前,您需要 minikube 的 Docker 配置。运行以下命令公开配置:
$ minikube -p minikube docker-env
你会得到类似图 6-8 的回报。
图 6-8
minikube 坞站配置
变量DOCKER_HOST
的值是 minikube 虚拟机 IP 地址加上端口号。您可以通过运行以下命令来检查 IP
$ minikube ip
现在您已经知道如何检索配置,您需要在终端会话中加载它。这样当你运行 Quarkus 插件时,jib
将知道如何连接到所需的 Docker。
运行这个:
$ eval $(minikube -p minikube docker-env)
让我们为这个应用创建一个新的名称空间:
$ kubectl create namespace second-deploy
更改kubectl
上下文以指向这个新的名称空间。扩展kubernetes-client
将使用这个信息来确定将创建的资源发送到哪个集群和名称空间。运行以下命令进行更改:
$ kubectl config set-context --current --namespace=second-deploy
然后像这样构建应用:
camel-hello-minikube $ mvn clean package \
-Dquarkus.container-image.group=apress \
-Dquarkus.kubernetes.deploy=true \
-Dquarkus.kubernetes.deployment-target=minikube
通过设置quarkus.kubernetes.deploy=true
,扩展将构建映像并将生成的清单推送到 Kubernetes。在这种情况下,您使用quarkus.kubernetes.deployment-target=minikube
瞄准了 Minikube。
转到target/kubernetes
并查看生成的文件,如清单 6-4 所示。
---
apiVersion: v1
kind: Service
metadata:
annotations:
labels:
app.kubernetes.io/name: camel-hello-minikube
app.kubernetes.io/version: 1.0.0
name: camel-hello-minikube
spec:
ports:
- name: http
nodePort: 30254
port: 8080
targetPort: 8080
selector:
app.kubernetes.io/name: camel-hello-minikube
app.kubernetes.io/version: 1.0.0
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
labels:
app.kubernetes.io/name: camel-hello-minikube
app.kubernetes.io/version: 1.0.0
name: camel-hello-minikube
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: camel-hello-minikube
app.kubernetes.io/version: 1.0.0
template:
metadata:
annotations:
labels:
app.kubernetes.io/name: camel-hello-minikube
app.kubernetes.io/version: 1.0.0
spec:
containers:
- env:
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: apress/camel-hello-minikube:1.0.0
imagePullPolicy: IfNotPresent
name: camel-hello-minikube
ports:
- containerPort: 8080
name: http
protocol: TCP
Listing 6-4minikube.yaml
如您所见,服务和部署已经生成。该服务被配置为NodePort
类型,这意味着它将在 minikube VM 中公开一个端口,这样您就可以从安装外部访问应用。部署是标准的。
在下一节中,您将看到如何向这个文档添加不同的参数。
您可以使用这个插件通过不设置quarkus.kubernetes.deploy
来创建清单。这样,您可以自己推送清单或将其用作模板。需要记住的一件重要事情是quarkus-minikube
有一些先决条件:
-
该映像将由 minikube 的 Docker 构建。
-
应用将使用节点端口公开。
如果您不希望创建这些特征,那么您可以使用quarkus-kubernetes
扩展来为您生成清单,这样会更加灵活,但是需要更多的参数。
使用以下命令检查是否成功创建了部署:
$ kubectl get deployment -n second-deploy
在测试应用之前,我们先来看看 minikube 的 Docker。要访问虚拟机,请使用以下命令:
$ minikube ssh
一旦进入,您可以使用 Docker 命令来检查 Docker 是如何配置的,例如,检查您期望生成的映像是否在那里,如图 6-9 所示。
图 6-9
ssh 小型库会话
您可以通过键入exit
并按 Enter 键来关闭会话。
您知道扩展根据清单文件中的内容为您提供了一个NodePort
服务,但是有另一种方法可以更容易地获得这些信息。运行以下命令:
$ minikube service list
观察图 6-10 查看输出。请记住,您可能会有不同的虚拟机 IP 和端口号。
图 6-10
服务列表
要测试该应用,只需发送一个指向所示 URL 的 cURL 请求(注意,您的 IP 可能略有不同),如下所示:
$ curl http://192.168.64.10:30254/helloMinikube
完成测试后,清理环境以节省资源。像这样删除名称空间:
$ kubectl delete namespace second-deploy
应用配置
在前面的章节中,您看到了如何使用 minikube 以及如何使用 Quarkus 扩展来部署应用,但是使用了非常简单的方法。有一些非常重要的配置,您的应用必须至少被认为是生产就绪的。我们会讨论这些。
环境变量
容器化的应用通常在启动时或启动前使用环境变量来配置自己。一些映像可能在启动脚本中使用这些值;有些人可能会直接在应用中使用它们(正如您将要做的那样),但重要的是,根据传递给它的配置,单个映像可以用于不同的目的或不同的环境。让我们看看如何将扩展和 Camel 放在一起实现这个重要的特性。
正如我在第一章谈论 Quarkus 时提到的,它实现了MicroProfile Config
规范。其实,并不是 Quarkus 实现了它,而是打包在其中的 SmallRye 配置项目。该项目允许您从五个不同的来源检索配置:
-
系统属性
-
环境变量
-
放置在工作目录中的
.env
文件 -
一个放在名为
config
的目录中的application.properties
文件,位于应用运行的同一层 -
一个打包了应用代码的
application.properties
文件
当设置clientIds
和groupId
名称时,您已经在 Kafka 示例中使用了系统属性,并且您已经在大多数示例中使用了与代码打包在一起的application.properties
文件,但是现在让我们集中在使用环境变量上。当使用容器和 Kubernetes 时,这种方法特别有趣,因为在运行映像时可以很容易地改变它的值。
在这个例子中,您将使用camel-env-msg
项目。在 IDE 中打开它。检查清单 6-5 中的项目路线。
public class EnvMSGRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
rest("/helloEnv")
.get()
.route()
.routeId("env-msg")
.log("Request Received")
.setBody(constant("{{app.msg}}"))
.endRest();
}
}
Listing 6-5EnvMSGRoute.java File
这是另一个简单的路线示例,重点向您展示 Camel 和 Quarkus 中的特定配置。这里有一个 REST 路由,它返回一个在常量中定义的消息。该常量引用一个名为app.msg
的属性键。查看清单 6-6 中的application.properties
文件。
# Route properties
application.message=Default Message
app.msg=${application.message}
# Kubernetes configuration
quarkus.container-image.group=apress
quarkus.kubernetes.env.vars.application-message=Message from ENV var
Listing 6-6Application.properties File
从应用与路线相关的属性开始,您有两个值。路由使用app.msg
条目来检索一个值,但是这个条目值是从另一个名为application.message
的属性中检索的。后者由 SmallRye 实现解决,因此您可以用它来定义默认值,也可以用它来替换环境变量。该库将寻找不同的模式来找到匹配的环境变量,但最常用的模式是由下划线(_)分隔的大写单词。
最后一部分是与您将如何为这个项目生成部署资源相关的属性。该属性可以这样理解:
quarkus.kubernetes.env.vars.[KEY]=[VALUE]
需要注意的一件重要事情是密钥声明中使用的模式。它将按照以下约定生成一个环境变量:
-
它用下划线替换每个既不是字母数字也不是下划线的字符。
-
它将名称转换为大写。
像application-message
这样的键会变成一个名为APPLICATION_MESSAGE
的变量。
让我们在将应用发送到 Minikube 之前在本地测试它。在终端窗口中,导航到camel-env-msg
目录并启动应用,如下所示:
camel-env-msg $ mvn quarkus:dev
在另一个终端窗口中,发送如下请求来测试应用:
$ curl -w "\n" http://localhost:8080/helloEnv
您将得到如图 6-11 所示的响应。
图 6-11
HelloEnv 响应
停止应用。在同一个终端中,声明一个环境变量,然后启动应用:
camel-env-msg $ export APPLICATION_MESSAGE='new message'
camel-env-msg $ mvn quarkus:dev
再次测试应用。这一次你的反应应该如图 6-12 所示。
图 6-12
新 helloEnv 响应
您能够通过使用环境变量来更改应用配置,但是它在部署资源中会是什么样子呢?
像这样打包应用:
camel-env-msg $ mvn clean package \
-Dquarkus.kubernetes.deployment-target=minikube
查看target/kubernetes
目录,打开minikube.yaml
文件,如清单 6-7 所示。
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
labels:
app.kubernetes.io/name: camel-env-msg
app.kubernetes.io/version: 1.0.0
name: camel-env-msg
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: camel-env-msg
app.kubernetes.io/version: 1.0.0
template:
metadata:
annotations:
labels:
app.kubernetes.io/name: camel-env-msg
app.kubernetes.io/version: 1.0.0
spec:
containers:
- env:
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: APPLICATION_MESSAGE
value: Message from ENV var
image: apress/camel-env-msg:1.0.0
imagePullPolicy: IfNotPresent
name: camel-env-msg
ports:
- containerPort: 8080
name: http
protocol: TCP
Listing 6-7Minikube.yaml Deployment
现在,资源的定义中有了所需的变量。让我们在 minikube 上测试一下。
在同一个终端中,像这样配置 Docker 变量:
camel-env-msg $ eval $(minikube -p minikube docker-env)
为此测试创建新的命名空间:
$ kubectl create namespace third-deploy
将您的客户端上下文设置为新的名称空间:
$ kubectl config set-context --current --namespace=third-deploy
然后您可以像这样部署应用:
camel-env-msg $ mvn clean package \
-Dquarkus.kubernetes.deployment-target=minikube \
-Dquarkus.kubernetes.deploy=true
部署完成后,使用以下命令获取 URL 信息
$ minikube service list
在我的例子中,结果是图 6-13 。
图 6-13
服务列表结果
在我的环境中,测试调用是这样的:
$ curl http://192.168.64.10:30410/helloEnv
预期的响应是资源定义中设置的“Message from ENV var
”,但是一旦构建了映像,您就可以对部署定义进行更改,并更改应用的行为方式。
您可以用多种方式更改资源。例如,您可以使用kubectl edit deployment ${name}
并使用命令行编辑器手动编辑资源,但是对于这个例子,您可以使用更直接的方法。
运行以下命令来更改部署:
$ kubectl patch deploy camel-env-msg --type='json' -p='[{"op": "replace","path":"/spec/template/spec/containers/0/env/1/value", "value":"new patched message"}]'
我没有在调用中传递名称空间,因为上下文已经设置为正确的名称空间。如果您更改了客户端配置中的上下文,只需在命令中添加-n third-deploy
。
修补部署将导致新的部署发生。通过运行以下命令检查事件,您可能会看到这一点:
$ kubectl get events
尝试另一个请求。您应该会得到如图 6-14 所示的响应。
图 6-14
修补的消息
完成测试后,您可以删除名称空间:
$ kubectl delete namespace third-deploy
这个例子的目的是向您展示使用环境变量来配置您的 Quarkus 应用是可能的,以及使用扩展来创建具有这些定义的部署资源是多么容易。
这里您在资源定义中设置变量,但是 Kubernetes 比这更灵活。您可以使用configMaps
或secrets
作为变量的值,尽管这不会改变应用访问变量的方式。
配置映射和机密
配置映射和机密是 Kubernetes 资源,允许用户从配置中分离映像。它们是一个键值文件,可以承载简单的字符串值来完成文件。两者的区别在于,机密通常用于保存更敏感的数据,因为默认情况下,机密存储为未加密的 base64 编码字符串。让我们看看如何使用 Quarkus 和 Camel 的这些资源。
对于这个例子,您将使用camel-cm-secret
项目。在您的 IDE 中打开它,让我们分析这个项目路径,如清单 6-8 所示。
public class ConfigMapSecretRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
rest("/ConfigMapSecret")
.get()
.route()
.routeId("cm-secret")
.log("Request Received")
.choice()
.when(header("password").isEqualTo(constant("{{password}}" )))
.setBody(constant("{{application.message}}"))
.log("Authorized")
.otherwise()
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(403))
.log("Not Authorized")
.endRest();
}
}
Listing 6-8ConfigMapSecretRoute.java File
您将使用这个 REST 路由来模拟一个身份验证过程。它将检查“password
”报头是否等于给定值,如果是,将向客户端发送一条消息。如果报头不存在或者不等于预期值,将向客户端返回一个403
响应代码。
使用谓词绝对不是什么新鲜事,但是新的技巧就在这个项目application.properties
文件中,如清单 6-9 所示。
# Route properties
%dev.application.message=Authorized
%dev.password=test
# Kubernetes configuration
quarkus.container-image.group=apress
## Secret mount
quarkus.kubernetes.mounts.my-volume.path=/work/config/application.properties
quarkus.kubernetes.mounts.my-volume.sub-path=application.properties
quarkus.kubernetes.secret-volumes.my-volume.secret-name=app-secret
## ConfigMap Environment Variable
quarkus.kubernetes.env.mapping.application-message.from-configmap=app-config
quarkus.kubernetes.env.mapping.application-message.with-key=app.msg
Listing 6-9Camel-cm-secret application.properties File
从路径属性开始,这里有一些新的东西。Quarkus 允许您使用相同的application.properties
文件来声明不同概要文件的属性。每个概要文件都用于特定的环境,比如开发、测试和生产。在这个例子中,您使用的是一个dev
概要文件,它将在您使用插件quarkus:dev
运行代码时使用,但是在运行 jar 时不使用。
在秘密挂载部分,您将在/work/config
目录下挂载一个秘密作为属性文件,并将其作为application.properties
进行加载。为了便于理解,您可以像这样阅读属性键:
quarkus.kubernetes.mounts.[volume name].path
quarkus.kubernetes.mounts.[volume name].sub-path
quarkus.kubernetes.secret-volumes.[volume name].secret-name
您已经看到了如何在 Quarkus 中使用环境变量,但是这里您以不同的方式使用它们。您将使用属性键从configMap
中获取值,而不是直接设置值。你可以这样读密钥:
quarkus.kubernetes.env.mapping.[ENV VAR].from-configmap
quarkus.kubernetes.env.mapping.[ENV VAR].with-key
让我们看看部署定义是什么样子的。像这样打包应用:
camel-cm-secret $ mvn clean package \
-Dquarkus.kubernetes.deployment-target=minikube
查看清单 6-10 中的target/kubenetes/minikube.yaml
文件。
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
labels:
app.kubernetes.io/name: camel-cm-secret
app.kubernetes.io/version: 1.0.0
name: camel-cm-secret
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: camel-cm-secret
app.kubernetes.io/version: 1.0.0
template:
metadata:
annotations:
labels:
app.kubernetes.io/name: camel-cm-secret
app.kubernetes.io/version: 1.0.0
spec:
containers:
- env:
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: APPLICATION_MESSAGE
valueFrom:
configMapKeyRef:
key: app.msg
name: app-config
image: apress/camel-cm-secret:1.0.0
imagePullPolicy: IfNotPresent
name: camel-cm-secret
ports:
- containerPort: 8080
name: http
protocol: TCP
volumeMounts:
- mountPath: /work/config/application.properties
name: my-volume
readOnly: false
subPath: application.properties
volumes:
- name: my-volume
secret:
defaultMode: 384
optional: false
secretName: app-secret
Listing 6-10Camel-cm-secret minikube.yaml Deployment
您有一个环境变量APPLICATION_MESSAGE
,使用一个configMap
引用作为它的值,并有一个卷来挂载这个秘密。现在您可以开始设置测试环境了。
首先,您需要创建一个新的名称空间来测试这个应用。在终端窗口中,运行以下命令:
$ kubectl create namespace forth-deploy
将客户端上下文设置为新创建的名称空间:
$ kubectl config set-context --current --namespace=forth-deploy
应用需要在名称空间中配置一个configMap
和一个密码。让我们提供它们,从秘密开始:
$ echo "password=admin" >> application.properties
$ kubectl create secret generic app-secret \
--from-file=application.properties
$ rm application.properties
然后像这样创建configMap
:
$ kubectl create cm app-config \
--from-literal=app.msg="Message from CM"
要部署应用,请导航到camel-cm-secret
目录。这次您将通过设置quarkus.kubernetes.node-port
属性来设置您将使用哪个node-port
:
camel-cm-secret $ eval $(minikube -p minikube docker-env)
camel-cm-secret $ mvn clean package \
-Dquarkus.kubernetes.deployment-target=minikube \
-Dquarkus.kubernetes.deploy=true \
-Dquarkus.kubernetes.node-port=30241
通过设置node-port
,您现在有了一个可预测的地址来发送请求。您可以使用以下命令在您的计算机中进行测试:
$ curl http://$(minikube ip):30241/ConfigMapSecret \
-H "password: admin"
期望收到与图 6-15 相同的信息。
图 6-15
ConfigMapSecret 响应
您可以通过发送错误的密码或更改密码或configMap
来继续测试,但请记住,对这些资源的更改不会触发新的部署。如果您已经更改了这些资源,您可以像这样“弹跳”应用窗格:
$ kubectl delete pod ${pod-name}
用 pod 名称替换变量${pod-name}
。您可以使用以下命令获取名称:
$ kubectl get pods
当您删除一个 pod 时,控制器将创建一个新的 pod 来替换已删除的 pod,从而保持部署的定义的所需状态。部署 pod 时会评估卷和环境变量值。
完成测试后,删除名称空间:
$ kubectl delete namespace forth-deploy
运行状况检查和探测
运行状况检查对于监控生产中的应用非常重要。您需要知道应用是否启动并运行,如果没有,则采取正确的措施。当您处理在 Kubernetes 中运行的容器化应用时,这个功能就变得必不可少了。我们来看看为什么。
使用 Kubernetes 的美妙之处在于,它为我们实现了自动化,使我们的应用保持运行,同时还具有可伸缩性。要做到这一点,Kubernetes 需要知道如何评估应用是否有响应并准备好接收请求。检查图 6-16 。
图 6-16
服务负载均衡
集群中的客户端应用通常会使用services
通过 name 访问其他 pods。虽然services
只是对 Kubernetes 如何处理服务发现和负载均衡的一个抽象,但您需要知道的重要一点是,它们有能力识别一个端点(pod)是否准备好接收请求。在图 6-16 中,第二个 pod 可能是一个新的 pod,因为部署被扩展,或者它可能是第二个副本,因为它进入失败状态而被重新启动。在这两种情况下,应用都没有准备好接收请求,因此服务层会将所有请求重定向到第一个 pod。
为了识别应用的状态,Kubernetes 定义了称为 probes 的测试例程。有三个:就绪、活跃度和启动。
Readiness
和Startup
是用于识别应用是否准备好接收连接的探针。对于在首次初始化时可能需要额外启动时间的遗留应用,启动探测器更为理想。在本书中,您将不会涉及启动探针,因为您正在创建启动速度非常快的现代应用。
Liveness
是检查应用是否响应的测试。通过这种方式,您可以确定正在运行的应用是否没有陷入死锁,如果是,就重新启动它。
您可以使用 HTTP 调用、TPC 端口检查或在容器中执行命令来执行测试。关于容错、尝试之间的延迟、超时等有不同的参数,使得这些诊断足够灵活,可以处理不同类型的应用。当研究这个例子时,你会看到一些可能性。
在应用方面,您需要提供测试实现。这正是你将在camel-health-check
项目中看到的。在 IDE 中打开它。
让我们从分析这个项目中使用的依赖项开始。打开清单 6-11 所示的pom.xml
文件。
...
<dependencies>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-container-image-jib</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-minikube</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>
</dependencies>
...
Listing 6-11camel-health-check Dependencies
Camel 提供了创建健康检查的机制,但是您将要使用的是另一个 SmallRye 项目,它实现了微概要健康规范 SmallRye Health。您将使用它,因为它非常灵活,而且还集成了 Kubernetes 插件,所以 Quarkus 配置将反映在生成的Deployment
文档中,而不必添加更多配置。
看一下清单 6-12 所示的测试路线。
public class HealthCheckRoute extends RouteBuilder {
@Override
public void configure() throws Exception {
rest("/healthApp")
.get()
.route()
.routeId("health-route")
.log("Request Received")
.setBody(constant("{{application.message}}"))
.endRest();
}
}
Listing 6-12HealthCheckRoute.java File
这是另一种“Hello World”类型的路线,因为你真正需要学习的不是路线,而是你如何处理健康检查。
首先打开清单 6-13 中所示的AppReadiness
类。
@Readiness
@Singleton
public class AppReadiness implements HealthCheck {
private static final Logger LOG = Logger.getLogger(AppReadiness.class);
@Inject
CamelContext context;
@Override
public HealthCheckResponse call() {
LOG.trace("Testing Readiness");
HealthCheckResponseBuilder builder;
if(context.isStarted()){
builder = HealthCheckResponse.named("Context UP").up();
}else{
builder = HealthCheckResponse.named("Context Down").down();
}
return builder.build();
}
}
Listing 6-13AppReadiness.java File
首先实现HealthCheck
接口,它定义了一个方法call()
。此方法用于确定应用是否准备好接收连接。因为这是一个简单的 REST 应用,所以就绪条件是 Camel 上下文被启动。为了检查上下文,您在 Singleton bean 中注入了上下文,并使用上下文方法isStarted()
来进行验证。根据返回的方法,给出肯定(up()
)或否定(down()
) HealCheckResponse
。为了使这个HealthCheck
有效,您只需要添加注释@Readiness
。
现在让我们看看活性测试是如何完成的。参见清单 6-14 。
@Liveness
@Singleton
public class AppLiveness implements HealthCheck {
private static final Logger LOG = Logger.getLogger(AppLiveness.class);
@Inject
CamelContext context;
@Override
public HealthCheckResponse call() {
LOG.trace("Testing Liveness");
HealthCheckResponseBuilder builder;
if(!context.isStopped()){
builder = HealthCheckResponse.named("Camel UP").up();
}else{
builder = HealthCheckResponse.named("Camel DOWN").down();
}
return builder.build();
}
}
Listing 6-14AppLiveness.java File
这个健康检查与AppReadiness
非常相似,但是不是检查上下文是否启动,而是检查上下文是否没有停止。如果上下文停止,这意味着您的路由没有运行,因此需要重新启动应用。
要将这种健康检查作为您的活性测试,您只需要向类添加@Liveness
注释。
Camel 上下文只有在您编程时才会停止,或者当应用收到 SIGTERM 以正常关闭时才会停止,但它仍然是如何实现活跃度的一个示例。
您可能已经注意到,在两次运行状况检查中都有跟踪日志。这些日志帮助您了解 Kubelet 是如何调用这些测试的。看一下application.properties
文件。您只为运行状况检查启用跟踪日志。参见清单 6-15 。
# Quarkus properties
quarkus.log.category."com.apress.integration.health".level
=TRACE
quarkus.log.min-level=TRACE
# Route properties
application.message=Hello from HealthApp
# Kubernetes configuration
quarkus.container-image.group=apress
Listing 6-15Camel-health-check application.properties File
让我们生成部署定义。在终端中,导航到camel-health-check
目录并运行以下命令:
camel-health-check $ mvn clean package \
-Dquarkus.kubernetes.deployment-target=minikube
打开生成的文件,target/kubernetes/minikube.yaml
。让我们检查一下Deployment
中的 pod 定义。见清单 6-16 。
...
template:
metadata:
annotations:
labels:
app.kubernetes.io/name: camel-health-check
app.kubernetes.io/version: 1.0.0
spec:
containers:
- env:
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: apress/camel-health-check:1.0.0
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 3
httpGet:
path: /q/health/live
port: 8080
scheme: HTTP
initialDelaySeconds: 0
periodSeconds: 30
successThreshold: 1
timeoutSeconds: 10
name: camel-health-check
ports:
- containerPort: 8080
name: http
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /q/health/ready
port: 8080
scheme: HTTP
initialDelaySeconds: 0
periodSeconds: 30
successThreshold: 1
timeoutSeconds: 10
Listing 6-16Deployment Pod Spec
正如您所看到的,就绪性和活性探测是用所有必要的信息声明的,使用测试参数的默认值。这些默认值足以让您在 minikube 中测试这个特定的应用。这个应用又轻又快,所以在容器启动后第一次尝试有零秒的延迟不是问题,因为应用在几毫秒内启动。测试将在 30 秒的时间间隔内进行,只有在连续三次失败后,探头才会被视为失败。要回到成功状态,只需要一次成功的尝试。
这些参数很重要,因为您不希望在考虑应用失败时过于激进。您的访问选择可能会降低应用的性能,但它可能仍在正常运行,重新启动应用可能只会加剧问题。
您尝试使用此配置来生成最佳配置,以允许 pod 在准备就绪时尽快接收连接,并在 pod 无法避免将错误传播到客户端应用时尽快进行识别。达到这种最佳配置的唯一方法是在不同的场景下测试应用。
要测试这个应用,打开一个终端窗口并导航到camel-health-check/
目录。首先为该部署创建一个新的名称空间:
camel-health-check $ kubectl create namespace fifth-deploy
将客户端上下文设置为新的名称空间:
camel-health-check $ kubectl config set-context--current --namespace=fifth-deploy
在运行部署之前,设置 Docker 变量:
camel-health-check $ eval $(minikube -p minikube docker-env)
部署应用:
camel-health-check $ mvn clean package \
-Dquarkus.kubernetes.deployment-target=minikube \
-Dquarkus.kubernetes.deploy=true \
-Dquarkus.kubernetes.node-port=30241
您可以使用此命令来测试应用:
$ curl http://$(minikube ip):30241/healthApp
测试有助于检查部署是否顺利,但是您在这里看到的是如何调用健康检查。为此,您需要检查日志。您可以通过运行以下命令来检查它们:
$ kubectl logs -f deploy/camel-health-check
使用-f
将允许您持续跟踪出现的日志。一旦你得到一些条目,比如清单 6-17 ,你可以使用 Control+ C 来停止它
2021-06-16 23:45:20,975 INFO [io.quarkus] (main) camel-health-check 1.0.0 on JVM (powered by Quarkus 1.13.0.Final) started in 3.051s. Listening on: http://0.0.0.0:8080
2021-06-16 23:45:20,976 INFO [io.quarkus] (main) Profile prod activated.
2021-06-16 23:45:20,977 INFO [io.quarkus] (main) Installed features: [camel-attachments, camel-core, camel-platform-http, camel-rest, camel-support-common, cdi, kubernetes, mutiny, smallrye-context-propagation, smallrye-health, vertx, vertx-web]
2021-06-16 23:45:43,078 TRACE [com.apr.int.hea.AppReadiness] (vert.x-worker-thread-0) Testing Readiness
2021-06-16 23:45:46,522 TRACE [com.apr.int.hea.AppLiveness] (vert.x-worker-thread-1) Testing Liveness
2021-06-16 23:46:12,661 TRACE [com.apr.int.hea.AppReadiness] (vert.x-worker-thread-2) Testing Readiness
2021-06-16 23:46:16,520 TRACE [com.apr.int.hea.AppLiveness] (vert.x-worker-thread-3) Testing Liveness
Listing 6-17Camel-health-check Logs Output
您可以看到,在应用正常启动后,每隔 30 秒就会对应用进行一次新的就绪性和活性测试。
另一个可以进行的测试是并排打开两个终端窗口。在一个终端中,运行以下命令来查看可用的窗格:
$ kubectl get pods -w
在第二个终端中,运行以下命令,通过添加一个副本来扩展部署:
$ kubectl scale deploy/camel-health-check --replicas=2
如图 6-17 所示,您将看到新的 pod 从Ready 0/1
移动到1/1,
需要一些时间。这是容器通过准备测试所需的时间。
图 6-17
POD 结垢
要求和限制
我谈到了调度器组件,它负责将 pod 分配给适当的节点。但是“适当”在这个上下文中是什么意思呢?除了诸如节点标签和节点特性之类的特定参数之外,它还意味着拥有容纳 pod 所需的资源量。由于调度程序无法猜测应用需要什么,所以您必须为您拥有的名称空间设置默认值,或者您可以在部署定义中设置这些值。请求和限制是告诉 Kubernetes 您认为应用应该消耗多少资源的方法。
这里您将关注两种不同的资源,CPU 和 RAM。还有其他资源可以限制使用,但是 CPU 和 RAM 是最常用于确定 pod 位置的资源。分析图 6-18 以查看 pod 放置是如何工作的。
图 6-18
pod 放置过程
在本例中,您有一个三节点集群,每个集群有 3GB 的 RAM 用于 pod 分配。您为该群集生成了一个新的 pod,只有Node 0
有足够的资源来托管这个 2GB 的 pod。
虽然图中没有展示,但 CPU 也是如此。每个节点都有其可用于 pod 分配的 CPU 容量。如果一个新的 pod 指定了它的要求,调度程序将尝试找到一个与 pod 请求的 CPU 和 RAM 数量相匹配的节点。
请求是一个 pod 预计从一个节点消耗的资源量,也是节点容量的基本衡量标准。
当 pod 定义没有声明请求时,调度程序会将它放在任何仍有资源可用的节点中,但可用资源可能不足以容纳这个新的 pod,从而导致内存不足(OOM)错误,或者在节点没有足够的 CPU 可用时影响应用性能。
您可以定义每个容器和每个箱的请求和限额。这里您将重点放在 pod 声明上,因为您将在示例中使用它。
应用资源消耗通常不是线性的。这将取决于正在处理的内容或在给定时间内的访问量,因此请求的资源量很少是实际使用的量。他们可以使用更多;他们可以少用。使用更少的资源对应用来说没有风险,因为它们将拥有正常工作所需的资源量,但这取决于使用了多少更少的资源,并且如果这种行为传播到许多 pod,您可能会有节点分配不足,其他节点缺乏资源,或者最终无法部署新的 pod,即使您在集群中有足够的物理资源。
限制用于为应用提供在最终需要时使用更多资源的灵活性,但也对可以消耗的资源量设置了严格的上限。如果一个应用试图使用超过其限制的内存,Kubelet
将终止该应用,使其重启以解决问题。
CPU 使用率不同。由于 CPU 的度量是时间分配,应用将不能使用超过其限制的 CPU。
为了实现这些概念,让我们在您的跑步迷你库上启用一些功能。首先,您需要添加一个组件来监控 pod 的资源使用情况。运行以下命令在 minikube 中安装度量服务器:
$ minikube addons enable metrics-server
一旦安装了附加组件,您就可以使用 minikube 的仪表板来可视化指标。运行以下命令在浏览器上打开它:
$ minikube dashboard
该命令将引导您进入仪表板主页,如图 6-19 所示。
图 6-19
minikube 仪表板
这个仪表板允许您在 minikube 中可视化、创建、编辑和删除 Kubernetes 资源。您一直在使用命令行,因为它使示例步骤更具可重复性,但是如果您是 GUI 爱好者,仪表板可能是一个不错的选择。
您希望在这个仪表板中看到的是您在最近一节中使用的应用消耗了多少资源。为此,请按照下列步骤操作:
-
在屏幕顶部的下拉框中。选择名称空间
fifth-deploy
。 -
在左侧面板中,单击窗格。
这就是了。您应该会看到类似于图 6-20 的内容。
图 6-20
minikube 指标仪表板
正如你所看到的,其中一个 POD 消耗了 2 毫核心的 CPU 和大约 73 兆字节的内存。
毫核心是声明 CPU 时间分配的度量。机器中的每个核心或虚拟核心都被给予总共 1000 毫核心点,与 CPU 的类型无关。这些点用于确定处理优先级,因此容器拥有的点越多,它消耗 CPU 的时间就越多。在我的例子中,我的 minikube 实例使用 MacOS 的默认值,2 个 CPU,这表示 2000 个毫核或 2 个核。
一兆字节只是表示内存分配的一种不同方式,其中 1 兆字节= 2-20 字节或 1,048,576 字节。同时 1MB 是 106 或 1,000,000 字节。如果你想知道 73Mi 在 MB 中意味着多少,只需将字节乘以 1.04858,即 76.546MB。
既然您从一开始就知道这个应用消耗了多少,那么让我们在部署配置中对它进行一些调整。
在您的 IDE 中打开camel-health-check
项目。打开application.properties
,添加以下几行:
#Requests
quarkus.kubernetes.resources.requests.memory=44Mi
quarkus.kubernetes.resources.requests.cpu=10m
#Limits
quarkus.kubernetes.resources.limits.memory=50Mi
quarkus.kubernetes.resources.limits.cpu=100m
让我们生成部署资源:
camel-health-check $ mvn clean package \
-Dquarkus.kubernetes.deployment-target=minikube
查看部署定义中的模板,如清单 6-18 所示。
...
template:
metadata:
annotations:
labels:
app.kubernetes.io/name: camel-health-check
app.kubernetes.io/version: 1.0.0
spec:
containers:
- env:
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: apress/camel-health-check:1.0.0
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 3
httpGet:
path: /q/health/live
port: 8080
scheme: HTTP
initialDelaySeconds: 0
periodSeconds: 30
successThreshold: 1
timeoutSeconds: 10
name: camel-health-check
ports:
- containerPort: 8080
name: http
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /q/health/ready
port: 8080
scheme: HTTP
initialDelaySeconds: 0
periodSeconds: 30
successThreshold: 1
timeoutSeconds: 10
resources:
limits:
cpu: 100m
memory: 50Mi
requests:
cpu: 10m
memory: 44Mi
Listing 6-18Template Definition with Request and Limits
定义被添加到 pod 中,因为使用这个插件,每个部署只有一个容器。
您知道 44Mi 比应用使用的要少,但是在这种情况下部署会发生什么情况呢?让我们通过测试来检查。
首先,删除旧的名称空间:
$ kubectl delete namespace fifth-deploy
现在让我们为这个测试创建一个新的名称空间,为客户机设置上下文,设置 Docker 变量,并部署应用。
导航到camel-health-check
目录并运行以下命令:
camel-health-check $ kubectl create namespace sixth-deploy
camel-health-check $ kubectl config set-context--current --namespace=sixth-deploy
camel-health-check $ eval $(minikube -p minikube docker-env)
camel-health-check $ mvn clean package \
-Dquarkus.kubernetes.deployment-target=minikube \
-Dquarkus.kubernetes.deploy=true \
-Dquarkus.kubernetes.node-port=30241
部署完成后,使用以下命令监视命名空间窗格:
$ kubectl get pod -w
你会看到部署永远不会正确完成,因为你没有给应用启动所需的内存量,当应用试图消耗超过其限制的内存时,Kubelet
将对其进行操作并杀死它,如图 6-21 所示。
图 6-21
OOMKilled
探测、请求和限制也需要彻底的测试,以便在预期资源消耗和访问选择下的资源消耗之间配置最佳平衡。请记住,在使用容器时,您最好水平扩展,这意味着创建更多的 pod,而不仅仅是向单个 pod 添加更多的资源。我说“理想”是因为你会发现容器不能伸缩的情况,在这些情况下只有垂直伸缩是可能的。
您已经完成了使用 minikube 的测试。此时,如果愿意,您可以停止它并删除您的实例:
$ minikube stop
$ minikube delete
摘要
这是本书的最后一章,也是大胆的一章。Kubernetes 涉及的范围太广了,但是我试图让对话更多地与应用相关,而不是管理 Kubernetes 集群的挑战。在本章中,您学习了以下内容:
-
Kubernetes 是什么以及使用它的好处
-
如何使用 minikube 从开发角度试验 Kubernetes
-
如何使用 Quarkus 扩展将 Quarkus 应用部署到 Kubernetes 中
-
将应用部署到 Kubernetes 中需要了解的应用配置
这本书的想法是为有集成需求并且必须使用容器和 Kubernetes 解决这些需求的开发人员和架构师创建一个坚实的学习路径。从如何在您的机器上运行 Camel 应用,到生成映像并将其部署到 Kubernetes,您已经看到了非常基础的内容,并提供了将来可以参考的实例。
我希望您能够使用现代技术和架构设计来创建自己的集成。我们将在未来的技术讨论中再见。