Spring Cloud Gateway 獲取請(qǐng)求體(Request Body)的多種方法
一、直接在全局?jǐn)r截器中獲取,偽代碼如下
private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest){
Flux<DataBuffer> body = serverHttpRequest.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
body.subscribe(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
DataBufferUtils.release(buffer);
bodyRef.set(charBuffer.toString());
});
return bodyRef.get();
}
存在的缺陷:其他攔截器無(wú)法再通過該方式獲取請(qǐng)求體(因?yàn)檎?qǐng)求體已被消費(fèi)),并且會(huì)拋出異常
Only one connection receive subscriber allowed.Caused by: java.lang.IllegalStateException: Only one connection receive subscriber allowed.
異常原因:實(shí)際上spring-cloud-gateway反向代理的原理是,首先讀取原請(qǐng)求的數(shù)據(jù),然后構(gòu)造一個(gè)新的請(qǐng)求,將原請(qǐng)求的數(shù)據(jù)封裝到新的請(qǐng)求中,然后再轉(zhuǎn)發(fā)出去。然而我們?cè)谒庋b之前讀取了一次request body,而request body只能讀取一次。因此就出現(xiàn)了上面的錯(cuò)誤。
再者受版本限制
這種方法在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重新包裝,繼續(xù)向下傳遞傳遞
@Override
public GatewayFilter apply(NameValueConfig nameValueConfig) {
return (exchange, chain) -> {
URI uri = exchange.getRequest().getURI();
URI ex = UriComponentsBuilder.fromUri(uri).build(true).toUri();
ServerHttpRequest request = exchange.getRequest().mutate().uri(ex).build();
if("POST".equalsIgnoreCase(request.getMethodValue())){//判斷是否為POST請(qǐng)求
Flux<DataBuffer> body = request.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
body.subscribe(dataBuffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer());
DataBufferUtils.release(dataBuffer);
bodyRef.set(charBuffer.toString());
});//讀取request body到緩存
String bodyStr = bodyRef.get();//獲取request body
System.out.println(bodyStr);//這里是我們需要做的操作
DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
request = new ServerHttpRequestDecorator(request){
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux;
}
};//封裝我們的request
}
return chain.filter(exchange.mutate().request(request).build());
};
}
protected DataBuffer stringBuffer(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}
該方案的缺陷:request body獲取不完整(因?yàn)楫惒皆颍?,只能獲取1024B的數(shù)據(jù)。并且請(qǐng)求體超過1024B,會(huì)出現(xiàn)響應(yīng)超慢(因?yàn)槲沂情_啟了熔斷)。
三、過濾器加路線定位器
翻查源碼發(fā)現(xiàn)ReadBodyPredicateFactory里面緩存了request body的信息,于是在自定義router中配置了ReadBodyPredicateFactory,然后在filter中通過cachedRequestBodyObject緩存字段獲取request body信息。
/**
* @description: 獲取POST請(qǐng)求的請(qǐng)求體
* ReadBodyPredicateFactory 發(fā)現(xiàn)里面緩存了request body的信息,
* 于是在自定義router中配置了ReadBodyPredicateFactory
* @modified:
*/
@EnableAutoConfiguration
@Configuration
public class RouteLocatorRequestBoby{
//自定義過濾器
@Resource
private ReqTraceFilter reqTraceFilter;
@Resource
private RibbonLoadBalancerClient ribbonLoadBalancerClient;
private static final String SERVICE = "/leap/**";
private static final String HTTP_PREFIX = "http://";
private static final String COLON = ":";
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
//通過負(fù)載均衡獲取服務(wù)實(shí)例
ServiceInstance instance = ribbonLoadBalancerClient.choose("PLATFORM-SERVICE");
//拼接路徑
StringBuilder forwardAddress = new StringBuilder(HTTP_PREFIX);
forwardAddress.append(instance.getHost())
.append(COLON)
.append(instance.getPort());
return builder.routes()
//攔截請(qǐng)求類型為POST Content-Type application/json application/json;charset=UTF-8
.route(r -> r
.header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON_VALUE + MediaType.APPLICATION_JSON_UTF8_VALUE)
.and()
.method(HttpMethod.POST)
.and()
//獲取緩存中的請(qǐng)求體
.readBody(Object.class, readBody -> {
return true;
})
.and()
.path(SERVICE)
//把請(qǐng)求體傳遞給攔截器reqTraceFilter
.filters(f -> {
f.filter(reqTraceFilter);
return f;
})
.uri(forwardAddress.toString())).build();
}
/**
* @description: 過濾器,用于獲取請(qǐng)求體,和處理請(qǐng)求體業(yè)務(wù),列如記錄日志
* @modified:
*/
@Component
public class ReqTraceFilter implements GlobalFilter, GatewayFilter,Ordered {
private static final String CONTENT_TYPE = "Content-Type";
private static final String CONTENT_TYPE_JSON = "application/json";
//獲取請(qǐng)求路由詳細(xì)信息Route route = exchange.getAttribute(GATEWAY_ROUTE_BEAN)
private static final String GATEWAY_ROUTE_BEAN = "org.springframework.cloud.gateway.support.ServerWebExchangeUtils.gatewayRoute";
private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
//判斷過濾器是否執(zhí)行
String requestUrl = RequestUtils.getCurrentRequest(request);
if (!RequestUtils.isFilter(requestUrl)) {
String bodyStr = "";
String contentType = request.getHeaders().getFirst(CONTENT_TYPE);
String method = request.getMethodValue();
//判斷是否為POST請(qǐng)求
if (null != contentType && HttpMethod.POST.name().equalsIgnoreCase(method) && contentType.contains(CONTENT_TYPE_JSON)) {
Object cachedBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
if(null != cachedBody){
bodyStr = cachedBody.toString();
}
}
if (HttpMethod.GET.name().equalsIgnoreCase(method)) {
bodyStr = request.getQueryParams().toString();
}
log.info("請(qǐng)求體內(nèi)容:{}",bodyStr);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 5;
}
}
該方案優(yōu)點(diǎn):這種解決,一不會(huì)帶來重復(fù)讀取問題,二不會(huì)帶來requestbody取不全問題。三在低版本的Spring Cloud Finchley.SR2也可以運(yùn)行。
缺點(diǎn):不支持multipart/form-data(異常415),這個(gè)致命。
四、通過org.springframework.cloud.gateway.filter.factory.rewrite包下有個(gè)ModifyRequestBodyGatewayFilterFactory,顧名思義,這就是修改 Request Body 的過濾器工廠類。
@Component
@Slf4j
public class ReqTraceFilter implements GlobalFilter, GatewayFilter, Ordered {
@Resource
private IPlatformFeignClient platformFeignClient;
/**
* httpheader,traceId的key名稱
*/
private static final String REQUESTID = "traceId";
private static final String CONTENT_TYPE = "Content-Type";
private static final String CONTENT_TYPE_JSON = "application/json";
private static final String GATEWAY_ROUTE_BEAN = "org.springframework.cloud.gateway.support.ServerWebExchangeUtils.gatewayRoute";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
//判斷過濾器是否執(zhí)行
String requestUrl = RequestUtils.getCurrentRequest(request);
if (!RequestUtils.isFilter(requestUrl)) {
String bodyStr = "";
String contentType = request.getHeaders().getFirst(CONTENT_TYPE);
String method = request.getMethodValue();
//判斷是否為POST請(qǐng)求
if (null != contentType && HttpMethod.POST.name().equalsIgnoreCase(method) && contentType.contains(CONTENT_TYPE_JSON)) {
ServerRequest serverRequest = new DefaultServerRequest(exchange);
List<String> list = new ArrayList<>();
// 讀取請(qǐng)求體
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
.flatMap(body -> {
//記錄請(qǐng)求體日志
final String nId = saveRequestOperLog(exchange, body);
//記錄日志id
list.add(nId);
return Mono.just(body);
});
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
headers.remove(HttpHeaders.CONTENT_LENGTH);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
return bodyInserter.insert(outputMessage, new BodyInserterContext())
.then(Mono.defer(() -> {
ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
httpHeaders.put(REQUESTID,list);
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
return chain.filter(exchange.mutate().request(decorator).build());
}));
}
if (HttpMethod.GET.name().equalsIgnoreCase(method)) {
bodyStr = request.getQueryParams().toString();
String nId = saveRequestOperLog(exchange, bodyStr);
ServerHttpRequest userInfo = exchange.getRequest().mutate()
.header(REQUESTID, nId).build();
return chain.filter(exchange.mutate().request(userInfo).build());
}
}
return chain.filter(exchange);
}
/**
* 保存請(qǐng)求日志
*
* @param exchange
* @param requestParameters
* @return
*/
private String saveRequestOperLog(ServerWebExchange exchange, String requestParameters) {
log.debug("接口請(qǐng)求參數(shù):{}", requestParameters);
ServerHttpRequest request = exchange.getRequest();
String ip = Objects.requireNonNull(request.getRemoteAddress()).getAddress().getHostAddress();
SaveOperLogVO vo = new SaveOperLogVO();
vo.setIp(ip);
vo.setReqUrl(RequestUtils.getCurrentRequest(request));
vo.setReqMethod(request.getMethodValue());
vo.setRequestParameters(requestParameters);
Route route = exchange.getAttribute(GATEWAY_ROUTE_BEAN);
//是否配置路由
if (route != null) {
vo.setSubsystem(route.getId());
}
ResEntity<String> res = platformFeignClient.saveOperLog(vo);
log.debug("當(dāng)前請(qǐng)求ID返回的數(shù)據(jù):{}", res);
return res.getData();
}
@Override
public int getOrder() {
return 5;
}
}
該方案:完美解決以上所有問題
參考文檔:https://www.codercto.com/a/52970.html
到此這篇關(guān)于Spring Cloud Gateway 獲取請(qǐng)求體(Request Body)的多種方法的文章就介紹到這了,更多相關(guān)Spring Cloud Gateway 獲取請(qǐng)求體內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
30分鐘入門Java8之lambda表達(dá)式學(xué)習(xí)
本篇文章主要介紹了30分鐘入門Java8之lambda表達(dá)式學(xué)習(xí),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-04-04
Spring Boot緩存實(shí)戰(zhàn) EhCache示例
本篇文章主要介紹了Spring Boot緩存實(shí)戰(zhàn) EhCache示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08
springboot?實(shí)現(xiàn)不同context-path下的會(huì)話共享
這篇文章主要介紹了springboot?實(shí)現(xiàn)不同context-path下的會(huì)話共享,基于很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01
mybatis教程之動(dòng)態(tài)sql語(yǔ)句_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了mybatis教程之動(dòng)態(tài)sql語(yǔ)句,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09
一篇文章教你如何用多種迭代寫法實(shí)現(xiàn)二叉樹遍歷
這篇文章主要介紹了C語(yǔ)言實(shí)現(xiàn)二叉樹遍歷的迭代算法,包括二叉樹的中序遍歷、先序遍歷及后序遍歷等,是非常經(jīng)典的算法,需要的朋友可以參考下2021-08-08
Maven腳手架如何基于jeecg實(shí)現(xiàn)快速開發(fā)
這篇文章主要介紹了Maven腳手架如何基于jeecg實(shí)現(xiàn)快速開發(fā),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10
Spring Boot緩存實(shí)戰(zhàn) Caffeine示例
本篇文章主要介紹了Spring Boot緩存實(shí)戰(zhàn) Caffeine示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-02-02

