结论先行
- 生成分隔标识
boundary
- 在
HttpPost
中设置Header
时带上boundary
- 创建
MultipartEntity
时需要设置boundary
实现代码如下
/**
* @param url 调用接口的地址
* @param paramMap 调用接口传入的方法体参数
*/
public static String postDataByFormData(String url, Map<String, Object> paramMap) {
HttpPost post = new HttpPost(url);
// 必须在post对象的header中设置boundary才能正常进行form-data调用, 此处的BOUNDARY可以用随机生成的UUID代替, 保证post对象和请求体中的boundary值相同
final String BOUNDARY = "----WebKitFormBoundary7MA4YWxkTrZu0gW";
post.setHeader("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
String result = "";
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
MultipartEntityBuilder builder = MultipartEntityBuilder.create()
// 必须: 参数体的builder对象需要设置好分界标识, 该值必须与post对象的boundary值相同
.setBoundary(BOUNDARY)
.setContentType(ContentType.create("multipart/form-data", Consts.UTF_8))
.setCharset(StandardCharsets.UTF_8);
// 设置传输的参数
paramMap.forEach((k, v) ->
builder.addTextBody(k, v.toString(), ContentType.create("multipart/form-data", Consts.UTF_8)));
// 创建请求实体
HttpEntity entity = builder.build();
post.setEntity(entity);
HttpResponse resp = httpClient.execute(post);
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
// 接收返回信息
result = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8);
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
背景
之前与其他系统的接口对接都使用application/json格式的请求,使用HttpClient感觉非常简单,这次发现对方使用的contentType是form-data方式的,使用postman轻轻松松调通,结果到java代码中却不好使,对方接口一直返回缺少参数。
Java版本:1.8
HttpClient
版本:4.5.6
postman测试结果
java代码中运行结果
上述结果的java代码如下
/**
* @param url 调用接口的地址
* @param paramMap 调用接口传入的方法体参数, 该map对象包含属性 timestamp, params及token校验
*/
public static String postDataByFormData(String url, Map<String, Object> paramMap) {
HttpPost post = new HttpPost(url);
String result = "";
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
MultipartEntityBuilder builder = MultipartEntityBuilder.create()
.setContentType(ContentType.create("multipart/form-data", Consts.UTF_8))
.setCharset(StandardCharsets.UTF_8);
paramMap.forEach((k, v) -> builder.addTextBody(k, v.toString(), ContentType.create("multipart/form-data", Consts.UTF_8)));
// 设置实体
HttpEntity entity = builder.build();
post.setEntity(entity);
HttpResponse resp = httpClient.execute(post);
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
// 返回
result = EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8);
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
问题处理思路
一开始我认为代码中修改contentType
应该就跟postman中一样简单,只需要修改传入的contentType
即可,结果失败了,于是我就在某度中搜索使用HttpClient调用form-data的写法,结果都不行,例如:
- 试试设置Mode为
HttpMultipartMode.BROWSER_COMPATIBLE
MultipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
- 添加参数时设置文本参数为
text/plain
:
builder.addTextBody(key, ContentType.DEFAULT_TEXT));
后面觉得在这上面耗着不行,只能换个思路,试试不用HttpClient的情况下是调用form-data类型的接口会不会有问题:
/**
* 不使用类库,通过java的原生api实现form-data请求
*/
public static String postDataByForm(String url, Map<String, Object> paramMap) throws IOException {
String res = "";
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
// 设置请求方法为POST
connection.setRequestMethod("POST");
final String BOUNDARY = "----WebKitFormBoundary7MA4YWxkTrZu0gW";
// 设置请求属性,模拟form-data提交
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
// 设置允许输出
connection.setDoOutput(true);
// 构建form-data的内容
StringBuilder data = new StringBuilder();
for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
data.append("--").append(BOUNDARY).append("\r\nContent-Disposition: form-data; name=\"")
.append(entry.getKey())
.append("\"\r\n\r\n")
.append(entry.getValue())
.append("\r\n");
}
data.append("--").append(BOUNDARY).append("\r\n");
// 写入请求体
try (OutputStream os = connection.getOutputStream()) {
os.write(data.toString().getBytes());
}
// 获取响应码
int responseCode = connection.getResponseCode();
System.out.println("Response Code: " + responseCode);
// 读取返回数据
StringBuilder strBuf = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
strBuf.append(line).append("\n");
}
res = strBuf.toString();
// 断开连接
connection.disconnect();
return res;
}
结果当然是对方能正常接收
。。。
因此肯定是使用HttpClient时,某个地方少添加了设置。通过查看原生java实现form-data接口的调用后,猜测应该是Content-Disposition
或者boundary
的问题,然后我想到借助ChatGpt的能力提供解决方案。
总结及反思
最后按这种方式也的确解决了问题,但不明白既然请求form-data类型的接口需要用到boundary
,为什么HttpClient
不实现这部分内容呢?
看了MultipartEntityBuilder
的build()
方法源码,是会判断boundary
如果为空,会生成一个随机值,只是这个值没有getter
方法可获取。所以还是要在HttpPost
对象进行boundary
的设置,否则请求体和请求头的值会不一致。问题还是出现在HttpPost
不会自动获取请求体的boundary
导致的需要手动设置,想了下原因,可能是HttpClient
为了解耦,让两个类的标识符要单独设置吧。大家有其他的想法也可以留下您们的评论。
拓展补充
后面做另外一个项目时发现引入了hutool
工具包,其中也有http
请求工具类,这个工具类使用起来简直是效率杀手,只需要在请求时用form
方式传参即可实现上面的需求:
public static String sendFormDataPost(String url, Map<String, Object> paramMap, Map<String, String> headerMap) {
HttpRequest post = HttpUtil.createPost(url);
post.timeout(10000);
// 添加请求头参数
post.addHeaders(headerMap);
String result = "";
// 使用form传参,不需要自己拼接一长串内容
post.form(paramMap);
// 设置实体
cn.hutool.http.HttpResponse response = post.execute();
if (response.getStatus() == HttpStatus.SC_OK) {
// 返回
result = response.body();
}
return result;
}