前言说明
项目采用 SpringBoot WebFlux 进行开发的,在对前端请求的内容进行处理时,发现无法得到 RequestBody 中的内容,要么为空,要么出现重复读取的异常;因之前用 SpringBoot 开发,通过常规方式,即可从 HttpServletRequest 中获得字符流对象,进行解析转换成字符串就可以正常使用;但 WebFlux 是即于 Netty 做为核心网络框架服务,其采用的通讯数据处理模式,与传统的 SrpingWeb 方案是有很大的差别,WebFlux 会将所有的请求的输入输出流适配转换成 Flux 对象,并进行统一处理;通过网上大量的查找资料,大部份给出的解决方案并未起效。后在另一个 CSDN 博客下找到了相同问题的最终解决方案,成功处理 RequestBody 读取为空和重复读取异常问题;
示例描述
请求地址: http://127.0.0.1:8081/test/server
请求内容为 JSON 格式,RequestBody 如下内容
{
"goodsTitle": "华为荣耀手机",
"goodsCode": "HW-888801124"
}
后台接收到前端发起的请求后,出现了异常,导致无法解析并获取到请求 RequestBody;
/**
* 接收请求Body数据JSON格式
* @param request
*/
@RequestMapping(value = "/server", method = {RequestMethod.GET, RequestMethod.POST})
public Mono<Object> server(ServerHttpRequest request) {
Flux<DataBuffer> body = request.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
body.subscribe(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
DataBufferUtils.release(buffer);
bodyRef.set(charBuffer.toString());
});
String bodyStr = bodyRef.get();
log.info("请求Body:" + bodyStr);
return Mono.just("ok");
}
会出现 Body 只读一次的异常提示,并且实际 body 无内容;
[2021-12-10 17:35:51.675][ERROR][reactor-http-nio-2][reactor.util.Loggers$Slf4JLogger][314] - Operator called default onErrorDropped reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalStateException: Only one connection receive subscriber allowed.
Caused by: java.lang.IllegalStateException: Only one connection receive subscriber allowed.
at reactor.netty.channel.FluxReceive.startReceiver(FluxReceive.java:183) ~[reactor-netty-core-1.0.6.jar:1.0.6]
at reactor.netty.channel.FluxReceive.subscribe(FluxReceive.java:144) ~[reactor-netty-core-1.0.6.jar:1.0.6]
at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:62) ~[reactor-core-3.4.5.jar:3.4.5]
原因是因为在 WebFlux 中,当接收到前端的请求后,早已经在 WebFlux 内部 Filter 过滤器处理链路上读取过 RequstBody,如:HiddenHttpMethodFilter 中,导制后面控制器中在一次读取该 RequstBody 就会出现上述异常;
解决方法一
参见网上网友的分享方案,对 RequestBody 最高优先级先读取一遍,在重新设置到后续 Filter 要加载的 ServerHttpRequestDecorator 对象中。
增加了以下 AnewRequestBodylFilter 自定义过滤器,Ordered 排序设为 Ordered.HIGHEST_PRECEDENCE 最高级别,即在所有过滤器中的执行排第一位;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
/**
* @Description 解决WebFlux无读正常读取RequestBody字符流数据问题
*/
@Slf4j
@Component
public class AnewRequestBodylFilter implements WebFilter,Ordered {
/**
* 参考处理方案:https://blog.csdn.net/tkq597284700/article/details/114291798
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String contentType = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
log.info("handle request body data, filter:[{}], contentType:[{}]", this.getClass().getName(), contentType);
// 只对contentType=application/json的数据进行重写RequesetBody
if (MediaType.APPLICATION_JSON_VALUE.equals(contentType)) {
AtomicReference<String> bodyRef = new AtomicReference<>();
returnDataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer());
DataBufferUtils.retain(dataBuffer);
bodyRef.set(charBuffer.toString());
String bodyStr = bodyRef.get();
log.info("read request body:\n" + bodyStr);
Flux<DataBuffer> cachedFlux = Flux
.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
//封装request,传给下一级
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@OverridepublicFlux<DataBuffer> getBody() {
return cachedFlux;
}
};
return chain.filter(exchange.mutate().request(mutatedRequest).build());
});
}
return chain.filter(exchange);
}
}
经过上述方案确实可以实现在控制器再次获得 RequetBody 内容,而不会存在读取内容为空或重复读取 RequesetBody 的问题;
[2021-12-10 17:49:56.593][INFO][reactor-http-nio-2][com.trace.controller.TestController][69] - 请求Body:{
"goodsTitle": "华为荣耀手机",
"goodsCode": "HW-888801124"
}
解决方法二
后在网上查找到另一种解决方案,而是直接解析 ServerHttpRequest 数据流,将数据成功写入到了 byte [] 数组中,转换成 String 后可以正常完整输出;具体如下:
@Autowired
private ServerCodecConfigurer serverCodecConfigurer ;
@RequestMapping( value = "http2", method = {RequestMethod.GET, RequestMethod.POST})
public Mono<Void> server2(final ServerHttpRequest request, final ServerHttpResponse response){
log.debug("connect...");
final ResolvableType reqDataType = ResolvableType.forClass(byte[].class);
Mono<DataBuffer> dataBuffer = serverCodecConfigurer.getReaders().stream()
.filter(reader -> reader.canRead(reqDataType, MediaType.ALL))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No Data"))
.readMono(reqDataType, request, Collections.emptyMap())
.cast(byte[].class)
.map(bytes -> {
final NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));
try {
final String reqBody = new String(bytes, StandardCharsets.UTF_8);
log.info("reqBody => \n {}", reqBody);
return nettyDataBufferFactory.wrap("ok".getBytes(StandardCharsets.UTF_8));
} catch (Throwable ex) {
log.warn("connect-exp: {}", ex.getMessage());
return nettyDataBufferFactory.wrap(ex.getMessage().getBytes(StandardCharsets.UTF_8));
}
});
return response.writeWith(dataBuffer);
}
postman 模拟前端发起请求,后台打印日志如下:
[2021-12-1018:00:01.196] [INFO] [reactor-http-nio-2] [com.trace.controller.TestController] [99] - reqBody =>
{
"goodsTitle": "华为荣耀手机",
"goodsCode": "HW-888801124"
}
参考资源
方案一参考来源: https://blog.csdn.net/tkq597284700/article/details/114291798
方案二参考来源: http://www.voycn.com/article/webfluxzhujiexiaduquserverhttprequest-qingqiubaowenliushujufangfa