本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent
为何需要封装异步 HTTP 客户端 WebClient
对于同步的请求,我们使用 spring-cloud-openfeign 封装的 FeignClient,并做了额外的定制。对于异步的请求,使用的是异步 Http 客户端即 WebClient。WebClient 使用也比较简单,举一个简单的例子即:
代码语言:javascript复制//使用 WebClient 的 Builder 创建 WebClient
WebClient client = WebClient.builder()
//指定基址
.baseUrl("http://httpbin.org")
//可以指定一些默认的参数,例如默认 Cookie,默认 HttpHeader 等等
.defaultCookie("cookieKey", "cookieValue")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
创建好 WebClient 后即可以使用这个 WebClient 进行调用:
代码语言:javascript复制// GET 请求 /anything 并将 body 转化为 String
Mono<String> stringMono = client.get().uri("/anything").retrieve().bodyToMono(String.class);
//这里为了测试,采用阻塞获取
String block = stringMono.block();
返回的结果如下所示(请求 http://httporg.bin/anything 会将请求中的所有内容原封不动返回,从这里我们可以看出上面测试的 Header 还有 cokkie 都被返回了):
代码语言:javascript复制{
"args": {},
"data": "",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip",
"Cookie": "TestCookie=TestCookieValue,getAnythingCookie=getAnythingCookieValue",
"Getanythingheader": "getAnythingHeaderValue",
"Host": "httpbin.org",
"Testheader": "TestHeaderValue",
"User-Agent": "ReactorNetty/1.0.7"
},
"json": null,
"method": "GET",
"origin": "12.12.12.12",
"url": "http://httpbin.org/anything"
}
我们也可以加入负载均衡的功能,让 WebClient 利用我们内部的 LoadBalancer,负载均衡调用其他微服务,首先注入负载均衡 Filter:
代码语言:javascript复制@Autowired
ReactorLoadBalancerExchangeFilterFunction lbFunction;
创建 WebClient 的时候,将这个 Filter 加入:
代码语言:javascript复制//使用 WebClient 的 Builder 创建 WebClient
WebClient client = WebClient.builder()
//指定基址微服务
.baseUrl("http://微服务名称")
//可以指定一些默认的参数,例如默认 Cookie,默认 HttpHeader 等等
.defaultCookie("cookieKey", "cookieValue")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
//负载均衡器,改写url
.filter(lbFunction)
.build();
这样,这个 WebClient 就能调用微服务了。
但是,这样还不能满足我们的需求:
- 需要在 WebClient 加入像 Feignclient 里面加的类似的重试与断路机制,线程隔离就不需要了,因为都是异步请求不会阻塞任务线程。
- 需要针对不同的微服务配置不同的连接超时以及响应超时来适应不同微服务。
- 这些配置都增加了代码的复杂度,我们需要减少这些代码对于业务的侵入性,最好能通过纯配置实现这些 WebClient 的初始化。
要实现的配置设计以及使用举例
首先,我们要实现的 WebClient,其 Filter 包含三个:
- 重试 Filter:重试的 Filter 要在负载均衡 Filter 之前,因为重试的时候,我们会从负载均衡器获取另一个实例进行重试,而不是在同一个实例上重试多次。
- 负载均衡 Filter,其实就是内置的
ReactorLoadBalancerExchangeFilterFunction
- 断路器 Filter:断路器需要在负载均衡之后,因为只有负载均衡之后才能拿到具体本地调用的服务实例,这样我们才能实现基于微服务实例方法级别的断路器。
需要重试的场景:
- 非 2xx 的响应码返回,并且方法是可以重试的方法。如何定义方法是可以重试的,首先 GET 方法是可以重试的,对于其他方法,根据配置中的是否配置了这个 URL 可以重试决定。
- 异常重试:
- 连接异常:例如连接超时,连接中断等等,所有请求的连接异常都可以重试,因为请求并没有发出去。
- 断路器异常:该服务实例方法级别的断路器打开了,需要直接重试其他实例,因为请求并没有发出去。
- 响应超时异常:这个重试逻辑和非 2xx 的响应码返回一样。
我们需要实现的配置方式是,通过这样配置 application.yml
:
webclient:
configs:
//微服务名称
testService1:
//请求基址,第一级域名作为微服务名称
baseUrl: http://testService1
//最多的 http 连接数量
maxConnection: 50
//连接超时
connectTimeout: 500ms
//响应超时
responseTimeout: 60s
//除了 GET 方法外,哪些路径还能重试
retryablePaths:
- /retryable/**
- /user/orders
加入这些配置,我们就能获取载对应微服务的 WebClient 的 Bean,例如:
代码语言:javascript复制//自动装载 我们自定义的 WebClient 的 NamedContextFactory,这个是我们后面要实现的
@Autowired
private WebClientNamedContextFactory webClientNamedContextFactory;
//通过微服务名称,获取对应的微服务调用的 WebClient
webClientNamedContextFactory.getWebClient("testService1");
接下来,我们会实现这些。