问题描述
最近遇到一个接口文档,要求GET请求的参数放在Body里面。虽然很奇怪,但是确实可以返回数据,以下是postman跑出来的结果。

首先要声明一点,GET请求携带Body参数是不遵守规范的。因此这接口文档是十分不专业的!
但是没办法,博主需要去对接这份接口文档,获取相应的数据。于是在Java程序中,使用hutool的客户端进行测试,奇怪的事情发生了!
这是我的测试代码,版本是jdk11,hutool是5.8.1。
大概逻辑就是创建一个请求方式为GET的HttpRequest,然后让这个http请求带上body参数,最后执行一下。

但是跑出来的结果让我感到很奇怪(结果如下图)。明明是相同的url和请求方式,携带的参数也是一样的。但是返回的结果却不一样!
(postman返回的结果是{"result": true,"progress": 100,"states": 3})
这就非常奇怪了,然后我就去问我的mentor,过了一会他告诉我,用hutool的http客户端发送带Body参数的GET请求,会被强行改成POST请求。于是我又去postman,把请求方法改成POST,重新测试了一下。发现返回的结果居然和hutool的http客户端执行得到的结果一模一样!

所以最终得出的结论就是:一个GET请求因为携带了Body参数,被强行修改为POST请求了!
到底是谁修改的呢?这个我还没得出确切的结论,可能是hutool的HttpClient在发送请求时修改了,也有可能是jdk修改的。
解决方法
那怎么才能在Java程序中发送一个带Body参数的GET请求呢?
这里提供两种方法。
方法一
既然在Java程序中用Http的客户端发送带Body参数的GET请求会被强行修改,那我就不用Java程序的Http客户端呗。我可以在Java程序中执行一个脚本,这个脚本的内容就是原生的curl命令,不就行了吗!
代码如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
public class HttpClient {
public String executeCurlRequest() throws IOException, InterruptedException {
// 构造 curl 命令
List<String> command = new ArrayList<>();
command.add("cmd.exe");
command.add("/c");
command.add("curl");
command.add("-k");
command.add("-X");
command.add("GET");
command.add("https://61.130.44.164/ipc/restful/mission");
command.add("-s");
command.add("-H");
command.add("Content-Type: application/json");
command.add("-d");
command.add("{\\\"opcode\\\": 1,\\\"username\\\": \\\"admin\\\",\\\"password\\\": \\\"633193332f4133c5398af514af18ed8d\\\",\\\"seqUuid\\\": \\\"89d97ad8-4f93-47c6-b24b-d33a39f1638e\\\"}");
// 使用 ProcessBuilder 执行命令
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.redirectErrorStream(true); // 合并标准输出和错误输出
// 启动进程
Process process = processBuilder.start();
// 读取命令输出
StringBuilder output = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
}
// 等待进程结束并检查退出码
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("Curl command failed with exit code: " + exitCode + ", output: " + output);
}
return output.toString();
}
public static void main(String[] args) {
try {
String s = new HttpClient().executeCurlRequest();
System.out.println(s);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
代码跑出来的结果如下:

终于得到了正确的结果!但是这个方法太不优雅了,一想到要写原生的curl命令,就头疼。有没有更优雅的方法呢?当然有!但是这个方法不是我原创的哈,是我的mentor想出来的。请看方法二。
方法二
我刚开始尝试了很多成熟的Http客户端,包括hutool HttpClient、Spring RestTemplate、OkHttp以及java.net包提供的HttpClient。但是都没用,要么会把GET请求强制转成POST请求,要么是没有提供GET请求添加Body参数的方法。
然而,还是我的经验不够丰富。我的mentor在Apache HttpClient基础上改造了一下,给出了如下的HttpClientUtil。没错,这个工具类可以在Java程序中优雅地发送带Body的GET请求。
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import java.net.URI;
@Slf4j
public class HttpClientUtil {
private static SSLConnectionSocketFactory sslsf = null;
private HttpClientUtil() {
}
static {
try {
sslsf = new SSLConnectionSocketFactory(
SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(),
NoopHostnameVerifier.INSTANCE);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
public static CloseableHttpClient getHttpClient() {
RequestConfig requestConfig = RequestConfig.custom()
//设置等待数据超时时间
.setSocketTimeout(50000)
//设置连接超时时间
.setConnectTimeout(5000)
.setConnectionRequestTimeout(5000)
.build();
return HttpClients.custom()
.setSSLSocketFactory(sslsf)
.setConnectionManagerShared(true).setDefaultRequestConfig(requestConfig)
.build();
}
public static String doGetJson(String url, String param) {
// 构建支持跳过 SSL 验证的 HttpClient 实例
CloseableHttpClient client = HttpClientUtil.getHttpClient();
String body = "";
HttpGetWithEntity httpGetWithEntity = new HttpGetWithEntity(url);
org.apache.http.HttpEntity httpEntity = new StringEntity(param, ContentType.APPLICATION_JSON);
httpGetWithEntity.setEntity(httpEntity);
//执行请求操作,并拿到结果(同步阻塞)
try (CloseableHttpResponse response = client.execute(httpGetWithEntity)) {
//获取结果实体
org.apache.http.HttpEntity entity = response.getEntity();
if (entity != null) {
//按指定编码转换结果实体为String类型
body = EntityUtils.toString(entity, "UTF-8");
}
} catch (Exception e) {
log.error("doGetJson() error.", e);
}
return body;
}
static class HttpGetWithEntity extends HttpEntityEnclosingRequestBase {
private final static String METHOD_NAME = "GET";
@Override
public String getMethod() {
return METHOD_NAME;
}
public HttpGetWithEntity() {
super();
}
public HttpGetWithEntity(final URI uri) {
super();
setURI(uri);
}
HttpGetWithEntity(final String uri) {
super();
setURI(URI.create(uri));
}
}
}
虽然写不出这么好的工具类,但是可以看懂。
那我来总结一下这个工具类的实现思路:
自己写一个类去继承HttpEntityEnclosingRequestBase,而HttpEntityEnclosingRequestBase又继承了一个抽象类HttpRequestBase,于是我们就可以重写HttpRequestBase的一个抽象方法getMethod。每次getMethod都直接返回GET。
为什么要去继承HttpEntityEnclosingRequestBase呢?因为只有继承了这个类,才能给Http请求加Body参数(在Apache HttpClient里面叫Entity)。
Apache原先继承HttpEntityEnclosingRequestBase的都是一些HttpPost,HttpPut,HttpDelete

也就是只允许POST、PUT、DELETE请求携带Body参数。那我们自己写一个类叫做HttpGetWithEntity,那么这个HttpGet不就也可以设置Body参数了吗!大功告成!
最后来看看测试结果:


881

被折叠的 条评论
为什么被折叠?



