JDK 内置的 HttpRequest 有坑,请绕道!

开发 前端
这篇文章,我们就来分析如何排查和解决这种错误,以及分析下HttpRequest的工作原理。

最近,使用了 Java 11内置的java.net.http.HttpRequest请求外部服务,发现日志中出现了很多如下图的错误:

这篇文章,我们就来分析如何排查和解决这种错误,以及分析下HttpRequest的工作原理。

排查过程:

遇到这种问题,首先google搜索下关键字:java.io.IOException: HTTP/1.1 header parser received no bytes

总结下 Google查询的结果,可以得到两个主要原因:

  • 服务器返回空响应,导致解析 response异常
  • 网络问题

针对第一种情况,到下游服务查看日志发现请求根本没有进来,于是把原因定位到网络问题。经过多次的测试后发现,错误是有规律性的出现,多年工作经验的直觉告诉我,这种http请求,一定会复用连接,会不会复用了一个失效的链接,于是把问题再次缩小。

那么,JDK内置的HttpRequest链接存活的时间是多久呢?

对,找官方资料,如下链接和图片:

官方默认的keepalive是1200s,是不是太大了,于是调整了 keepalive的时间,修改参数的方式:

# 方法1. 启动指令中增加如下参数
-Djdk.httpclient.keepalive.timeout=10

# 方法2. 代码中配置如下参数
System.setProperty("jdk.httpclient.keepalive.timeout", "10s");

很奇怪,为什么JDK没有提供变量来设置这个参数,而是作为JVM 系统属性设置???不管怎样,经过一番验证之后,问题解决。

所以,如果有使用 JDK内置HttpRequest的小伙伴,一定要注意这个坑。

既然讲到了HttpRequest,不如顺道把它的工作原理也分析下。

一、 JDK 内置 HttpRequest 的实现原理

1. 基础架构

JDK 内置的 HTTP 客户端基于异步非阻塞 I/O(NIO)设计,采用了事件驱动的架构。这种设计使其能够高效地处理大量并发连接,同时保持较低的资源消耗。HttpClient 是核心类,负责创建和配置 HTTP 请求,而 HttpRequest 则用于定义具体的请求细节。

2. 异步与同步请求

HttpClient 支持同步和异步两种请求方式:

  • 同步请求:调用 send 方法,线程会被阻塞直到服务器响应返回。这种方式适用于简单的请求场景,但在高并发环境下可能导致线程阻塞问题。
  • 异步请求:调用 sendAsync 方法,返回一个 CompletableFuture 对象,允许在请求进行时执行其他操作,提升应用的响应性和吞吐量。

3. 支持的协议

内置 HTTP 客户端支持 HTTP/1.1 和 HTTP/2 协议。HTTP/2 的引入带来了多路复用、头部压缩和服务器推送等特性,显著提升了传输效率。客户端会根据服务器支持的协议自动选择最优协议,确保最佳的传输性能。

4. 连接管理

HttpClient 内部维护着连接池,自动管理 HTTP 连接的复用和关闭。通过连接池机制,可以避免频繁建立和关闭连接带来的性能损耗。连接池根据请求的目标主机和协议进行分类管理,确保高效的资源利用。

5. 安全与认证

内置客户端提供丰富的安全特性,包括 SSL/TLS 支持、证书验证和多种认证机制(如 Basic、Digest、Bearer 认证等)。开发者可以通过配置 SSLContext 和相关认证信息,确保请求的安全性。

6. 中间件与过滤器

HttpClient 允许开发者添加自定义的过滤器和拦截器,对请求和响应进行预处理和后处理。这为实现日志记录、请求重试、错误处理等功能提供了灵活的扩展点。

二、优缺点

1. 优点

  • 简化的 API:相比于传统的 HttpURLConnection,HttpClient 提供了更现代化和简洁的 API,降低了使用难度和代码复杂度。
  • 异步支持:内置的异步请求机制允许更高效地处理并发请求,提升了应用的性能和响应性。
  • 协议支持:自动支持 HTTP/2,使得应用能够利用更高效的传输协议,无需额外配置。
  • 内置安全特性:丰富的安全配置选项让开发者能够轻松地实现安全的网络通信,包括 SSL/TLS 和多种认证方式。
  • 连接池管理:自动的连接池管理减少了资源管理的负担,提升了连接的复用性和整体性能。
  • 跨平台一致性:作为 JDK 的一部分,HttpClient 在不同操作系统和环境下表现一致,减少了跨平台开发的难度。

2. 缺点

  • 功能限制:虽然 HttpClient 覆盖了大多数常见的 HTTP 功能,但在某些高级用例下,可能缺乏第三方库(如 Apache HttpClient 或 OkHttp)提供的特定功能。
  • 版本依赖:HttpClient 是从 Java 11 开始引入的,对于使用更早版本 JDK 的项目,需要依赖外部库来实现相似功能。
  • 社区和生态:相比于成熟的第三方 HTTP 客户端,JDK 内置的 HttpClient 在社区支持和生态上仍有待发展,可能缺乏某些特定场景下的最佳实践和解决方案。
  • 性能优化:尽管 HttpClient 已经具备良好的性能,但在极端高并发或特定优化需求下,可能无法完全满足专业级别的性能调优需求。

三、核心参数

在使用 HttpRequest 时,开发者需要配置多个参数以定义请求的行为和特性。以下是一些核心参数及其说明:

1. 请求 URI

每个 HTTP 请求都需要一个目标 URI,指定资源的位置。例如:

URI uri = URI.create("https://api.example.com/data");
HttpRequest request = HttpRequest.newBuilder()
    .uri(uri)
    .build();

2. HTTP 方法

HttpRequest 支持常见的 HTTP 方法,如 GET、POST、PUT、DELETE 等。可以通过 method 方法或专门的快捷方法设置:

// 使用快捷方法设置 GET 请求
HttpRequest getRequest = HttpRequest.newBuilder()
    .uri(uri)
    .GET()
    .build();

// 使用 method 方法设置 POST 请求
HttpRequest postRequest = HttpRequest.newBuilder()
    .uri(uri)
    .method("POST", HttpRequest.BodyPublishers.ofString("request body"))
    .build();

3. 请求头

可以通过 headers 方法添加一个或多个请求头,或使用 header 方法逐个添加:

HttpRequest request = HttpRequest.newBuilder()
    .uri(uri)
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer token")
    .GET()
    .build();

4. 请求体

对于需要发送数据的请求(如 POST、PUT),需要配置请求体。HttpRequest.BodyPublisher 提供多种数据发布方式:

HttpRequest postRequest = HttpRequest.newBuilder()
    .uri(uri)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"key\":\"value\"}"))
    .build();

支持的 BodyPublisher 包括:

  • ofString(String): 发送字符串数据
  • ofFile(Path): 发送文件内容
  • ofByteArray(byte[]): 发送字节数组
  • noBody(): 无请求体(适用于 GET 请求)

5. 超时设置

可以为请求设置超时时间,防止请求长时间挂起:

HttpRequest request = HttpRequest.newBuilder()
    .uri(uri)
    .timeout(Duration.ofSeconds(10))
    .GET()
    .build();

6. 重定向策略

通过 HttpClient 的构建器可以设置重定向的策略,如跟随重定向、禁止重定向等:

HttpClient client = HttpClient.newBuilder()
    .followRedirects(HttpClient.Redirect.NORMAL)
    .build();

7. 优先级

可以为请求设置优先级,影响请求的调度顺序:

HttpRequest request = HttpRequest.newBuilder()
    .uri(uri)
    .priority(10)
    .GET()
    .build();

优先级值越高,表示请求越重要。

8. 版本协议

可以指定使用的 HTTP 版本,如 HTTP/1.1 或 HTTP/2:

HttpRequest request = HttpRequest.newBuilder()
    .uri(uri)
    .version(HttpClient.Version.HTTP_2)
    .GET()
    .build();

9. 代理设置

HttpClient 支持通过代理服务器发送请求,可以在 HttpClient 构建器中配置:

HttpClient client = HttpClient.newBuilder()
    .proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 8080)))
    .build();

10. 身份认证

通过 Authenticator 配置认证信息,以便客户端在需要时自动提供认证凭证:

Authenticator authenticator = new Authenticator() {
    @Override
    protected PasswordAuthentication getPasswordAuthentication() {
        return new PasswordAuthentication("user", "password".toCharArray());
    }
};

HttpClient client = HttpClient.newBuilder()
    .authenticator(authenticator)
    .build();

四、示例分析

为了更好地理解 HttpRequest 的使用,这里提供一个简单的示例:发送一个 POST 请求,并异步处理响应。

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

publicclass HttpClientExample {
    public static void main(String[] args) {
        // 创建 HttpClient 实例,配置超时和重定向策略
        HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .followRedirects(HttpClient.Redirect.NORMAL)
            .build();

        // 构建 POST 请求,设置 URI、请求头和请求体
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.example.com/data"))
            .header("Content-Type", "application/json")
            .timeout(Duration.ofSeconds(5))
            .POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"John Doe\",\"age\":30}"))
            .build();

        // 发送异步请求,并处理响应
        client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
            .thenApply(HttpResponse::body)
            .thenAccept(responseBody -> {
                System.out.println("Response received:");
                System.out.println(responseBody);
            })
            .exceptionally(e -> {
                System.err.println("Request failed: " + e.getMessage());
                returnnull;
            });

        // 防止主线程提前退出
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

代码解析:

  • HttpClient 创建:通过 HttpClient.newBuilder() 创建一个 HttpClient 实例,配置了连接超时和自动跟随标准重定向。
  • HttpRequest 构建:定义了一个 POST 请求,目标 URI 为 https://api.example.com/data,设置了 Content-Type 请求头,并通过 BodyPublishers.ofString 发送 JSON 格式的请求体。
  • 发送异步请求:调用 sendAsync 方法发送请求,指定响应体处理器为 ofString,即将响应体转换为字符串。
  • 处理响应:使用 thenApply 和 thenAccept 链式调用处理响应体,打印到控制台。如果请求失败,通过 exceptionally 捕获并打印错误信息。
  • 主线程等待:由于请求是异步发送的,主线程需要等待一段时间以确保响应能够处理。实际应用中,可以使用更优雅的方式管理线程同步。

五、总结

本文,我们从使用 JDK内置的HttpRequest遇到的坑以及如何解决它,到工作原理的分析,HttpRequest为 Java 开发者提供了一个强大且易用的 HTTP 客户端工具。但是,相比于一些成熟的第三方库(比如 Apache HttpClient)还是稍显不足。

因此,在使用一个工具或者框架时,最好能先了解其实现原理、优缺点等,可以做到提前避免出现上面类似的问题,或者出现问题时能快速定位和解决问题。

责任编辑:赵宁宁 来源: 猿java
相关推荐

2024-05-29 08:49:45

2009-07-08 17:33:46

JDK5.0内置工具

2009-07-09 11:02:37

JDK5.0内置工具

2011-06-27 09:49:53

Windows CEAndroid

2020-05-26 13:45:46

Python函数字符串

2024-06-14 10:26:30

2015-09-15 15:51:43

恶意软件CAPTCHA绕道

2022-03-21 19:24:15

Objects方法false

2019-03-08 09:45:49

漏洞URL恶意软件

2020-11-03 06:57:10

MyBatis数据库

2023-11-30 08:34:29

批量消息消息队列

2022-01-17 18:21:09

数据库社交引流

2019-09-18 15:20:16

MyBatisSQL数据库

2024-10-09 09:07:10

JVM优化String类JDK1.6

2022-07-19 19:53:58

Http请求AI写诗

2017-06-15 13:15:39

Python协程

2020-12-21 06:15:15

程序员互联网年龄

2024-07-12 08:52:50

2023-12-01 07:38:33

微服务订单服务

2012-05-07 13:52:45

PHP
点赞
收藏

51CTO技术栈公众号