将 FeignClient 的请求记录成 cURL 格式

共 5318字,需浏览 11分钟

 ·

2021-07-03 22:54

cURL


cURL 是一个命令行工具,常用来快速构造 Http 请求。使用它,可以提高传统的前后端分离开发时联调中的沟通效率,在文章《使用 cURL 提高前后端开发连调中的沟通效率》中专门讨论过。而在微服务架构成为主流的今天,在平时的开发过程中,各个服务间也会有相互调用,免不了和前后端类似的联调,在不同团队间沟通时,仍然建议采用 cURL 来进行沟通,减少不必要的来回。


cURL 其实功能特别多,用法也很灵活。但是典型的语法形式,不如参考 Postman 生成的。可以看出 cURL 命令文本的格式一般是:


curl --location --request [httpMethod] '[url]' \--header 'header1: value1' \--header 'header2: value2' \--data-raw 'request body'


Postman


Postman 是一个 API 开发协作平台,特别方便构造 Http 请求,强大的功能里,有一部分功能可以被看成是 cURL 的图形用户界面。你可以通过图形界面构造一个实际的请求,然后查看其对应的代码形式。在《同步用户微信头像的 NodeJs 实现》中就利用 Postman 的这个功能查看了请求对应的 NodeJs 代码,但是在这里,我们需要查看的是对应的 cURL 代码:




Feign


前端项目调用后端项目,一般通过 JavaScript 世界里的 HttpClient 进行。后端和后端的相互调用,其实本质上也是通过相应的开发语言的世界中的某种 Client 端进行的。如果后端和后端之间通过 Http 协议沟通,而且开发语言是 Java,甚至是基于 Spring 框架的项目,那么很常见的,会使用 Feign 这个客户端。Feign 是 Java 世界里的 HttpClient,由 Netflix 开发。


默认的 Feign 日志


平时的开发联调中,服务间调用失败的原因查找,往往需要不同的团队进行沟通,调用方的痛点是,明明是上游被调用方的服务不稳定,但是偏偏要自己拿出证据,帮助他们定位/重现问题。然而 Feign 的日志是这样的:



即,一条请求的不同部分分散在多条日志里,想要重放请求非常不便,需要再次手动组装。


解决方案


在日志里,将对外请求打印成 cURL 命令。想要重放,或者把失败的请求发送给别的团队,只需要把 cURL 命令复制出来就可以了。


比如,对于上面的原始 Feign 请求日志,打印成下面这样:


curl --location --request POST 'http://target.com/campaign/detailPage' \ --header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJsZWdvX21lbWJlciIsImlzcyI6Ijk3NTUxNzExNDY4MzMyMDAiLCJzZXJ2aWNlS2V5IjoiNTAyOTc0NjY1NTc3MTE0MCIsImV4cCI6MTYyNTIwMTU3NSwidXNlck5hbWUiOiJkZXZfaW50ZXJmYWNlX2NsZWludF93bXBAbGVnby5jbiIsImlhdCI6MTYyNTE5Nzk3NX0.OTN2rBU8wQ1LG_B6mYjwiQMCtzxGf2rMEx0-w1VOxGmMdLQXm5sodEn-30_b91Rx '\ --header 'Content-Length: 182 '\ --header 'Content-Type: application/json '\ --header 'x-request-id: 6987220C0BCE4C84BC010B58B8E3DB2D '\ --header 'X-Transaction-Id: 8ced0076-629a-4f19-99a0-6302ca493b5b '\ \--data-raw '{  "ascOrDesc":"asc",  "orderBy":"update_time",  "pageIndex":1,  "pageSize":100,  "status":["Online","Offline"],  "tierValues":["3"],  "updateTimeEnd":null,  "updateTimeStart":null}'


思路


Feign 提供了 RequestInterceptor 接口,可以用来添加/删除/修改/查询请求的任何部分,只需要在配置类里面创建类型为 RequestInterceptor 类型的 Bean 即可。因此可以利用这个 RequestInterceptor 机制,将拥有的请求详情做一个序列化,并打印到日志里。只是这个序列化,是序列化成一个 cURL 命令文本而已。


序列化时,需要从请求详情里读取目标服务器域名、资源路径、请求方法、头部字段以及请求负载 payload,然后灌进 cURL 的字符串模版里。


TDD


简单来说,TDD 是一种先写测试,再写实现的测试驱动开发模式。通过 TDD,不但可以构建重构屏障,还能起到活文档的作用,更能够逼迫实现代码从一开始就考虑到可测性,以及更好的代码可读性(TDD 的红-绿-重构循环♻️)。总之,可以强迫开发过程中保持良好的代码设计。


测试用例


把思路用代码描述下来,就是说在 Feign 的配置里,给它添加一个 toCurl 方法,它可以将某个请求详情,序列化成 cURL 命令行文本:


package com.hardway.infrastructure.rpc.common;
import feign.Request;import feign.RequestTemplate;import feign.Target;import lombok.var;import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class FakeTarget implements Target {
@Override public Class type() { return null; }
@Override public String name() { return null; }
@Override public String url() { return "https://fake.endpoint.com"; }
@Override public Request apply(RequestTemplate requestTemplate) { return null; }}
class FeignConfigTest {
@Test void toCurl() { var template = new feign.RequestTemplate(); template.feignTarget(new FakeTarget()); template.method(Request.HttpMethod.POST); template.body("Hello");
var sut = new FeignConfig(); var res = sut.toCurl(template);
assertEquals("curl --location --request POST 'https://fake.endpoint.com/' \\\n --header 'Content-Length: 5 '\\\n \\\n--data-raw 'Hello'", res); }}


实现代码


package com.hardway.infrastructure.rpc.common;
import feign.RequestInterceptor;import lombok.val;import lombok.var;import org.slf4j.LoggerFactory;import org.slf4j.MDC;import org.springframework.cloud.openfeign.EnableFeignClients;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;
import java.nio.charset.StandardCharsets;import java.util.Arrays;import java.util.Locale;import java.util.stream.Collectors;
@EnableFeignClients(basePackages = "com.hardway.infrastructure.rpc")@Configurationpublic class FeignConfig { org.slf4j.Logger logger = LoggerFactory.getLogger(FeignConfig.class);
@Bean public RequestInterceptor requestInterceptor() { return template -> { template.header("x-tracing-id", MDC.get("x-tracing-id")); logger.debug("curl to replay:\n" + toCurl(template)); }; }
public String toCurl(feign.RequestTemplate template) { var headers = Arrays.stream(template.headers().entrySet().toArray()).map(header -> header.toString().replace('=', ':').replace('[', ' ').replace(']', ' ')).map(h -> String.format(" --header '%s'\\\n", h)).collect(Collectors.joining()); val httpMethod = template.method().toUpperCase(Locale.ROOT); val url = template.feignTarget().url() + template.url(); val body = new String(template.body(), StandardCharsets.UTF_8);
return String.format("curl --location --request %s '%s' \\\n%s \\\n--data-raw '%s'", httpMethod, url, headers, body); }}



实际运行效果



总结


这是一篇 js 工程师对 java 工程师的劝退系列文章之一。


从 js 工程师的视角,将请求日志打印 cURL 是很常见的,但是在实际 java 工程里,这很不常见,因此造成了很多效率低下的沟通。希望这篇文章能够帮助减少这种低效的沟通:




题外话


本人是 js 老手, java 新手,的确很容易带着偏见来看待 java 工程(主要是现实中的 java 工程实在是给我太多不好的印象了)。写得不好的,请多多担待,之前写过一篇《邪恶的字段注入》,引起某些 java 工程师的极度不适,接连评论批评我,但是我想说,如果我对 java 的态度激怒了你,那么,我的目的达到了:让你印象深刻,并在工作中刻意提高和改进。




浏览 54
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报