SpringCloud升级之路2020.0.x版-39. 改造 resilience4j 粘合 WebClient

要想实现我们上一节中提到的:
- 需要在重试以及断路中加一些日志,便于日后的优化
- 需要定义重试的 Exception,并且与断路器相结合,将非 2xx 的响应码也封装成特定的异常
- 需要在断路器相关的 Operator 中增加类似于 FeignClient 中的负载均衡的数据更新,使得负载均衡更加智能
我们需要将 resilience4j 本身提供的粘合库做一些改造,其实主要就是对 resilience4j 实现的 project reactor 的 Operator 进行改造。
关于断路器的改造
首先,WebClient 的返回对象只可能是 ClientResponse 类型,所以我们这里改造出来的 Operator 不必带上形参,只需要针对 ClientResponse 即可,即:
public class ClientResponseCircuitBreakerOperator implements UnaryOperator<Publisher<ClientResponse>> {
...
}
在原有的断路器逻辑中,我们需要加入针对 GET 方法以及之前定义的可以重试的路径匹配配置可以重试的逻辑,这需要我们拿到原有请求的 URL 信息。但是 ClientResponse 中并没有暴露这些信息的接口,其默认实现 DefaultClientResponse(我们只要没有自己给 WebClient 加入特殊的改造逻辑,实现都是 DefaultClientResponse) 中的 request() 方法可以获取请求 HttpRequest,其中包含 url 信息。但是这个类还有方法都是 package-private 的,我们需要反射出来:
ClientResponseCircuitBreakerSubscriber
private static final Class<?> aClass;
private static final Method request;
static {
try {
aClass = Class.forName("org.springframework.web.reactive.function.client.DefaultClientResponse");
request = ReflectionUtils.findMethod(aClass, "request");
request.setAccessible(true);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
之后,在获取到 ClientResponse 之后记录断路器的逻辑中,需要加入上面提到的关于重试的改造,以及负载均衡器的记录:
ClientResponseCircuitBreakerSubscriber
protected void hookOnNext(ClientResponse clientResponse) {
if (!isDisposed()) {
if (singleProducer && successSignaled.compareAndSet(false, true)) {
int rawStatusCode = clientResponse.rawStatusCode();
HttpStatus httpStatus = HttpStatus.resolve(rawStatusCode);
try {
HttpRequest httpRequest = (HttpRequest) request.invoke(clientResponse);
//判断方法是否为 GET,以及是否在可重试路径配置中,从而得出是否可以重试
if (httpRequest.getMethod() != HttpMethod.GET && !webClientProperties.retryablePathsMatch(httpRequest.getURI().getPath())) {
//如果不能重试,则直接返回结果
circuitBreaker.onResult(circuitBreaker.getCurrentTimestamp() - start, circuitBreaker.getTimestampUnit(), clientResponse);
} else {
if (httpStatus != null && httpStatus.is2xxSuccessful()) {
//如果成功,则直接返回结果
circuitBreaker.onResult(circuitBreaker.getCurrentTimestamp() - start, circuitBreaker.getTimestampUnit(), clientResponse);
} else {
/**
* 如果异常,参考 DefaultClientResponse 的代码进行异常封装
* @see org.springframework.web.reactive.function.client.DefaultClientResponse#createException
*/
Exception exception;
if (httpStatus != null) {
exception = WebClientResponseException.create(rawStatusCode, httpStatus.getReasonPhrase(), clientResponse.headers().asHttpHeaders(), EMPTY, null, null);
} else {
exception = new UnknownHttpStatusCodeException(rawStatusCode, clientResponse.headers().asHttpHeaders(), EMPTY, null, null);
}
circuitBreaker.onError(circuitBreaker.getCurrentTimestamp() - start, circuitBreaker.getTimestampUnit(), exception);
downstreamSubscriber.onError(exception);
return;
}
}
} catch (Exception e) {
log.fatal("judge request method in circuit breaker error! the resilience4j feature would not be enabled: {}", e.getMessage(), e);
circuitBreaker.onResult(circuitBreaker.getCurrentTimestamp() - start, circuitBreaker.getTimestampUnit(), clientResponse);
}
}
eventWasEmitted.set(true);
downstreamSubscriber.onNext(clientResponse);
}
}
同样的,在原有的完成,取消还有失败的记录逻辑中,也加上记录负载均衡数据:
ClientResponseCircuitBreakerSubscriber
@Override
protected void hookOnComplete() {
if (successSignaled.compareAndSet(false, true)) {
serviceInstanceMetrics.recordServiceInstanceCalled(serviceInstance, true);
circuitBreaker.onSuccess(circuitBreaker.getCurrentTimestamp() - start, circuitBreaker.getTimestampUnit());
}
downstreamSubscriber.onComplete();
}
@Override
public void hookOnCancel() {
if (!successSignaled.get()) {
serviceInstanceMetrics.recordServiceInstanceCalled(serviceInstance, true);
if (eventWasEmitted.get()) {
circuitBreaker.onSuccess(circuitBreaker.getCurrentTimestamp() - start, circuitBreaker.getTimestampUnit());
} else {
circuitBreaker.releasePermission();
}
}
}
@Override
protected void hookOnError(Throwable e) {
serviceInstanceMetrics.recordServiceInstanceCalled(serviceInstance, false);
circuitBreaker.onError(circuitBreaker.getCurrentTimestamp() - start, circuitBreaker.getTimestampUnit(), e);
downstreamSubscriber.onError(e);
}
粘合 WebClient 与 resilience4j 的同时覆盖重试逻辑
由于前面的断路器中,我们针对可以重试的非 2XX 响应封装成为 WebClientResponseException。所以在重试器中,我们需要加上针对这个异常的重试。
同时,需要将重试器放在负载均衡器之前,因为每次重试,都要从负载均衡器中获取一个新的实例。同时,断路器需要放在负载均衡器之后,因为只有在这个之后,才能获取到本次调用的实例,我们的的断路器是针对实例方法级别的:
WebClientDefaultConfiguration.java
@Bean
public WebClient getWebClient(
ReactorLoadBalancerExchangeFilterFunction lbFunction,
WebClientConfigurationProperties webClientConfigurationProperties,
Environment environment,
RetryRegistry retryRegistry,
CircuitBreakerRegistry circuitBreakerRegistry,
ServiceInstanceMetrics serviceInstanceMetrics
) {
String name = environment.getProperty(WebClientNamedContextFactory.PROPERTY_NAME);
Map<String, WebClientConfigurationProperties.WebClientProperties> configs = webClientConfigurationProperties.getConfigs();
if (configs == null || configs.size() == 0) {
throw new BeanCreationException("Failed to create webClient, please provide configurations under namespace: webclient.configs");
}
WebClientConfigurationProperties.WebClientProperties webClientProperties = configs.get(name);
if (webClientProperties == null) {
throw new BeanCreationException("Failed to create webClient, please provide configurations under namespace: webclient.configs." + name);
}
String serviceName = webClientProperties.getServiceName();
//如果没填写微服务名称,就使用配置 key 作为微服务名称
if (StringUtils.isBlank(serviceName)) {
serviceName = name;
}
String baseUrl = webClientProperties.getBaseUrl();
//如果没填写 baseUrl,就使用微服务名称填充
if (StringUtils.isBlank(baseUrl)) {
baseUrl = "http://" + serviceName;
}
Retry retry = null;
try {
retry = retryRegistry.retry(serviceName, serviceName);
} catch (ConfigurationNotFoundException e) {
retry = retryRegistry.retry(serviceName);
}
//覆盖其中的异常判断
retry = Retry.of(serviceName, RetryConfig.from(retry.getRetryConfig()).retryOnException(throwable -> {
//WebClientResponseException 会重试,因为在这里能 catch 的 WebClientResponseException 只对可以重试的请求封装了 WebClientResponseException
//参考 ClientResponseCircuitBreakerSubscriber 的代码
if (throwable instanceof WebClientResponseException) {
log.info("should retry on {}", throwable.toString());
return true;
}
//断路器异常重试,因为请求没有发出去
if (throwable instanceof CallNotPermittedException) {
log.info("should retry on {}", throwable.toString());
return true;
}
if (throwable instanceof WebClientRequestException) {
WebClientRequestException webClientRequestException = (WebClientRequestException) throwable;
HttpMethod method = webClientRequestException.getMethod();
URI uri = webClientRequestException.getUri();
//判断是否为响应超时,响应超时代表请求已经发出去了,对于非 GET 并且没有标注可以重试的请求则不能重试
boolean isResponseTimeout = false;
Throwable cause = throwable.getCause();
//netty 的读取超时一般是 ReadTimeoutException
if (cause instanceof ReadTimeoutException) {
log.info("Cause is a ReadTimeoutException which indicates it is a response time out");
isResponseTimeout = true;
} else {
//对于其他一些框架,使用了 java 底层 nio 的一般是 SocketTimeoutException,message 为 read time out
//还有一些其他异常,但是 message 都会有 read time out 字段,所以通过 message 判断
String message = throwable.getMessage();
if (StringUtils.isNotBlank(message) && StringUtils.containsIgnoreCase(message.replace(" ", ""), "readtimeout")) {
log.info("Throwable message contains readtimeout which indicates it is a response time out");
isResponseTimeout = true;
}
}
//如果请求是 GET 或者标注了重试,则直接判断可以重试
if (method == HttpMethod.GET || webClientProperties.retryablePathsMatch(uri.getPath())) {
log.info("should retry on {}-{}, {}", method, uri, throwable.toString());
return true;
} else {
//否则,只针对请求还没有发出去的异常进行重试
if (isResponseTimeout) {
log.info("should not retry on {}-{}, {}", method, uri, throwable.toString());
} else {
log.info("should retry on {}-{}, {}", method, uri, throwable.toString());
return true;
}
}
}
return false;
}).build());
HttpClient httpClient = HttpClient
.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) webClientProperties.getConnectTimeout().toMillis())
.doOnConnected(connection ->
connection
.addHandlerLast(new ReadTimeoutHandler((int) webClientProperties.getResponseTimeout().toSeconds()))
.addHandlerLast(new WriteTimeoutHandler((int) webClientProperties.getResponseTimeout().toSeconds()))
);
Retry finalRetry = retry;
String finalServiceName = serviceName;
return WebClient.builder()
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(configurer -> configurer
.defaultCodecs()
//最大 body 占用 16m 内存
.maxInMemorySize(16 * 1024 * 1024))
.build())
.clientConnector(new ReactorClientHttpConnector(httpClient))
//Retry在负载均衡前
.filter((clientRequest, exchangeFunction) -> {
return exchangeFunction
.exchange(clientRequest)
.transform(ClientResponseRetryOperator.of(finalRetry));
})
//负载均衡器,改写url
.filter(lbFunction)
//实例级别的断路器需要在负载均衡获取真正地址之后
.filter((clientRequest, exchangeFunction) -> {
ServiceInstance serviceInstance = getServiceInstance(clientRequest);
serviceInstanceMetrics.recordServiceInstanceCall(serviceInstance);
CircuitBreaker circuitBreaker;
//这时候的url是经过负载均衡器的,是实例的url
//需要注意的一点是,使用异步 client 的时候,最好不要带路径参数,否则这里的断路器效果不好
//断路器是每个实例每个路径一个断路器
String instancId = clientRequest.url().getHost() + ":" + clientRequest.url().getPort() + clientRequest.url().getPath();
try {
//使用实例id新建或者获取现有的CircuitBreaker,使用serviceName获取配置
circuitBreaker = circuitBreakerRegistry.circuitBreaker(instancId, finalServiceName);
} catch (ConfigurationNotFoundException e) {
circuitBreaker = circuitBreakerRegistry.circuitBreaker(instancId);
}
log.info("webclient circuit breaker [{}-{}] status: {}, data: {}", finalServiceName, instancId, circuitBreaker.getState(), JSON.toJSONString(circuitBreaker.getMetrics()));
return exchangeFunction.exchange(clientRequest).transform(ClientResponseCircuitBreakerOperator.of(circuitBreaker, serviceInstance, serviceInstanceMetrics, webClientProperties));
}).baseUrl(baseUrl)
.build();
}
private ServiceInstance getServiceInstance(ClientRequest clientRequest) {
URI url = clientRequest.url();
DefaultServiceInstance defaultServiceInstance = new DefaultServiceInstance();
defaultServiceInstance.setHost(url.getHost());
defaultServiceInstance.setPort(url.getPort());
return defaultServiceInstance;
}
这样,我们就实现了我们封装的基于配置的 WebClient
微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer:

SpringCloud升级之路2020.0.x版-39. 改造 resilience4j 粘合 WebClient的更多相关文章
- SpringCloud升级之路2020.0.x版-1.背景
本系列为之前系列的整理重启版,随着项目的发展以及项目中的使用,之前系列里面很多东西发生了变化,并且还有一些东西之前系列并没有提到,所以重启这个系列重新整理下,欢迎各位留言交流,谢谢!~ Spring ...
- SpringCloud升级之路2020.0.x版-41. SpringCloudGateway 基本流程讲解(1)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 接下来,将进入我们升级之路的又一大模块,即网关模块.网关模块我们废弃了已经进入维护状态的 ...
- SpringCloud升级之路2020.0.x版-6.微服务特性相关的依赖说明
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford spring-cl ...
- SpringCloud升级之路2020.0.x版-10.使用Log4j2以及一些核心配置
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Log4 ...
- SpringCloud升级之路2020.0.x版-34.验证重试配置正确性(1)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在前面一节,我们利用 resilience4j 粘合了 OpenFeign 实现了断路器. ...
- SpringCloud升级之路2020.0.x版-43.为何 SpringCloudGateway 中会有链路信息丢失
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在开始编写我们自己的日志 Filter 之前,还有一个问题我想在这里和大家分享,即在 Sp ...
- SpringCloud升级之路2020.0.x版-38. 实现自定义 WebClient 的 NamedContextFactory
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 实现 WeClient 的 NamedContextFactory 我们要实现的是不同微服 ...
- SpringCloud升级之路2020.0.x版-42.SpringCloudGateway 现有的可供分析的请求日志以及缺陷
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 网关由于是所有外部用户请求的入口,记录这些请求中我们需要的元素,对于线上监控以及业务问题定 ...
- SpringCloud升级之路2020.0.x版-29.Spring Cloud OpenFeign 的解析(1)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在使用云原生的很多微服务中,比较小规模的可能直接依靠云服务中的负载均衡器进行内部域名与服务 ...
随机推荐
- 自然语言处理标注工具——Brat(安装、测试、使用)
一.Brat标注工具安装 1.安装条件: (1)运行于Linux系统(window系统下虚拟机内linux系统安装也可以) (2)目前brat最新版本(v1.3p1)仅支持python2版本运行使用( ...
- HTML基本标记
头部标记 <head></head> 说明:元素的作用范围是整篇文档.元素中可以有元信息定义.文档样式表定义和脚本等信息,定义在HTML语言头部的内容往往不会在网页上直接显示. ...
- JavaFx 监听剪切板实现(Kotlin)
原文地址: JavaFx 监听剪切板实现(Kotlin) | Stars-One的杂货小窝 软件有个需求,想要实现监听剪切板的内容,若内容符合预期,则进行相关的操作,就可以免去用户手动粘贴的操作,提供 ...
- linux kill信号详解
大家对kill -9 肯定非常熟悉,在工作中也经常用到.特别是你去重启tomcat时.可是多半看来,我们对-9的理解只是表面而已. 很少有人(包括我)认真的去了解一下 kill -n 这个n到底是什么 ...
- 洛谷4208 JSOI2008最小生成树计数(矩阵树定理+高斯消元)
qwq 这个题目真的是很好的一个题啊 qwq 其实一开始想这个题,肯定是无从下手. 首先,我们会发现,对于无向图的一个最小生成树来说,只有当存在一些边与内部的某些边权值相同的时候且能等效替代的时候,才 ...
- Python&Selenium 数据驱动测试【unittest+ddt+xml】
一.摘要 本博文将介绍Python和Selenium做自动化测试时,基于unittest框架,借助ddt模块,使用xml文件作为测试输入. 二.xml文件 保存路径:D:\\Programs\\Pyt ...
- 小白自制Linux开发板 五. Debian文件系统制作,以及WIFI配置、交换分区配置
该片文章将完整记录一个Debian的最小文件系统的生成,以及自定义配置WIFI组件.网络组件和交换分区配置 本文章参考:https://whycan.com/t_4236.htmlhttp://www ...
- AIbee 笔试
CSS选择器 div+p 选择紧接在div元素之后的所有< p >元素 C++删除数组最后一个元素. 例如[1 2 3 4] 最后变为 [1 2 3] 用splice的删除,增加和替换 a ...
- (课内)信安数基RSA-level3-5
emmmm感觉其实自己对这个的理解完全不够,原理只能写出这么个东西(悲) 代码完全是 攻击方式中(1)(2)内容的实现. lambda是一种可以理解为匿名函数的写法:写在这里看起来很酷炫(bushi) ...
- 莱特币(LTC)开发环境搭建
Linux系统下搭建莱特币LTC开发环境 1.简介 2.LTC 客户端下载 3.解压到一个固定的目录中 4.启动客户端 4.1.参数解释: 4.2.更多详细的配置 5.执行命令测试一下 6.获取莱特币 ...