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 ...
随机推荐
- 递归实现1-n的全排列(JAVA语言)
思路: For example: 123的全排列= 1在最前面 23的全排列 + 2在最前面 13的全排列 + 3最前面 12的全排列 所以只需交换和最前面元素的位置,生成剩余元素的全排列即可. im ...
- P1208 [USACO1.3]混合牛奶 Mixing Milk(JAVA语言)
思路 按单价排序然后贪心 题目描述 由于乳制品产业利润很低,所以降低原材料(牛奶)价格就变得十分重要.帮助Marry乳业找到最优的牛奶采购方案. Marry乳业从一些奶农手中采购牛奶,并且每一位奶农为 ...
- [Fundamental of Power Electronics]-PART II-8. 变换器传递函数-8.5 交流传递函数以及阻抗的测量/8.6 本章小结
8.5 交流传递函数以及阻抗的测量 测量原型变换器和变换器系统的传递函数是非常好的工程实践过程.这样的实践可以验证系统是否被正确地建模和设计.此外,通过测量单个电路元件的端阻抗来表征其特性也是非常有用 ...
- 透视HTTP协议,带你拨开纷繁复杂的表象
一个HTTP,打趴80%面试者! HTTP是一个在计算机世界里专门在两点之间传输文字.图片.音频.视频等超文本数据的约定和规范.如果你不懂HTTP协议,那就相当于是个只有半桶水的程序员. 在这个专栏中 ...
- 自学转行JAVA,没有项目经历怎么找工作?
应届生或者是刚参加工作的转行人员都有这样一个疑惑,刚学出来没有工作经验,但是企业又要求你必须要有工作经验,但是刚毕业找不到工作就不可能有工作经验,感觉陷入一个死循环.其实这种情况那些企业是不可能不知道 ...
- 使用 EPPlus 封装的 excel 表格导入功能 (二) delegate 委托 --永远滴神
使用 EPPlus 封装的 excel 表格导入功能 (二) delegate 委托 --永远滴神 前言 接上一篇 使用 EPPlus 封装的 excel 表格导入功能 (一) 前一篇的是大概能用但是 ...
- unzip解压中文乱码
1 问题描述 直接 unzip xxx.zip 乱码,肯定是编码问题了不用问.但是unzip没有指定编码的选项: 网上的解决方案如下: unzip -O GBK/GB18030CP936 xx.zip ...
- NTP时间同步服务
NTP时间服务器 作用:ntp主要是用于对计算机的时间同步管理操作. 时间是对服务器来说是很重要的,一般很多网站都需要读取服务器时间来记录相关信息,如果时间不准,则可能造成很大的影响. 部署安装NTP ...
- 1075 PAT Judge
The ranklist of PAT is generated from the status list, which shows the scores of the submissions. Th ...
- Google字体API使用简单示例
一.前面的话 Google总会做些造福大众的事情,例如提供了web在线字体的API,这玩意其实去年就有了,但是字体种类手指头+脚趾头就可以数出来.but 最近,貌似Google对字体API进行了升级, ...