Debug|OpenFeign Get请求变成了Post请求

2022-04-301777

Debug|OpenFeign Get请求变成了Post请求

请求代码

请求模块

@RequestMapping("/helloLocal")
public class HelloController {
    @Autowired
    private RemoteUserService remoteUserService;

    @GetMapping("/sayHelloTest")
    public AppUserDTO sayHelloTest(Long useId) {
        // 获取用户信息
        return remoteUserService.getUserInfoByUserIdTest(useId).getData();
    }

    @GetMapping("/sayHello")
    public AppUserDTO sayHello(Long userId) {
        // 获取用户信息
        return remoteUserService.getUserInfoByUserId(userId).getData();
    }
}

OpenFeign 接口

public interface RemoteUserService {
    /**
     * 根据用户Id获取用户信息
     *
     * @param userId
     * @return 结果
     */
    @GetMapping(value = "/helloRemove")
    Result<AppUserDTO> getUserInfoByUserIdTest(Long userId);

    /**
     * 根据用户Id获取用户信息
     *
     * @param userId
     * @return 结果
     */
    @GetMapping(value = "/helloRemove")
    Result<AppUserDTO> getUserInfoByUserId(@RequestParam("userId") Long userId);
}

被请求模块

@RestController
@RequestMapping("/helloRemove")
public class AppUserController extends BaseController {

    @ApiOperation(value = "根据用户Id获取用户信息", notes = "")
    @GetMapping
    public Result<AppUserDTO> get(Long userId) {
       return Result.ok(AppUserDTO.builder().username("张三").sex("男").userId(userId).build());
    }

    @ApiOperation(value = "根据用户Id更改用户性别为女性", notes = "")
    @PostMapping
    public Result<AppUserDTO> change(Long userId) {
        return Result.ok(AppUserDTO.builder().username("张三").sex("女").userId(userId).build());
    }

}

测试

  • 测试一

    • 请求:/auth/helloLocal/sayHello?userId=1

    • 猜想结果:

      {
        "userId": 1,
        "username": "张三",
        "sex": "男"
      }
  • 测试二

    • 请求:/auth/helloLocal/sayHelloTest?useId=1

    • 猜想结果:同测试一结果一至。

虽然两个请求看似都是远程调用同一个方法,但是获取到的结果并非直至,且真正调用到的方法也不相同。

  • 测试一最终请求到的是 /helloRemove ,方法为 GET

    • 结果

      {
        "userId": 1,
        "username": "张三",
        "sex": "男"
      }
  • 测试二最终请求到的是/helloRemove ,方法为 POST

    • 结果

      {
        "userId": null,
        "username": "张三",
        "sex": "女"
      }

接下来通过 Debug 的过程,看看究竟是什么导致两个测试的结果不相同。

Debug

  • 此次测试使用 Sentinel 框架进行熔断降级。所以首先进入的是SentinelInvocationHandler的invoke方法中

    @Override
        public Object invoke(final Object proxy, final Method method, final Object[] args)
                throws Throwable {
            //……
                // resource default is HttpMethod:protocol://url
                if (methodMetadata == null) {
                    result = methodHandler.invoke(args);
                }
                //……
    
            return result;
        }
  • 本次测试的openFeign 通过SynchronousMethodHandler 这个同步方法处理器完成准备请求参数、发送Http请求并解析结果整个过程

     @Override
      public Object invoke(Object[] argv) throws Throwable {
       //……
        while (true) {
          try {
            return executeAndDecode(template, options);
          } catch (RetryableException e) {
           //…… 
        }
      }
  • 接下来查看请求和解码的过程

     Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
        Request request = targetRequest(template);
    
        if (logLevel != Logger.Level.NONE) {
          logger.logRequest(metadata.configKey(), logLevel, request);
        }
    
        Response response;
        long start = System.nanoTime();
        try {
          response = client.execute(request, options);
         //……
        } catch (IOException e) {
          //……
        }
      }
    • 从当前 Request 中还是能够看到,请求的方法以然为 GET

      GET http://luwei-system/helloRemove HTTP/1.1
      Accept-Encoding: gzip
      Accept-Encoding: deflate
      Content-Length: 1
      Content-Type: application/json
      X-Forwarded-For: 127.0.0.1
      
  • 接下来就到达负责负载均衡的 LoadBalancerFeignClient ,使用默认的 Ribbon 完成

    @Override
        public Response execute(Request request, Request.Options options) throws IOException {
            try {
                URI asUri = URI.create(request.url());
                String clientName = asUri.getHost();
                URI uriWithoutHost = cleanUrl(request.url(), clientName);
                FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
                        this.delegate, request, uriWithoutHost);
    
                IClientConfig requestConfig = getClientConfig(options, clientName);
                return lbClient(clientName)
                        .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
            }
            catch (ClientException e) {
                //……
            }
        }
  • 到达执行请求与负载均衡的执行方法里

     public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
            LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);
    
            try {
                return command.submit(
                    new ServerOperation<T>() {
                        @Override
                        public Observable<T> call(Server server) {
                            URI finalUri = reconstructURIWithServer(server, request.getUri());
                            S requestForServer = (S) request.replaceUri(finalUri);
                            try {
                                return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                            } 
                            catch (Exception e) {
                                return Observable.error(e);
                            }
                        }
                    })
                    .toBlocking()
                    .single();
            } catch (Exception e) {
               //……
        }
  • 直到 Ribbon 交出请求的方法为止,当前的request请求 method 仍同上述的请求一样为 GET

    @Override
        public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
                throws IOException {
            //……
            Response response = request.client().execute(request.toRequest(), options);
            return new RibbonResponse(request.getUri(), response);
        }
  • 接下来,到达了SeataFeignClient结果交接棒,完成分布式事务的装备。

    @Override
        public Response execute(Request request, Request.Options options) throws IOException {
    
            Request modifiedRequest = getModifyRequest(request);
            return this.delegate.execute(modifiedRequest, options);
        }
    
  • 交接到OpenFeign 的 Client接口,完成发送的方法

      @Override
        public Response execute(Request request, Options options) throws IOException {
          HttpURLConnection connection = convertAndSend(request, options);
          return convertResponse(connection, request);
        }
    
  • 进入到了convertAndSend已经是接近了真相,一路走下去进入了下面对body的判断
    HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
    // ……
      if (request.body() != null) {
        if (disableRequestBuffering) {
          if (contentLength != null) {
            connection.setFixedLengthStreamingMode(contentLength);
          } else {
            connection.setChunkedStreamingMode(8196);
          }
        }
        connection.setDoOutput(true);
        OutputStream out = connection.getOutputStream();
       //……
      }
    }
  • 进入了HttpURLConnection的方法内
public synchronized OutputStream getOutputStream() throws IOException {
    this.connecting = true;
    SocketPermission var1 = this.URLtoSocketPermission(this.url);
    if (var1 != null) {
       //……
    } else {
        return this.getOutputStream0();
    }
}
  • 进入到 OutputStream getOutputStream0(),从而看到了关键,使得Get请求转变为了POST请求
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";
                }

           //……
        } catch (IOException var7) {
            this.disconnectInternal();
            throw var7;
        }
    }
  • 到目前为止,connection 的请求就彻底编程了 Post 请求了。在其中的过程,能够推断出问题的关键就是对执行请求的过程中对 request.body 进行了非空的判断。因此,要找出请求方法的被动变化,就需要直到究竟是什么时候我的请求出现 body 这玩意。

  • 事情就要回到了 SentinelInvocationHandler执行 invoke()的时候

    • RequestTemplate 在工厂方法里面根据参数来生产出来的

      @Override
        public Object invoke(Object[] argv) throws Throwable {
          RequestTemplate template = buildTemplateFromArgs.create(argv);
          //……
        }
    • 稍微快进一下,create 的方法里面有一个 resolve 的方法,里面就有 body ,是从请求参数里面获取的,定位参数则是根据 metadata.bodyIndex(),里面记录了请求参数中 body 的为止。这时就会产生疑惑,为什么 Get 请求会有 body 的 index 呢,到底这个又是从那里来的。

       @Override
          protected RequestTemplate resolve(Object[] argv,
                                            RequestTemplate mutable,
                                            Map<String, Object> variables) {
            Object body = argv[metadata.bodyIndex()];
            checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());
           //……
          }
    • 要找到这个设定 bodyInndex 的来源则是 MethodMetadata 方法元数据这个类上面找答案了。

    • 要找到这个来源,则要再回到程序启动的时候。SentinelContractHolder 把各个远程调用的方法数据组装成各个相应的MethodMetadata 实例中,在调用的时候就直接使用。

      • 测试一:RemoteUserService#getUserInfoByUserId(Long) 的bodyType 参数的值、bodyIndex 参数的直均为null
      • 测试二:RemoteUserService#getUserInfoByUserIdTest(Long) 的bodyType 的值为class java.lang.Long、bodyIndex 的值为0
    • 到此为止,就能够解释到为什么同样是 Get 请求,测试一的请求不会有body,而测试二会有。两个测试的不同之处就在与远程调用的方法中请求的参数有无带有@RequeseParam这个注解。

    • 至于这个参数到底为何能够在 OpenFeign 的请求里面生效,就要把目光放到FeignClientsConfiguration 这个配置类上面,里面有一个很妙的地方。

      @Bean
          @ConditionalOnMissingBean
          public Contract feignContract(ConversionService feignConversionService) {
              return new SpringMvcContract(this.parameterProcessors, feignConversionService);
          }
      • 表明了直接使用了SpringMVC的Contract,这就说明了 httpAnnotation 为什么能够生效了。
  • 那接下来的问题就是,只有@RequesrParam 能够帮我们保住 GET 请求吗?SpringMvcContract 里面就解答了疑惑。SpringMVC默认的这几个注解,OpenFeign 也是能够承认的。

    • 接下来尝试了@SpringQueryMap@PathVariable 均可以实现
private List<AnnotatedParameterProcessor> getDefaultAnnotatedArgumentsProcessors() {

   List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();

   annotatedArgumentResolvers.add(new MatrixVariableParameterProcessor());
   annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
   annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
   annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
   annotatedArgumentResolvers.add(new QueryMapParameterProcessor());
   annotatedArgumentResolvers.add(new RequestPartParameterProcessor());

   return annotatedArgumentResolvers;
}
  • 接下来就是看看,如果是 DELET 和 PUT 请求会被影响到吗?
    • 经过简单的测试,DELETE 和 PUT 确实能够正常执行到,至于为何只有GET请求才会被转换,而其他两个不会,就需要接下去的进一步学习了。
  • 到此为止,本次粗制滥造的 debug 过程就差不多结束了,还有很多不足的地方,希望大家能够提出宝贵的意见。同时也感谢在此次过程中提供帮助的朋友与同事,一同协助才能够一步一步地找出问题的根源,进一步避免工作中的问题。同时也以此精力,警惕要着重注意到注解的使用,规避问题。
分享
点赞1
打赏
上一篇:Docker常用命令笔记(一)
下一篇:如何利用“偷懒”方式实现滑动验证