FeignClient默认连接方式HttpURLConnection之坑---get请求变为post,访问405

目录

 

背景:     

现象:

分析:

总结:


 

背景:     

      在项目中,使用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请求,故而就会有问题了

你可能感兴趣的:(spring全家桶,okhttp,feignclient,GET转POST,405状态码)