在接入Spring-Cloud-Gateway時,可能有需求進行緩存Json-Body數據或者Form-Urlencoded數據的狀況。
因爲Spring-Cloud-Gateway是以WebFlux爲基礎的響應式架構設計,因此在原有Zuul基礎上遷移過來的過程當中,傳統的編程思路,並不適合於Reactor Stream的開發。
網絡上有許多緩存案例,可是在測試過程當中出現各類Bug問題,在緩存Body時,須要考慮總體的響應式操做,才能更合理的緩存數據
定義一個
GatewayContext
類,用於存儲請求中的數據
import lombok.Getter; import lombok.Setter; import lombok.ToString; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @Getter @Setter @ToString public class GatewayContext { public static final String CACHE_GATEWAY_CONTEXT = "cacheGatewayContext"; /** * cache json body */ private String cacheBody; /** * cache formdata */ private MultiValueMap<String, String> formData; /** * cache reqeust path */ private String path; }
實現GlobalFilter
和Ordered
接口用於緩存請求中的數據
1 . 該示例只支持緩存下面3種MediaType
java
APPLICATION_JSON
--Json數據APPLICATION_JSON_UTF8
--Json數據APPLICATION_FORM_URLENCODED
--FormData表單數據2 . 經驗總結:react
Header
中的content-length
已保證傳輸數據的大小一致import io.netty.buffer.ByteBufAllocator; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.server.HandlerStrategies; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; @Slf4j public class GatewayContextFilter implements GlobalFilter, Ordered { /** * default HttpMessageReader */ private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders(); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { /** * save request path and serviceId into gateway context */ ServerHttpRequest request = exchange.getRequest(); String path = request.getPath().pathWithinApplication().value(); GatewayContext gatewayContext = new GatewayContext(); gatewayContext.getAllRequestData().addAll(request.getQueryParams()); gatewayContext.setPath(path); /** * save gateway context into exchange */ exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT,gatewayContext); HttpHeaders headers = request.getHeaders(); MediaType contentType = headers.getContentType(); long contentLength = headers.getContentLength(); if(contentLength>0){ if(MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_JSON_UTF8.equals(contentType)){ return readBody(exchange, chain,gatewayContext); } if(MediaType.APPLICATION_FORM_URLENCODED.equals(contentType)){ return readFormData(exchange, chain,gatewayContext); } } log.debug("[GatewayContext]ContentType:{},Gateway context is set with {}",contentType, gatewayContext); return chain.filter(exchange); } @Override public int getOrder() { return Integer.MIN_VALUE; } /** * ReadFormData * @param exchange * @param chain * @return */ private Mono<Void> readFormData(ServerWebExchange exchange,GatewayFilterChain chain,GatewayContext gatewayContext){ HttpHeaders headers = exchange.getRequest().getHeaders(); return exchange.getFormData() .doOnNext(multiValueMap -> { gatewayContext.setFormData(multiValueMap); log.debug("[GatewayContext]Read FormData:{}",multiValueMap); }) .then(Mono.defer(() -> { Charset charset = headers.getContentType().getCharset(); charset = charset == null? StandardCharsets.UTF_8:charset; String charsetName = charset.name(); MultiValueMap<String, String> formData = gatewayContext.getFormData(); /** * formData is empty just return */ if(null == formData || formData.isEmpty()){ return chain.filter(exchange); } StringBuilder formDataBodyBuilder = new StringBuilder(); String entryKey; List<String> entryValue; try { /** * repackage form data */ for (Map.Entry<String, List<String>> entry : formData.entrySet()) { entryKey = entry.getKey(); entryValue = entry.getValue(); if (entryValue.size() > 1) { for(String value : entryValue){ formDataBodyBuilder.append(entryKey).append("=").append(URLEncoder.encode(value, charsetName)).append("&"); } } else { formDataBodyBuilder.append(entryKey).append("=").append(URLEncoder.encode(entryValue.get(0), charsetName)).append("&"); } } }catch (UnsupportedEncodingException e){ //ignore URLEncode Exception } /** * substring with the last char '&' */ String formDataBodyString = ""; if(formDataBodyBuilder.length()>0){ formDataBodyString = formDataBodyBuilder.substring(0, formDataBodyBuilder.length() - 1); } /** * get data bytes */ byte[] bodyBytes = formDataBodyString.getBytes(charset); int contentLength = bodyBytes.length; ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator( exchange.getRequest()) { /** * change content-length * @return */ @Override public HttpHeaders getHeaders() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.putAll(super.getHeaders()); if (contentLength > 0) { httpHeaders.setContentLength(contentLength); } else { httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked"); } return httpHeaders; } /** * read bytes to Flux<Databuffer> * @return */ @Override public Flux<DataBuffer> getBody() { return DataBufferUtils.read(new ByteArrayResource(bodyBytes),new NettyDataBufferFactory(ByteBufAllocator.DEFAULT),contentLength); } }; ServerWebExchange mutateExchange = exchange.mutate().request(decorator).build(); log.debug("[GatewayContext]Rewrite Form Data :{}",formDataBodyString); return chain.filter(mutateExchange); })); } /** * ReadJsonBody * @param exchange * @param chain * @return */ private Mono<Void> readBody(ServerWebExchange exchange,GatewayFilterChain chain,GatewayContext gatewayContext){ /** * join the body */ return DataBufferUtils.join(exchange.getRequest().getBody()) .flatMap(dataBuffer -> { /* * read the body Flux<DataBuffer>, and release the buffer * //TODO when SpringCloudGateway Version Release To G.SR2,this can be update with the new version's feature * see PR https://github.com/spring-cloud/spring-cloud-gateway/pull/1095 */ byte[] bytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); DataBufferUtils.release(dataBuffer); Flux<DataBuffer> cachedFlux = Flux.defer(() -> { DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); DataBufferUtils.retain(buffer); return Mono.just(buffer); }); /** * repackage ServerHttpRequest */ ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) { @Override public Flux<DataBuffer> getBody() { return cachedFlux; } }; /** * mutate exchage with new ServerHttpRequest */ ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build(); /** * read body string with default messageReaders */ return ServerRequest.create(mutatedExchange, messageReaders) .bodyToMono(String.class) .doOnNext(objectValue -> { gatewayContext.setCacheBody(objectValue); log.debug("[GatewayContext]Read JsonBody:{}",objectValue); }).then(chain.filter(mutatedExchange)); }); } }
在後續Filter中,能夠直接從ServerExchange中獲取GatewayContext,就能夠獲取到緩存的數據,若是須要緩存其餘數據,則能夠根據本身的需求,添加到
GatewayContext
中便可
GatewayContext gatewayContext = exchange.getAttribute(GatewayContext.CACHE_GATEWAY_CONTEXT);
本人整理了一個Plugin擴展包,歡迎Issue和Star
spring-cloud-gateway-plugin