经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Spring » 查看文章
SpringCloud Finchley Gateway 缓存请求Body和Form表单的实现
来源:jb51  时间:2019/1/16 9:29:22  对本文有异议

在接入Spring-Cloud-Gateway时,可能有需求进行缓存Json-Body数据或者Form-Urlencoded数据的情况。

由于Spring-Cloud-Gateway是以WebFlux为基础的响应式架构设计,所以在原有Zuul基础上迁移过来的过程中,传统的编程思路,并不适合于Reactor Stream的开发。

网络上有许多缓存案例,但是在测试过程中出现各种Bug问题,在缓存Body时,需要考虑整体的响应式操作,才能更合理的缓存数据

下面提供缓存Json-Body数据或者Form-Urlencoded数据的具体实现方案,该方案经测试,满足各方面需求,以及避免了网络上其他缓存方案所出现的问题

定义一个GatewayContext类,用于存储请求中缓存的数据

  1. import lombok.Getter;
  2. import lombok.Setter;
  3. import lombok.ToString;
  4. import org.springframework.util.LinkedMultiValueMap;
  5. import org.springframework.util.MultiValueMap;
  6.  
  7. @Getter
  8. @Setter
  9. @ToString
  10. public class GatewayContext {
  11.  
  12. public static final String CACHE_GATEWAY_CONTEXT = "cacheGatewayContext";
  13.  
  14. /**
  15. * cache json body
  16. */
  17. private String cacheBody;
  18. /**
  19. * cache formdata
  20. */
  21. private MultiValueMap<String, String> formData;
  22. /**
  23. * cache reqeust path
  24. */
  25. private String path;
  26. }

实现GlobalFilter和Ordered接口用于缓存请求数据

1 . 该示例只支持缓存下面3种MediaType

  • APPLICATION_JSON--Json数据
  • APPLICATION_JSON_UTF8--Json数据
  • APPLICATION_FORM_URLENCODED--FormData表单数据

2 . 经验总结:

  • 在缓存Body时,不能够在Filter内部直接进行缓存,需要按照响应式的处理方式,在异步操作路途上进行缓存Body,由于Body只能读取一次,所以要读取完成后要重新封装新的request和exchange才能保证请求正常传递到下游
  • 在缓存FormData时,FormData也只能读取一次,所以在读取完毕后,需要重新封装request和exchange,这里要注意,如果对FormData内容进行了修改,则必须重新定义Header中的content-length已保证传输数据的大小一致
  1. import com.choice.cloud.architect.usergate.option.FilterOrderEnum;
  2. import com.choice.cloud.architect.usergate.support.GatewayContext;
  3. import io.netty.buffer.ByteBufAllocator;
  4. import lombok.extern.slf4j.Slf4j;
  5. import org.springframework.cloud.gateway.filter.GatewayFilterChain;
  6. import org.springframework.cloud.gateway.filter.GlobalFilter;
  7. import org.springframework.core.Ordered;
  8. import org.springframework.core.io.ByteArrayResource;
  9. import org.springframework.core.io.buffer.DataBuffer;
  10. import org.springframework.core.io.buffer.DataBufferUtils;
  11. import org.springframework.core.io.buffer.NettyDataBufferFactory;
  12. import org.springframework.http.HttpHeaders;
  13. import org.springframework.http.MediaType;
  14. import org.springframework.http.codec.HttpMessageReader;
  15. import org.springframework.http.server.reactive.ServerHttpRequest;
  16. import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
  17. import org.springframework.util.MultiValueMap;
  18. import org.springframework.web.reactive.function.server.HandlerStrategies;
  19. import org.springframework.web.reactive.function.server.ServerRequest;
  20. import org.springframework.web.server.ServerWebExchange;
  21. import reactor.core.publisher.Flux;
  22. import reactor.core.publisher.Mono;
  23.  
  24. import java.io.UnsupportedEncodingException;
  25. import java.net.URLEncoder;
  26. import java.nio.charset.Charset;
  27. import java.nio.charset.StandardCharsets;
  28. import java.util.List;
  29. import java.util.Map;
  30.  
  31. @Slf4j
  32. public class GatewayContextFilter implements GlobalFilter, Ordered {
  33.  
  34. /**
  35. * default HttpMessageReader
  36. */
  37. private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
  38.  
  39. @Override
  40. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  41. /**
  42. * save request path and serviceId into gateway context
  43. */
  44. ServerHttpRequest request = exchange.getRequest();
  45. String path = request.getPath().pathWithinApplication().value();
  46. GatewayContext gatewayContext = new GatewayContext();
  47. gatewayContext.getAllRequestData().addAll(request.getQueryParams());
  48. gatewayContext.setPath(path);
  49. /**
  50. * save gateway context into exchange
  51. */
  52. exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT,gatewayContext);
  53. HttpHeaders headers = request.getHeaders();
  54. MediaType contentType = headers.getContentType();
  55. long contentLength = headers.getContentLength();
  56. if(contentLength>0){
  57. if(MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_JSON_UTF8.equals(contentType)){
  58. return readBody(exchange, chain,gatewayContext);
  59. }
  60. if(MediaType.APPLICATION_FORM_URLENCODED.equals(contentType)){
  61. return readFormData(exchange, chain,gatewayContext);
  62. }
  63. }
  64. log.debug("[GatewayContext]ContentType:{},Gateway context is set with {}",contentType, gatewayContext);
  65. return chain.filter(exchange);
  66.  
  67. }
  68.  
  69.  
  70. @Override
  71. public int getOrder() {
  72. return Integer.MIN_VALUE;
  73. }
  74.  
  75. /**
  76. * ReadFormData
  77. * @param exchange
  78. * @param chain
  79. * @return
  80. */
  81. private Mono<Void> readFormData(ServerWebExchange exchange,GatewayFilterChain chain,GatewayContext gatewayContext){
  82. HttpHeaders headers = exchange.getRequest().getHeaders();
  83. return exchange.getFormData()
  84. .doOnNext(multiValueMap -> {
  85. gatewayContext.setFormData(multiValueMap);
  86. log.debug("[GatewayContext]Read FormData:{}",multiValueMap);
  87. })
  88. .then(Mono.defer(() -> {
  89. Charset charset = headers.getContentType().getCharset();
  90. charset = charset == null? StandardCharsets.UTF_8:charset;
  91. String charsetName = charset.name();
  92. MultiValueMap<String, String> formData = gatewayContext.getFormData();
  93. /**
  94. * formData is empty just return
  95. */
  96. if(null == formData || formData.isEmpty()){
  97. return chain.filter(exchange);
  98. }
  99. StringBuilder formDataBodyBuilder = new StringBuilder();
  100. String entryKey;
  101. List<String> entryValue;
  102. try {
  103. /**
  104. * remove system param ,repackage form data
  105. */
  106. for (Map.Entry<String, List<String>> entry : formData.entrySet()) {
  107. entryKey = entry.getKey();
  108. entryValue = entry.getValue();
  109. if (entryValue.size() > 1) {
  110. for(String value : entryValue){
  111. formDataBodyBuilder.append(entryKey).append("=").append(URLEncoder.encode(value, charsetName)).append("&");
  112. }
  113. } else {
  114. formDataBodyBuilder.append(entryKey).append("=").append(URLEncoder.encode(entryValue.get(0), charsetName)).append("&");
  115. }
  116. }
  117. }catch (UnsupportedEncodingException e){
  118. //ignore URLEncode Exception
  119. }
  120. /**
  121. * substring with the last char '&'
  122. */
  123. String formDataBodyString = "";
  124. if(formDataBodyBuilder.length()>0){
  125. formDataBodyString = formDataBodyBuilder.substring(0, formDataBodyBuilder.length() - 1);
  126. }
  127. /**
  128. * get data bytes
  129. */
  130. byte[] bodyBytes = formDataBodyString.getBytes(charset);
  131. int contentLength = bodyBytes.length;
  132. ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
  133. exchange.getRequest()) {
  134. /**
  135. * change content-length
  136. * @return
  137. */
  138. @Override
  139. public HttpHeaders getHeaders() {
  140. HttpHeaders httpHeaders = new HttpHeaders();
  141. httpHeaders.putAll(super.getHeaders());
  142. if (contentLength > 0) {
  143. httpHeaders.setContentLength(contentLength);
  144. } else {
  145. httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
  146. }
  147. return httpHeaders;
  148. }
  149.  
  150. /**
  151. * read bytes to Flux<Databuffer>
  152. * @return
  153. */
  154. @Override
  155. public Flux<DataBuffer> getBody() {
  156. return DataBufferUtils.read(new ByteArrayResource(bodyBytes),new NettyDataBufferFactory(ByteBufAllocator.DEFAULT),contentLength);
  157. }
  158. };
  159. ServerWebExchange mutateExchange = exchange.mutate().request(decorator).build();
  160. log.debug("[GatewayContext]Rewrite Form Data :{}",formDataBodyString);
  161. return chain.filter(mutateExchange);
  162. }));
  163. }
  164.  
  165. /**
  166. * ReadJsonBody
  167. * @param exchange
  168. * @param chain
  169. * @return
  170. */
  171. private Mono<Void> readBody(ServerWebExchange exchange,GatewayFilterChain chain,GatewayContext gatewayContext){
  172. /**
  173. * join the body
  174. */
  175. return DataBufferUtils.join(exchange.getRequest().getBody())
  176. .flatMap(dataBuffer -> {
  177. /**
  178. * read the body Flux<Databuffer>
  179. */
  180. DataBufferUtils.retain(dataBuffer);
  181. Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
  182. /**
  183. * repackage ServerHttpRequest
  184. */
  185. ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
  186. @Override
  187. public Flux<DataBuffer> getBody() {
  188. return cachedFlux;
  189. }
  190. };
  191. /**
  192. * mutate exchage with new ServerHttpRequest
  193. */
  194. ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
  195. /**
  196. * read body string with default messageReaders
  197. */
  198. return ServerRequest.create(mutatedExchange, messageReaders)
  199. .bodyToMono(String.class)
  200. .doOnNext(objectValue -> {
  201. gatewayContext.setCacheBody(objectValue);
  202. log.debug("[GatewayContext]Read JsonBody:{}",objectValue);
  203. }).then(chain.filter(mutatedExchange));
  204. });
  205. }
  206.  
  207. }

在后续Filter中,可以直接从ServerExchange中获取GatewayContext,就可以获取到缓存的数据,如果需要缓存其他数据,则可以根据自己的需求,添加到GatewayContext中即可

复制代码 代码如下:
GatewayContext gatewayContext = exchange.getAttribute(GatewayContext.CACHE_GATEWAY_CONTEXT);

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号