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

目录

 

背景:     

现象:

分析:

总结:


 

背景:     

      在项目中,使用feignClient 进行http 服务调用,feignClient的默认连接方式为HttpURLConnection,因为HttpURLConnection没有连接池,并发高的时候,会有一定的网络开销,在做项目优化的时候,替换改为okHttp以便复用其连接池。基于这个思路,按照feignClient的配置要求,在yaml中进行配置替换后,简单验证没问题,则正常上线了。。。。

现象:

不出意外的还是出了意外,服务调用出现了405 - 用来访问的 HTTP 调用不被允许(方法不被允许),赶紧回滚排查。

排查报错的调研发现,调用写法形如下:

@GetMapping("/inner/xxx/xx/xx")
Result<String> 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<String> 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<OutputStream>() {
                        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请求,故而就会有问题了

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`@FeignClient` 是 Spring Cloud 中用于声明一个基于 Feign 实现的 REST 客户端的注解。通过该注解,我们可以定义一个接口,并在接口中声明需要调用的 HTTP 接口的地址、请求方法、请求参数等信息,然后 Spring Cloud 将自动根据接口定义生成一个动态代理对象,我们只需要像使用本地的 Bean 一样使用该接口即可。 `@FeignClient` 注解常用的属性包括: - `name`:指定 Feign 客户端的名称,该名称将作为 Spring Bean 注册到容器中,默认为当前注解标记的接口名的小写形式。 - `url`:指定 Feign 客户端请求的 URL,可以是完整的 URL 或者是相对 URL。当与 `name` 属性同指定,`url` 属性优先级更高。 - `configuration`:指定 Feign 客户端的配置类,用于进行客户端级别的配置,比如请求超间、重试次数等。 - `fallback`:指定 Feign 客户端的 Fallback 类,用于处理请求失败的逻辑,例如返回默认值或者抛出异常。 - `fallbackFactory`:指定 Feign 客户端的 Fallback 工厂类,用于处理请求失败的逻辑,与 `fallback` 属性的区别在于,`fallbackFactory` 可以获取原始的异常信息,从而进行更加详细的处理。 示例代码: ``` @FeignClient(name = "example-service", url = "http://localhost:8080") public interface ExampleFeignClient { @GetMapping("/example") public String getExample(); } ``` 在上述示例中,`@FeignClient` 注解声明了一个名为 `example-service` 的 Feign 客户端,该客户端请求的 URL 为 `http://localhost:8080`。接口中声明了一个 `getExample()` 方法,用于调用名为 `example` 的 HTTP 接口。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值