Spring Cloud Gateway 記錄請(qǐng)求應(yīng)答數(shù)據(jù)日志操作
我就廢話不多說(shuō)了,大家還是直接看代碼吧~
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; public String getCacheBody() { return cacheBody; } public void setCacheBody(String cacheBody) { this.cacheBody = cacheBody; } public MultiValueMap<String, String> getFormData() { return formData; } public void setFormData(MultiValueMap<String, String> formData) { this.formData = formData; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } }
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; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; 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.HttpMethod; 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.stereotype.Component; 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 io.netty.buffer.ByteBufAllocator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; // https://segmentfault.com/a/1190000017898354 @Component public class LogRequestGlobalFilter implements GlobalFilter { /** * default HttpMessageReader */ private static final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders(); private Logger log = LoggerFactory.getLogger(LogRequestGlobalFilter.class); @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.setPath(path); /** * save gateway context into exchange */ exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT, gatewayContext); HttpHeaders headers = request.getHeaders(); MediaType contentType = headers.getContentType(); log.info("start-------------------------------------------------"); log.info("HttpMethod:{},Url:{}", request.getMethod(), request.getURI().getRawPath()); log.info("Headers token: {}", headers.getFirst("token")); if (request.getMethod() == HttpMethod.GET) { log.info("end-------------------------------------------------"); } if (request.getMethod() == HttpMethod.POST) { Mono<Void> voidMono = null; if (MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_JSON_UTF8.equals(contentType)) { voidMono = readBody(exchange, chain, gatewayContext); } if (MediaType.APPLICATION_FORM_URLENCODED.equals(contentType)) { voidMono = readFormData(exchange, chain, gatewayContext); } return voidMono; } /* log.debug( "[GatewayContext]ContentType:{},Gateway context is set with {}", contentType, gatewayContext);*/ return chain.filter(exchange); } /** * ReadFormData * * @param exchange * @param chain * @return */ private Mono<Void> readFormData( ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext) { final ServerHttpRequest request = exchange.getRequest(); HttpHeaders headers = request.getHeaders(); return exchange.getFormData() .doOnNext(multiValueMap -> { gatewayContext.setFormData(multiValueMap); log.info("Post x-www-form-urlencoded:{}", multiValueMap); log.info( "end-------------------------------------------------"); }) .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( request) { /** * 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.info("[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 -> { log.info("PostBody:{}", objectValue); log.info( "end-------------------------------------------------"); gatewayContext.setCacheBody(objectValue); /* log.debug("[GatewayContext]Read JsonBody:{}", objectValue);*/ }).then(chain.filter(mutatedExchange)); }); } }
import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.net.InetSocketAddress; import java.net.URI; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicReference; @Component @Slf4j public class LogResponseGlobalFilter implements GlobalFilter, Ordered { private static final String REQUEST_PREFIX = "Request Info [ "; private static final String REQUEST_TAIL = " ]"; private static final String RESPONSE_PREFIX = "Response Info [ "; private static final String RESPONSE_TAIL = " ]"; private StringBuilder normalMsg = new StringBuilder(); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); DataBufferFactory bufferFactory = response.bufferFactory(); normalMsg.append(RESPONSE_PREFIX); ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) { @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { if (body instanceof Flux) { Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body; return super.writeWith(fluxBody.map(dataBuffer -> { // probably should reuse buffers byte[] content = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(content); String responseResult = new String(content, Charset.forName("UTF-8")); normalMsg.append("status=").append(this.getStatusCode()); normalMsg.append(";header=").append(this.getHeaders()); normalMsg.append(";responseResult=").append(responseResult); normalMsg.append(RESPONSE_TAIL); log.info(normalMsg.toString()); return bufferFactory.wrap(content); })); } return super.writeWith(body); // if body is not a flux. never got there. } }; return chain.filter(exchange.mutate().response(decoratedResponse).build()); } @Override public int getOrder() { return -2; } }
補(bǔ)充知識(shí):Spring Cloud Gateway 2.x 打印 Log
場(chǎng)景
在服務(wù)網(wǎng)關(guān)層面,需要打印出用戶每次的請(qǐng)求body和其他的參數(shù),gateway使用的是Reactor響應(yīng)式編程,和Zuul網(wǎng)關(guān)獲取流的寫法還有些不同,
不過(guò)基本的思路是一樣的,都是在filter中讀取body流,然后緩存回去,因?yàn)閎ody流,框架默認(rèn)只允許讀取一次。
思路
1. 添加一個(gè)filter做一次請(qǐng)求的攔截
GatewayConfig.java
添加一個(gè)配置類,配置一個(gè)高優(yōu)先級(jí)的filter,并且注入一個(gè)PayloadServerWebExchangeDecorator 對(duì)request和response做包裝的類。
package com.demo.gateway2x.config; import com.demo.gateway2x.decorator.PayloadServerWebExchangeDecorator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.web.server.WebFilter; @Configuration public class GatewayConfig { @Bean @Order(Ordered.HIGHEST_PRECEDENCE) //過(guò)濾器順序 public WebFilter webFilter() { return (exchange, chain) -> chain.filter(new PayloadServerWebExchangeDecorator(exchange)); } }
PayloadServerWebExchangeDecorator.java
這個(gè)類中,我們實(shí)現(xiàn)了框架的ServerWebExchangeDecorator類,同時(shí)注入了自定義的兩個(gè)類,PartnerServerHttpRequestDecorator 和 PartnerServerHttpResponseDecorator ,
這兩個(gè)類用于后面對(duì)請(qǐng)求與響應(yīng)的攔截。
package com.demo.gateway2x.decorator; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchangeDecorator; public class PayloadServerWebExchangeDecorator extends ServerWebExchangeDecorator { private PartnerServerHttpRequestDecorator requestDecorator; private PartnerServerHttpResponseDecorator responseDecorator; public PayloadServerWebExchangeDecorator(ServerWebExchange delegate) { super(delegate); requestDecorator = new PartnerServerHttpRequestDecorator(delegate.getRequest()); responseDecorator = new PartnerServerHttpResponseDecorator(delegate.getResponse()); } @Override public ServerHttpRequest getRequest() { return requestDecorator; } @Override public ServerHttpResponse getResponse() { return responseDecorator; } }
2. 在請(qǐng)求進(jìn)入時(shí),對(duì)request做一次攔截
PartnerServerHttpRequestDecorator.java
這個(gè)類實(shí)現(xiàn)了 ServerHttpRequestDecorator , 并在構(gòu)造函數(shù)中,使用響應(yīng)式編程,調(diào)用了打印log的方法,注意關(guān)注 Mono<DataBuffer> mono = DataBufferUtils.join(flux); ,
這里將Flux合并成了一個(gè)Mono,因?yàn)槿绻贿@么做,body內(nèi)容過(guò)多,將會(huì)被分段打印,這里是一個(gè)恒重要的點(diǎn),
在打印RequestParamsHandle.chain打印過(guò)日志后,我們又返回了一個(gè)dataBuffer,用作向下傳遞,否則dataBuffer被讀取過(guò)一次后就不能繼續(xù)使用了。
package com.demo.gateway2x.decorator; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import static reactor.core.scheduler.Schedulers.single; @Slf4j public class PartnerServerHttpRequestDecorator extends ServerHttpRequestDecorator { private Flux<DataBuffer> body; public PartnerServerHttpRequestDecorator(ServerHttpRequest delegate) { super(delegate); Flux<DataBuffer> flux = super.getBody(); if (ParamsUtils.CHAIN_MEDIA_TYPE.contains(delegate.getHeaders().getContentType())) { Mono<DataBuffer> mono = DataBufferUtils.join(flux); body = mono.publishOn(single()).map(dataBuffer -> RequestParamsHandle.chain(delegate, log, dataBuffer)).flux(); } else { body = flux; } } @Override public Flux<DataBuffer> getBody() { return body; } }
RequestParamsHandle.java
這個(gè)類主要用來(lái)讀取dataBuffer并做了日志打印處理,也可以做一些其他的例如參數(shù)校驗(yàn)等使用。
package com.demo.gateway2x.decorator; import com.alibaba.fastjson.JSON; import org.slf4j.Logger; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.StringUtils; import java.util.HashMap; import java.util.Map; public class RequestParamsHandle { public static <T extends DataBuffer> T chain(ServerHttpRequest delegate, Logger log, T buffer) { ParamsUtils.BodyDecorator bodyDecorator = ParamsUtils.buildBodyDecorator(buffer); // 參數(shù)校驗(yàn) 和 參數(shù)打印 log.info("Payload: {}", JSON.toJSONString(validParams(getParams(delegate, bodyDecorator.getBody())))); return (T) bodyDecorator.getDataBuffer(); } public static Map<String,Object> getParams(ServerHttpRequest delegate, String body) { // 整理參數(shù) Map<String,Object> params = new HashMap<>(); if (delegate.getQueryParams() != null) { params.putAll(delegate.getQueryParams()); } if (!StringUtils.isEmpty(body)) { params.putAll(JSON.parseObject(body)); } return params; } public static Map<String,Object> validParams(Map<String,Object> params) { // todo 參數(shù)校驗(yàn) return params; } }
3. 在結(jié)果返回時(shí),對(duì)response做一次攔截
PartnerServerHttpResponseDecorator.java
這個(gè)類和上面的request的異曲同工,攔截響應(yīng)流,并做記錄入處理。
package com.demo.gateway2x.decorator; import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import static reactor.core.scheduler.Schedulers.single; @Slf4j public class PartnerServerHttpResponseDecorator extends ServerHttpResponseDecorator { PartnerServerHttpResponseDecorator(ServerHttpResponse delegate) { super(delegate); } @Override public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) { return super.writeAndFlushWith(body); } @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { final MediaType contentType = super.getHeaders().getContentType(); if (ParamsUtils.CHAIN_MEDIA_TYPE.contains(contentType)) { if (body instanceof Mono) { final Mono<DataBuffer> monoBody = (Mono<DataBuffer>) body; return super.writeWith(monoBody.publishOn(single()).map(dataBuffer -> ResponseParamsHandle.chain(log, dataBuffer))); } else if (body instanceof Flux) { Mono<DataBuffer> mono = DataBufferUtils.join(body); final Flux<DataBuffer> monoBody = mono.publishOn(single()).map(dataBuffer -> ResponseParamsHandle.chain(log, dataBuffer)).flux(); return super.writeWith(monoBody); } } return super.writeWith(body); } }
ResponseParamsHandle.java
響應(yīng)流的日志打印
package com.demo.gateway2x.decorator; import org.slf4j.Logger; import org.springframework.core.io.buffer.DataBuffer; public class ResponseParamsHandle { public static <T extends DataBuffer> T chain(Logger log, T buffer) { ParamsUtils.BodyDecorator bodyDecorator = ParamsUtils.buildBodyDecorator(buffer); // 參數(shù)校驗(yàn) 和 參數(shù)打印 log.info("Payload: {}", bodyDecorator.getBody()); return (T) bodyDecorator.getDataBuffer(); } }
下面是實(shí)際操作,發(fā)送一次http請(qǐng)求:
控制臺(tái)log結(jié)果:
github源碼地址:https://github.com/qiaomengnan16/gateway-2x-log-demo
總結(jié)
gateway和zuul打印參數(shù)的方式思路是一致的,只是gateway采用的是reactor,寫法上與zuul的直接讀取流有些不同,這里需要知道的是Flux需要轉(zhuǎn)換為Mono這個(gè)地方,如果不轉(zhuǎn)換容易分多批打印。
以上這篇Spring Cloud Gateway 記錄請(qǐng)求應(yīng)答數(shù)據(jù)日志操作就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- SpringCloud-Gateway轉(zhuǎn)發(fā)WebSocket失敗問(wèn)題及解決
- spring?cloud?gateway中配置uri三種方式
- spring?cloud?gateway中netty線程池小優(yōu)化
- Spring?Cloud?Gateway中netty線程池優(yōu)化示例詳解
- SpringCloudGateway使用Skywalking時(shí)日志打印traceId解析
- SpringCloud?Gateway之請(qǐng)求應(yīng)答日志打印方式
- Spring Cloud gateway 網(wǎng)關(guān)如何攔截Post請(qǐng)求日志
- 基于Spring-cloud-gateway實(shí)現(xiàn)全局日志記錄的方法
相關(guān)文章
Java泛型在集合使用與自定義及繼承上的體現(xiàn)和通配符的使用
泛型又稱參數(shù)化類型,是Jdk5.0 出現(xiàn)的新特性,解決數(shù)據(jù)類型的安全性問(wèn)題,在類聲明或?qū)嵗瘯r(shí)只要指定好需要的具體的類型即可。Java泛型可以保證如果程序在編譯時(shí)沒(méi)有發(fā)出警告,運(yùn)行時(shí)就不會(huì)產(chǎn)生ClassCastException異常。同時(shí),代碼更加簡(jiǎn)潔、健壯2021-09-09SpringBoot Devtools實(shí)現(xiàn)項(xiàng)目熱部署的方法示例
這篇文章主要介紹了SpringBoot Devtools實(shí)現(xiàn)項(xiàng)目熱部署的方法示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-01-01Java SiteMesh新手學(xué)習(xí)教程代碼案例
這篇文章主要介紹了Java SiteMesh新手學(xué)習(xí)教程代碼案例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10Springboot實(shí)現(xiàn)圖片上傳功能的示例代碼
本篇文章主要介紹了SpringBoot如何實(shí)現(xiàn)圖片上傳功能,文中通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2022-09-09詳解SpringMVC驗(yàn)證框架Validation特殊用法
本篇文章主要介紹了詳解SpringMVC驗(yàn)證框架Validation特殊用法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02IDEA下Maven的pom文件導(dǎo)入依賴出現(xiàn)Auto build completed with errors的問(wèn)題
這篇文章主要介紹了IDEA下Maven的pom文件導(dǎo)入依賴出現(xiàn)Auto build completed with errors,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06