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 过程就差不多结束了,还有很多不足的地方,希望大家能够提出宝贵的意见。同时也感谢在此次过程中提供帮助的朋友与同事,一同协助才能够一步一步地找出问题的根源,进一步避免工作中的问题。同时也以此精力,警惕要着重注意到注解的使用,规避问题。