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 ...
随机推荐
- P1115_最大子段和(JAVA语言)
思路:贪心.累加求和,若和小于0则设置和为0,因为和小于0时对这段序列和无正作用,只会使整体变小,所以我们把小于0的段舍弃,从下一个数开始求序列和. 题目描述 给出一段序列,选出其中连续且非空的一段使 ...
- C++覆盖,隐藏,重载
code[class*="language-"], pre[class*="language-"] { color: rgba(51, 51, 51, 1); ...
- Java中BO、DAO、DO、DTO、PO、POJO、VO的概念
在程序开发中,经常会碰到各种专业术语,这里统一做一下解释,有遗漏或理解错误的恳请指正. BO(Business Object)业务对象 主要作用是把业务逻辑封装为一个对象,这个对象可以包括一个或多个其 ...
- [Skill]从零掌握80%的业务查询SQL语句
前言 本篇文章的主要目的是帮助初学者快速入门SQL查询,从而解决实际业务中80%的SQL查询问题. 本文主要框架如下: 上篇:介绍SQL的语法顺序和执行顺序 中篇:介绍条件子句.分组查询和排序的细节 ...
- (七)docker-compose 安装
一.Docker-Compose简介 Docker-Compose项目是Docker官方的开源项目,负责实现对Docker容器集群的快速编排. Docker-Compose将所管理的容器分为三层,分别 ...
- All in All UVA - 10340
You have devised a new encryption technique which encodes a message by inserting between its charac ...
- web前端的超神之路
前端超神之路 前端基础知识 HTML :用户实现页面的工具 CSS:用于美化界面的工具 javascript:用于操作html元素和css样式,让你的页面效果更美观 前端进阶知识 jQuery:用于简 ...
- 7.bug生命周期
new:测试发现并提交bug,状态为new/active; open: 分配bug到开发人员,状态为open: fixed:开发人员处理完bug,将状态改为fixed: closed/reopen:测 ...
- SpringBoot的旅游项目——day01(学习记录附赠源码)
前言 学完SpringBoot的项目,Github地址,欢迎start,一起学习! 第一天 一.技术选型 基于SpringBoot+VUE的前后端分离的仿照马蜂窝的项目. 后端选用的技术为: Spri ...
- [BUAA2021软工助教]案例分析作业总结
目录 一.作业链接 二.优秀作业推荐 A+作业推荐 A作业推荐 三.总结 所有案例分析总结 特色与优点 问题与建议 不同类产品案例分析Bug汇总 CSDN问答社区.Stack Overflow.Seg ...