Spring Cloud Gateway 之获取请求体(Request Body)的几种方式
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)的几种方式的更多相关文章
- Spring Cloud Gateway 动态修改请求参数解决 # URL 编码错误传参问题
Spring Cloud Gateway 动态修改请求参数解决 # URL 编码错误传参问题 继实现动态修改请求 Body 以及重试带 Body 的请求之后,我们又遇到了一个小问题.最近很多接口,收到 ...
- 获取【请求体】数据的3种方式(精)(文末代码) request.getInputStream() request.getInputStream() request.getReader()
application/x- www-form-urlencoded是Post请求默认的请求体内容类型,也是form表单默认的类型.Servlet API规范中对该类型的请求内容提供了request. ...
- Spring Cloud Gateway修改请求和响应body的内容
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- 快速突击 Spring Cloud Gateway
认识 Spring Cloud Gateway Spring Cloud Gateway 是一款基于 Spring 5,Project Reactor 以及 Spring Boot 2 构建的 API ...
- 微服务网关实战——Spring Cloud Gateway
导读 作为Netflix Zuul的替代者,Spring Cloud Gateway是一款非常实用的微服务网关,在Spring Cloud微服务架构体系中发挥非常大的作用.本文对Spring Clou ...
- 深入学习spring cloud gateway 限流熔断
前言 Spring Cloud Gateway 目前,Spring Cloud Gateway是仅次于Spring Cloud Netflix的第二个最受欢迎的Spring Cloud项目(就GitH ...
- Spring Cloud Gateway 没有链路信息,我 TM 人傻了(上)
本系列是 我TM人傻了 系列第五期[捂脸],往期精彩回顾: 升级到Spring 5.3.x之后,GC次数急剧增加,我TM人傻了 这个大表走索引字段查询的 SQL 怎么就成全扫描了,我TM人傻了 获取异 ...
- Spring cloud gateway
==================================为什么需要API gateway?==================================企业后台微服务互联互通, 因为 ...
- Spring Cloud实战: 基于Spring Cloud Gateway + vue-element-admin 实现的RBAC权限管理系统,实现网关对RESTful接口方法权限和自定义Vue指令对按钮权限的细粒度控制
一. 前言 信我的哈,明天过年. 这应该是农历年前的关于开源项目 的最后一篇文章了. 有来商城 是基于 Spring Cloud OAuth2 + Spring Cloud Gateway + JWT ...
随机推荐
- C语言数组寻址
C语言数组 数组的定义 数组是用来存放数据类型相同且逻辑意义相同的数据 数组的大小 数组的大小不能是变量,必须是常量或者常量表达式,常量表达式由编译器编译时自动求值. 也可以不指定数组大小,但必须对数 ...
- Hi3559AV100 NNIE开发(6)RFCN中NNIE实现关键线程函数->SAMPLE_SVP_NNIE_Rfcn_ViToVo()进行数据流分析
前面随笔给出了NNIE开发的基本知识,下面几篇随笔将着重于Mobilefacenet NNIE开发,实现mobilefacenet.wk的chip版本,并在Hi3559AV100上实现mobilefa ...
- Android学习中出现的问题
•问题1:多行文字如何实现跑马灯效果? 博客链接:Androidd Studio 之多行文字跑马灯特效 解决状态:已解决 •问题2:cause: unable to find valid certif ...
- C#类的一些基础知识(静态方法可以不用实例化调用)
将类成员函数声明为public static无需实例化即可调用类成员函数 using System; namespace ConsoleApp { class Program { static voi ...
- Java执行groovy脚本的两种方式
记录Java执行groovy脚本的两种方式,简单粗暴: 一种是通过脚本引擎ScriptEngine提供的eval(String)方法执行脚本内容:一种是执行groovy脚本: 二者都通过Invocab ...
- MySQL数据类型全解析
1 概述 本文主要介绍了MySQL的各种数据类型,版本为8.x, MySQL的数据类型可以分为六类:数值类型,日期时间类型,字符串类型,二进制类型,JSON类型与空间数据类型. 2 数值类型 数值类型 ...
- JDBC_03_反射机制注册驱动
反射机制注册驱动 代码:以类加载的方式注册驱动,因为注册驱动的代码是一个静态代码块,所以用Class.forname()加载该类,静态代码块也会进行加载 import java.sql.*; publ ...
- Day13_64_线程的优先级
线程的调度 * 单核CPU在某一时刻只能执行一条指令,线程只有得到CPU的时间片,也就是使用权,才可以执行指令 * 在单核CPU的机器上线程不是并行运行的,而是频繁切换运行的,只有在多个CPU上线程才 ...
- BeetleX数据分析中间服务V3
V3版可以对更多的数据场景分析,可以用在系统日志,销售数据,医疗门诊等不同行业的数据进行分析透视.而它的目标并不是简单地进行数据汇总,更注重于不同时间段和不同标签之前的数据的汇总和差异对比,通过数据的 ...
- 【故障公告】数据库服务器 CPU 100% 引发网站故障
悄悄地它又突然来了 -- 数据库服务器 CPU 100% 问题,上次光临时间是 3-30 8:48,这次是 4-28 9:41. 这次我们做出了快速反应,发现后立即进行主备切换,这次一次切换成功,CP ...