SpringBoot WebFlux 读取 RequestBody 数据

前言说明

项目采用 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

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值