這篇文章主要講解了“如何實(shí)現(xiàn)SpringCloud Gateway請(qǐng)求響應(yīng)日志”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“如何實(shí)現(xiàn)SpringCloud Gateway請(qǐng)求響應(yīng)日志”吧!
專注于為中小企業(yè)提供成都網(wǎng)站設(shè)計(jì)、網(wǎng)站制作服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)寧武免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動(dòng)了上1000+企業(yè)的穩(wěn)健成長(zhǎng),幫助中小企業(yè)通過網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。
首先我們先定義一個(gè)日志體
@Data public class GatewayLog { /**訪問實(shí)例*/ private String targetServer; /**請(qǐng)求路徑*/ private String requestPath; /**請(qǐng)求方法*/ private String requestMethod; /**協(xié)議 */ private String schema; /**請(qǐng)求體*/ private String requestBody; /**響應(yīng)體*/ private String responseData; /**請(qǐng)求ip*/ private String ip; /**請(qǐng)求時(shí)間*/ private Date requestTime; /**響應(yīng)時(shí)間*/ private Date responseTime; /**執(zhí)行時(shí)間*/ private long executeTime; }
【關(guān)鍵】在網(wǎng)關(guān)定義日志過濾器,獲取輸入輸出參數(shù)
/** * 日志過濾器,用于記錄日志 * @author jianzh6 * @date 2020/3/24 17:17 */ @Slf4j @Component public class AccessLogFilter implements GlobalFilter, Ordered { @Autowired private AccessLogService accessLogService; private final List> messageReaders = HandlerStrategies.withDefaults().messageReaders(); @Override public int getOrder() { return -100; } @Override @SuppressWarnings("unchecked") public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); // 請(qǐng)求路徑 String requestPath = request.getPath().pathWithinApplication().value(); Route route = getGatewayRoute(exchange); String ipAddress = WebUtils.getServerHttpRequestIpAddress(request); GatewayLog gatewayLog = new GatewayLog(); gatewayLog.setSchema(request.getURI().getScheme()); gatewayLog.setRequestMethod(request.getMethodValue()); gatewayLog.setRequestPath(requestPath); gatewayLog.setTargetServer(route.getId()); gatewayLog.setRequestTime(new Date()); gatewayLog.setIp(ipAddress); MediaType mediaType = request.getHeaders().getContentType(); if(MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)){ return writeBodyLog(exchange, chain, gatewayLog); }else{ return writeBasicLog(exchange, chain, gatewayLog); } } private Mono writeBasicLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog accessLog) { StringBuilder builder = new StringBuilder(); MultiValueMap queryParams = exchange.getRequest().getQueryParams(); for (Map.Entry > entry : queryParams.entrySet()) { builder.append(entry.getKey()).append("=").append(StringUtils.join(entry.getValue(), ",")); } accessLog.setRequestBody(builder.toString()); //獲取響應(yīng)體 ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog); return chain.filter(exchange.mutate().response(decoratedResponse).build()) .then(Mono.fromRunnable(() -> { // 打印日志 writeAccessLog(accessLog); })); } /** * 解決 request body 只能讀取一次問題, * 參考: org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory * @param exchange * @param chain * @param gatewayLog * @return */ @SuppressWarnings("unchecked") private Mono writeBodyLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) { ServerRequest serverRequest = ServerRequest.create(exchange,messageReaders); Mono modifiedBody = serverRequest.bodyToMono(String.class) .flatMap(body ->{ gatewayLog.setRequestBody(body); return Mono.just(body); }); // 通過 BodyInserter 插入 body(支持修改body), 避免 request body 只能獲取一次 BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class); HttpHeaders headers = new HttpHeaders(); headers.putAll(exchange.getRequest().getHeaders()); // the new content type will be computed by bodyInserter // and then set in the request decorator headers.remove(HttpHeaders.CONTENT_LENGTH); CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers); return bodyInserter.insert(outputMessage,new BodyInserterContext()) .then(Mono.defer(() -> { // 重新封裝請(qǐng)求 ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage); // 記錄響應(yīng)日志 ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog); // 記錄普通的 return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build()) .then(Mono.fromRunnable(() -> { // 打印日志 writeAccessLog(gatewayLog); })); })); } /** * 打印日志 * @author javadaily * @date 2021/3/24 14:53 * @param gatewayLog 網(wǎng)關(guān)日志 */ private void writeAccessLog(GatewayLog gatewayLog) { log.info(gatewayLog.toString()); } private Route getGatewayRoute(ServerWebExchange exchange) { return exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); } /** * 請(qǐng)求裝飾器,重新計(jì)算 headers * @param exchange * @param headers * @param outputMessage * @return */ private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) { return new ServerHttpRequestDecorator(exchange.getRequest()) { @Override public HttpHeaders getHeaders() { long contentLength = headers.getContentLength(); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.putAll(super.getHeaders()); if (contentLength > 0) { httpHeaders.setContentLength(contentLength); } else { // TODO: this causes a 'HTTP/1.1 411 Length Required' // on // httpbin.org httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked"); } return httpHeaders; } @Override public Flux getBody() { return outputMessage.getBody(); } }; } /** * 記錄響應(yīng)日志 * 通過 DataBufferFactory 解決響應(yīng)體分段傳輸問題。 */ private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, GatewayLog gatewayLog) { ServerHttpResponse response = exchange.getResponse(); DataBufferFactory bufferFactory = response.bufferFactory(); return new ServerHttpResponseDecorator(response) { @Override public Mono writeWith(Publisher extends DataBuffer> body) { if (body instanceof Flux) { Date responseTime = new Date(); gatewayLog.setResponseTime(responseTime); // 計(jì)算執(zhí)行時(shí)間 long executeTime = (responseTime.getTime() - gatewayLog.getRequestTime().getTime()); gatewayLog.setExecuteTime(executeTime); // 獲取響應(yīng)類型,如果是 json 就打印 String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR); if (ObjectUtil.equal(this.getStatusCode(), HttpStatus.OK) && StringUtil.isNotBlank(originalResponseContentType) && originalResponseContentType.contains("application/json")) { Flux extends DataBuffer> fluxBody = Flux.from(body); return super.writeWith(fluxBody.buffer().map(dataBuffers -> { // 合并多個(gè)流集合,解決返回體分段傳輸 DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); DataBuffer join = dataBufferFactory.join(dataBuffers); byte[] content = new byte[join.readableByteCount()]; join.read(content); // 釋放掉內(nèi)存 DataBufferUtils.release(join); String responseResult = new String(content, StandardCharsets.UTF_8); gatewayLog.setResponseData(responseResult); return bufferFactory.wrap(content); })); } } // if body is not a flux. never got there. return super.writeWith(body); } }; } }
代碼較長(zhǎng)建議直接拷貝到編輯器,只要注意下面一個(gè)關(guān)鍵點(diǎn):
getOrder()方法返回的值必須要<-1,「否則標(biāo)準(zhǔn)的NettyWriteResponseFilter將在您的過濾器被調(diào)用的機(jī)會(huì)之前發(fā)送響應(yīng),即不會(huì)執(zhí)行獲取后端響應(yīng)參數(shù)的方法」
通過上面的兩步我們已經(jīng)可以獲取到請(qǐng)求的輸入輸出參數(shù)了,在 writeAccessLog()中將其輸出到了日志文件,大家可以在Postman發(fā)送請(qǐng)求觀察日志。
如果需要將日志持久化方便后期檢索的話可以考慮將日志存儲(chǔ)在MongoDB中,實(shí)現(xiàn)過程很簡(jiǎn)單。(安裝MongoDB可以參考這篇文章:實(shí)戰(zhàn)|MongoDB的安裝配置)
引入MongoDB
org.springframework.boot spring-boot-starter-data-mongodb-reactive
由于gateway是基于webflux,所以我們需要選擇reactive版本。
在GatewayLog上添加對(duì)應(yīng)的注解
@Data @Document public class GatewayLog { @Id private String id; ... }
建立AccessLogRepository
@Repository public interface AccessLogRepository extends ReactiveMongoRepository{ }
建立Service
public interface AccessLogService { /** * 保存AccessLog * @param gatewayLog 請(qǐng)求響應(yīng)日志 * @return 響應(yīng)日志 */ MonosaveAccessLog(GatewayLog gatewayLog); }
建立實(shí)現(xiàn)類
@Service public class AccessLogServiceImpl implements AccessLogService { @Autowired private AccessLogRepository accessLogRepository; @Override public MonosaveAccessLog(GatewayLog gatewayLog) { return accessLogRepository.insert(gatewayLog); } }
在Nacos配置中心添加MongoDB對(duì)應(yīng)配置
spring: data: mongodb: host: xxx.xx.x.xx port: 27017 database: accesslog username: accesslog password: xxxxxx
執(zhí)行請(qǐng)求,打開MongoDB客戶端,查看日志結(jié)果
感謝各位的閱讀,以上就是“如何實(shí)現(xiàn)SpringCloud Gateway請(qǐng)求響應(yīng)日志”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)如何實(shí)現(xiàn)SpringCloud Gateway請(qǐng)求響應(yīng)日志這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!