HttpCilent进行Post请求form-data接口,服务方接收不到参数

结论先行

  1. 生成分隔标识boundary
  2. 在HttpPost中设置Header时带上boundary
  3. 创建MultipartEntity时需要设置boundary

实现代码如下

java 复制代码
/**
 * @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代码如下

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的写法,结果都不行,例如:

  1. 试试设置Mode为HttpMultipartMode.BROWSER_COMPATIBLE
java 复制代码
MultipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
  1. 添加参数时设置文本参数为text/plain
java 复制代码
builder.addTextBody(key, ContentType.DEFAULT_TEXT));

后面觉得在这上面耗着不行,只能换个思路,试试不用HttpClient的情况下是调用form-data类型的接口会不会有问题:

java 复制代码
/**
 * 不使用类库,通过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不实现这部分内容呢?

看了MultipartEntityBuilderbuild()方法源码,是会判断boundary如果为空,会生成一个随机值,只是这个值没有getter方法可获取。所以还是要在HttpPost对象进行boundary的设置,否则请求体和请求头的值会不一致。问题还是出现在HttpPost不会自动获取请求体的boundary导致的需要手动设置,想了下原因,可能是HttpClient为了解耦,让两个类的标识符要单独设置吧。大家有其他的想法也可以留下您们的评论。

相关推荐
yuanbenshidiaos10 分钟前
c++---------数据类型
java·jvm·c++
向宇it13 分钟前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
Lojarro27 分钟前
【Spring】Spring框架之-AOP
java·mysql·spring
莫名其妙小饼干30 分钟前
网上球鞋竞拍系统|Java|SSM|VUE| 前后端分离
java·开发语言·maven·mssql
isolusion42 分钟前
Springboot的创建方式
java·spring boot·后端
zjw_rp1 小时前
Spring-AOP
java·后端·spring·spring-aop
Oneforlove_twoforjob1 小时前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言
TodoCoder2 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
向宇it2 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
小蜗牛慢慢爬行2 小时前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate