目录
背景:
现象:
分析:
总结:
在项目中,使用feignClient 进行http 服务调用,feignClient的默认连接方式为HttpURLConnection,因为HttpURLConnection没有连接池,并发高的时候,会有一定的网络开销,在做项目优化的时候,替换改为okHttp以便复用其连接池。基于这个思路,按照feignClient的配置要求,在yaml中进行配置替换后,简单验证没问题,则正常上线了。。。。
不出意外的还是出了意外,服务调用出现了405 - 用来访问的 HTTP 调用不被允许(方法不被允许),赶紧回滚排查。
排查报错的调研发现,调用写法形如下:
@GetMapping("/inner/xxx/xx/xx")
Result test(@RequestBody TestDto dto);
明明是get请求,且之前是正常的,为什么替换了okHttp后,会出现服务调用不被允许呢,进一步排查,发现最终发起的是post请求,百思不得其解。。。。。
从feignClient的源码开始排查,feignClient 默认是feign.Client.Default#execute 执行http请求,源码为:
@Override
public Response execute(Request request, Options options) throws IOException {
// convertAndSend 获取连接
HttpURLConnection connection = convertAndSend(request, options);
return convertResponse(connection).toBuilder().request(request).build();
}
进入查看convertAndSend的逻辑,重点是
//如果请求的请求体不为空,则设置 connection.setDoOutput(true);,记住这个
HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
final HttpURLConnection
connection =
(HttpURLConnection) new URL(request.url()).openConnection();
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
if (sslContextFactory != null) {
sslCon.setSSLSocketFactory(sslContextFactory);
}
if (hostnameVerifier != null) {
sslCon.setHostnameVerifier(hostnameVerifier);
}
}
connection.setConnectTimeout(options.connectTimeoutMillis());
connection.setReadTimeout(options.readTimeoutMillis());
connection.setAllowUserInteraction(false);
connection.setInstanceFollowRedirects(true);
connection.setRequestMethod(request.method());
Collection contentEncodingValues = request.headers().get(CONTENT_ENCODING);
boolean
gzipEncodedRequest =
contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
boolean
deflateEncodedRequest =
contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);
boolean hasAcceptHeader = false;
Integer contentLength = null;
for (String field : request.headers().keySet()) {
if (field.equalsIgnoreCase("Accept")) {
hasAcceptHeader = true;
}
for (String value : request.headers().get(field)) {
if (field.equals(CONTENT_LENGTH)) {
if (!gzipEncodedRequest && !deflateEncodedRequest) {
contentLength = Integer.valueOf(value);
connection.addRequestProperty(field, value);
}
} else {
connection.addRequestProperty(field, value);
}
}
}
// Some servers choke on the default accept string.
if (!hasAcceptHeader) {
connection.addRequestProperty("Accept", "*/*");
}
//如果请求的请求体不为空,则设置 connection.setDoOutput(true);,记住这个
if (request.body() != null) {
if (contentLength != null) {
connection.setFixedLengthStreamingMode(contentLength);
} else {
connection.setChunkedStreamingMode(8196);
}
connection.setDoOutput(true);
OutputStream out = connection.getOutputStream();
if (gzipEncodedRequest) {
out = new GZIPOutputStream(out);
} else if (deflateEncodedRequest) {
out = new DeflaterOutputStream(out);
}
try {
out.write(request.body());
} finally {
try {
out.close();
} catch (IOException suppressed) { // NOPMD
}
}
}
return connection;
}
重点是这段逻辑,请记住:如果请求的请求体不为空,则设置 connection.setDoOutput(true);
if (request.body() != null) {
// 忽略其他逻辑
// ....
connection.setDoOutput(true);
OutputStream out = connection.getOutputStream();
}
继续跟进 OutputStream out = connection.getOutputStream();的源码
@Override
public synchronized OutputStream getOutputStream() throws IOException {
connecting = true;
SocketPermission p = URLtoSocketPermission(this.url);
if (p != null) {
try {
return AccessController.doPrivilegedWithCombiner(
new PrivilegedExceptionAction() {
public OutputStream run() throws IOException {
return getOutputStream0();
}
}, null, p
);
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
} else {
return getOutputStream0();
}
}
继续查看:sun.net.www.protocol.http.HttpURLConnection#getOutputStream0
private synchronized OutputStream getOutputStream0() throws IOException {
try {
if (!doOutput) {
throw new ProtocolException("cannot write to a URLConnection"
+ " if doOutput=false - call setDoOutput(true)");
}
if (method.equals("GET")) {
method = "POST"; // Backward compatibility
}
//忽略其他逻辑
//......
}
由此看到了,在上一步,因为逻辑中发现有请求体,设置了connection.setDoOutput(true);此处,doOutput 为true时,如果请求是GET请求,会转为POST请求,结果真相大白。。。。
此种问题的出现,本质还是对rest接口定义不规范造成的。比如之前被调用方可能是用@RequestMapping注解,没特殊指定是get请求,还是post请求,则两种请求都可以,后面调用方可能改为了指定是post请求约束。我们作为调用方,表象是用的get请求,实际走的是post请求,所以没有影响,后面改为okHttp后,okHttp不会做这种特殊的转换,所以我们的请求还是get请求,故而就会有问题了