从零开始学Java:第31章 网络和 HTTP:让 Java 程序和外部服务通信

从零开始学Java:第31章 网络和 HTTP:让 Java 程序和外部服务通信

第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 请求:

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。

十六、练习

  1. HttpClient发送一个 GET 请求,打印状态码和 body。
  2. requireSuccess方法,非 2xx 抛异常。
  3. 构造一个 POST JSON 请求。
  4. 用 Jackson 把对象转 JSON。
  5. 写一个BookApiClient,封装findBook(isbn)
  6. 给请求设置连接超时和请求超时。
  7. 思考:HTTP 接口失败时,业务层应该重试、提示用户,还是直接失败?

十七、本章小结

你现在应该理解:

  • HTTP 是请求响应模型。
  • 请求包含 URL、方法、header、body。
  • 响应包含状态码、header、body。
  • GET 常用于查询,POST 常用于提交。
  • 状态码必须检查。
  • Java 11 的HttpClient可以发送 HTTP 请求。
  • JSON 应使用 Jackson 等库解析。
  • 网络请求必须考虑超时、IO 异常和中断。
  • HTTP 代码应该封装在 API Client 中,不要散落在业务层。

下一章讲数据库和 JDBC。HTTP 是程序和远程服务通信,JDBC 是程序和数据库通信。它们都会遇到外部资源、异常、连接管理和数据格式问题。