原文:Cloud Native Integration with Apache Camel

协议:CC BY-NC-SA 4.0

一、欢迎来到 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-jarquarkus.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-routepath2-route实现了相同的逻辑,并且都使用了logger-route,但是我想让您看到的是,尽管每个交换在不同的路由中生成日志,但是path-routerlogger- 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 类,MyObjectAnotherObject,它们有一个名为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 :

  1. 客户端通过将资源所有者的用户代理定向到授权端点来启动流。客户端包括其客户端标识符、请求的范围、本地状态和重定向 URI,一旦授权(或拒绝)访问,授权服务器就会将用户代理发送回该重定向。

  2. 授权服务器对资源所有者进行身份验证(通过用户代理),并确定资源所有者是同意还是拒绝客户端的访问请求。

  3. 假设资源所有者授权访问,授权服务器使用之前提供的重定向 URI(在请求中或在客户端注册期间)将用户代理重定向回客户端。重定向 URI 包括授权码和客户端先前提供的任何本地状态。

  4. 客户端通过包含上一步中接收到的授权代码,向授权服务器的令牌端点请求访问令牌。当发出请求时,客户端向授权服务器进行身份验证。客户端包括重定向 URI,用于获得验证的授权码。

  5. 授权服务器对客户端进行身份验证,验证授权代码,并确保 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.0OpenID 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 MapSingleton 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

在这里,您创建了一个策略,向任何用户授予角色viewedit的权限,并将这个策略映射到/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-timercamel-quarkus-bean扩展。在整个示例中,您一直在使用它们。新的是quarkus-oidc-clientcamel-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 的概念和集成模式的了解越来越多,您将通过讨论持久性来继续您的旅程。

该描述摘自 https://datatracker.ietf.org/doc/html/rfc6749#section-4.1 的 OAuth 2.0 规范

2

此描述摘自 OpenID 网站, https://openid.net/connect/

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐