第31章 网络和 HTTP:让 Java 程序和外部服务通信
到目前为止,你写的程序主要在本机运行:读命令行、写文件、处理内存对象。真实应用经常要和外部系统通信。
比如:
- 查询天气。
- 调用短信服务。
- 请求后端接口。
- 下载文件。
- 把订单提交给支付系统。
- Android App 调用服务器接口。
这些都离不开网络和 HTTP。
这一章先不做复杂 Web 服务器,而是从客户端角度理解 HTTP:Java 程序如何发送请求,如何接收响应,如何处理状态码、JSON、超时和异常。
一、客户端和服务器
网络通信里常见两类角色:
客户端:发起请求的一方。 服务器:接收请求并返回响应的一方。浏览器访问网站:
浏览器 -> 服务器 服务器 -> HTML/CSS/JSJava 程序调用接口:
Java程序 -> API服务器 API服务器 -> JSONAndroid 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 请求:
importjava.net.URI;importjava.net.http.HttpClient;importjava.net.http.HttpRequest;importjava.net.http.HttpResponse;importjava.time.Duration;publicclassGetDemo{publicstaticvoidmain(String[]args)throwsException{HttpClientclient=HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();HttpRequestrequest=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。
intstatus=response.statusCode();if(status>=200&&status<300){System.out.println("成功:"+response.body());}elseif(status==404){System.out.println("资源不存在");}else{System.out.println("请求失败,状态码:"+status+",响应:"+response.body());}可以封装:
publicstaticStringrequireSuccess(HttpResponse<String>response){intstatus=response.statusCode();if(status>=200&&status<300){returnresponse.body();}thrownewIllegalStateException("HTTP请求失败,状态码:"+status+",响应:"+response.body());}这样业务代码不会到处重复判断。
八、发送 POST JSON
请求体是 JSON:
{"isbn":"001","title":"Java入门"}Java 代码:
Stringjson="{\"isbn\":\"001\",\"title\":\"Java入门\"}";HttpRequestrequest=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:
ObjectMappermapper=newObjectMapper();Stringjson=mapper.writeValueAsString(bookRequest);九、把响应 JSON 转成对象
假设响应:
{"isbn":"001","title":"Java入门","author":"作者A"}定义 DTO:
publicclassBookResponse{privateStringisbn;privateStringtitle;privateStringauthor;publicStringgetIsbn(){returnisbn;}publicvoidsetIsbn(Stringisbn){this.isbn=isbn;}publicStringgetTitle(){returntitle;}publicvoidsetTitle(Stringtitle){this.title=title;}publicStringgetAuthor(){returnauthor;}publicvoidsetAuthor(Stringauthor){this.author=author;}}解析:
ObjectMappermapper=newObjectMapper();BookResponsebook=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。
初学阶段先掌握同步请求。异步请求和并发、线程池关系更复杂,后面可以继续深入。
十二、下载文件
把响应保存到文件:
HttpRequestrequest=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 客户端类
把请求封装起来:
publicclassBookApiClient{privatefinalHttpClientclient;privatefinalURIbaseUri;privatefinalObjectMappermapper;publicBookApiClient(StringbaseUrl){this.client=HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();this.baseUri=URI.create(baseUrl);this.mapper=newObjectMapper();}publicBookResponsefindBook(Stringisbn){try{URIuri=baseUri.resolve("/books/"+isbn);HttpRequestrequest=HttpRequest.newBuilder().uri(uri).timeout(Duration.ofSeconds(10)).header("Accept","application/json").GET().build();HttpResponse<String>response=client.send(request,HttpResponse.BodyHandlers.ofString());Stringbody=requireSuccess(response);returnmapper.readValue(body,BookResponse.class);}catch(IOExceptione){thrownewIllegalStateException("调用图书接口失败",e);}catch(InterruptedExceptione){Thread.currentThread().interrupt();thrownewIllegalStateException("调用图书接口被中断",e);}}privateStringrequireSuccess(HttpResponse<String>response){intstatus=response.statusCode();if(status>=200&&status<300){returnresponse.body();}thrownewIllegalStateException("HTTP状态码异常:"+status+",响应:"+response.body());}}这段代码有几个要点:
- 网络 IO 可能失败,要处理
IOException。 - 线程可能被中断,要恢复中断标记。
- HTTP 状态码不是 2xx 时,不当成成功。
- JSON 解析失败也会抛异常。
十四、不要把 HTTP 代码散在业务里
不推荐:
publicvoidborrowBook(Stringisbn,StringreaderId){HttpClientclient=HttpClient.newHttpClient();// 这里调用远程图书接口// 然后继续借书}更好的分层:
BookApiClient:负责 HTTP LibraryService:负责借书业务业务层调用客户端:
BookResponsebook=bookApiClient.findBook(isbn);HTTP 细节集中在一个类里,方便测试和替换。
十五、常见错误
1. 只看 body,不看状态码
404、500 也可能有 body,但不是成功。
2. 不设置超时
网络请求可能一直卡住。
3. JSON 手写拼接
简单演示可以,真实项目用 Jackson。
4. InterruptedException 后不恢复中断
推荐:
catch(InterruptedExceptione){Thread.currentThread().interrupt();thrownewIllegalStateException("请求被中断",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 是程序和数据库通信。它们都会遇到外部资源、异常、连接管理和数据格式问题。