上游服务提供的方法非常比较奇特,查询接口,定义的GET方法,参数通过request body传递的,在使用Feign Client封装GET方法调用时,会遇到一个报错,“405 Method Not Allowed”。通过查询,知道这个错误原因是HTTP调用方法错误,比如:定义的API是GET方法,通过POST方法(非GET方法)调用,就会返回这个错误。
@RequestLine("GET /api/user/get/")
Object getUser(@HeaderMap Map headers, UserRequest request);
奇怪代码明明写得是使用GET方法啊,进一步查资料,得知原因是Feign client框架本身有一个坑:Feign client框架,默认情况下使用的是HttpURLConnection完成实际的http请求调用,但是HttpURLConnection本身不支持GET方法调用时带有body,带有body的调用方法,只能是POST方法。
// sun.net.www.protocol.http.HttpURLConnection
private synchronized OutputStream getOutputStream0() throws IOException {
try {
if(!this.doOutput) {
throw new ProtocolException("cannot write to a URLConnection if doOutput=false - call setDoOutput(true)");
} else {
if(this.method.equals("GET")) {
this.method = "POST";
}
// ........
}
}
}
HTTP GET方法调用,到底支不支持带有body呢,HTTP协议是支持的,没有禁止,但是呢,不建议这么做,不是一个良好的习惯,因为有些浏览器
啥的可能不支持,这个时候,你写的方法就尴尬了。
Stackoverflow解释如下:
In other words, any HTTP request message is allowed to contain a message body, and thus must parse messages with that in mind. Server semantics for GET, however,
are restricted such that a body, if any, has no semantic meaning to the request. The requirements on parsing are separate from the requirements on method semantics.
So, yes, you can send a body with GET, and no, it is never useful to do so.
This is part of the layered design of HTTP/1.1 that will become clear again once the spec is partitioned (work in progress).
方案一:
网上可以很容易搜索到这个解决方法,相关博客非常多,直接copy的情况,太严重了。但是实际验证,没有生效,具体原因待排查。
1.yml配置文件中,加入feign的配置项:feign.httpclient.enabled: true
2.增加如下maven依赖。
方案原理:HttpURLConnection不支持GET方法带有body的调用,ApacheHttpClient支持GET方法带有body的调用。这个配置,就是将feign client默认使用的HTTP调用方式,从HttpURLConnection切换到ApacheHttpClient方式。
方法不生效原因:
1.可能HTTP调用方式没有切换成功,也就是配置没有生效。(确定是这个原因,因为我使用的Feign方式:Feign.builder()默认生成的就是HttpURLConnection方式的http请求调用。相关源码如下:
// feign.Feign.Builder
private Client client = new Client.Default(null, null);
// feign.Client.Default
final HttpURLConnection connection = (HttpURLConnection) new URL(request.url()).openConnection();
因此相关配置修改是不生效的,需要重新生成一个client才行,比如:ApacheHttpClient
2.可能ApacheHttpClient本身也不支持GET方法带有body的请求。(因为直接使用ApacheHttpClient,发现没有支持GET方法带有body的调用方式)
补充:(待验证,证明)
feign分别尝试了Java原生URLConnection,OkHttp,ApacheHttpClient三种方式:
1.URLConnection 报405错误,说明http方法不对,但是feign配置是GET方法,查feign的日志也是用的GET方法。后来发现原因是URLConnection在的
原因:对于有request body的GET方法,自动改为POST方法了。
2.OkHttp 直接报错:method GET must not have a request body.
3.ApacheHttpClient完美支持。
方案二:
使用AsyncHttpClient,因为AsyncHttpClient支持GET方法带有Body的调用。
网上也可以很容易搜索到这个解决方法,感觉都是复制粘贴的,没有经过验证和实证,内容完全一样,但是都缺少最关键的信息,没有给出需要引用的jar包,怎么使用测试呢?而需要引用的jar包还不好找到,实在是大坑。
1.引入maven依赖
2.解决方法demo
public static String get(String url, String bodyData, Map
// 构建请求
BoundRequestBuilder requestBuilder = asyncHttpClient.prepareGet(url).setBody(bodyData);
headers.forEach(requestBuilder::addHeader);
List
requestBuilder.execute()
.toCompletableFuture()
.thenAccept(list::add)
.join();
if (list.isEmpty()) {
return null;
}
Response response = list.get(0);
if (response.getStatusCode() != 200) {
return null;
}
return response.getResponseBody();
}
备注1:
1.方法可以返回map,增加:new ObjectMapper().readValue(response.getResponseBody(), Map.class);
2.方法本身必须返回json 对象的string才行,不能是非json对象的string,否则解析异常。
备注2:
1.没有body的get方法,去掉.setBody(bodyData)即可。
2.没有header的get方法调用,去掉headers.forEach(requestBuilder::addHeader);即可。
网上资源很多、很丰富,各种问题解决方案很多,但是也存在很多缺陷,不去验证、实践,根本不知道里面有问题,因此不要随便copy别人的博客,往往copy的博客本身就存在潜在的问题,copy之前,请试验一下,证明方法是正确的,减少给需要同学的误导。
5.参考资料
1.https://yanbin.blog/why-http-get-cannot-sent-data-with-reuqest-body/(http://www.programmersought.com/article/752346261/)
2.https://blog.csdn.net/f641385712/article/details/82431502
3.https://segmentfault.com/q/1010000011958034
4.https://stackoverflow.com/questions/978061/http-get-with-request-body
update by 20200617:
提供另一种方式:
使用Apache的HttpClient,调用Post方法;
关键点: 重写Post方法的参数HttpEntity;import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; public class HttpGetWithEntity extends HttpEntityEnclosingRequestBase { public final static String METHOD_NAME = "GET"; @Override public String getMethod() { return METHOD_NAME; } } =================================================调用HttpClient HttpGetWithEntity e = new HttpGetWithEntity(); ... e.setEntity(yourEntity); ... response = httpclient.execute(e);