前言
在Java 11正式发布之前,Java生态中发起HTTP请求的主流方案要么是古老难用的HttpURLConnection,要么是依赖第三方库的Apache HttpClient、OkHttp。前者API设计反人类、不支持HTTP/2、异步能力极弱;后者虽然功能完善,但会引入额外依赖,在一些轻量化工具、中间件开发场景中容易出现版本冲突问题。
Java 11将原本在Java 9、10中处于孵化器状态的java.net.http.HttpClient正式纳入标准库,原生支持同步/异步请求、HTTP/2、WebSocket、响应式流背压,自带连接池能力,无需任何第三方依赖即可满足绝大多数HTTP请求场景。本文将从基础API、进阶实战、性能优化、坑点避坑四个维度,全面讲解HttpClient的使用方案。
一、HttpClient核心特性总览
HttpClient从设计之初就对标主流第三方HTTP客户端的能力,核心特性包括:
- 协议支持:同时支持HTTP/1.1和HTTP/2,默认优先使用HTTP/2,服务端不支持时自动降级到HTTP/1.1
- 调用模式:同时提供同步阻塞、异步非阻塞两套API,异步返回值为
CompletableFuture,支持链式调用 - 内置能力:自动连接池管理、自动重定向、原生TLS 1.3支持、请求/响应拦截扩展
- 响应式支持:支持响应式流处理大请求/响应体,避免OOM问题
- 扩展能力:支持自定义线程池、SSL上下文、请求超时规则等配置
二、基础API实战
所有示例代码均基于Java 11+版本,无需引入任何第三方依赖即可运行。
2.1 同步GET请求
最基础的同步GET请求示例,用于请求普通网页或接口:
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.io.IOException;
public class SyncGetDemo {
public static void main(String[] args) throws IOException, InterruptedException {
// 1. 创建HttpClient实例,默认配置:HTTP/2优先、连接超时30秒、自动跟随重定向关闭
HttpClient httpClient = HttpClient.newHttpClient();
// 2. 构建请求对象
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://www.baidu.com"))
// 添加自定义请求头
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.header("Accept-Language", "zh-CN,zh;q=0.9")
.GET() // 默认为GET请求,可省略
.build();
// 3. 发送同步请求,指定响应体处理为字符串
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
// 4. 处理响应结果
System.out.println("响应状态码:" + response.statusCode());
System.out.println("响应内容前100字符:" + response.body().substring(0, 100) + "...");
System.out.println("响应使用协议:" + response.version());
}
}
运行代码即可看到请求结果,默认情况下如果服务端支持HTTP/2,response.version()会返回HTTP_2。
2.2 POST请求(JSON/表单提交)
POST请求支持多种请求体格式,最常用的是JSON提交和表单提交:
2.2.1 JSON提交
java
public class PostJsonDemo {
public static void main(String[] args) throws IOException, InterruptedException {
HttpClient httpClient = HttpClient.newHttpClient();
// 构造JSON请求体,Java 15+支持文本块语法
String jsonBody = """
{
"username": "zhangsan",
"age": 25,
"email": "zhangsan@example.com"
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://jsonplaceholder.typicode.com/posts")) // 公开测试接口
.header("Content-Type", "application/json; charset=utf-8")
// POST方法传入请求体发布器
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("响应状态码:" + response.statusCode());
System.out.println("响应内容:" + response.body());
}
}
2.2.2 表单提交
java
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class PostFormDemo {
public static void main(String[] args) throws IOException, InterruptedException {
HttpClient httpClient = HttpClient.newHttpClient();
// 构造表单参数
Map<String, String> formParams = Map.of(
"username", "zhangsan",
"password", "123456",
"rememberMe", "true"
);
// 拼接表单字符串
String formBody = formParams.entrySet().stream()
.map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)
+ "=" + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8))
.reduce((p1, p2) -> p1 + "&" + p2)
.orElse("");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/login"))
.header("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
.POST(HttpRequest.BodyPublishers.ofString(formBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("登录响应状态:" + response.statusCode());
}
}
2.3 文件下载
对于大文件下载,不要使用BodyHandlers.ofString(),避免将整个文件加载到内存导致OOM,直接使用BodyHandlers.ofFile()写入本地磁盘:
java
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class FileDownloadDemo {
public static void main(String[] args) throws IOException, InterruptedException {
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/large-file.zip"))
.timeout(java.time.Duration.ofMinutes(10)) // 大文件下载设置更长超时
.build();
// 直接将响应体写入本地文件
HttpResponse<java.nio.file.Path> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofFile(Paths.get("D:/download/large-file.zip"),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
System.out.println("文件下载完成,保存路径:" + response.body());
}
}
三、异步请求实战
异步请求是HttpClient的核心优势之一,基于CompletableFuture实现,非常适合高并发HTTP调用场景。
3.1 基础异步请求
java
public class AsyncGetDemo {
public static void main(String[] args) {
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
.build();
// 发送异步请求,非阻塞
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
// 正常响应处理
.thenAccept(response -> {
System.out.println("异步请求完成,状态码:" + response.statusCode());
System.out.println("响应内容:" + response.body());
})
// 异常处理
.exceptionally(e -> {
System.out.println("异步请求失败:" + e.getMessage());
return null;
})
// 等待请求完成(演示用,实际业务可不需要join)
.join();
}
}
3.2 批量异步请求
在实际业务中经常需要同时调用多个接口,等待所有接口返回后统一处理,用HttpClient异步API可以非常高效的实现:
java
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class AsyncBatchDemo {
// JSON工具类,需要引入jackson-databind依赖
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static void main(String[] args) {
// 自定义IO密集型线程池,替代默认的ForkJoinPool(适合CPU密集型)
ExecutorService executor = new ThreadPoolExecutor(
10,
50,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("http-client-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 构建HttpClient使用自定义线程池
HttpClient httpClient = HttpClient.newBuilder()
.executor(executor)
.connectTimeout(java.time.Duration.ofSeconds(5))
.build();
// 待请求的接口列表
List<String> urls = List.of(
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/posts/2",
"https://jsonplaceholder.typicode.com/posts/3"
);
// 批量提交异步请求
List<CompletableFuture<Post>> futureList = urls.stream()
.map(url -> {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(java.time.Duration.ofSeconds(10))
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(resp -> {
if (resp.statusCode() != 200) {
throw new RuntimeException("请求失败,状态码:" + resp.statusCode());
}
try {
return OBJECT_MAPPER.readValue(resp.body(), Post.class);
} catch (Exception e) {
throw new RuntimeException("JSON反序列化失败", e);
}
})
.exceptionally(e -> {
System.out.println("请求" + url + "失败:" + e.getMessage());
return null;
});
})
.toList();
// 等待所有请求完成后统一处理
CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0]))
.thenRun(() -> {
List<Post> resultList = futureList.stream()
.map(CompletableFuture::join)
.filter(Objects::nonNull)
.toList();
System.out.println("批量请求完成,共获取" + resultList.size() + "条数据:");
resultList.forEach(post -> System.out.println(post.getId() + ": " + post.getTitle()));
executor.shutdown();
})
.join();
}
// 响应实体类
static class Post {
private Long id;
private Long userId;
private String title;
private String body;
// 省略getter/setter,实际项目可使用Lombok@Data注解
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
}
}
四、性能优化最佳实践
HttpClient本身性能已经非常优秀,但不合理的使用依然会导致性能问题,以下是生产环境的优化方案:
4.1 客户端单例化
HttpClient实例自带连接池,每次new HttpClient()都会创建新的连接池、线程池资源,频繁创建会导致端口占用过高、资源浪费,生产环境必须使用单例客户端,推荐用枚举实现:
java
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public enum GlobalHttpClient {
INSTANCE;
private final HttpClient httpClient;
GlobalHttpClient() {
// 全局线程池配置
ExecutorService executor = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() * 2,
Runtime.getRuntime().availableProcessors() * 10,
6