HTTP Client 与 Files 增强(JDK 11)
飞翔科技的微服务架构升级项目中,黄俪(前端)向小崔(后端)提了个需求。
"小崔哥,能不能帮我写个简单的 HTTP 接口调用?我前端要对接你们的员工查询 API。"
小崔正准备引入 Apache HttpClient 依赖,白歌拦住了他。
"不用加依赖了。JDK 11 内置的
HttpClient支持 HTTP/2 和 WebSocket,异步模式写起来比 OkHttp 还简洁。还有——" 白歌翻到另一个文件,"你们那个配置文件读取,之前用了 5 行BufferedReader代码对不?JDK 11 的Files.readString()一行就能替换。"小崔看着 pom.xml 里即将被删掉的三个依赖,感叹道:"JDK 升级最爽的时候,就是能删 pom.xml 的时候。"
定义表
HTTP Client 核心组件
| 类/方法 | 功能 | 说明 |
|---|---|---|
HttpClient | HTTP 客户端实例 | 通过 Builder 模式配置(超时、代理、重定向策略等) |
HttpRequest | HTTP 请求 | 通过 Builder 构建 URL、Header、Body、方法(GET/POST/PUT/DELETE) |
HttpResponse<T> | HTTP 响应 | 泛型响应体,通过 BodyHandlers 指定反序列化方式 |
BodyHandlers.ofString() | 将响应体解析为 String | 最常用的响应体处理器 |
BodyHandlers.ofFile(Path) | 将响应体直接写入文件 | 大文件下载 |
BodyPublishers.ofString() | 从 String 创建请求体 | POST/PUT 请求体的最简方式 |
send() | 同步发送请求 | 阻塞直到响应返回 |
sendAsync() | 异步发送请求 | 返回 CompletableFuture<HttpResponse<T>> |
Files 新增方法
| 方法 | 功能 | 等效 JDK 8 写法 |
|---|---|---|
Files.readString(Path) | 读取文件全部内容为 String(UTF-8) | new String(Files.readAllBytes(path), StandardCharsets.UTF_8) |
Files.writeString(Path, CharSequence) | 写入字符串到文件(UTF-8) | Files.write(path, str.getBytes(StandardCharsets.UTF_8)) |
Mermaid 时序图:HttpClient 同步 vs 异步请求
HTTP Client 同步请求
3.1 GET 请求 — 员工信息查询
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class HttpClientSyncDemo {
public static void main(String[] args) throws Exception {
// 构建 HttpClient(设置超时、HTTP/2 支持)
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
// 构建 HTTP GET 请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get?name=大翔"))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(5))
.GET()
.build();
// 同步发送请求,响应体解析为 String
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());
// 输出结果
System.out.println("状态码: " + response.statusCode());
System.out.println("响应头: " + response.headers().map());
System.out.println("响应体: " + response.body());
}
}
3.2 POST 请求 — 提交员工数据
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class HttpClientPostDemo {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
// JSON 请求体
String jsonBody = """
{
"name": "孔蓝",
"department": "技术部",
"position": "后端开发",
"salary": 15000
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/post"))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(5))
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());
System.out.println("状态码: " + response.statusCode());
System.out.println("响应体 (前200字符): " +
response.body().substring(0, Math.min(200, response.body().length())));
}
}
输出(模拟):
状态码: 200
响应体: {
"args": {},
"data": "{\"name\":\"孔蓝\",\"department\":\"技术部\"}",
"json": {"name":"孔蓝","department":"技术部","position":"后端开发","salary":15000},
...
}
HTTP Client 异步请求
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class HttpClientAsyncDemo {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
// === 场景:并发查询多个员工信息 ===
List<Integer> employeeIds = List.of(1001, 1002, 1003, 1004, 1005);
// 为每个员工创建一个异步请求
List<CompletableFuture<String>> futures = employeeIds.stream()
.map(id -> {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get?id=" + id))
.timeout(Duration.ofSeconds(5))
.GET()
.build();
return client.sendAsync(request,
HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body);
})
.collect(Collectors.toList());
// 等待所有请求完成并合并结果
CompletableFuture<Void> allDone = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0]));
allDone.thenRun(() -> {
System.out.println("所有请求完成!");
futures.forEach(f ->
System.out.println("结果(截取): " +
f.getNow("未完成").substring(0,
Math.min(50, f.getNow("").length()))));
}).join(); // 等待所有异步操作完成
System.out.println("\n所有查询完成。");
}
}
Files.readString / writeString — 一行式文件读写
5.1 基础用法对比
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class FilesReadWriteDemo {
public static void main(String[] args) throws IOException {
// === 写入文件:JDK 8 vs JDK 11 ===
String content = """
飞翔科技 - 2026年Q2部门报告
============================
技术部: 完成3个核心模块重构
市场部: 新增客户120家
运维部: 系统可用性99.97%
""";
Path reportPath = Path.of("temp_report.txt");
// --- JDK 8 写法:需要转 byte[] ---
// Files.write(reportPath, content.getBytes(StandardCharsets.UTF_8));
// --- JDK 11 写法:直接写 CharSequence ---
Files.writeString(reportPath, content);
System.out.println("写入完成: " + reportPath.toAbsolutePath());
// === 读取文件:JDK 8 vs JDK 11 ===
// --- JDK 8 写法:readAllBytes 再 new String ---
// byte[] bytes = Files.readAllBytes(reportPath);
// String read1 = new String(bytes, StandardCharsets.UTF_8);
// --- JDK 11 写法:一行读为 String ---
String read = Files.readString(reportPath);
System.out.println("\n读取内容:");
System.out.println(read);
// === 附加写入 ===
Files.writeString(reportPath,
"\n财务部: 成本控制在预算内",
StandardOpenOption.APPEND);
String updated = Files.readString(reportPath);
System.out.println("追加后: " + updated);
// 清理测试文件
Files.deleteIfExists(reportPath);
}
}
输出:
写入完成: C:\Users\AOXIANG\Desktop\Javase\17_JDK9-11新特性\temp_report.txt
读取内容:
飞翔科技 - 2026年Q2部门报告
============================
技术部: 完成3个核心模块重构
市场部: 新增客户120家
运维部: 系统可用性99.97%
追加后: ...财务部: 成本控制在预算内
5.2 实战:配置文件解析器
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.stream.Collectors;
public class ConfigParser {
public static void main(String[] args) throws IOException {
// 创建示例配置文件
String configContent = """
# 飞翔科技服务器配置
server.host=192.168.1.100
server.port=8080
database.url=jdbc:mysql://localhost:3306/flytech
database.username=admin
database.pool.size=20
cache.ttl.seconds=3600
""";
Path configPath = Path.of("temp_config.properties");
Files.writeString(configPath, configContent);
// === JDK 11 一键读取 + Stream 解析 ===
Map<String, String> config = Files.readString(configPath)
.lines() // JDK 11
.filter(line -> !line.startsWith("#")) // 跳过注释
.filter(line -> !line.isBlank()) // JDK 11
.map(line -> line.split("=", 2))
.collect(Collectors.toMap(
parts -> parts[0].strip(), // JDK 11
parts -> parts[1].strip() // JDK 11
));
System.out.println("解析配置:");
config.forEach((k, v) ->
System.out.printf(" %-20s → %s%n", k, v));
// 清理
Files.deleteIfExists(configPath);
}
}
输出:
解析配置:
server.host → 192.168.1.100
server.port → 8080
database.url → jdbc:mysql://localhost:3306/flytech
database.username → admin
database.pool.size → 20
cache.ttl.seconds → 3600
HTTP Client 高级特性
6.1 文件下载(BodyHandlers.ofFile)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
public class FileDownloadDemo {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/image/png"))
.GET()
.build();
// 直接写入文件,避免全部加载到内存
Path outputPath = Path.of("temp_downloaded.png");
HttpResponse<Path> response = client.send(
request,
HttpResponse.BodyHandlers.ofFile(outputPath));
System.out.println("下载完成!");
System.out.println("状态码: " + response.statusCode());
System.out.println("文件路径: " + response.body().toAbsolutePath());
System.out.println("文件大小: " + Files.size(outputPath) + " bytes");
// 清理
Files.deleteIfExists(outputPath);
}
}
6.2 错误处理最佳实践
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class HttpClientErrorHandling {
public static void main(String[] args) {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
// 故意访问一个不存在的路径
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/status/404"))
.GET()
.build();
try {
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString());
// 手动检查状态码
if (response.statusCode() == 200) {
System.out.println("成功: " + response.body());
} else if (response.statusCode() == 404) {
System.out.println("资源不存在 (404)");
} else if (response.statusCode() >= 500) {
System.out.println("服务器错误: " + response.statusCode());
} else {
System.out.println("其他状态码: " + response.statusCode());
}
} catch (java.net.http.HttpTimeoutException e) {
System.err.println("请求超时: " + e.getMessage());
} catch (java.io.IOException e) {
System.err.println("网络错误: " + e.getMessage());
} catch (InterruptedException e) {
System.err.println("请求被中断: " + e.getMessage());
Thread.currentThread().interrupt();
}
}
}
易错场景
7.1 Files.readString 的文件编码
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReadStringEncodingPitfall {
public static void main(String[] args) throws Exception {
// readString 默认使用 UTF-8
// 如果文件是 GBK 编码,需要显式指定
Path gbkFile = Path.of("temp_gbk.txt");
// 模拟写入 GBK 编码文件
Files.writeString(gbkFile, "飞翔科技", Charset.forName("GBK"));
// ❌ 默认 UTF-8 读取 GBK 文件 → 乱码
// String garbled = Files.readString(gbkFile);
// ✅ 显式指定编码
String correct = Files.readString(gbkFile, Charset.forName("GBK"));
System.out.println("指定编码读取: " + correct);
Files.deleteIfExists(gbkFile);
}
}
7.2 HttpClient 的重用
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class HttpClientReusePitfall {
// ✅ HttpClient 是线程安全的,应作为单例复用
// ❌ 不要每次请求都 new HttpClient()
private static final HttpClient SHARED_CLIENT = HttpClient.newHttpClient();
public static void fetchUser(int id) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get?id=" + id))
.GET()
.build();
HttpResponse<String> response = SHARED_CLIENT.send(
request, HttpResponse.BodyHandlers.ofString());
System.out.println("用户" + id + " 状态: " + response.statusCode());
}
public static void main(String[] args) throws Exception {
// 复用同一个客户端实例
fetchUser(1001);
fetchUser(1002);
}
}
面试考点
问题一:"JDK 11 的 HttpClient 相比 HttpURLConnection 有哪些优势?"
四个关键优势。第一,原生支持 HTTP/2,HttpURLConnection 只支持 HTTP/1.1。第二,内置异步模式(
sendAsync返回CompletableFuture),而 HttpURLConnection 必须手动开线程。第三,Builder 模式构建请求,代码更清晰。第四,内置 WebSocket 支持。
问题二:"Files.readString() 和 Files.readAllLines() 如何选择?"
readString将整个文件读入一个字符串,适合小文件(配置、JSON、HTML 模板)。readAllLines返回List<String>,适合需要逐行处理的场景(日志分析、CSV 解析)。如果要流式处理大文件的每一行,用Files.lines()(返回惰性 Stream)。
问题三:"HttpClient.sendAsync() 返回的是什么?如何处理结果?"
返回
CompletableFuture<HttpResponse<T>>。可以用.thenApply()处理正常响应,.exceptionally()处理异常,.thenCombine()合并多个请求结果。多个异步请求可以并发发送,不需要手动管理线程池。
白歌在下班前更新了飞翔科技的技术栈文档,写道:
"JDK 11 的
HttpClient和Files.readString/writeString是两种不同类型但同样重要的进步。前者让 Java 拥有了一流的 HTTP 客户端,告别了第三方依赖的困扰;后者让最常见的小文件读写场景从 5 行代码缩减到 1 行——省下的不只是打字时间,还有 Bug 的藏身之处。"