SpringCloud Finchley Gateway 緩存請求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ǔ)上遷移過來的過程中,傳統(tǒng)的編程思路,并不適合于Reactor Stream的開發(fā)。
網(wǎng)絡(luò)上有許多緩存案例,但是在測試過程中出現(xiàn)各種Bug問題,在緩存Body時(shí),需要考慮整體的響應(yīng)式操作,才能更合理的緩存數(shù)據(jù)
下面提供緩存Json-Body數(shù)據(jù)或者Form-Urlencoded數(shù)據(jù)的具體實(shí)現(xiàn)方案,該方案經(jīng)測試,滿足各方面需求,以及避免了網(wǎng)絡(luò)上其他緩存方案所出現(xiàn)的問題
定義一個(gè)GatewayContext類,用于存儲請求中緩存的數(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接口用于緩存請求數(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才能保證請求正常傳遞到下游
- 在緩存FormData時(shí),F(xiàn)ormData也只能讀取一次,所以在讀取完畢后,需要重新封裝request和exchange,這里要注意,如果對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中即可
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
java finally塊執(zhí)行時(shí)機(jī)全面分析
下面小編就為大家?guī)硪黄猨ava finally塊執(zhí)行時(shí)機(jī)全面分析。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-08-08
Nacos-SpringBoot框架啟動不加載bootstrap.yml的解決
這篇文章主要介紹了Nacos-SpringBoot框架啟動不加載bootstrap.yml的解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11
詳解spring cloud中使用Ribbon實(shí)現(xiàn)客戶端的軟負(fù)載均衡
這篇文章主要介紹了詳解spring cloud中使用Ribbon實(shí)現(xiàn)客戶端的軟負(fù)載均衡,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-01-01
eclipse實(shí)現(xiàn)DSA數(shù)字簽名
這篇文章主要為大家詳細(xì)介紹了eclipse實(shí)現(xiàn)DSA數(shù)字簽名算法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06
Java中將字符串String轉(zhuǎn)換為整數(shù)int的多種方法
在Java中將String類型轉(zhuǎn)換為int類型是一個(gè)常見的操作,下面這篇文章主要給大家介紹了關(guān)于Java中將字符串String轉(zhuǎn)換為整數(shù)int的多種方法,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-07-07
Required?request?body?is?missing的問題及解決
這篇文章主要介紹了Required?request?body?is?missing的問題及解決方案,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
解決ApplicationContext獲取不到Bean的問題
這篇文章主要介紹了解決ApplicationContext獲取不到Bean的問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06

