第31章 网络和 HTTP:让 Java 程序和外部服务通信
到目前为止,你写的程序主要在本机运行:读命令行、写文件、处理内存对象。真实应用经常要和外部系统通信。
比如:
- 查询天气。
- 调用短信服务。
- 请求后端接口。
- 下载文件。
- 把订单提交给支付系统。
- Android App 调用服务器接口。
这些都离不开网络和 HTTP。
这一章先不做复杂 Web 服务器,而是从客户端角度理解 HTTP:Java 程序如何发送请求,如何接收响应,如何处理状态码、JSON、超时和异常。
一、客户端和服务器
网络通信里常见两类角色:
客户端:发起请求的一方。
服务器:接收请求并返回响应的一方。
浏览器访问网站:
浏览器 -> 服务器
服务器 -> HTML/CSS/JS
Java 程序调用接口:
Java程序 -> API服务器
API服务器 -> JSON
Android App 调后端:
Android App -> 后端服务
后端服务 -> JSON数据
HTTP 是最常见的应用层协议。
二、HTTP 请求和响应
一次 HTTP 通信可以理解为:
请求 Request
响应 Response
请求包含:
- URL。
- 方法。
- 请求头。
- 请求体。
响应包含:
- 状态码。
- 响应头。
- 响应体。
例如:
GET /books/001 HTTP/1.1
Host: api.example.com
Accept: application/json
响应:
HTTP/1.1 200 OK
Content-Type: application/json
{"isbn":"001","title":"Java入门"}
三、URL 的组成
https://api.example.com:443/books/001?detail=true
拆开:
| 部分 | 含义 |
|---|---|
| https | 协议 |
| api.example.com | 主机名 |
| 443 | 端口 |
| /books/001 | 路径 |
| detail=true | 查询参数 |
常见端口:
- HTTP 默认 80。
- HTTPS 默认 443。
实际开发中,接口文档会告诉你 URL、方法、参数和响应格式。
四、HTTP 方法
常见方法:
| 方法 | 常见含义 |
|---|---|
| GET | 查询 |
| POST | 新增或提交 |
| PUT | 整体更新 |
| PATCH | 部分更新 |
| DELETE | 删除 |
例子:
GET /books
GET /books/001
POST /books
PUT /books/001
DELETE /books/001
这不是 Java 语法,而是接口设计约定。
五、状态码
常见状态码:
| 状态码 | 含义 |
|---|---|
| 200 | 成功 |
| 201 | 创建成功 |
| 400 | 请求参数错误 |
| 401 | 未登录 |
| 403 | 无权限 |
| 404 | 资源不存在 |
| 409 | 冲突,比如重复创建 |
| 500 | 服务器内部错误 |
调用接口时不能只看有没有响应体。要先看状态码。
200:正常处理响应。
400/404:通常是请求方问题。
500:服务器问题。
六、Java HttpClient
Java 11 开始,标准库提供 HttpClient。
发送 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 GetDemo {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/books/001"))
.timeout(Duration.ofSeconds(10))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
}
}
这里有三个对象:
HttpClient:负责发送请求。HttpRequest:表示请求。HttpResponse<String>:表示响应,body 是字符串。
七、处理状态码
不要直接使用 body。
int status = response.statusCode();
if (status >= 200 && status < 300) {
System.out.println("成功:" + response.body());
} else if (status == 404) {
System.out.println("资源不存在");
} else {
System.out.println("请求失败,状态码:" + status + ",响应:" + response.body());
}
可以封装:
public static String requireSuccess(HttpResponse<String> response) {
int status = response.statusCode();
if (status >= 200 && status < 300) {
return response.body();
}
throw new IllegalStateException("HTTP请求失败,状态码:" + status + ",响应:" + response.body());
}
这样业务代码不会到处重复判断。
八、发送 POST JSON
请求体是 JSON:
{"isbn":"001","title":"Java入门"}
Java 代码:
String json = "{\"isbn\":\"001\",\"title\":\"Java入门\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/books"))
.timeout(Duration.ofSeconds(10))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
Content-Type 表示你发出去的数据格式。
Accept 表示你希望服务器返回什么格式。
真实项目里 JSON 不应该手写字符串,应该用 Jackson:
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(bookRequest);
九、把响应 JSON 转成对象
假设响应:
{"isbn":"001","title":"Java入门","author":"作者A"}
定义 DTO:
public class BookResponse {
private String isbn;
private String title;
private String author;
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
解析:
ObjectMapper mapper = new ObjectMapper();
BookResponse book = mapper.readValue(response.body(), BookResponse.class);
DTO 是 Data Transfer Object,数据传输对象。
它和你的领域模型 Book 可以相同,也可以不同。接口返回什么,DTO 就按接口格式设计。
十、超时很重要
网络请求不能无限等。
连接超时:
HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
请求超时:
HttpRequest.newBuilder()
.timeout(Duration.ofSeconds(10))
如果不设置超时,接口卡住时,你的程序可能长时间无响应。
命令行程序会卡住。
服务器程序会占住线程。
Android App 会影响用户体验。
十一、同步请求和异步请求
同步:
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
当前线程会等待响应。
异步:
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println);
异步请求返回 CompletableFuture。
初学阶段先掌握同步请求。异步请求和并发、线程池关系更复杂,后面可以继续深入。
十二、下载文件
把响应保存到文件:
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/file.zip"))
.build();
HttpResponse<Path> response = client.send(
request,
HttpResponse.BodyHandlers.ofFile(Path.of("data", "file.zip"))
);
System.out.println(response.statusCode());
System.out.println(response.body());
BodyHandlers.ofFile 会把响应体写到文件。
如果文件很大,不要先读成字符串。
十三、一个简单 API 客户端类
把请求封装起来:
public class BookApiClient {
private final HttpClient client;
private final URI baseUri;
private final ObjectMapper mapper;
public BookApiClient(String baseUrl) {
this.client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
this.baseUri = URI.create(baseUrl);
this.mapper = new ObjectMapper();
}
public BookResponse findBook(String isbn) {
try {
URI uri = baseUri.resolve("/books/" + isbn);
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.timeout(Duration.ofSeconds(10))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String body = requireSuccess(response);
return mapper.readValue(body, BookResponse.class);
} catch (IOException e) {
throw new IllegalStateException("调用图书接口失败", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("调用图书接口被中断", e);
}
}
private String requireSuccess(HttpResponse<String> response) {
int status = response.statusCode();
if (status >= 200 && status < 300) {
return response.body();
}
throw new IllegalStateException("HTTP状态码异常:" + status + ",响应:" + response.body());
}
}
这段代码有几个要点:
- 网络 IO 可能失败,要处理
IOException。 - 线程可能被中断,要恢复中断标记。
- HTTP 状态码不是 2xx 时,不当成成功。
- JSON 解析失败也会抛异常。
十四、不要把 HTTP 代码散在业务里
不推荐:
public void borrowBook(String isbn, String readerId) {
HttpClient client = HttpClient.newHttpClient();
// 这里调用远程图书接口
// 然后继续借书
}
更好的分层:
BookApiClient:负责 HTTP
LibraryService:负责借书业务
业务层调用客户端:
BookResponse book = bookApiClient.findBook(isbn);
HTTP 细节集中在一个类里,方便测试和替换。
十五、常见错误
1. 只看 body,不看状态码
404、500 也可能有 body,但不是成功。
2. 不设置超时
网络请求可能一直卡住。
3. JSON 手写拼接
简单演示可以,真实项目用 Jackson。
4. InterruptedException 后不恢复中断
推荐:
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("请求被中断", e);
}
5. 把 HTTP 调用散落到各个业务方法
应该封装成 API Client。
十六、练习
- 用
HttpClient发送一个 GET 请求,打印状态码和 body。 - 写
requireSuccess方法,非 2xx 抛异常。 - 构造一个 POST JSON 请求。
- 用 Jackson 把对象转 JSON。
- 写一个
BookApiClient,封装findBook(isbn)。 - 给请求设置连接超时和请求超时。
- 思考:HTTP 接口失败时,业务层应该重试、提示用户,还是直接失败?
十七、本章小结
你现在应该理解:
- HTTP 是请求响应模型。
- 请求包含 URL、方法、header、body。
- 响应包含状态码、header、body。
- GET 常用于查询,POST 常用于提交。
- 状态码必须检查。
- Java 11 的
HttpClient可以发送 HTTP 请求。 - JSON 应使用 Jackson 等库解析。
- 网络请求必须考虑超时、IO 异常和中断。
- HTTP 代码应该封装在 API Client 中,不要散落在业务层。
下一章讲数据库和 JDBC。HTTP 是程序和远程服务通信,JDBC 是程序和数据库通信。它们都会遇到外部资源、异常、连接管理和数据格式问题。

990

被折叠的 条评论
为什么被折叠?



