Java 使用 OkHttp 调用外部接口:GET、POST、请求头与 JSON 参数详解
在企业项目中,经常需要对接第三方系统,例如 OA、ERP、支付平台、物流平台或文件服务。本文用一个可运行的 Java 8 示例,讲清楚如何使用 OkHttp 和 Fastjson 调用 GET、POST 接口,以及 URL 参数、请求头、请求体应该怎么放。
文中地址、Token 均为示例,请替换成真实配置。账号、密码、AK/SK、Token 等敏感数据不要写死在生产代码中。
一、一次 HTTP 请求由哪些部分组成?
一次请求通常包含以下三部分:
| 部分 | 作用 | 常见内容 |
|---|---|---|
| URL 参数(Query Params) | 跟在 URL 后面的键值对 | 查询条件、分页、签名、回调地址 |
| 请求头(Headers) | 描述报文格式或携带认证信息 | Content-Type、Authorization、Accept |
| 请求体(Body) | 提交具体业务数据 | JSON、表单、文件流 |
例如下面这个请求:
text
POST https://api.example.com/v1/orders?page=1&pageSize=20
Content-Type: application/json
Authorization: Bearer xxxxx
{
"customerCode": "C001",
"amount": 99.90
}
page、pageSize是 URL 参数。Content-Type、Authorization是请求头。customerCode、amount是 JSON 请求体。
GET 和 POST 都能使用 URL 参数。
- GET 通常用于查询,参数通常在 URL 中。
- POST 通常用于提交数据,业务数据通常在 Body 中。
- 某些接口会要求 POST 同时携带 URL 参数和 JSON Body,这完全正常。
- 每个字段具体放在哪里,必须以第三方接口文档为准。
二、添加 Maven 依赖
下面的示例使用 OkHttp 3.14.9 和 Fastjson 1.2.83,均兼容 Java 8。
xml
<dependencies>
<!-- HTTP 客户端 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.14.9</version>
</dependency>
<!-- JSON 序列化与反序列化 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>
如果项目已经统一了 JSON 工具版本,应以项目的依赖版本为准;同一个项目中不建议为了一个接口引入多套 JSON 框架。
三、完整可运行示例
新建 ExternalApiDemo.java,复制以下代码即可学习和调试。
java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* OkHttp 调用外部接口示例。
*
* 本类仅用于演示 GET、POST、URL 参数、Header 和 JSON Body 的使用方式。
*/
public class ExternalApiDemo {
/**
* 第三方服务基础地址。
* 正式项目请放入 application.yml、Nacos、环境变量或密钥管理系统。
*/
private static final String BASE_URL = "https://api.example.com";
/**
* 示例 Token。正式项目不要写死在源码中。
*/
private static final String ACCESS_TOKEN = "请替换为真实Token";
private static final MediaType JSON_MEDIA_TYPE =
MediaType.parse("application/json; charset=utf-8");
private static final OkHttpClient CLIENT = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
public static void main(String[] args) {
try {
String getResult = getUser("10001");
System.out.println("GET 响应:" + getResult);
String postResult = createOrder("C001", "P10001", 2);
System.out.println("POST 响应:" + postResult);
} catch (IOException e) {
System.err.println("调用外部接口失败:" + e.getMessage());
}
}
/**
* GET 示例:根据用户编号查询用户。
*
* 最终请求类似:
* GET https://api.example.com/v1/users?userId=10001&includeDetail=true
*
* @param userId 用户编号
* @return 第三方响应内容
* @throws IOException 网络调用失败时抛出
*/
public static String getUser(String userId) throws IOException {
String endpoint = BASE_URL + "/v1/users";
HttpUrl baseHttpUrl = HttpUrl.parse(endpoint);
if (baseHttpUrl == null) {
throw new IllegalArgumentException("接口地址不正确:" + endpoint);
}
// 使用 HttpUrl 统一处理中文、空格、& 等字符的 URL 编码。
HttpUrl requestUrl = baseHttpUrl.newBuilder()
.addQueryParameter("userId", userId)
.addQueryParameter("includeDetail", "true")
.build();
Request request = new Request.Builder()
.url(requestUrl)
.addHeader("Accept", "application/json")
.addHeader("Authorization", "Bearer " + ACCESS_TOKEN)
.get()
.build();
try (Response response = CLIENT.newCall(request).execute()) {
ResponseBody responseBody = response.body();
String responseText = responseBody == null ? "" : responseBody.string();
if (!response.isSuccessful()) {
throw new IOException("GET 调用失败,HTTP 状态码:" + response.code()
+ ",响应内容:" + responseText);
}
return responseText;
}
}
/**
* POST 示例:创建订单。
*
* URL 参数:source=java-demo。
* JSON 请求体:客户编码、产品编码和数量。
*
* @param customerCode 客户编码
* @param productCode 产品编码
* @param quantity 购买数量
* @return 第三方响应内容
* @throws IOException 网络调用失败时抛出
*/
public static String createOrder(String customerCode, String productCode,
Integer quantity) throws IOException {
String endpoint = BASE_URL + "/v1/orders";
HttpUrl baseHttpUrl = HttpUrl.parse(endpoint);
if (baseHttpUrl == null) {
throw new IllegalArgumentException("接口地址不正确:" + endpoint);
}
// POST 也可以有 URL 参数,是否需要由第三方文档决定。
HttpUrl requestUrl = baseHttpUrl.newBuilder()
.addQueryParameter("source", "java-demo")
.build();
// 用 JSONObject 组织 JSON 数据;真实项目也可以使用 DTO。
JSONObject requestData = new JSONObject();
requestData.put("customerCode", customerCode);
requestData.put("productCode", productCode);
requestData.put("quantity", quantity);
String requestJson = JSON.toJSONString(requestData);
RequestBody requestBody = RequestBody.create(JSON_MEDIA_TYPE, requestJson);
Request request = new Request.Builder()
.url(requestUrl)
.addHeader("Accept", "application/json")
.addHeader("Authorization", "Bearer " + ACCESS_TOKEN)
.post(requestBody)
.build();
try (Response response = CLIENT.newCall(request).execute()) {
ResponseBody responseBody = response.body();
String responseText = responseBody == null ? "" : responseBody.string();
if (!response.isSuccessful()) {
throw new IOException("POST 调用失败,HTTP 状态码:" + response.code()
+ ",响应内容:" + responseText);
}
return responseText;
}
}
}
四、代码逐段理解
1. 为什么使用 HttpUrl 拼 URL 参数?
不推荐手工字符串拼接:
java
String url = BASE_URL + "/v1/users?userId=" + userId;
当 userId 含中文、空格、&、? 等特殊字符时,很容易出现编码问题,甚至改变参数含义。
建议统一使用:
java
HttpUrl requestUrl = baseHttpUrl.newBuilder()
.addQueryParameter("userId", userId)
.build();
OkHttp 会正确进行 URL 编码。
2. Headers 应该放什么?
常见请求头:
java
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/json")
.addHeader("Authorization", "Bearer " + token)
其中,使用 RequestBody.create(JSON_MEDIA_TYPE, requestJson) 时,OkHttp 已经能识别 JSON 的 Content-Type。
是否把 Token、签名、应用编号放 Header,必须按照对方文档来。不要主观认为"POST 参数就必须放 Header"。
3. Body 应该放什么?
POST 的 Body 通常用于承载业务对象:
json
{
"customerCode": "C001",
"productCode": "P10001",
"quantity": 2
}
如果接口文档要求 x-www-form-urlencoded 或 form-data,就不能继续使用 JSON Body,需要换成对应的 RequestBody 类型。
4. 为什么一定要判断 response.isSuccessful()?
网络请求成功到达服务器,不代表业务成功。isSuccessful() 用来判断 HTTP 状态码是否为 2xx。
java
if (!response.isSuccessful()) {
throw new IOException("HTTP 状态码:" + response.code());
}
常见状态码:
| 状态码 | 常见含义 |
|---|---|
| 200 | 请求成功 |
| 400 | 参数格式或必填参数不正确 |
| 401 | 未认证,Token 缺失或失效 |
| 403 | 没有访问权限 |
| 404 | 地址、路径或环境不正确,接口也可能未部署 |
| 405 | 请求方式不对,例如把 POST 写成 GET |
| 415 | Content-Type 与接口要求不一致 |
| 500 | 第三方服务内部异常 |
五、POST、Params、Headers、Body 的关系
下面是一个常见的"POST + URL 参数 + JSON Body"结构:
text
POST https://api.example.com/v1/documents/query?appId=demo×tamp=123456&sign=abc
Headers:
Content-Type: application/json
Body:
{
"documentCodes": ["DOC001", "DOC002"]
}
它表示:
appId、timestamp、sign:接口认证或网关校验参数,按文档要求放 URL 参数。Content-Type:告诉服务端请求体是 JSON。documentCodes:实际要查询的业务数据,放 JSON Body。
某些网关会要求签名参数放 URL,有些会要求放 Header,还有些要求放 Body。不要把用于计算签名的密钥(例如 appSecret、SK)发送给对方。
六、对接外部接口的推荐步骤
- 向对方确认接口完整地址、测试环境和生产环境地址。
- 确认请求方式:GET、POST、PUT、DELETE。
- 对照文档逐项确认字段位置:URL 参数、Header 还是 Body。
- 确认认证方式:Token、Basic Auth、AK/SK 签名、OAuth2 等。
- 用 Postman 或 Apifox 先调通,再写 Java 代码。
- 在代码中配置连接超时、读取超时,并检查 HTTP 状态码。
- 保存必要的失败日志和 traceId,方便与第三方联合排查。
- 密钥、Token、账号密码放到配置中心或环境变量,禁止提交到 Git。
七、常见问题
1. 明明是 POST,为什么参数还要放 URL?
POST 只规定了请求方式,不限制参数位置。认证、签名、分页等参数是否放 URL,取决于接口文档。
2. GET 可以带 Body 吗?
HTTP 规范没有完全禁止,但大多数服务器、中间件和客户端对 GET Body 支持不一致。常规查询接口应使用 URL 参数,不要依赖 GET Body。
3. 返回 404 一定是代码问题吗?
不一定。404 通常表示路径、端口、服务上下文、请求方法或部署环境不对,也可能是接口尚未发布。先保存实际请求 URL、请求方式、响应内容和时间,再请对方核查服务日志。
4. 日志可以打印完整请求吗?
可以记录 URL、状态码、业务编号和响应摘要,但不要记录密码、AK/SK、完整 Token、Cookie、身份证号、银行卡号等敏感信息。必要时应脱敏后再写日志。
八、总结
- GET 常用于查询,参数通常放 URL。
- POST 常用于提交,业务数据通常放 JSON Body。
- POST 也可以携带 URL 参数。
- Header 主要承载报文说明和接口明确要求的认证信息。
- 接口对接最重要的是遵循第三方文档,而不是按个人习惯放参数。
- 上线前务必处理超时、异常、状态码、密钥保护和日志脱敏。
如果你觉得本文有帮助,欢迎点赞、收藏和关注。后续可以继续扩展文件上传下载、Form 表单、Token 自动刷新、AK/SK 签名和 Spring Boot 封装等内容。