Spring Cloud Gateway 获取请求体

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

  1. private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest){
  2.  
  3. Flux<DataBuffer> body = serverHttpRequest.getBody();
  4.  
  5. AtomicReference<String> bodyRef = new AtomicReference<>();
  6.  
  7. body.subscribe(buffer -> {
  8.  
  9. CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
  10.  
  11. DataBufferUtils.release(buffer);
  12.  
  13. bodyRef.set(charBuffer.toString());
  14.  
  15. });
  16.  
  17. return bodyRef.get();
  18.  
  19. }

  

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

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重新包装,继续向下传递传递

  1. @Override
  2. public GatewayFilter apply(NameValueConfig nameValueConfig) {
  3. return (exchange, chain) -> {
  4. URI uri = exchange.getRequest().getURI();
  5. URI ex = UriComponentsBuilder.fromUri(uri).build(true).toUri();
  6. ServerHttpRequest request = exchange.getRequest().mutate().uri(ex).build();
  7. if("POST".equalsIgnoreCase(request.getMethodValue())){//判断是否为POST请求
  8. Flux<DataBuffer> body = request.getBody();
  9. AtomicReference<String> bodyRef = new AtomicReference<>();
  10. body.subscribe(dataBuffer -> {
  11. CharBuffer charBuffer = StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer());
  12. DataBufferUtils.release(dataBuffer);
  13. bodyRef.set(charBuffer.toString());
  14. });//读取request body到缓存
  15. String bodyStr = bodyRef.get();//获取request body
  16. System.out.println(bodyStr);//这里是我们需要做的操作
  17. DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
  18. Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
  19.  
  20. request = new ServerHttpRequestDecorator(request){
  21. @Override
  22. public Flux<DataBuffer> getBody() {
  23. return bodyFlux;
  24. }
  25. };//封装我们的request
  26. }
  27. return chain.filter(exchange.mutate().request(request).build());
  28. };
  29. }
  30.   protected DataBuffer stringBuffer(String value) {
  31. byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
  32.  
  33. NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
  34. DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
  35. buffer.write(bytes);
  36. return buffer;
  37. }

  

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

三、过滤器加路线定位器

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

  1. /**
  2. * @description: 获取POST请求的请求体
  3. * ReadBodyPredicateFactory 发现里面缓存了request body的信息,
  4. * 于是在自定义router中配置了ReadBodyPredicateFactory
  5. * @modified:
  6. */
  7. @EnableAutoConfiguration
  8. @Configuration
  9. public class RouteLocatorRequestBoby{
  10.    //自定义过滤器
  11. @Resource
  12. private ReqTraceFilter reqTraceFilter;
  13.   
  14. @Resource
  15. private RibbonLoadBalancerClient ribbonLoadBalancerClient;
  16.  
  17. private static final String SERVICE = "/leap/**";
  18.  
  19. private static final String HTTP_PREFIX = "http://";
  20.  
  21. private static final String COLON = ":";
  22.  
  23. @Bean
  24. public RouteLocator myRoutes(RouteLocatorBuilder builder) {
  25. //通过负载均衡获取服务实例
  26. ServiceInstance instance = ribbonLoadBalancerClient.choose("PLATFORM-SERVICE");
  27. //拼接路径
  28. StringBuilder forwardAddress = new StringBuilder(HTTP_PREFIX);
  29. forwardAddress.append(instance.getHost())
  30. .append(COLON)
  31. .append(instance.getPort());
  32. return builder.routes()
  33. //拦截请求类型为POST Content-Type application/json application/json;charset=UTF-8
  34. .route(r -> r
  35. .header(HttpHeaders.CONTENT_TYPE,
  36. MediaType.APPLICATION_JSON_VALUE + MediaType.APPLICATION_JSON_UTF8_VALUE)
  37. .and()
  38. .method(HttpMethod.POST)
  39. .and()
  40. //获取缓存中的请求体
  41. .readBody(Object.class, readBody -> {
  42. return true;
  43. })
  44. .and()
  45. .path(SERVICE)
  46. //把请求体传递给拦截器reqTraceFilter
  47. .filters(f -> {
  48. f.filter(reqTraceFilter);
  49. return f;
  50. })
  51. .uri(forwardAddress.toString())).build();
  52. }
  53.  
  54. /**
  55. * @description: 过滤器,用于获取请求体,和处理请求体业务,列如记录日志
  56. * @modified:
  57. */
  58. @Component
  59. public class ReqTraceFilter implements GlobalFilter, GatewayFilter,Ordered {
  60.  
  61. private static final String CONTENT_TYPE = "Content-Type";
  62.  
  63. private static final String CONTENT_TYPE_JSON = "application/json";
  64.   
  65. //获取请求路由详细信息Route route = exchange.getAttribute(GATEWAY_ROUTE_BEAN)
  66. private static final String GATEWAY_ROUTE_BEAN = "org.springframework.cloud.gateway.support.ServerWebExchangeUtils.gatewayRoute";
  67.  
  68. private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";
  69. @Override
  70. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  71. ServerHttpRequest request = exchange.getRequest();
  72. //判断过滤器是否执行
  73. String requestUrl = RequestUtils.getCurrentRequest(request);
  74. if (!RequestUtils.isFilter(requestUrl)) {
  75. String bodyStr = "";
  76. String contentType = request.getHeaders().getFirst(CONTENT_TYPE);
  77. String method = request.getMethodValue();
  78. //判断是否为POST请求
  79. if (null != contentType && HttpMethod.POST.name().equalsIgnoreCase(method) && contentType.contains(CONTENT_TYPE_JSON)) {
  80. Object cachedBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
  81. if(null != cachedBody){
  82. bodyStr = cachedBody.toString();
  83. }
  84. }
  85. if (HttpMethod.GET.name().equalsIgnoreCase(method)) {
  86. bodyStr = request.getQueryParams().toString();
  87. }
  88.  
  89. log.info("请求体内容:{}",bodyStr);
  90. }
  91. return chain.filter(exchange);
  92. }
  93.  
  94. @Override
  95. public int getOrder() {
  96. return 5;
  97. }
  98. }

  

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

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

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

  1. @Component
  2. @Slf4j
  3. public class ReqTraceFilter implements GlobalFilter, GatewayFilter, Ordered {
  4.  
  5. @Resource
  6. private IPlatformFeignClient platformFeignClient;
  7.  
  8. /**
  9. * httpheader,traceId的key名称
  10. */
  11. private static final String REQUESTID = "traceId";
  12.  
  13. private static final String CONTENT_TYPE = "Content-Type";
  14.  
  15. private static final String CONTENT_TYPE_JSON = "application/json";
  16.  
  17. private static final String GATEWAY_ROUTE_BEAN = "org.springframework.cloud.gateway.support.ServerWebExchangeUtils.gatewayRoute";
  18.  
  19. @Override
  20. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  21. ServerHttpRequest request = exchange.getRequest();
  22. //判断过滤器是否执行
  23. String requestUrl = RequestUtils.getCurrentRequest(request);
  24. if (!RequestUtils.isFilter(requestUrl)) {
  25. String bodyStr = "";
  26. String contentType = request.getHeaders().getFirst(CONTENT_TYPE);
  27. String method = request.getMethodValue();
  28. //判断是否为POST请求
  29. if (null != contentType && HttpMethod.POST.name().equalsIgnoreCase(method) && contentType.contains(CONTENT_TYPE_JSON)) {
  30. ServerRequest serverRequest = new DefaultServerRequest(exchange);
  31. List<String> list = new ArrayList<>();
  32. // 读取请求体
  33. Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
  34. .flatMap(body -> {
  35. //记录请求体日志
  36. final String nId = saveRequestOperLog(exchange, body);
  37. //记录日志id
  38. list.add(nId);
  39. return Mono.just(body);
  40. });
  41.  
  42. BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
  43. HttpHeaders headers = new HttpHeaders();
  44. headers.putAll(exchange.getRequest().getHeaders());
  45. headers.remove(HttpHeaders.CONTENT_LENGTH);
  46.  
  47. CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
  48. return bodyInserter.insert(outputMessage, new BodyInserterContext())
  49. .then(Mono.defer(() -> {
  50. ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
  51. exchange.getRequest()) {
  52. @Override
  53. public HttpHeaders getHeaders() {
  54. long contentLength = headers.getContentLength();
  55. HttpHeaders httpHeaders = new HttpHeaders();
  56. httpHeaders.putAll(super.getHeaders());
  57. httpHeaders.put(REQUESTID,list);
  58. if (contentLength > 0) {
  59. httpHeaders.setContentLength(contentLength);
  60. } else {
  61. httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
  62. }
  63. return httpHeaders;
  64. }
  65.  
  66. @Override
  67. public Flux<DataBuffer> getBody() {
  68. return outputMessage.getBody();
  69. }
  70. };
  71.  
  72. return chain.filter(exchange.mutate().request(decorator).build());
  73. }));
  74. }
  75. if (HttpMethod.GET.name().equalsIgnoreCase(method)) {
  76. bodyStr = request.getQueryParams().toString();
  77. String nId = saveRequestOperLog(exchange, bodyStr);
  78. ServerHttpRequest userInfo = exchange.getRequest().mutate()
  79. .header(REQUESTID, nId).build();
  80. return chain.filter(exchange.mutate().request(userInfo).build());
  81. }
  82.  
  83. }
  84. return chain.filter(exchange);
  85. }
  86.  
  87. /**
  88. * 保存请求日志
  89. *
  90. * @param exchange
  91. * @param requestParameters
  92. * @return
  93. */
  94. private String saveRequestOperLog(ServerWebExchange exchange, String requestParameters) {
  95. log.debug("接口请求参数:{}", requestParameters);
  96. ServerHttpRequest request = exchange.getRequest();
  97. String ip = Objects.requireNonNull(request.getRemoteAddress()).getAddress().getHostAddress();
  98. SaveOperLogVO vo = new SaveOperLogVO();
  99. vo.setIp(ip);
  100. vo.setReqUrl(RequestUtils.getCurrentRequest(request));
  101. vo.setReqMethod(request.getMethodValue());
  102. vo.setRequestParameters(requestParameters);
  103.  
  104. Route route = exchange.getAttribute(GATEWAY_ROUTE_BEAN);
  105. //是否配置路由
  106. if (route != null) {
  107. vo.setSubsystem(route.getId());
  108. }
  109. ResEntity<String> res = platformFeignClient.saveOperLog(vo);
  110. log.debug("当前请求ID返回的数据:{}", res);
  111. return res.getData();
  112. }
  113.  
  114. @Override
  115. public int getOrder() {
  116. return 5;
  117. }
  118. }

  

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

参考文档: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. 递归实现1-n的全排列(JAVA语言)

    思路: For example: 123的全排列= 1在最前面 23的全排列 + 2在最前面 13的全排列 + 3最前面 12的全排列 所以只需交换和最前面元素的位置,生成剩余元素的全排列即可. im ...

  2. P1208 [USACO1.3]混合牛奶 Mixing Milk(JAVA语言)

    思路 按单价排序然后贪心 题目描述 由于乳制品产业利润很低,所以降低原材料(牛奶)价格就变得十分重要.帮助Marry乳业找到最优的牛奶采购方案. Marry乳业从一些奶农手中采购牛奶,并且每一位奶农为 ...

  3. [Fundamental of Power Electronics]-PART II-8. 变换器传递函数-8.5 交流传递函数以及阻抗的测量/8.6 本章小结

    8.5 交流传递函数以及阻抗的测量 测量原型变换器和变换器系统的传递函数是非常好的工程实践过程.这样的实践可以验证系统是否被正确地建模和设计.此外,通过测量单个电路元件的端阻抗来表征其特性也是非常有用 ...

  4. 透视HTTP协议,带你拨开纷繁复杂的表象

    一个HTTP,打趴80%面试者! HTTP是一个在计算机世界里专门在两点之间传输文字.图片.音频.视频等超文本数据的约定和规范.如果你不懂HTTP协议,那就相当于是个只有半桶水的程序员. 在这个专栏中 ...

  5. 自学转行JAVA,没有项目经历怎么找工作?

    应届生或者是刚参加工作的转行人员都有这样一个疑惑,刚学出来没有工作经验,但是企业又要求你必须要有工作经验,但是刚毕业找不到工作就不可能有工作经验,感觉陷入一个死循环.其实这种情况那些企业是不可能不知道 ...

  6. 使用 EPPlus 封装的 excel 表格导入功能 (二) delegate 委托 --永远滴神

    使用 EPPlus 封装的 excel 表格导入功能 (二) delegate 委托 --永远滴神 前言 接上一篇 使用 EPPlus 封装的 excel 表格导入功能 (一) 前一篇的是大概能用但是 ...

  7. unzip解压中文乱码

    1 问题描述 直接 unzip xxx.zip 乱码,肯定是编码问题了不用问.但是unzip没有指定编码的选项: 网上的解决方案如下: unzip -O GBK/GB18030CP936 xx.zip ...

  8. NTP时间同步服务

    NTP时间服务器 作用:ntp主要是用于对计算机的时间同步管理操作. 时间是对服务器来说是很重要的,一般很多网站都需要读取服务器时间来记录相关信息,如果时间不准,则可能造成很大的影响. 部署安装NTP ...

  9. 1075 PAT Judge

    The ranklist of PAT is generated from the status list, which shows the scores of the submissions. Th ...

  10. Google字体API使用简单示例

    一.前面的话 Google总会做些造福大众的事情,例如提供了web在线字体的API,这玩意其实去年就有了,但是字体种类手指头+脚趾头就可以数出来.but 最近,貌似Google对字体API进行了升级, ...