vlambda博客
学习文章列表

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

Building Applications Using the MicroProfile API

此时,您应该对如何在 Quarkus 应用程序中使用最常见的 Java API(CDI、REST、JSON、JPA)有了很好的理解。在本章中,我们将添加一大堆称为 MicroProfile 规范的 API。通过掌握本章的主题,您将能够构建 基于 Java EE 核心特性构建的组件,从而允许在 实现微服务、提高应用程序的健壮性并降低 过度设计和重新发明相同模式的风险时获得直接的开发体验。 您将学习的主题包括如何为您的服务添加容错和健康检查,如何检查服务的指标,如何跟踪和记录它们,以及如何为您的服务创建精益 REST 客户端端点。其他功能,例如配置、安全性和反应式消息传递,将在接下来的章节中介绍。

在本章中,我们将介绍以下主题:

  • An overview of the MicroProfile API and how it can complement the Enterprise API
  • How the MicroProfile API can fit into your Quarkus projects
  • Some exposure on how to run the MicroProfile API in the cloud

Getting started with the MicroProfile API

Java Enterprise API 是一组用于构建应用程序的优秀技术,但它一直缺少一些您想要将应用程序迁移到云中所需的功能。例如,没有特定的 API 来处理可以注入服务的配置属性,也没有正式的方式来描述客户端如何与 REST 端点交互。此外,包含一些功能肯定会有所帮助,以便我们可以监控应用程序的健康或负载平衡请求;这些目前由具有定制技术的供应商管理。

Eclipse MicroProfile 项目是一项由顶级应用程序供应商推动的协作计划,旨在优化 Java 应用程序的 Enterprise API,包括我们在此处提到的所有功能。

Eclipse MicroProfile 规范的鸟瞰图显示了该环境在 3.2 版本中的丰富程度:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

在本章中,我们将深入研究 MicroProfile 规范的以下领域:

  • The Eclipse MicroProfile Configuration: Provides a unified way to configure your services by injecting the configuration data from a static file or from environment variables.
  • The Eclipse MicroProfile Health Check: Provides the ability to probe the state of a service; for example, whether it's running or not, whether it lacks disk space, or whether there is an issue with the database connection.
  • The Eclipse MicroProfile Fault Tolerance: Allows you to define a strategy in the event of your services failing, for example, configuring timeouts, retry policies, fallback methods, and Circuit Breaker processing.
  • The Eclipse MicroProfile Metrics: Provides a standard way for MicroProfile services to export monitoring data to external agents. Metrics also provide a common Java API that exposes their telemetry data.
  • The Eclipse MicroProfile OpenAPI: Provides a set of Java interfaces to document your services in a standard way.
  • The Eclipse MicroProfile OpenTracing: Provides a set of instrumentation libraries for tracing components such as JAX-RS and CDI.
  • The Eclipse MicroProfile Rest Client: This builds upon the JAX-RS API and provides a type safe, unified approach for invoking RESTful services over HTTP.

尽管本章没有讨论,但 Quarkus 还支持 MicroProfile JWT RBAC,它概述了使用基于 OpenID Connect (OIDC) 的建议JSON Web Tokens (JWTs) 用于您的服务端点中的基于角色的访问控制 (RBAC)。在下一章关于安全性的内容中,我们将更详细地讨论这个主题。

Getting started with MicroProfile projects

要了解单个 MicroProfile API,您将需要以下项目,这些项目可以在本书 GitHub 存储库的 Chapter06 文件夹下找到:

  • fault-tolerance: A project that shows us how to use the MicroProfile Fault Tolerance API
  • health: A project that focuses on the MicroProfile Health Check API
  • openapi-swagger: A project that implements OpenAPI interfaces
  • opentracing: A project that implements the OpenTracing API
  • rest-client: A project that focuses on the MicroProfile REST Client API

前面的大部分项目都源自我们在 第 5 章使用 Quarkus 管理数据持久性。因此,一个基本要求是启动并运行 PostgreSQL 数据库,以便我们可以运行我们的项目。我们提醒您,只需一行脚本即可完成此操作:

docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name quarkus_test -e POSTGRES_USER=quarkus -e POSTGRES_PASSWORD=quarkus -e POSTGRES_DB=quarkusdb -p 5432:5432 postgres:10.5

接下来,我们建议将整个 Chapter06 文件夹导入到您的 IDE 中,这样您就可以在继续本章的过程中获得完整的项目集。话虽如此,我们将从讨论 MicroProfile 健康检查 API 开始。

The Eclipse MicroProfile Health Check

在云环境中,必须允许服务向定义的端点报告并最终发布整体健康状态。这可以通过 MicroProfile 健康检查来实现,它允许服务在可用时将整体状态报告为 "UP",如果不可用则报告为 "DOWN"。服务编排器可以收集此信息,然后可以使用运行状况报告做出决策。

让我们通过 Chapter06/health 示例将这些概念付诸实践。首先,为了使用健康扩展,我们在 pom.xml 文件中包含以下依赖项:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-smallrye-health</artifactId>
</dependency>

一旦前面的库可用,我们可以添加 org.eclipse.microprofile.health.HealthCheck 接口的实现,它允许我们检查服务状态。以下是 DBHealthCheck 类,用于验证 PostgreSQL 数据库连接的状态:

@Health
@ApplicationScoped
public class DBHealthCheck implements HealthCheck {

    @ConfigProperty(name = "db.host")
    String host;

    @ConfigProperty(name = "db.port")
    Integer port;

    @Override
    public HealthCheckResponse call() {

        HealthCheckResponseBuilder responseBuilder = 
        HealthCheckResponse.named("Database connection
         health check");

        try {
            serverListening(host,port);
            responseBuilder.up();
        } catch (Exception e) {
            // cannot access the database
            responseBuilder.down()
                    .withData("error", e.getMessage());
        }
        return responseBuilder.build();
    }

    private void serverListening(String host, int port) throws 
     IOException
    {
        Socket s = new Socket(host, port);
        s.close();
    }
}

此类包含 MicroProfile 规范的两个核心实现:

  1. First of all, we have the @Health annotation, which works in combination with the @ApplicationScoped CDI context to return the health status check each time a request to http://localhost:9080/health is received.
  2. This class also uses the MicroProfile Configuration API to inject the PostgreSQL database host and port it into the bean. The following is an excerpt from the application.properties file:
db.host=${POSTGRESQL_SERVICE_HOST:localhost}
db.port=${POSTGRESQL_SERVICE_PORT:5432}
quarkus.datasource.url=jdbc:postgresql://${db.host}:${db.port}/postgres
如您所见,如果 POSTGRESQL_SERVICE_HOSTPOSTGRESQL_SERVICE_PORT 环境变量未设置,默认值( localhost5432) 被使用并存储在 db.hostdb.port 变量。

通过 TCP 套接字到达目标主机和端口,如果尝试成功,将返回 responseBuilder.up()。否则,responseBuilder.down() 将指示失败。

您可以使用以下命令启动 Quarkus 项目:

$ mvn compile quarkus:dev

然后,假设数据库已启动并正在运行,让我们尝试访问 http://localhost:9080/health 端点:

curl http://localhost:8080/health
{
    "status": "UP",
    "checks": [
        {
            "name": "Database connection health check",
            "status": "UP"
        },
        {
            "name": "File system Readiness check",
            "status": "UP"
        }
    ]
}

响应确认数据库连接的状态。我们还要验证数据库不可用的情况。来自 PostgreSQL shell 的简单 Ctrl + C 将发送适当的信号来停止进程。您应该在控制台上看到以下输出:

2019-07-27 09:47:25.564 UTC [54] LOG:  shutting down
2019-07-27 09:47:25.601 UTC [1] LOG:  database system is shut down

现在,再次通过 /health 端点检查数据库连接的状态:

{
    "status": "DOWN",
    "checks": [
        {
            "name": "Database connection health check",
            "status": "DOWN",
            "data": {
                "error": "Connection refused (Connection refused)"
            }
        }
    ]
}

从上面的输出可以看出,返回的 JSON 将状态更改为 "DOWN" 并在错误字段中设置错误消息。这个例子设置了我们的第一个里程碑:检查应用程序的健康状况。我们可以通过使用 liveness 和 readiness 检查来进一步完善我们的健康检查策略,我们将在下一节中讨论。

Using liveness and readiness checks

根据最新的 MicroProfile 规范,健康检查现在将与更具体的模型一起使用,以帮助我们确定潜在问题的原因。因此,建议您将旧版 @HealthCheck 迁移到以下检查之一:

  • Readiness checks: This check can indicate that a service is temporarily unable to serve traffic. This can be due to, for example, the fact that an application may be loading some configuration or data. In such cases, you don't want to shut down the application but, at the same time, you don't want to send it requests either. Readiness checks are supposed to cover this scenario.
  • Liveness checks: Services running 24/7 can sometimes undergo a transition to broken states, for example, because they have hit OutOfMemoryError. Therefore, they cannot recover except by being restarted. You can, however, be notified of this scenario by defining a liveness check that probes the liveness of the service.

为了实现这两种检查,您可以简单地将 @org.eclipse.microprofile.health.HealthCheck 注释替换为更具体的注释,例如 @org.eclipse.microprofile.health.Liveness @org.eclipse.microprofile.health.Readiness

在下面的例子中,我们实现了一个 @Readiness 检查来验证一个锁文件是否存在(例如,由于一个挂起的任务)并发出一个 "DOWN" 状态当检测到此文件时:

@Readiness
@ApplicationScoped
public class ReadinessHealthCheck implements HealthCheck {
 
     @Override
     public HealthCheckResponse call() {
         HealthCheckResponseBuilder responseBuilder = 
          HealthCheckResponse.named("File system Readiness check");
 
         boolean tempFileExists = 
          Files.exists(Paths.get("/tmp/tmp.lck"));
         if (!tempFileExists) {
             responseBuilder.up();
         }
         else {
             responseBuilder.down().withData("error", "Lock file 
              detected!");
         }
         return responseBuilder.build(); 
     }
}

就绪检查通过 "/health/ready" URI 进行验证。您可以通过请求以下 URL 来检查这一点:http://localhost:8080/health/ready

$ curl http://localhost:8080/health/ready

如果未检测到文件,您将看到类似于以下输出的内容:

{
     "status": "UP",
     "checks": [
         {
             "name": "File system Readiness check",
             "status": "UP"
         }
     ]
 }

现在,让我们学习如何向我们的服务添加活动性检查。我们将检查运行服务所需的可用内存量,并根据我们设置为 1 GB 的某个内存阈值返回活动检查:

@Liveness
@ApplicationScoped
public class MemoryHealthCheck implements HealthCheck {
    long threshold = 1024000000;
    @Override
    public HealthCheckResponse call() {
        HealthCheckResponseBuilder responseBuilder =
          HealthCheckResponse.named("MemoryHealthCheck 
        Liveness check");
        long freeMemory = Runtime.getRuntime().freeMemory();

        if (freeMemory >= threshold) {
            responseBuilder.up();
        }
        else {
            responseBuilder.down()
                    .withData("error", "Not enough free memory!
                     Please restart application");
        }
        return responseBuilder.build();
    }

}

您现在可以使用 cURL 验证服务的活跃性,如下所示:

curl http://localhost:8080/health/live

由于默认 Quarkus JVM 设置不允许我们在阈值中设置的内存量,因此服务的状态将指示 "DOWN",如下所示:

{
     "status": "DOWN",
     "checks": [
         {
             "name": "MemoryHealthCheck Liveness check",
             "status": "DOWN",
             "data": {
                 "error": "Not enough free memory! Please restart 
                  application"
             }
         }
     ]
 }

在我们继续检查清单中的下一个 API 之前,有必要检查一下如何使用 Kubernetes 探测检查在云环境中触发健康检查。我们将在下一节中学习如何做到这一点。

Letting OpenShift manage unhealthy services

在到目前为止的示例中,我们已经了解了如何检测不同的健康检查场景。运行 Kubernetes 原生环境的最大优势之一是您可以自动对应用程序状态的变化做出反应。更具体地说,可以通过应用程序的部署描述符探测以下检查:

  • Liveness probe: Kubernetes provides a liveness probe to determine whether the container that it's been configured in is still running. Should the liveness probe fail, the kubelet agent kills and restarts the container.
  • Readiness probe: Kubernetes provides the readiness probe to signal that an application is temporarily unable to serve traffic, for example, because a configuration is being loaded. In such cases, you don't want to stop the application, but you don't want to allow any requests in either.

如您所见,上述探测与最新规范中定义的 MicroProfile 健康检查相匹配。作为概念验证,我们将我们的示例应用程序作为二进制构建部署到 MiniShift 中。像往常一样,我们将从 shell(或 Web 控制台,如果您更喜欢这种方式)创建一个新项目:

oc new-project quarkus-microprofile

您可能还记得,我们需要将一个 PostgreSQL 应用程序添加到我们的项目中,我们的检查将发现该应用程序:

oc new-app -e POSTGRESQL_USER=quarkus -e POSTGRESQL_PASSWORD=quarkus -e POSTGRESQL_DATABASE=quarkusdb postgresql

然后,您终于可以将我们刚刚构建的 Quarkus MicroProfile Health 应用程序推送到云端。以下脚本将用于此目的:

 # Build native application
 mvn package -Pnative -Dnative-image.docker-build=true -DskipTests=true
 
 # Create a new Binary Build named "quarkus-microprofile"
 oc new-build --binary --name=quarkus-microprofile -l app=quarkus-microprofile
 
 # Set the dockerfilePath attribute into the Build Configuration
 oc patch bc/quarkus-microprofile -p '{"spec":{"strategy":{"dockerStrategy":{"dockerfilePath":"src/main/docker/Dockerfile.native"}}}}'
 
 # Start the build, uploading content from the local folder:
 oc start-build quarkus-microprofile --from-dir=. --follow
 
 # Create a new Application, using as Input the "quarkus-microprofile" Image Stream:
 oc new-app --image-stream=quarkus-microprofile:latest
 
 # Expose the Service through a Route:
 oc expose svc/quarkus-microprofile

前面的脚本对您来说应该不是什么新鲜事,所以让我们转到 OpenShift 控制台,我们可以在其中检查项目的状态:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

现在,检查项目的 Deployments 配置并选择 Edit Health Checks

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

Health Checks UI 中,您可以选择要添加的健康检查:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

让我们从 Readiness Probe 开始。通过选择它,您将进入以下 UI:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

要选择的关键参数是 Path,它应该与我们的 MicroProfile 就绪 URI (health/ready) 匹配。除此之外,您还可以配置以下属性:

  • initialDelaySeconds: The number of seconds after the container has started before liveness or readiness probes are initiated.
  • timeoutSeconds: The number of seconds after which the probe times out. Defaults to 1 second. The minimum value is 1.

现在,让我们配置 Liveness Probe

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

除了 Path,它将是 health/live,we 可以保留其他默认值。 将您的更改另存为-是。现在,让我们尝试打破一些东西。例如,我们将在应用程序运行的 Pod 中创建一个锁定文件。这将立即触发就绪探测失败。让我们使用以下命令从 shell 中检查 Pod 列表:

$ oc get pods

返回的输出如下:

NAME                           READY     STATUS      RESTARTS   AGE
quarkus-microprofile-1-build   0/1       Completed   0          20m
quarkus-microprofile-1-rxp4r   1/1       Running     0          20m

好的,我们现在将对这个正在运行的 Pod 运行一个远程 shell:

$ oc rsh quarkus-microprofile-1-rxp4r

我们进去了。现在,创建一个名为 /tmp/tmp.lck 的文件

sh-4.4$ touch /tmp/tmp.lck

几秒钟后(取决于初始延迟设置),您的 Pod 将不再可用。您可以从 Overview 面板中看到:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

这种变化也会体现在系统事件中,可以通过oc get events命令捕获,如下:

$ oc get events
quarkus-microprofile-3-mzl6f.15b54cfc42ddb728    Pod                     spec.containers{quarkus-microprofile}    Warning   Unhealthy kubelet, localhost            
Readiness probe failed: HTTP probe failed with statuscode: 503

最后,值得一提的是,我们的应用程序还包括一个活动性检查,它验证可用内存量是否大于某个阈值。无论您是否达到阈值,活跃度探测都取决于 MiniShift 启动时允许的内存量。对 OpenShift 应用程序内存大小的题外话会让我们超出本书的范围,但值得通过查看官方文档了解更多信息:https://docs.openshift.com/container-platform/3.9/dev_guide/application_memory_sizing.html

维护应用程序的状态以使其正常运行是设计应用程序时要考虑的关键因素。另一方面,使您的服务能够对故障或性能下降做出反应同样重要。不用担心,尽管 下一节将教您如何使用 MicroProfile Fault Tolerance API 处理故障。

The Eclipse MicroProfile Fault Tolerance API

Fault Tolerance 规范是一个基本 API,可用于通过支持一组可提高应用程序弹性的策略来处理微服务的不可用性。可以使用以下容错策略:

  • Timeout: Defines a timeout for the execution of a service call
  • Fallback: Provides a contingency solution when a failure occurs
  • Retry: Allows you to retry execution based on criteria
  • Bulkhead: Isolates partial service failures while the rest of the system can still work
  • Circuit Breaker: Defines criteria for automatic fast-fails to prevent system degradation caused by overloading
  • Asynchronous: Allows us to invoke an operation asynchronously

让我们通过使用 Chapter06/fault-tolerance 示例在实践中查看这些概念。首先,为了使用容错扩展,我们在 pom.xml 文件中包含以下依赖项:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>io.quarkus:quarkus-smallrye-fault-tolerance</artifactId>
</dependency>

让我们从 Timeout、Fallback 和 Retry 策略开始,它们通常一起使用,因为它们相互补充。

Using Timeout, Fallback, and Retry to create resilient services

简单来说,@org.eclipse.microprofile.faulttolerance.Timeout 注释可用于指定允许在方法中返回响应的最长时间(以毫秒为单位)。这是一个在 250 毫秒后超时的示例 findAll 方法:

@Timeout(250)
public List<Customer> findAll() {

    randomSleep();
    return entityManager.createNamedQuery("Customers.findAll", Customer.class)
            .getResultList();
}

private void randomSleep() {
    try {
        Thread.sleep(new Random().nextInt(400));
    } catch (java.lang.InterruptedException e) {
        e.printStackTrace();
    }

}

finder 方法触发的随机睡眠可用于允许一些偶尔的执行失败。

为了减少超时或其他故障,您可以使用 @Fallback 策略装饰您的方法,以便在发生故障时指定备用执行路径:

@Timeout(250)
@Fallback(fallbackMethod = "findAllStatic")
public List<Customer> findAll() {

    randomSleep();
    return entityManager.createNamedQuery("Customers.findAll", 
    Customer.class)
            .getResultList();

}
private List<Customer> findAllStatic() {
    LOGGER.info("Building Static List of Customers");
    return buildStaticList();

}

在此示例中,如果 findAll 方法出现任何故障,我们会将执行重定向到 findAllStatic 方法。 findAllStatic 方法将返回 Customer 对象的静态列表(请查看本章的源代码示例以查看其实现)。

Applying a retry policy to your failures

有时,您的方法执行失败是由网络拥塞等临时问题引起的。如果我们确信问题可以按照我们的业务 SLA 解决,我们可以包含一个 @Retry 注释,以允许我们重复执行失败的方法一定次数。

例如,通过添加 @Retry(maxRetries = 3) 注释,我们将尝试从数据库中再加载 3 次数据,然后再使用静态客户列表:

@Timeout(250)
@Fallback(fallbackMethod = "findAllStatic")
@Retry(maxRetries = 3)
public List<Customer> findAll() {

    randomSleep();
    return entityManager.createNamedQuery("Customers.findAll", 
     Customer.class)
            .getResultList();

}

值得一提的是,@Retry 注解可以配置为仅重试特定异常的子集。这可以在以下示例中看到,我们在 RuntimeExceptionTimeoutException 上使用 @Retry

@Retry(retryOn = {RuntimeException.class, TimeoutException.class}, maxRetries = 3)

现在,让我们学习如何将名为 Circuit Breaker 的容错模式应用于我们的服务。

Circuit Breaker

Circuit Breaker 是创建弹性服务的核心模式。它可用于通过立即拒绝新请求来防止可重复的异常。 MicroProfile Fault Tolerance API 使用 @CircuitBreaker 注释来控制传入请求。软件断路器类似于电气断路器,因为它具有以下状态:

  • Closed state: A closed-circuit represents a fully functional system that's available to its clients.
  • Half-open circuit: When some failures are detected, the state can change to half-open. In this state, it checks whether the failed component is restored. If so, it closes the circuit. Otherwise, it moves to an open state.
  • Open state: An open state means the service is temporarily disabled. After checks have been made, you can verify whether it's safe to switch to a half-open state.

这是一个例子:

@CircuitBreaker(successThreshold = 5, requestVolumeThreshold = 4, failureRatio=0.75,
        delay = 1000)
public List<Orders> findAll(Long customerId) {

    possibleFailure();
    return  (List<Orders>) 
    entityManager.createNamedQuery("Orders.findAll")
            .setParameter("customerId", customerId)
            .getResultList();
}
private void possibleFailure() {
    if (new Random().nextFloat() < 0.5f) {
    throw new RuntimeException("Resource failure.");
}

在前面的示例中,@CircuitBreaker 策略适用于 OrderRepository 类的 findAll 方法。因此,如果在最后四次调用中 75% 失败,则电路将转换为打开状态。电路将保持打开状态 1,000 ms。当电路打开时,将抛出 CircuitBreakerOpenException 而不是实际调用该方法。

请注意,与重试方法一样,@CircuitBreaker 也允许我们通过 failon 注释参数定义失败标准。这可以在以下示例中看到:

@CircuitBreaker(failOn={RuntimeException.class}, successThreshold = 5, requestVolumeThreshold = 4, failureRatio=0.75, delay = 1000)

在前面的例子中,如果在方法中抛出 RuntimeException,那么 CircuitBreaker 会将执行计数为失败;否则,视为成功。

现在我们了解了核心 Fault Tolerance API 的背景,让我们学习如何使用隔板和异步模式进一步增强应用程序的健壮性。

Using asynchronous and bulkhead policies

异步编程对于企业开发者来说并不是一种新模式。但是,当与 BulkHead 策略结合使用时,您可以为您的微服务实现强大的容错模式。简而言之,如果你用 @Asynchronous 注释一个方法,它将在单独的线程上异步执行。

在下面的示例中,我们在 createOrder 方法中执行一些逻辑,该方法通过 writeSomeLogging 方法在单独的线程中进行一些调试,该方法返回一个 CompletableFuture 实例:

public void createOrder(Orders order, Customer c) {
    order.setCustomer(c);
    entityManager.persist(order);
    writeSomeLogging(order.getItem());

}
@Asynchronous
private Future writeSomeLogging(String item) {
        LOGGER.info("New Customer order at: "+new java.util.Date());
        LOGGER.info("Item: {}", item);
        return CompletableFuture.completedFuture("ok");
}

@Bulkhead@Asynchronous 一起使用时,将使用线程池隔离方法。线程池方法允许我们配置最大并发请求以及一定的队列大小,就像信号量一样。这是更新后的示例,其中包括 @Bulkhead 策略:

// maximum 5 concurrent requests allowed, maximum 10 requests allowed in the waiting queue
@Asynchronous
@Bulkhead(value = 5, waitingTaskQueue = 10)
private Future writeSomeLogging(String item) {
        LOGGER.info("New Customer order at: "+new java.util.Date());
        LOGGER.info("Item: {}", item);
        return CompletableFuture.completedFuture("ok");
}

这是对 MicroProfile API 中可用的容错策略的一次快速浏览。让我们继续下一部分,这是关于捕获服务指标的。

The Eclipse MicroProfile Metrics API

MicroProfile Metrics 规范为我们提供了一种将服​​务的监控数据导出到管理代理的统一方式。这有助于我们对一些关键统计指标进行主动检查,例如请求服务的次数和速率、每个请求的持续时间等。

让我们开始编码。在这里,我们将重点关注 Chapter06/metrics 示例。首先,为了使用度量扩展,我们在 pom.xml 文件中包含了以下依赖项:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>io.quarkus:quarkus-smallrye-metrics</artifactId>
</dependency>

现在,我们将概述已添加到 REST 服务之上的指标注释。让我们从 @Counted 注释开始,它跟踪请求的次数:

@GET
@Counted(description = "Customer list count", absolute = true)
public List<Customer> getAll() {
    return customerRepository.findAll();
}

@Counted 注释中,我们提供了描述并将 absolute 标志设置为 true,这意味着类的包名不会被添加到前面到指标名称。

让我们编译并运行应用程序:

$ mvn compile quarkus:dev

现在,让我们重新加载主页,这将触发客户列表。接下来,我们将收集一些指标。我们的指标有两个入口点:

  • http://localhost:8080/metrics: This endpoint will return all the metrics, including system metrics where the application is running.
  • http://localhost:8080/metrics/application: This endpoint will just return metrics that are emitted by the applications that have been deployed.

我们这里会选择后一个选项,如下:

$ curl http:/localhost:8080/metrics/applications

由于我们已经加载了两次主页,所以预期的输出应该如下:

 # HELP application:get_all Customer list count
 # TYPE application:get_all counter
 application:get_all 2.0

下一个注释是 @Timed 注释,它跟踪事件的持续时间。让我们也将它应用于 getAll 方法:

@Timed(name = "timerCheck", description = "How much time it takes to load the Customer list", unit = MetricUnits.MILLISECONDS)
public List<Customer> getAll() {
    return customerRepository.findAll();
}

您应该能够检索到有关上述方法的调用率的详细报告(包括速率/秒、速率/分钟速率/5 分钟,以及统计分位数指标)。为简洁起见,以下是其中的摘录:

# TYPE application:com_packt_quarkus_chapter6_customer_endpoint_timer_check_rate_per_second 
application:com_packt_quarkus_chapter6_customer_endpoint_timer_check_rate_per_second 0.04980015712212517
# TYPE application:com_packt_quarkus_chapter6_customer_endpoint_timer_check_one_min_rate_per_second 
application:com_packt_quarkus_chapter6_customer_endpoint_timer_check_one_min_rate_per_second 0.09447331054820299
# TYPE application:com_packt_quarkus_chapter6_customer_endpoint_timer_check_five_min_rate_per_second 
application:com_packt_quarkus_chapter6_customer_endpoint_timer_check_five_min_rate_per_second 0.17214159528501158

. . . .
application:com_packt_quarkus_chapter6_customer_endpoint_timer_check_seconds{quantile="0.999"} 0.004191615

另一方面,如果您只需要记录单个数据单元的基本指标,则可以使用 @Gauge 注释:

@Gauge(name = "peakOfOrders", unit = MetricUnits.NONE, description = "Highest number of orders")
public Number highestNumberOfOrders() {
    return orderRepository.countAll();
}

两个请求到达上述方法后,Gauge 指标将显示以下指标:

# HELP application:com_packt_quarkus_chapter6_order_endpoint_peak_of_orders Highest number of orders
# TYPE application:com_packt_quarkus_chapter6_order_endpoint_peak_of_orders gauge
application:com_packt_quarkus_chapter6_order_endpoint_peak_of_orders 2.0

在快速介绍了 MicroProfile 指标之后,让我们学习如何使用 OpenAPI 和 Swagger 记录我们的端点资源。

Configuring OpenAPI and the Swagger UI

OpenAPI 规范旨在提供一组 Java 接口和编程模型,可以从 JAX-RS 服务本地生成 OpenAPI v3 文档。 Quarkus 中的默认 OpenAPI 实现为可以通过 /openapi 端点生成的所有公开服务提供了一个开箱即用的标准文档。

不过,您可以使用特定注释进一步增强 JAX-RS 服务,以提供有关端点、其参数和响应的更多见解。继续代码,我们将关注 Chapter06/openapi-swagger 示例。正如您可以从其配置中检查的那样,我们在项目中添加了以下扩展:

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-openapi</artifactId>
 </dependency>

由于我们的项目中有几个可用的 REST 端点,我们可以在 http://localhost:8080/openapi 处检查生成的 OpenAPI 文档。这是我们的客户服务应用程序的(截断的)输出:

$ curl http://localhost:8080/openapi
 ---
 openapi: 3.0.1
 info:
   title: Generated API
   version: "1.0"
 paths:
   /customers:
     get:
       responses:
         200:
           description: OK
           content:
             application/json:
               schema:
                 type: array
                 items:
                   type: object
                   properties:
                     id:
                       format: int64
                       type: integer
                     name:
                       type: string
                     orders:
                       type: array
                       items:
                         type: object
                         properties:
                           id:
                             format: int64
                             type: integer
                           item:
                             type: string
                           price:
                             format: int64
                             type: integer
                     surname:
                       type: string

如您所见,我们以最小的努力生成了一个描述我们服务功能的 JSON 文档,而无需直接访问底层源代码或任何其他文档。

除此之外,OpenAPI 还可以用作强大的 UI 的基础,例如 Swagger,它是可视化 API 并与之交互的绝佳工具。它的 UI 是根据您的 OpenAPI 规范自动生成的。

为了使用 Swagger,您只需指向 http://localhost:8080/swagger-ui/。通过这样做,您将最终进入 Swagger 主页:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

从那里,您可以通过展开它并单击 Try it out 按钮轻松测试任何可用的操作:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

将生成默认响应正文。根据您的需要调整它,然后单击 Execute。结果,您将在 Response body 文本区域中看到我们操作的返回值(如果有):

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

或者,您可以单击 Download 按钮在本地保存响应。

Customizing the output of OpenAPI

根据 OpenAPI 规范(https://swagger.io/specification/),可以自定义/openapi Servlet 返回的对象的完整模式。这可以通过向端点类和方法添加特定的注释来完成。尽管这些注释都不是强制性的,但我们将提到一些可以提高 OpenAPI 模式可读性的常见注释。

例如,@org.eclipse.microprofile.openapi.annotations.tags.Tag 注释可以用作限定符来描述与端点相关的一组特定操作。此注释可以在类级别应用。为了描述单个资源方法,可以使用 org.eclipse.microprofile.openapi.annotations.Operation 标签,可以在方法级别应用。然后,操作参数的描述可以包含在 org.eclipse.microprofile.openapi.annotations.parameters.Parameter 标记中。最后,org.eclipse.microprofile.openapi.annotations.responses.APIResponse 标记描述了来自 API 操作的单个响应。您可以将多个 APIResponse 注释附加到单个方法以控制每个响应代码的响应。

以下示例显示了在实践中应用于 CustomerEndpoint 类的自定义:

@Tag(name = "OpenAPI Example", description = "Quarkus CRUD Example")
public class CustomerEndpoint {

    @Inject CustomerRepository customerRepository;

    @Operation(operationId = "all", description = "Getting All 
     customers")
    @APIResponse(responseCode = "200", description = "Successful 
     response.")
    @GET
    public List<Customer> getAll() {
        return customerRepository.findAll();
    }

    @POST
    public Response create( @Parameter(description = "The new 
     customer.", required = true) Customer customer) {

        customerRepository.createCustomer(customer);
        return Response.status(201).build();
    }

    @PUT
    public Response update(@Parameter(description = "The customer to 
     update.", required = true) Customer customer) {
        customerRepository.updateCustomer(customer);
        return Response.status(204).build();
    }
    @DELETE
    public Response delete(@Parameter(description = "The customer to 
     delete.", required = true) @QueryParam("id") Long customerId) {
        customerRepository.deleteCustomer(customerId);
        return Response.status(204).build();
    }

}

为简洁起见,我们刚刚使用 OpenAPI 注释标记了 CustomerEndpoint 服务。我们留给您更新 OrderEndpoint 服务,以便您验证自己的新技能。

The Eclipse MicroProfile OpenTracing API

分布式跟踪在微服务时代发挥着关键作用,因为它可以让您跟踪跨不同服务的请求流。为了完成微服务跟踪,我们可以检测我们的服务将消息记录到分布式跟踪服务器,该服务器可以收集、存储和以各种格式显示这些信息。

OpenTracing 规范没有说明哪个分布式系统负责收集跟踪数据,但广泛采用的端到端开源解决方案是 Jaeger (https://www.jaegertracing.io/),完全实现了OpenTracing标准。

让我们切换到 Chapter06/opentracing 示例来看看 OpenTracing 的实际应用。首先,为了使用 opentracing 扩展,必须将以下依赖项添加到您的项目中:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>io.quarkus:quarkus-smallrye-opentracing</artifactId>
</dependency>
事实上,当添加这个扩展时,一个实现 io.opentracing.Tracer 对象将可用于您的应用程序。这意味着您的所有 HTTP 请求都将被自动跟踪。

在配置方面,我们需要提供有关 Jaeger 端点的一些详细信息。这可以通过 application.properties 文件或使用环境变量来完成。下面显示了我们如何配置 application.properties 文件以向在 localhost 上运行并侦听端口 14268 的 Jaeger 端点发出跟踪通知:

quarkus.jaeger.service-name=quarkus-service
quarkus.jaeger.sampler-type=const
quarkus.jaeger.sampler-param=1
quarkus.jaeger.endpoint=http://localhost:14268/api/traces

在前面的配置中,我们还定义了服务名称(quarkus-service)和采样器类型。在采样器类型定义中," 总是对所有轨迹做出相同的决定。它要么对所有轨迹进行采样 (sampler-param=1),要么对它们都不采样 (sampler-param=2)。

现在,我们可以启动 Jaeger 服务了。最简单的方法是将其作为 Docker 容器运行。以下命令将启动 jaegertracing/all-in-one 容器镜像,将 Docker 容器的 UDP/TCP 端口转发到 localhost:

docker run -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 -p 5775:5775/udp -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 -p 16686:16686 -p 14268:14268 -p 9411:9411 jaegertracing/all-in-one:latest

现在,我们可以开始使用我们的客户服务应用程序并使用它执行一些操作。然后,我们可以登录 Jaeger 控制台,该控制台位于 http://localhost:16686

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

如上图所示,在 Jaeger UI 的左侧面板中,您可以看到一个名为 Service 的组合框,其中包含可用于跟踪的服务列表。您应该在其中看到默认的 jaeger 查询服务,它允许我们跟踪查询服务。如果您已将 Quarkus 应用程序配置为发出通知,您应该能够看到 quarkus-service 已登记。选择它,然后选中下一个组合框,即 Operation。此组合框包含已为该特定服务跟踪的所有操作。这是包含组合框的 UI 的部分视图:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

如果您选择 all,在屏幕上您应该能够看到对 quarkus-service 的所有 HTTP 请求的所有跟踪,如以下屏幕截图:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

从那里,您可以选择通过单击来收集有关单个跟踪的更多详细信息。您将看到一个全面的时间表,其中包含执行时间、远程调用者和报告的错误等详细信息:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用MicroProfile API构建应用程序

如果您想下载并详细说明跟踪文件,您还可以通过选择右上角的 Trace JSON 来选择将您的操作跟踪为 JSON。

使用 Jaeger 跟踪您的应用程序有很多可能性。如果您想成为追踪自己的忍者,我们建议您参考 https://www.jaegertracing.io/微服务!

The Eclipse MicroProfile REST Client API

我们将在本章中讨论的最后一个 MicroProfile 扩展是 REST 客户端扩展。此 API 的目标是为您提供一种类型安全的方式来在微服务架构中调用 REST 服务。

不要将 MicroProfile REST 客户端 API 与 JAX-RS 客户端 API 混淆!它们实现了不同的标准:JAX-RS 客户端 API 是根据 JSR 370 ( https://www.jcp.org/en/jsr/detail?id=370 ),而 MicroProfile REST Client API 遵循此处指定的标准: http://microprofile.io/project/eclipse/microprofile-rest-client .

为了了解 REST 客户端 API,我们将使用它作为 Chapter06/rest-client 应用程序的模板。这个项目只不过是我们客户服务的精简版,它只包含接口而不是服务实现。在配置方面,我们在rest-client项目的pom.xml文件中添加了如下依赖:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-rest-client</artifactId>
</dependency>

接下来,我们用两个接口替换了具体的服务实现:一个名为 CustomerEndpointItf,另一个名为 OrdersEndpointItf。这是 CustomerEndpointITf

@RegisterRestClient
@Path("customers")
@Produces("application/json")
@Consumes("application/json")
public interface CustomerEndpointItf {
    @GET
    List<Customer> getAll();

    @POST
    Response create(Customer customer);

    @PUT
    Response update(Customer customer);

    @DELETE
    Response delete(Long customerId);
}

这是 OrdersEndpointItf

@RegisterRestClient
@Path("orders")
@Produces("application/json")
@Consumes("application/json")
public interface OrderEndpointItf {
    @GET
    List<Orders> getAll(@QueryParam("customerId") Long customerId);

    @POST
    @Path("/{customer}")
    Response create(Orders order, @PathParam("customer") Long 
     customerId);

    @PUT
    Response update(Orders order);

    @DELETE
    @Path("/{order}")
    Response delete(@PathParam("order") Long orderId);
}

注意 @org.eclipse.microprofile.rest.client.inject.RegisterRestClient 注释,它使 REST 客户端可以使用 @org.eclipse.microprofile.rest.client 通过 CDI 注入。 inject.RestClient 注释。让我们在 CustomerEndpoint 中学习如何在实践中做到这一点:

public class CustomerEndpoint {

    @Inject
    @RestClient
    CustomerEndpointItf customer;

    @GET
    public List<Customer> getAll() {
        return customer.getAll();
    }
    @POST
    public Response create(Customer c) {
        return customer.create(c);
    }
    @PUT
    public Response update(Customer c) {
        return customer.update(c);
    }
    @DELETE
    public Response delete(Long customerId) {
        return customer.delete(customerId);
    }

}

如您所见,我们通过将执行委托给我们注册为 REST 客户端的接口来替换 REST 客户端实现。此时,您可能想知道 REST 客户端如何知道远程端点。这是一个很好的问题,答案包含在 application.properties 文件中:

com.packt.quarkus.chapter6.restclient.CustomerEndpointItf/mp-rest/url=http://localhost:8080
com.packt.quarkus.chapter6.restclient.CustomerEndpointItf/mp-rest/scope=java.inject.Singleton
com.packt.quarkus.chapter6.restclient.OrderEndpointItf/mp-rest/url=http://localhost:8080
com.packt.quarkus.chapter6.restclient.OrderEndpointItf/mp-rest/scope=java.inject.Singleton

从第一行可以看出,对 REST 客户端接口的所有请求都将导致对远程端点基本 URL 的调用,该 URL 使用以下表达式进行限定:

<Fully Qualified REST Client Interface>/mp-rest/url=<Remote REST base URL>

此外,REST 客户端接口的默认范围已配置为 singleton,它指示 Quarkus 将单例实例化一次,并在注入期间将其引用传递给其他对象。其他支持的范围值是 @Dependent@ApplicationScoped@RequestScoped,后者是默认值。查看 CDI 规范以获取有关不同范围的更多详细信息 (http://www.cdi-spec.org/ )。

为了运行测试,我们需要一个通过 http://localhost:8080/customers 端点返回 Customers 列表和 列表的应用程序通过 http://localhost:8080/orders 端点订购。为此,我们可以启动实现上述端点的任何版本的客户服务应用程序,如下所示:

cd Chapter05/hibernate

$ mvn quarkus:dev

让我们回到我们的例子:

cd Chapter06/rest-client

现在,我们可以使用以下命令运行 REST 客户端测试:

$ mvn compile test

您应该在控制台中看到以下输出:

[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.988 s - in com.packt.quarkus.chapter6.restclient.CustomerEndpointTest
 2019-08-04 19:29:43,592 INFO  [io.quarkus] (main) Quarkus stopped in 0.003s
[INFO] Results:
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

这意味着您设法对远程客户端点运行完整的 CRUD 操作。作为证明,您应该能够看到在服务控制台上执行的 SQL 语句:

    select
         orders0_.id as id1_1_0_,
         orders0_.customer_id as customer4_1_0_,
         orders0_.item as item2_1_0_,
         orders0_.price as price3_1_0_,
         customer1_.id as id1_0_1_,
         customer1_.name as name2_0_1_,
         customer1_.surname as surname3_0_1_
     from
         Orders orders0_
     left outer join
         Customer customer1_
             on orders0_.customer_id=customer1_.id
     where
         orders0_.id=?

请注意,上述日志要求您已打开 SQL 跟踪,如 Chapter 5 使用 Quarkus 管理数据持久性

Summary

在本章中,我们全面概述了 MicroProfile 规范以及如何将其与 Quarkus 应用程序集成。

我们首先概述了 MicroProfile API 以及它如何适应基于云的微服务的整体情况。然后,我们介绍了主要的 MicroProfile 规范。

首先,我们查看了 Health API 以及它如何报告服务的活跃度和准备情况。然后,我们介绍了可用于设计弹性服务的容错 API。接下来,我们讨论了应用程序的遥测数据以及如何使用 Metrics API 收集这些数据。我们涵盖的另一个关键方面是记录服务和跟踪请求流,这可以使用 OpenAPI 和跟踪规范来执行。最后,我们学习了如何创建 REST 客户端来简化我们与远程服务的交互。

到目前为止,您应该对如何设计一个完整的 Quarkus Enterprise 应用程序有一个清晰的认识,尽管我们还没有掌握一个关键方面:Quarkus 应用程序安全性。这就是我们将在下一章中学习的内容。