1. 概述

通常,当在 Web 应用程序中使用 HTTP 调用时,我们需要一种方法来捕获有关请求和响应的某种指标。通常,这是为了监视应用程序进行的 HTTP 调用的大小和频率。

OkHttp是适用于 Android 和 Java 应用程序的高效 HTTP 和 HTTP/2 客户端。在之前的教程中,我们了解了如何使用 OkHttp 的基础知识

在本教程中,我们将了解如何使用事件捕获这些类型的指标。

2. 活动

顾名思义,事件为我们提供了一种强大的机制来记录与整个 HTTP 调用生命周期相关的应用程序指标。

为了订阅我们感兴趣的所有事件,我们需要做的就是定义一个 EventListener 并重写我们想要捕获的事件的方法。

例如,如果我们只想监视失败和成功的调用,这尤其有用。在这种情况下,我们只需重写与事件侦听器类中的这些事件相对应的特定方法。稍后我们会更详细地看到这一点。

在我们的应用程序中使用事件至少有几个优点:

  • 我们可以使用事件来监控应用程序进行的 HTTP 调用的大小和频率
  • 这可以帮助我们快速确定应用程序中可能存在瓶颈的位置

最后,我们还可以使用事件来确定我们的网络是否也存在潜在问题。

3. 依赖关系

当然,我们需要将标准 okhttp 依赖项添加到 pom.xml 中:

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.9.1</version>
</dependency>

我们还需要另一个专门用于我们的测试的依赖项。让我们添加 OkHttp mockwebserver 工件

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>mockwebserver</artifactId>
    <version>4.9.1</version>
    <scope>test</scope>
</dependency>

现在我们已经配置了所有必要的依赖项,我们可以继续编写我们的第一个事件侦听器。

4. 活动方式及顺序

但在开始定义自己的事件侦听器之前, 我们将退后一步,简要了解一下我们可以使用哪些事件方法以及我们期望事件到达的顺序 。当我们稍后深入研究一些真实的例子时,这将对我们有所帮助。

假设我们正在处理一个成功的 HTTP 调用,没有重定向或重试。然后我们就可以期待这个典型的方法调用流程。

4.1. 调用开始()

该方法是我们的入口点,一旦我们将调用加入队列或客户端执行它,我们就会调用它。

4.2. proxySelectStart()proxySelectEnd()

第一个方法在代理选择之前调用,同样在代理选择之后调用,包括按照尝试顺序排列的代理列表。当然,如果未配置代理,则此列表可以为空。

4.3. dnsStart()dnsEnd()

这些方法在 DNS 查找之前和 DNS 解析之后立即调用。

4.4. 连接开始()连接结束()

这些方法在建立和关闭套接字连接之前调用。

4.5. secureConnectStart()secureConnectEnd()

如果我们的调用使用 HTTPS,那么我们将在 connectStartconnectEnd 之间散布这些安全连接变体。

4.6. 连接获取()连接释放()

在获取或释放连接后调用。

4.7. requestHeadersStart()requestHeadersEnd()

这些方法将在发送请求标头之前和之后立即调用。

4.8. requestBodyStart()requestBodyEnd()

顾名思义,在发送请求正文之前调用。当然,这仅适用于包含正文的请求。

4.9. 响应头开始()响应头结束()

当响应头第一次从服务器返回时以及收到响应头后立即调用这些方法。

4.10. responseBodyStart()responseBodyEnd()

同样,当响应正文首次从服务器返回时以及收到正文后立即调用。

除了这些方法之外,我们还可以使用三种其他方法来捕获故障:

4.11. callFailed()responseFailed()requestFailed()

如果我们的调用永久失败,则说明请求写入失败,或者响应读取失败。

5. 定义一个简单的事件监听器

让我们从定义我们自己的偶数监听器开始。为了让事情变得非常简单, 我们的事件监听器将记录调用何时开始和结束以及一些请求和响应标头信息

public class SimpleLogEventsListener extends EventListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleLogEventsListener.class);

    @Override
    public void callStart(Call call) {
        LOGGER.info("callStart at {}", LocalDateTime.now());
    }

    @Override
    public void requestHeadersEnd(Call call, Request request) {
        LOGGER.info("requestHeadersEnd at {} with headers {}", LocalDateTime.now(), request.headers());
    }

    @Override
    public void responseHeadersEnd(Call call, Response response) {
        LOGGER.info("responseHeadersEnd at {} with headers {}", LocalDateTime.now(), response.headers());
    }

    @Override
    public void callEnd(Call call) {
        LOGGER.info("callEnd at {}", LocalDateTime.now());
    }
}

正如我们所看到的, 要创建侦听器,我们需要做的就是从 EventListener 类扩展。 然后我们可以继续重写我们关心的事件的方法。

在我们的简单侦听器中,我们记录调用开始和结束的时间以及请求和响应标头到达时的时间。

5.1.将其连接在一起

要真正使用这个监听器,我们需要做的就是在构建 OkHttpClient 实例时调用 eventListener 方法,它应该可以正常工作:

OkHttpClient client = new OkHttpClient.Builder() 
  .eventListener(new SimpleLogEventsListener())
  .build();

在下一节中,我们将了解如何测试新的侦听器。

5.2.测试事件监听器

现在,我们已经定义了第一个事件监听器;让我们继续编写我们的第一个集成测试:

@Rule
public MockWebServer server = new MockWebServer();

@Test
public void givenSimpleEventLogger_whenRequestSent_thenCallsLogged() throws IOException {
    server.enqueue(new MockResponse().setBody("Hello Baeldung Readers!"));
        
    OkHttpClient client = new OkHttpClient.Builder()
      .eventListener(new SimpleLogEventsListener())
      .build();

    Request request = new Request.Builder()
      .url(server.url("/"))
      .build();

    try (Response response = client.newCall(request).execute()) {
        assertEquals("Response code should be: ", 200, response.code());
        assertEquals("Body should be: ", "Hello Baeldung Readers!", response.body().string());
    }
 }

首先,我们使用 OkHttp MockWebServer JUnit 规则

这是一个轻量级、可编写脚本的 Web 服务器,用于测试 HTTP 客户端,我们将用它来测试事件侦听器 。通过使用此规则,我们将为每个集成测试创建一个干净的服务器实例。

考虑到这一点,现在让我们来看看测试的关键部分:

  • 首先,我们设置一个模拟响应,其中在正文中包含一条简单的消息
  • 然后,我们构建 OkHttpClient 并配置 SimpleLogEventsListener
  • 最后,我们发送请求并使用断言检查收到的响应代码和正文

5.3.运行测试

当我们运行测试时,我们将看到记录的事件:

callStart at 2021-05-04T17:51:33.024
...
requestHeadersEnd at 2021-05-04T17:51:33.046 with headers User-Agent: A Baeldung Reader
Host: localhost:51748
Connection: Keep-Alive
Accept-Encoding: gzip
...
responseHeadersEnd at 2021-05-04T17:51:33.053 with headers Content-Length: 23
callEnd at 2021-05-04T17:51:33.055

6. 把它们放在一起

现在,假设我们想要构建简单的日志记录示例,并记录调用链中每个步骤所用的时间:

public class EventTimer extends EventListener {

    private long start;

    private void logTimedEvent(String name) {
        long now = System.nanoTime();
        if (name.equals("callStart")) {
            start = now;
        }
        long elapsedNanos = now - start;
        System.out.printf("%.3f %s%n", elapsedNanos / 1000000000d, name);
    }

    @Override
    public void callStart(Call call) {
        logTimedEvent("callStart");
    }

    // More event listener methods
}

这与我们的第一个示例非常相似,但这次我们捕获从每个事件的调用开始起的经过时间。 通常,这对于检测网络延迟可能非常有趣

让我们看一下是否在一个真实的网站(例如我们自己的https://www.baeldung.com/ )上运行它:

0.000 callStart
0.012 proxySelectStart
0.012 proxySelectEnd
0.012 dnsStart
0.175 dnsEnd
0.183 connectStart
0.248 secureConnectStart
0.608 secureConnectEnd
0.608 connectEnd
0.609 connectionAcquired
0.612 requestHeadersStart
0.613 requestHeadersEnd
0.706 responseHeadersStart
0.707 responseHeadersEnd
0.765 responseBodyStart
0.765 responseBodyEnd
0.765 connectionReleased
0.765 callEnd

由于此调用通过 HTTPS 进行,因此我们还将看到 secureConnectStartsecureConnectStart 事件。

7. 监控失败的呼叫

到目前为止,我们主要关注成功的 HTTP 请求,但我们也可以捕获失败的事件:

@Test (expected = SocketTimeoutException.class)
public void givenConnectionError_whenRequestSent_thenFailedCallsLogged() throws IOException {
    OkHttpClient client = new OkHttpClient.Builder()
      .eventListener(new EventTimer())
      .build();

    Request request = new Request.Builder()
      .url(server.url("/"))
      .build();

    client.newCall(request).execute();
}

在此示例中,我们故意避免设置模拟 Web 服务器,这意味着我们当然会看到 SocketTimeoutException 形式的灾难性失败。

现在让我们看一下运行测试时的输出:

0.000 callStart
...
10.008 responseFailed
10.009 connectionReleased
10.009 callFailed

正如预期的那样,我们将看到呼叫开始,然后 10 秒后,发生连接超时,因此,我们看到记录了 responseFailedcallFailed 事件。

8. 关于并发的简单介绍

到目前为止,我们假设没有同时执行多个调用。 如果我们想适应这种情况,那么我们需要在配置 OkHttpClient 时使用 eventListenerFactory 方法

我们可以使用工厂为每个 HTTP 调用创建一个新的 EventListener 实例。当我们使用这种方法时,可以在侦听器中保留特定于调用的状态。

9. 结论

在本文中,我们了解了如何使用 OkHttp 捕获事件。首先,我们首先解释什么是事件,并了解我们可以使用哪些类型的事件以及它们在处理 HTTP 调用时到达的顺序。

然后我们了解了如何定义一个简单的事件记录器来捕获部分 HTTP 调用以及如何编写集成测试。

与往常一样,本文的完整源代码可以在 GitHub 上获取。