一、背景

使用SpringWebFlux的WebFilter时,由于不熟悉或一些思考疏忽,容易出现未知的异常。记录一下排查与解决方案,给大家分享一下。

二、问题

2.1 问题描述

在测试接口方法时,出现的错误信息如下(对一些项目路径做了修改):

java.lang.IllegalStateException: COMPLETED
at org.springframework.http.server.reactive.AbstractListenerReadPublisher$State.subscribe(AbstractListenerReadPublisher.java:451)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ springfox.boot.starter.autoconfigure.SwaggerUiWebFluxConfiguration$CustomWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ com.xxx.config.LoginWebFilter$$EnhancerBySpringCGLIB$$f3da6bdf [DefaultWebFilterChain]
*__checkpoint ⇢ com.xxx.config.TraceIdFilter [DefaultWebFilterChain]
*__checkpoint ⇢ HTTP POST "/abc/test/testMethod" [ExceptionHandlingWebHandler]
Original Stack Trace:
at org.springframework.http.server.reactive.AbstractListenerReadPublisher$State.subscribe(AbstractListenerReadPublisher.java:451)
at org.springframework.http.server.reactive.AbstractListenerReadPublisher.subscribe(AbstractListenerReadPublisher.java:105)

2.2 解决问题

通过查看错误信息描述,checkpoint点都在webfilter中,由于对webflux也不是特别熟,所以就只有一个个测试。

通过一系列操作, 把swagger移除,细读TraceIdFilter(内容不多),主要归功于原方案是正确的,修改后错误,最后才定位问题出现在LoginWebFilter。

说说插曲,原实现方式(有阻塞逻辑,没出现上述异常),代码如下:

@Configuration
@Slf4j
@Order(-10)
public class LoginWebFilter implements WebFilter {
// 略... @Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest(); if (!enableGateway) {
String token = Optional.ofNullable(request.getHeaders().getFirst(Constants.TOKEN))
.orElse("");
// 获取用户信息
User user = getUser(token);
if (user != null) {
ServerHttpRequest mutateRequest = exchange.getRequest().mutate()
.build();
exchange = exchange.mutate().request(mutateRequest).build();
}
}
return chain.filter(exchange);
} private User getUser(String token) {
if (StringUtils.isNotBlank(token)) {
return redisTemplate.opsForValue().get("xxx:tk:" + token)
.flatMap(str -> Mono.justOrEmpty(JsonUtils.toObj(str, User.class))).block();
}
return null;
}
}

这样写,没有复杂的业务逻辑,从上到下,完全OJBK,但是调整后,就出现了上述异常。

改完后的问题代码如下:

// 错误
public class LoginWebFilter implements WebFilter {
/...略
@Autowired
private ReactiveStringRedisTemplate redisTemplate; @Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { if (!enableGateway) {
ServerHttpRequest request = exchange.getRequest();
String token = Optional.ofNullable(request.getHeaders().getFirst(Constants.TOKEN))
.orElse(""); return getUser(token).flatMap(user -> {
ServerHttpRequest mutateRequest = exchange.getRequest().mutate()
.header(UserUtils.MEMBER_ID, user.getMemId())
.header(UserUtils.MOBILE, user.getMobile())
.build(); ServerWebExchange newexchange = exchange.mutate().request(mutateRequest).build();
return chain.filter(newexchange);
// 问题点
}).switchIfEmpty(chain.filter(exchange));
}
return chain.filter(exchange);
}
// 不在用block
private Mono<User> getUser(String token) {
if (StringUtils.isNotBlank(token)) {
return redisTemplate.opsForValue().get("xxx:tk:" + token)
.flatMap(str -> Mono.justOrEmpty(JsonUtils.toObj(str, User.class)));
}
return Mono.empty();
}
}

2.3 如何解决

对比改造前和改造后的代码,其实差异不大,那问题出现在哪呢?

由于对webflux也不是特别熟,那就只能一点点试(太蠢了)。 最后发现问题出现在了switchIfEmpty(chain.filter(exchange)),在去掉了switchIfEmpty(chain.filter(exchange)),就不会在出现上述异常。

修改后部分代码如下:

// 半正确
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { if (!enableGateway) { ServerHttpRequest request = exchange.getRequest();
String token = Optional.ofNullable(request.getHeaders().getFirst(Constants.TOKEN))
.orElse(“”); return getUser(token).flatMap(user -> {
ServerHttpRequest mutateRequest = exchange.getRequest().mutate()
.header(UserUtils.MEMBER_ID, user.getMemId())
.header(UserUtils.MOBILE, user.getMobile())
.build(); ServerWebExchange newexchange = exchange.mutate().request(mutateRequest).build();
return chain.filter(newexchange);
});
}
return chain.filter(exchange);
}

虽然现在不回在出现异常,但是去掉switchIfEmpty后,代码逻辑是不完整的,当获取不到User时,返回Mono.emtpy,那会直接结束流程,不在执行剩下的filter或其他逻辑。真是连环坑,一坑接一坑。所以对代码需要调整一番,调整后如下:

// 有点正确 但是不多
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { if (!enableGateway) { ServerHttpRequest request = exchange.getRequest();
String token = Optional.ofNullable(request.getHeaders().getFirst(Constants.TOKEN))
.orElse(“”); return getUser(token).switchIfEmpty(Mono.error(() -> new BizException(ErrorCode.USER_IS_NULL_ERROR)))
.flatMap(user -> {
ServerHttpRequest mutateRequest = exchange.getRequest().mutate()
.header(UserUtils.MEMBER_ID, user.getMemId())
.header(UserUtils.MOBILE, user.getMobile())
.build(); ServerWebExchange newexchange = exchange.mutate().request(mutateRequest).build();
return chain.filter(newexchange);
}).onErrorResume(e -> chain.filter(exchange)); }
return chain.filter(exchange);
}

当获取用户为空后,抛出异常,然后在兜底,当异常的时候执行chain.filter(exchange)(好蠢的方式.. 但是解决问题了)。

2.4 意外之喜

各位看官,就在我写完上完上面的代码修改方案之后,读了一下修改完后的代码,突然发现问题出在哪了,所以连夜修改了代码方式。现在我听我细细道来。

2.4.1 问题点

原因点chain.filter(exchange)重复执行

switchIfEmpty(chain.filter(exchange))这个点本意是想用在当getUser 方法为空时,执行其它WebFilter的逻辑,从而不影响主流程。

忽略了一点是:当chain.filter(newexchange)这个方法执行完后,返回的也是Mono<Void>,也是为空。所以无论如何,代码最后的逻辑都会走到switchIfEmpty(chain.filter(exchange))

但是当getUser获取到用户后,会重复执行chain.filter(exchange),如下

  • return chain.filter(newexchange)
  • switchIfEmpty(chain.filter(exchange))

由于第一次执行完chain.filter(exchange),request、response都已经关闭,所以出现了xx COMPLETE,那看来的确符合逻辑。

2.4.2 验证猜想

这个验证方式还是挺简单的,那就是分别传入正常的TOKEN和错误的TOKEN。

具体操作:.....(本人已完成)

结论:

当传入错误的token的时候,确实没有抛出异常,完美执行。但是当传入正确的token,出现了熟悉的异常。

2.4.3 代码调整

知道问题的原因,那就好调整代码了。修改后如下:

public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (!enableGateway) {
ServerHttpRequest request = exchange.getRequest();
String token = Optional.ofNullable(request.getHeaders().getFirst(Constants.TOKEN))
.orElse(request.getHeaders().getFirst("suuid")); return getUser(token).map(user -> {
ServerHttpRequest mutateRequest = exchange.getRequest().mutate()
.header(UserUtils.MEMBER_ID, user.getMemId())
.header(UserUtils.MOBILE, user.getMobile())
.build();
return exchange.mutate().request(mutateRequest).build();
// 调整当getUser为空时,返回的内容
}).switchIfEmpty(Mono.just(exchange)).flatMap(chain::filter); }
return chain.filter(exchange);
}

至此,问题就完全解决拉!心里美滋滋!

三、总结

1、遇到问题,还是要多看看呀,细细思考一下

2、多看代码,发现问题,实现完美的解决方案

(WebFlux)004、WebFilter踩坑记录的更多相关文章

  1. unionId突然不能获取的踩坑记录

    昨天(2016-2-2日),突然发现系统的一个微信接口使用不了了.后来经查发现,是在网页授权获取用户基本信息的时候,unionid获取失败导致的. 在网页授权获取用户基本信息的介绍中(http://m ...

  2. CentOS7.4安装MySQL踩坑记录

    CentOS7.4安装MySQL踩坑记录 time: 2018.3.19 CentOS7.4安装MySQL时网上的文档虽然多但是不靠谱的也多, 可能因为版本与时间的问题, 所以记录下自己踩坑的过程, ...

  3. ubuntu 下安装docker 踩坑记录

    ubuntu 下安装docker 踩坑记录 # Setp : 移除旧版本Docker sudo apt-get remove docker docker-engine docker.io # Step ...

  4. SpringBoot + Shiro + shiro.ini 的踩坑记录

    0.写在前面的话 好久没写博客了,诶,好多时候偷懒直接就抓网上的资料丢笔记里了,也就没有自己提炼,偷懒偷懒.然后最近参加了一个网络课程,要交作业的那种,为了能方便看下其他同学的作业,就写了个爬虫把作业 ...

  5. 你真的了解字典(Dictionary)吗? C# Memory Cache 踩坑记录 .net 泛型 结构化CSS设计思维 WinForm POST上传与后台接收 高效实用的.NET开源项目 .net 笔试面试总结(3) .net 笔试面试总结(2) 依赖注入 C# RSA 加密 C#与Java AES 加密解密

    你真的了解字典(Dictionary)吗?   从一道亲身经历的面试题说起 半年前,我参加我现在所在公司的面试,面试官给了一道题,说有一个Y形的链表,知道起始节点,找出交叉节点.为了便于描述,我把上面 ...

  6. google nmt 实验踩坑记录

       最近因为要做一个title压缩的任务,所以调研了一些text summary的方法.    text summary 一般分为抽取式和生成式两种.前者一般是从原始的文本中抽取出重要的word o ...

  7. ABP框架踩坑记录

    ABP框架踩坑记录 ASP.NET Boilerplate是一个专用于现代Web应用程序的通用应用程序框架. 它使用了你已经熟悉的工具,并根据它们实现最佳实践. 文章目录 使用MySQL 配置User ...

  8. SpringBoot+SpringSecurity+Thymeleaf认证失败返回错误信息踩坑记录

    Spring boot +Spring Security + Thymeleaf认证失败返回错误信息踩坑记录 步入8102年,现在企业开发追求快速,Springboot以多种优秀特性引领潮流,在众多使 ...

  9. IDFA踩坑记录

    IDFA踩坑记录: 1.iOS10.0 以下,即使打开“限制广告跟踪”,依然可以读取idfa: 2.打开“限制广告跟踪”,然后再关闭“限制广告跟踪”,idfa会改变: 3.越狱机器安装开发证书打的包, ...

随机推荐

  1. 【一本通提高博弈论】[ZJOI2009]取石子游戏

    [ZJOI2009]取石子游戏 题目描述 在研究过 Nim 游戏及各种变种之后,Orez 又发现了一种全新的取石子游戏,这个游戏是这样的: 有 n n n 堆石子,将这 n n n 堆石子摆成一排.游 ...

  2. 小C的记事本_via牛客网

    题目 链接:https://ac.nowcoder.com/acm/contest/28537/G 来源:牛客网 时间限制:C/C++ 2秒,其他语言4秒 空间限制:C/C++ 131072K,其他语 ...

  3. 基于gRPC编写golang简单C2远控

    概述 构建一个简单的远控木马需要编写三个独立的部分:植入程序.服务端程序和管理程序. 植入程序是运行在目标机器上的远控木马的一部分.植入程序会定期轮询服务器以查找新的命令,然后将命令输出发回给服务器. ...

  4. SpringCloud gateway自定义请求的 httpClient

    本文为博主原创,转载请注明出处: 引用 的 spring cloud gateway 的版本为 2.2.5 : SpringCloud gateway 在实现服务路由并请求的具体过程是在 org.sp ...

  5. Redis常见数据类型

    String 常用命令: get.set.incr.decr mget等操作,普通的key/value存储都可以归为此类 Hash 常用命令: hget,hset,hgetall 等. List(队列 ...

  6. 天人合一物我相融,站点升级渐进式Web应用PWA(Progressive Web Apps)实践

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_216 PWA(Progressive web apps,渐进式 Web 应用)使用现代的 Web API 以及传统的渐进式增强策略 ...

  7. Vue mixin(混入) && 插件

    1 # mixin(混入) 2 # 功能:可以把多个组件公用的配置提取成一个混入对象 3 # 使用方法: 4 # 第一步:{data(){return {...}}, methods:{...},.. ...

  8. 如何仿造websocket请求?

    之前两次singnalr. websocket实时推送相关: .NET WebSockets 核心原理初体验 SignalR 从开发到生产部署避坑指南 tag: 浏览器--->nginx--&g ...

  9. 使用java随机生成有个性的用户名,LOL地名+水浒传,合计2808个有意思的用户名

    * 随机生成用户名 * 取水浒传108好汉名字 * 取LOL地名26个,组合而成 * 一共可以生成2808个不同特色的用户名 如果你在上网的时候,用户名难取的话,这里有很多可选择的用户名,现提供100 ...

  10. HTTP的三次握手和四次挥手,以及DNS流程解析

    首先模拟一个场景:你在浏览器输入自己想要访问的地址,浏览器发送请求到服务端,服务端进行响应,浏览器进行数据页面渲染,从而你得到自己想要访问地址的页面 总体流程图: DNS:可以认为域名与对应的ip转化 ...