Spring Cloud Gateway 获取请求体

一、直接在全局拦截器中获取,伪代码如下

private String  resolveBodyFromRequest(ServerHttpRequest serverHttpRequest){

        Flux<DataBuffer> body = serverHttpRequest.getBody();

        AtomicReference<String> bodyRef = new AtomicReference<>();

        body.subscribe(buffer -> {

            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());

            DataBufferUtils.release(buffer);

            bodyRef.set(charBuffer.toString());

        });

        return bodyRef.get();

    }

  

存在的缺陷:其他拦截器无法再通过该方式获取请求体(因为请求体已被消费),并且会抛出异常

Only one connection receive subscriber allowed.Caused by: java.lang.IllegalStateException: Only one connection receive subscriber allowed.

异常原因:实际上spring-cloud-gateway反向代理的原理是,首先读取原请求的数据,然后构造一个新的请求,将原请求的数据封装到新的请求中,然后再转发出去。然而我们在他封装之前读取了一次request body,而request body只能读取一次。因此就出现了上面的错误。

再者受版本限制

这种方法在spring-boot-starter-parent 2.0.6.RELEASE + Spring Cloud Finchley.SR2 body 中生效,

但是在spring-boot-starter-parent 2.1.0.RELEASE + Spring Cloud Greenwich.M3 body 中不生效,总是为空

二、先在全局过滤器中获取,然后再把request重新包装,继续向下传递传递

 @Override
public GatewayFilter apply(NameValueConfig nameValueConfig) {
return (exchange, chain) -> {
URI uri = exchange.getRequest().getURI();
URI ex = UriComponentsBuilder.fromUri(uri).build(true).toUri();
ServerHttpRequest request = exchange.getRequest().mutate().uri(ex).build();
if("POST".equalsIgnoreCase(request.getMethodValue())){//判断是否为POST请求
Flux<DataBuffer> body = request.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
body.subscribe(dataBuffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer());
DataBufferUtils.release(dataBuffer);
bodyRef.set(charBuffer.toString());
});//读取request body到缓存
String bodyStr = bodyRef.get();//获取request body
System.out.println(bodyStr);//这里是我们需要做的操作
DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer); request = new ServerHttpRequestDecorator(request){
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux;
}
};//封装我们的request
}
return chain.filter(exchange.mutate().request(request).build());
};
}
  protected DataBuffer stringBuffer(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8); NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}

  

该方案的缺陷:request body获取不完整(因为异步原因),只能获取1024B的数据。并且请求体超过1024B,会出现响应超慢(因为我是开启了熔断)。

三、过滤器加路线定位器

翻查源码发现ReadBodyPredicateFactory里面缓存了request body的信息,于是在自定义router中配置了ReadBodyPredicateFactory,然后在filter中通过cachedRequestBodyObject缓存字段获取request body信息。

/**
* @description: 获取POST请求的请求体
* ReadBodyPredicateFactory 发现里面缓存了request body的信息,
* 于是在自定义router中配置了ReadBodyPredicateFactory
* @modified:
*/
@EnableAutoConfiguration
@Configuration
public class RouteLocatorRequestBoby{
   //自定义过滤器
@Resource
private ReqTraceFilter reqTraceFilter;
  
@Resource
private RibbonLoadBalancerClient ribbonLoadBalancerClient; private static final String SERVICE = "/leap/**"; private static final String HTTP_PREFIX = "http://"; private static final String COLON = ":"; @Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
//通过负载均衡获取服务实例
ServiceInstance instance = ribbonLoadBalancerClient.choose("PLATFORM-SERVICE");
//拼接路径
StringBuilder forwardAddress = new StringBuilder(HTTP_PREFIX);
forwardAddress.append(instance.getHost())
.append(COLON)
.append(instance.getPort());
return builder.routes()
//拦截请求类型为POST Content-Type application/json application/json;charset=UTF-8
.route(r -> r
.header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON_VALUE + MediaType.APPLICATION_JSON_UTF8_VALUE)
.and()
.method(HttpMethod.POST)
.and()
//获取缓存中的请求体
.readBody(Object.class, readBody -> {
return true;
})
.and()
.path(SERVICE)
//把请求体传递给拦截器reqTraceFilter
.filters(f -> {
f.filter(reqTraceFilter);
return f;
})
.uri(forwardAddress.toString())).build();
} /**
* @description: 过滤器,用于获取请求体,和处理请求体业务,列如记录日志
* @modified:
*/
@Component
public class ReqTraceFilter implements GlobalFilter, GatewayFilter,Ordered { private static final String CONTENT_TYPE = "Content-Type"; private static final String CONTENT_TYPE_JSON = "application/json";
  
//获取请求路由详细信息Route route = exchange.getAttribute(GATEWAY_ROUTE_BEAN)
private static final String GATEWAY_ROUTE_BEAN = "org.springframework.cloud.gateway.support.ServerWebExchangeUtils.gatewayRoute"; private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
//判断过滤器是否执行
String requestUrl = RequestUtils.getCurrentRequest(request);
if (!RequestUtils.isFilter(requestUrl)) {
String bodyStr = "";
String contentType = request.getHeaders().getFirst(CONTENT_TYPE);
String method = request.getMethodValue();
//判断是否为POST请求
if (null != contentType && HttpMethod.POST.name().equalsIgnoreCase(method) && contentType.contains(CONTENT_TYPE_JSON)) {
Object cachedBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
if(null != cachedBody){
bodyStr = cachedBody.toString();
}
}
if (HttpMethod.GET.name().equalsIgnoreCase(method)) {
bodyStr = request.getQueryParams().toString();
} log.info("请求体内容:{}",bodyStr);
}
return chain.filter(exchange);
} @Override
public int getOrder() {
return 5;
}
}

  

该方案优点:这种解决,一不会带来重复读取问题,二不会带来requestbody取不全问题。三在低版本的Spring Cloud Finchley.SR2也可以运行。

缺点:不支持 multipart/form-data(异常415),这个致命。

四、通过 org.springframework.cloud.gateway.filter.factory.rewrite 包下有个 ModifyRequestBodyGatewayFilterFactory ,顾名思义,这就是修改 Request Body 的过滤器工厂类。

@Component
@Slf4j
public class ReqTraceFilter implements GlobalFilter, GatewayFilter, Ordered { @Resource
private IPlatformFeignClient platformFeignClient; /**
* httpheader,traceId的key名称
*/
private static final String REQUESTID = "traceId"; private static final String CONTENT_TYPE = "Content-Type"; private static final String CONTENT_TYPE_JSON = "application/json"; private static final String GATEWAY_ROUTE_BEAN = "org.springframework.cloud.gateway.support.ServerWebExchangeUtils.gatewayRoute"; @Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
//判断过滤器是否执行
String requestUrl = RequestUtils.getCurrentRequest(request);
if (!RequestUtils.isFilter(requestUrl)) {
String bodyStr = "";
String contentType = request.getHeaders().getFirst(CONTENT_TYPE);
String method = request.getMethodValue();
//判断是否为POST请求
if (null != contentType && HttpMethod.POST.name().equalsIgnoreCase(method) && contentType.contains(CONTENT_TYPE_JSON)) {
ServerRequest serverRequest = new DefaultServerRequest(exchange);
List<String> list = new ArrayList<>();
// 读取请求体
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
.flatMap(body -> {
//记录请求体日志
final String nId = saveRequestOperLog(exchange, body);
//记录日志id
list.add(nId);
return Mono.just(body);
}); BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
headers.remove(HttpHeaders.CONTENT_LENGTH); CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
return bodyInserter.insert(outputMessage, new BodyInserterContext())
.then(Mono.defer(() -> {
ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
httpHeaders.put(REQUESTID,list);
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
} @Override
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
}; return chain.filter(exchange.mutate().request(decorator).build());
}));
}
if (HttpMethod.GET.name().equalsIgnoreCase(method)) {
bodyStr = request.getQueryParams().toString();
String nId = saveRequestOperLog(exchange, bodyStr);
ServerHttpRequest userInfo = exchange.getRequest().mutate()
.header(REQUESTID, nId).build();
return chain.filter(exchange.mutate().request(userInfo).build());
} }
return chain.filter(exchange);
} /**
* 保存请求日志
*
* @param exchange
* @param requestParameters
* @return
*/
private String saveRequestOperLog(ServerWebExchange exchange, String requestParameters) {
log.debug("接口请求参数:{}", requestParameters);
ServerHttpRequest request = exchange.getRequest();
String ip = Objects.requireNonNull(request.getRemoteAddress()).getAddress().getHostAddress();
SaveOperLogVO vo = new SaveOperLogVO();
vo.setIp(ip);
vo.setReqUrl(RequestUtils.getCurrentRequest(request));
vo.setReqMethod(request.getMethodValue());
vo.setRequestParameters(requestParameters); Route route = exchange.getAttribute(GATEWAY_ROUTE_BEAN);
//是否配置路由
if (route != null) {
vo.setSubsystem(route.getId());
}
ResEntity<String> res = platformFeignClient.saveOperLog(vo);
log.debug("当前请求ID返回的数据:{}", res);
return res.getData();
} @Override
public int getOrder() {
return 5;
}
}

  

该方案:完美解决以上所有问题

参考文档:https://www.codercto.com/a/52970.html

Spring Cloud Gateway 之获取请求体(Request Body)的几种方式的更多相关文章

  1. Spring Cloud Gateway 动态修改请求参数解决 # URL 编码错误传参问题

    Spring Cloud Gateway 动态修改请求参数解决 # URL 编码错误传参问题 继实现动态修改请求 Body 以及重试带 Body 的请求之后,我们又遇到了一个小问题.最近很多接口,收到 ...

  2. 获取【请求体】数据的3种方式(精)(文末代码) request.getInputStream() request.getInputStream() request.getReader()

    application/x- www-form-urlencoded是Post请求默认的请求体内容类型,也是form表单默认的类型.Servlet API规范中对该类型的请求内容提供了request. ...

  3. Spring Cloud Gateway修改请求和响应body的内容

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  4. 快速突击 Spring Cloud Gateway

    认识 Spring Cloud Gateway Spring Cloud Gateway 是一款基于 Spring 5,Project Reactor 以及 Spring Boot 2 构建的 API ...

  5. 微服务网关实战——Spring Cloud Gateway

    导读 作为Netflix Zuul的替代者,Spring Cloud Gateway是一款非常实用的微服务网关,在Spring Cloud微服务架构体系中发挥非常大的作用.本文对Spring Clou ...

  6. 深入学习spring cloud gateway 限流熔断

    前言 Spring Cloud Gateway 目前,Spring Cloud Gateway是仅次于Spring Cloud Netflix的第二个最受欢迎的Spring Cloud项目(就GitH ...

  7. Spring Cloud Gateway 没有链路信息,我 TM 人傻了(上)

    本系列是 我TM人傻了 系列第五期[捂脸],往期精彩回顾: 升级到Spring 5.3.x之后,GC次数急剧增加,我TM人傻了 这个大表走索引字段查询的 SQL 怎么就成全扫描了,我TM人傻了 获取异 ...

  8. Spring cloud gateway

    ==================================为什么需要API gateway?==================================企业后台微服务互联互通, 因为 ...

  9. Spring Cloud实战: 基于Spring Cloud Gateway + vue-element-admin 实现的RBAC权限管理系统,实现网关对RESTful接口方法权限和自定义Vue指令对按钮权限的细粒度控制

    一. 前言 信我的哈,明天过年. 这应该是农历年前的关于开源项目 的最后一篇文章了. 有来商城 是基于 Spring Cloud OAuth2 + Spring Cloud Gateway + JWT ...

随机推荐

  1. MIT 6.824拾遗(一)聊聊basic-paxos

    前言 The Paxos algorithm, when presented in plain English, is very simple. ------ Lamport,<Paxos Ma ...

  2. 设计模式——从工厂方法模式到 IOC/DI思想

    回顾简单工厂 回顾:从接口的角度去理解简单工厂模式 前面说到了简单工厂的本质是选择实现,说白了是由一个专门的类去负责生产我们所需要的对象,从而将对象的创建从代码中剥离出来,实现松耦合.我们来看一个例子 ...

  3. 你要偷偷学会排查线上CPU飙高的问题,然后惊艳所有人!

    GitHub 20k Star 的Java工程师成神之路,不来了解一下吗! GitHub 20k Star 的Java工程师成神之路,真的不来了解一下吗! GitHub 20k Star 的Java工 ...

  4. Redis持久化——内存快照(RDB)

    最新:Redis持久化--如何选择合适的持久化方式 最新:Redis持久化--AOF日志 最新:Redis持久化--内存快照(RDB) 一文回顾Redis五大对象(数据类型) Redis对象--有序集 ...

  5. 第14 章 : Kubernetes Service讲解

    Kubernetes Service 本文将主要分享以下四方面的内容: 为什么需要 K8s service: K8s service 用例解读: K8s service 操作演示: K8s servi ...

  6. [Fundamental of Power Electronics]-PART I-1.引言-1.2 1.3 电力电子技术的几个应用、本书内容

    1.2 电力电子技术的几个应用 高效开关变换器面临的功率范围从 (1)小于1瓦(电池供电的便携式设备内的DC-DC转换器)到(2)计算机及办公设备中的几十,几百,数千瓦到(3)变速电机驱动器中上千瓦及 ...

  7. springboot基础项目搭建(十五篇)

    springboot系列一.springboot产生背景及介绍 springboot系列二.springboot项目搭建 springboot系列三.springboot 单元测试.配置访问路径.多个 ...

  8. Java代码格式化规范实践总结

    目标说明 统一良好的代码格式规范可以有效提升开发团队之间的「协作效率」,如果不同的开发团队或者开发人员采用不同的代码格式规范,那么每次Format代码都会导致大量的变化,在Code Review及Me ...

  9. 消息中间件-ActiveMQ支持的消息协议

    package com.study.mq.a1_example.helloworld.queue; import org.apache.activemq.ActiveMQConnectionFacto ...

  10. python工业互联网应用实战13—基于selenium的功能测试

    本章节我们再来说说测试,单元测试和功能测试.单元测试我们在数据验证章节简单提过了,本章我们进一步如何用单元测试来测试view的功能代码:同时,也涉及一下基于selenium的功能测试做法.笔者过去的项 ...