SpringCloud Finchley Gateway 緩存請(qǐng)求Body和Form表單的實(shí)現(xiàn)
在接入Spring-Cloud-Gateway時(shí),可能有需求進(jìn)行緩存Json-Body數(shù)據(jù)或者Form-Urlencoded數(shù)據(jù)的情況。
由于Spring-Cloud-Gateway是以WebFlux為基礎(chǔ)的響應(yīng)式架構(gòu)設(shè)計(jì),所以在原有Zuul基礎(chǔ)上遷移過(guò)來(lái)的過(guò)程中,傳統(tǒng)的編程思路,并不適合于Reactor Stream的開(kāi)發(fā)。
網(wǎng)絡(luò)上有許多緩存案例,但是在測(cè)試過(guò)程中出現(xiàn)各種Bug問(wèn)題,在緩存Body時(shí),需要考慮整體的響應(yīng)式操作,才能更合理的緩存數(shù)據(jù)
下面提供緩存Json-Body數(shù)據(jù)或者Form-Urlencoded數(shù)據(jù)的具體實(shí)現(xiàn)方案,該方案經(jīng)測(cè)試,滿足各方面需求,以及避免了網(wǎng)絡(luò)上其他緩存方案所出現(xiàn)的問(wèn)題
定義一個(gè)GatewayContext類,用于存儲(chǔ)請(qǐng)求中緩存的數(shù)據(jù)
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; }
實(shí)現(xiàn)GlobalFilter和Ordered接口用于緩存請(qǐng)求數(shù)據(jù)
1 . 該示例只支持緩存下面3種MediaType
- APPLICATION_JSON--Json數(shù)據(jù)
- APPLICATION_JSON_UTF8--Json數(shù)據(jù)
- APPLICATION_FORM_URLENCODED--FormData表單數(shù)據(jù)
2 . 經(jīng)驗(yàn)總結(jié):
- 在緩存Body時(shí),不能夠在Filter內(nèi)部直接進(jìn)行緩存,需要按照響應(yīng)式的處理方式,在異步操作路途上進(jìn)行緩存Body,由于Body只能讀取一次,所以要讀取完成后要重新封裝新的request和exchange才能保證請(qǐng)求正常傳遞到下游
- 在緩存FormData時(shí),F(xiàn)ormData也只能讀取一次,所以在讀取完畢后,需要重新封裝request和exchange,這里要注意,如果對(duì)FormData內(nèi)容進(jìn)行了修改,則必須重新定義Header中的content-length已保證傳輸數(shù)據(jù)的大小一致
import com.choice.cloud.architect.usergate.option.FilterOrderEnum; import com.choice.cloud.architect.usergate.support.GatewayContext; 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 { /** * remove system param ,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> */ DataBufferUtils.retain(dataBuffer); Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount()))); /** * 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)); }); } }
在后續(xù)Filter中,可以直接從ServerExchange中獲取GatewayContext,就可以獲取到緩存的數(shù)據(jù),如果需要緩存其他數(shù)據(jù),則可以根據(jù)自己的需求,添加到GatewayContext中即可
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
java finally塊執(zhí)行時(shí)機(jī)全面分析
下面小編就為大家?guī)?lái)一篇java finally塊執(zhí)行時(shí)機(jī)全面分析。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-08-08Nacos-SpringBoot框架啟動(dòng)不加載bootstrap.yml的解決
這篇文章主要介紹了Nacos-SpringBoot框架啟動(dòng)不加載bootstrap.yml的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11詳解spring cloud中使用Ribbon實(shí)現(xiàn)客戶端的軟負(fù)載均衡
這篇文章主要介紹了詳解spring cloud中使用Ribbon實(shí)現(xiàn)客戶端的軟負(fù)載均衡,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-01-01eclipse實(shí)現(xiàn)DSA數(shù)字簽名
這篇文章主要為大家詳細(xì)介紹了eclipse實(shí)現(xiàn)DSA數(shù)字簽名算法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06Java中將字符串String轉(zhuǎn)換為整數(shù)int的多種方法
在Java中將String類型轉(zhuǎn)換為int類型是一個(gè)常見(jiàn)的操作,下面這篇文章主要給大家介紹了關(guān)于Java中將字符串String轉(zhuǎn)換為整數(shù)int的多種方法,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-07-07Required?request?body?is?missing的問(wèn)題及解決
這篇文章主要介紹了Required?request?body?is?missing的問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12解決ApplicationContext獲取不到Bean的問(wèn)題
這篇文章主要介紹了解決ApplicationContext獲取不到Bean的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06