• Feign get接口传输对象引发一场追寻


    一个报错引发的追寻之路:

    Feign get接口传输对象,调用方接口代码:

    @FeignClient(name = "manage")
    public interface AccessApiService {
    
        @RequestMapping(value = "/interface/listWithRules", method = RequestMethod.GET)
        Result<PageQueryResult<InterfaceInfo>> listWithRules(ApplicationSearchParams params);
    }
    

    Feign中定义的一个get接口,参数是一个对象,调用后返回一个 「Request method ‘POST’ not supported」的报错。

    追寻开始

    断点打进去看,查看feign.Client.Default内部类的execute方法,最终使用了jdk自带的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";
     }
    

    传输对象这种情况,被粗暴的用修改请求method的方式来解决,在http协议里复杂的内容可以放入body。所以就有了上面莫名其妙的报错。我们想,如果不修改请求方式,依然使用get,我们就必须要吧对象中的参数一个个拿出来拼接到url里,试想下这个对象再复杂一些,比如包了其他的对象呢,怎么拼接?此时我们想一个对象直接转json丢body传输的确是个好办法,怪不的上面想这么粗暴的办法。
    但是修改请求方式破坏了服务提供放的接口,看起来不是很优雅。找解决方案的时候说使用httpclient代替jdk自带方式就可以解决问题了。
    具体如下配置:

    1,开启feign的httpclient
    feign:
      httpclient:
        enabled: true
    
    2,引入feign-httpclient,注意版本需要和自己依赖的feign版本保持一致
    <dependency>
      <groupId>com.netflix.feign</groupId>
      <artifactId>feign-httpclient</artifactId>
      <version>${feign-httpclient}</version>
    </dependency>
    
    3,确认引入httpclient包
    <dependency>
     <groupId>org.apache.httpcomponents</groupId>
     <artifactId>httpclient</artifactId>
     <version>4.5.3</version>
    </dependency>
    

    如此请求服务就通了,可是会发现对象的参数都是空的,还有一步:
    服务提供的接口上需要加上@RequestBody来接参数。对,就是@RequestBody!也就是在body里拿的参数。

    Result<PageQueryResult<InterfaceInfo>> listWithRules(@RequestBody ApplicationSearchParams params);
    

    那么问题来了,一个get请求怎么来的body呢?
    好吧,http协议只是一个协议,理论上只要你想实现无论用什么请求方式都可以带body,只是我们规范约定body是post请求专用而已。
    那么httpclient并不会做这个事,还是feign在调用httpclient时,产生了一个get请求并且带着body。
    具体代码在feign.httpclient.ApacheHttpClient#toHttpUriRequest:

    if (request.body() != null) {
        entity = null;
        Object entity;
        if (request.charset() != null) {
            ContentType contentType = this.getContentType(request);
            content = new String(request.body(), request.charset());
            entity = new StringEntity(content, contentType);
        } else {
            entity = new ByteArrayEntity(request.body());
        }
    
        requestBuilder.setEntity((HttpEntity)entity);
    }
    

    至此,似乎我们只能选择后者来解决了,这种方式似乎也是有点畸形,毕竟都破坏掉http协议规范了。
    所以不推荐使用,推荐所有get请求,调用方的接口代码全部写成类似如下:

    @RequestMapping(value = "/interface/listWithRules", method = RequestMethod.GET)
    Result<PageQueryResult<InterfaceInfo>> listWithRules(
            @RequestParam(value = "gatewayName") String gatewayName, @RequestParam(value = "modifiedTime") Integer modifiedTime, @RequestParam(value = "pageIndex") Integer pageIndex, @RequestParam(value = "pageSize") Integer pageSize);
    

    而服务提供方可以使用对象。这里注意每个参数注解RequestParam必须要带有value字段,否则会有这个报错:
    「RequestParam.value() was empty on parameter 0」
    问题又来了,在spring mvc使用中都是默认参数名字叫什么这个value就叫什么的呀,怎么这里还必须要自己定义一下呢?
    你遇到的问题世界上总有人已经遇到过,我觉得这个解答是最好的,其他你都不用再看了:
    https://stackoverflow.com/questions/44313482/fiegn-client-with-spring-boot-requestparam-value-was-empty-on-parameter-0/52099007

    再展开一下,feign在解析@RequestParam注解时的代码在org.springframework.cloud.netflix.feign.annotation.RequestParamParameterProcessor 如下:

    public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
       int parameterIndex = context.getParameterIndex();
       Class<?> parameterType = method.getParameterTypes()[parameterIndex];
       MethodMetadata data = context.getMethodMetadata();
    
       if (Map.class.isAssignableFrom(parameterType)) {
          checkState(data.queryMapIndex() == null, "Query map can only be present once.");
          data.queryMapIndex(parameterIndex);
    
          return true;
       }
    
       RequestParam requestParam = ANNOTATION.cast(annotation);
       String name = requestParam.value();
       checkState(emptyToNull(name) != null,
             "RequestParam.value() was empty on parameter %s",
             parameterIndex);
       context.setParameterName(name);
    
       Collection<String> query = context.setTemplateParameter(name,
             data.template().queries().get(name));
       data.template().query(name, query);
       return true;
    }
    

    因为通过反射又无法拿到方法字段的名字(jdk8以上,可以通过增加编译参数开启这个能力),所以feign就放弃了,而spring 为什么能做到获取到方法字段名称呢?
    因为它有一套使用asm为基础解系class文件的能力,就是直接打开class看,那还有什么查不到的哦。具体类:LocalVariableTableParameterNameDiscoverer 这个类已经关系的asm了。

    追寻结束

    到这里一场追寻告一段落,会想一下也很简单,为了解决get请求传输复杂对象的情况,一个http请求必然会面临这个问题,但是话说回来,一个get接口规范上不应该会定义什么复杂对象,而是几个参数而已,如果需呀很复杂的对象才能完成这个get接口,可能需呀思考这个接口设计的合理性了。

  • 相关阅读:
    在IIS中浏览网站时出现:无法打开登录所请求的数据库 "***",登录失败
    Java中的深拷贝和浅拷贝(转载)
    Java的Final和C#的Const,Readonly比较分析(转载)
    C#中的Sealed和J#中的Final比较(转载)
    Java全系列帮助文档下载
    The Willpower Instinct(自控力,意志力)
    瓦片地图的前世今生(转载)
    创建CUDA项目
    CUDA warning C4819的消除
    并行调用
  • 原文地址:https://www.cnblogs.com/killbug/p/9776287.html
Copyright © 2020-2023  润新知