Appearance
第41课:网络编程 - HTTP
🎯 学习目标
- 掌握 HttpURLConnection(旧 API,JDK 自带)
- 掌握 HttpClient(JDK 11+,现代推荐)
- 实现 RESTful 调用(GET/POST/JSON)
- 理解 HTTP 请求/响应结构、状态码、头部
- 知道 OkHttp/Apache HttpClient 等第三方库
📖 一、概念讲解:HTTP 协议回顾
1. HTTP 请求/响应
请求:
GET /api/users?id=1 HTTP/1.1 ← 请求行(方法 路径 版本)
Host: example.com ← 请求头
Accept: application/json
<空行>
<body>(GET 一般无)
响应:
HTTP/1.1 200 OK ← 状态行(版本 状态码 原因)
Content-Type: application/json ← 响应头
Content-Length: 123
<空行>
<body>2. 常用方法与状态码
- 方法:GET(查)/POST(增)/PUT(改)/DELETE(删)/PATCH
- 状态码:2xx 成功(200)、3xx 重定向(301/302)、4xx 客户端错误(400/404/401)、5xx 服务端错误(500/502/503)
3. RESTful 风格
用 HTTP 方法表达操作,URL 表达资源:
GET /users/1查询用户1POST /users创建用户PUT /users/1更新用户1DELETE /users/1删除用户1
📖 二、HttpURLConnection(旧 API)
java
URL url = new URL("https://api.github.com/users/octocat");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
int code = conn.getResponseCode();
InputStream is = (code < 400) ? conn.getInputStream() : conn.getErrorStream();
String body = new String(is.readAllBytes(), StandardCharsets.UTF_8);
conn.disconnect();局限:API 笨重(URL.openConnection)、手动管理连接、无连接池、同步阻塞。JDK 11+ 推荐用 HttpClient 替代。
📖 三、HttpClient(JDK 11+,推荐)
现代、支持 HTTP/2、同步与异步、连接池:
java
import java.net.http.*;
import java.net.URI;
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(5))
.build();
// GET
HttpRequest getReq = HttpRequest.newBuilder()
.uri(URI.create("https://api.github.com/users/octocat"))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(5))
.GET()
.build();
HttpResponse<String> resp = client.send(getReq, HttpResponse.BodyHandlers.ofString());
System.out.println(resp.statusCode());
System.out.println(resp.body());
// POST JSON
String json = "{\"name\":\"Alice\"}";
HttpRequest postReq = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/api/users"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
// 异步
client.sendAsync(getReq, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println);优势:构建器模式、HTTP/2、异步(返回 CompletableFuture)、连接池复用、API 清晰。
📖 四、第三方库
- OkHttp:Android 常用,连接池、拦截器、简洁 API。
- Apache HttpClient:成熟、功能全,企业项目常见。
- Spring WebClient / RestTemplate:Spring 生态内集成。
实际项目多用第三方库或 Spring 的客户端,它们封装更好(连接池、拦截器、JSON 自动转换)。
⚠️ 五、常见陷阱
陷阱1:不设超时
不设 connect/read timeout,服务器不响应时永久阻塞。务必设超时。
陷阱2:不处理非 2xx 响应
4xx/5xx 时 getInputStream 抛异常或返回 errorStream,需检查状态码。
陷阱3:连接不关闭
HttpURLConnection.disconnect、HttpClient 资源未释放。用 try-with-resources 或复用 HttpClient。
陷阱4:编码问题
body 用 UTF-8 解码,别用平台默认。JSON Content-Type 指定 charset。
陷阱5:HTTPS 证书
自签名证书需配置 SSLContext 信任,否则 SSLHandshakeException。
🆚 六、Java vs C / 库选择
| 特性 | C(libcurl) | Java |
|---|---|---|
| HTTP 客户端 | libcurl | HttpClient/OkHttp |
| 异步 | 回调 | CompletableFuture |
| HTTP/2 | libcurl 支持 | HttpClient 支持 |
对 C 程序员:Java 的 HttpClient 对应 C 的 libcurl,但面向对象、支持异步(CompletableFuture)。HTTP 客户端是分布式调用的基础(调外部 API、微服务间调用)。
💡 七、最佳实践
- JDK 11+ 用 HttpClient,弃用 HttpURLConnection。
- 必设超时(connect + read),防永久阻塞。
- 检查状态码,处理 4xx/5xx。
- 复用 HttpClient(连接池),别每次新建。
- JSON 用 Jackson/Gson 序列化,别手拼字符串。
- 异步用 sendAsync + CompletableFuture,非阻塞。
- HTTPS 证书/代理 按需配置。
📝 练习预告
完成 练习/Ex41_HTTP.java 中的 6 道题:
- HttpClient GET 请求
- HttpClient POST JSON
- 状态码与响应头
- 异步请求(sendAsync)
- HttpURLConnection(旧 API 对比)
- 综合:调用公开 API 并解析 JSON
完成后对比 答案/Sol41.java,查看逐行讲解与多解法。
📖 八、HTTP 客户端生命周期
HttpClient 应该复用,而不是每次请求都创建。
推荐写法:
java
public final class HttpClients {
public static final HttpClient DEFAULT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.version(HttpClient.Version.HTTP_2)
.build();
private HttpClients() {
}
}原因:
text
复用连接池。
减少 TLS 握手成本。
减少对象创建。
统一超时和代理配置。
便于统一加日志、指标和重试策略。请求对象 HttpRequest 可以每次创建,因为它描述单次请求;客户端对象 HttpClient 应尽量复用。
📖 九、超时、重试与幂等
HTTP 调用必须设置超时:
java
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/api/users"))
.timeout(Duration.ofSeconds(5))
.GET()
.build();超时分类:
text
connect timeout:连接建立超时。
request timeout:整个请求等待超时。
read timeout:第三方库中常见,JDK HttpClient 用 request timeout 表达。重试前必须考虑幂等性:
| 方法 | 是否通常幂等 | 说明 |
|---|---|---|
| GET | 是 | 重试通常安全 |
| PUT | 是 | 同一资源重复更新通常安全 |
| DELETE | 通常是 | 重复删除结果应一致 |
| POST | 通常不是 | 可能重复创建或扣款 |
POST 如果要重试,应使用业务幂等键:
text
Idempotency-Key: 7f4d0c2e-....没有幂等设计的重试可能比失败更危险。
📖 十、状态码处理策略
不要只判断是否等于 200。
常见策略:
text
2xx:成功。
3xx:重定向,确认客户端是否自动跟随。
400:请求参数错误,不应盲目重试。
401/403:认证或授权失败。
404:资源不存在,可能是正常业务结果。
409:冲突,常见于并发更新。
429:限流,应退避重试。
500:服务端错误,可按策略重试。
502/503/504:网关或上游不可用,可短暂重试。示例:
java
int code = response.statusCode();
if (code >= 200 && code < 300) {
return response.body();
}
if (code == 404) {
throw new NotFoundException("resource not found");
}
if (code == 429 || code >= 500) {
throw new RetryableHttpException(code, response.body());
}
throw new HttpException(code, response.body());错误响应体也要读取并记录,否则排查时只剩状态码。
🧪 十一、封装简易 API Client
不要在业务代码里到处散落 HttpClient.send。
可以封装一个小客户端:
java
public class ApiClient {
private final HttpClient client;
private final URI baseUri;
public ApiClient(HttpClient client, URI baseUri) {
this.client = client;
this.baseUri = baseUri;
}
public String get(String path) {
HttpRequest request = HttpRequest.newBuilder()
.uri(baseUri.resolve(path))
.timeout(Duration.ofSeconds(5))
.header("Accept", "application/json")
.GET()
.build();
return send(request);
}
private String send(HttpRequest request) {
try {
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
);
return handle(response);
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("request interrupted", e);
}
}
private String handle(HttpResponse<String> response) {
int code = response.statusCode();
if (code >= 200 && code < 300) {
return response.body();
}
throw new IllegalStateException("http " + code + ": " + response.body());
}
}封装层可以统一处理:
text
基础 URL。
公共请求头。
超时。
状态码。
日志。
指标。
异常转换。
JSON 序列化和反序列化。🔐 十二、HTTPS/TLS 注意事项
HTTPS 在 HTTP 之下增加 TLS。
常见问题:
text
证书过期。
域名和证书不匹配。
自签名证书不被信任。
公司代理替换证书。
TLS 协议版本不兼容。
本机时间错误导致证书校验失败。生产原则:
text
不要为了省事关闭证书校验。
内网自签名证书应导入信任库。
记录 TLS 握手失败的异常原因。
关注证书过期监控。如果需要自定义信任库,应该集中配置,而不是在业务代码里散落 SSL 逻辑。
🛠 十三、HTTP 调用排查清单
请求失败时按下面顺序定位:
text
URL 是否正确。
DNS 是否能解析。
网络是否能连通。
代理是否配置。
TLS 证书是否可信。
请求方法是否正确。
请求头是否缺失。
Content-Type 和 Accept 是否正确。
请求体 JSON 是否合法。
状态码和错误响应体是什么。
是否触发限流。
是否超时。
是否有重试放大问题。日志建议:
text
记录方法、URL 路径、状态码、耗时。
记录 traceId/requestId。
错误时记录响应体摘要。
不要记录敏感 header。
不要完整记录大响应体。📊 十四、客户端库选择
| 场景 | 推荐 |
|---|---|
| JDK 11+ 基础调用 | java.net.http.HttpClient |
| Android 或需要拦截器生态 | OkHttp |
| 复杂连接池和企业历史项目 | Apache HttpClient |
| Spring MVC 同步调用 | RestTemplate 或 RestClient |
| 响应式非阻塞调用 | WebClient |
选择原则:
text
能用 JDK HttpClient 解决的简单调用,不必额外引库。
团队已有统一客户端时,优先遵守团队封装。
微服务内部调用要统一超时、重试、熔断、指标。
高并发异步场景要关注线程模型和连接池。🔍 十五、自测问题
text
为什么 HttpClient 应该复用?
connect timeout 和 request timeout 有什么区别?
为什么 POST 重试需要幂等键?
404、429、503 应该如何分别处理?
为什么不能简单关闭 HTTPS 证书校验?
HTTP 调用日志应该记录哪些信息?
什么时候选择 WebClient 而不是 JDK HttpClient?
InterruptedException 为什么要重新设置中断标记?✅ 十六、掌握标准
学完本课后,应能做到:
text
能用 JDK HttpClient 发送 GET 和 POST JSON。
能设置连接超时和请求超时。
能处理状态码和错误响应体。
能解释同步 send 与异步 sendAsync 的差异。
能复用 HttpClient 并封装 API Client。
能说明重试和幂等的关系。
能排查常见 HTTPS 证书问题。
能根据场景选择 HttpClient、OkHttp、Apache HttpClient 或 WebClient。HTTP 编程的重点不是“能发请求”,而是让失败、超时、重试、日志和安全都可控。
🎓 下一步
- 第42课:正则表达式 — Pattern/Matcher、常用正则