Spring Cloud gateway 網(wǎng)關(guān)如何攔截Post請(qǐng)求日志
gateway版本是 2.0.1
1.pom結(jié)構(gòu)
(部分內(nèi)部項(xiàng)目依賴已經(jīng)隱藏)
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!--監(jiān)控相關(guān)--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- redis --> <!--<dependency>--> <!--<groupId>org.springframework.boot</groupId>--> <!--<artifactId>spring-boot-starter-data-redis</artifactId>--> <!--</dependency>--> <!-- test-scope --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.1.11</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.1.11</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.6</version> </dependency> <!--第三方的jdbctemplatetool--> <dependency> <groupId>org.crazycake</groupId> <artifactId>jdbctemplatetool</artifactId> <version>1.0.4-RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- alibaba start --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency>
2.表結(jié)構(gòu)
CREATE TABLE `zc_log_notes` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '日志信息記錄表主鍵id', `notes` varchar(255) DEFAULT NULL COMMENT '操作記錄信息', `amenu` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '一級(jí)菜單', `bmenu` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '二級(jí)菜單', `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作人ip地址,先用varchar存', `params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '請(qǐng)求值', `response` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '返回值', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作時(shí)間', `create_user` int(11) DEFAULT NULL COMMENT '操作人id', `end_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '響應(yīng)時(shí)間', `status` int(1) NOT NULL DEFAULT '1' COMMENT '響應(yīng)結(jié)果1成功0失敗', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=103 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='日志信息記錄表';
3.實(shí)體結(jié)構(gòu)
@Table(catalog = "zhiche", name = "zc_log_notes") public class LogNotes { /** * 日志信息記錄表主鍵id */ private Integer id; /** * 操作記錄信息 */ private String notes; /** * 一級(jí)菜單 */ private String amenu; /** * 二級(jí)菜單 */ private String bmenu; /** * 操作人ip地址,先用varchar存 */ private String ip; /** * 請(qǐng)求參數(shù)記錄 */ private String params; /** * 返回結(jié)果記錄 */ private String response; /** * 操作時(shí)間 */ private Date createTime; /** * 操作人id */ private Integer createUser; /** * 響應(yīng)時(shí)間 */ private Date endTime; /** * 響應(yīng)結(jié)果1成功0失敗 */ private Integer status; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getNotes() { return notes; } public void setNotes(String notes) { this.notes = notes; } public String getAmenu() { return amenu; } public void setAmenu(String amenu) { this.amenu = amenu; } public String getBmenu() { return bmenu; } public void setBmenu(String bmenu) { this.bmenu = bmenu; } public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public Integer getCreateUser() { return createUser; } public void setCreateUser(Integer createUser) { this.createUser = createUser; } public Date getEndTime() { return endTime; } public void setEndTime(Date endTime) { this.endTime = endTime; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } public String getParams() { return params; } public void setParams(String params) { this.params = params; } public String getResponse() { return response; } public void setResponse(String response) { this.response = response; } public void setAppendResponse(String response){ if (StringUtils.isNoneBlank(this.response)) { this.response = this.response + response; } else { this.response = response; } } }
4.dao層和Service層省略..
5.filter代碼
1. RequestRecorderGlobalFilter 實(shí)現(xiàn)了GlobalFilter和Order
package com.zc.gateway.filter; import com.zc.entity.LogNotes; import com.zc.gateway.service.FilterService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; 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.DataBufferUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.net.URI; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; /** * @author qiwenshuai * @note 目前只記錄了request方式為POST請(qǐng)求的方式 * @since 19-5-16 17:29 by jdk 1.8 */ @Component public class RequestRecorderGlobalFilter implements GlobalFilter, Ordered { @Autowired FilterService filterService; private Logger logger = LoggerFactory.getLogger(RequestRecorderGlobalFilter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest originalRequest = exchange.getRequest(); URI originalRequestUrl = originalRequest.getURI(); //只記錄http的請(qǐng)求 String scheme = originalRequestUrl.getScheme(); if ((!"http".equals(scheme) && !"https".equals(scheme))) { return chain.filter(exchange); } //這是我要打印的log-StringBuilder StringBuilder logbuilder = new StringBuilder(); //我自己的log實(shí)體 LogNotes logNotes = new LogNotes(); // 返回解碼 RecorderServerHttpResponseDecorator response = new RecorderServerHttpResponseDecorator(exchange.getResponse(), logNotes, filterService); //請(qǐng)求解碼 RecorderServerHttpRequestDecorator recorderServerHttpRequestDecorator = new RecorderServerHttpRequestDecorator(exchange.getRequest()); //增加過濾攔截吧 ServerWebExchange ex = exchange.mutate() .request(recorderServerHttpRequestDecorator) .response(response) .build(); // 觀察者模式 打印一下請(qǐng)求log // 這里可以在 配置文件中我進(jìn)行配置 // if (logger.isDebugEnabled()) { response.beforeCommit(() -> Mono.defer(() -> printLog(logbuilder, response))); // } return recorderOriginalRequest(logbuilder, ex, logNotes) .then(chain.filter(ex)) .then(); } private Mono<Void> recorderOriginalRequest(StringBuilder logBuffer, ServerWebExchange exchange, LogNotes logNotes) { logBuffer.append(System.currentTimeMillis()) .append("------------"); ServerHttpRequest request = exchange.getRequest(); Mono<Void> result = recorderRequest(request, logBuffer.append("\n原始請(qǐng)求:\n"), logNotes); try { filterService.addLog(logNotes); } catch (Exception e) { logger.error("保存請(qǐng)求參數(shù)出現(xiàn)錯(cuò)誤, e->{}", e.getMessage()); } return result; } /** * 記錄原始請(qǐng)求邏輯 */ private Mono<Void> recorderRequest(ServerHttpRequest request, StringBuilder logBuffer, LogNotes logNotes) { URI uri = request.getURI(); HttpMethod method = request.getMethod(); HttpHeaders headers = request.getHeaders(); logNotes.setIp(headers.getHost().getHostString()); logNotes.setAmenu("一級(jí)菜單"); logNotes.setBmenu("二級(jí)菜單"); logNotes.setNotes("操作記錄"); logBuffer .append(method.toString()).append(' ') .append(uri.toString()).append('\n'); logBuffer.append("------------請(qǐng)求頭------------\n"); headers.forEach((name, values) -> { values.forEach(value -> { logBuffer.append(name).append(":").append(value).append('\n'); }); }); Charset bodyCharset = null; if (hasBody(method)) { long length = headers.getContentLength(); if (length <= 0) { logBuffer.append("------------無body------------\n"); } else { logBuffer.append("------------body 長度:").append(length).append(" contentType:"); MediaType contentType = headers.getContentType(); if (contentType == null) { logBuffer.append("null,不記錄body------------\n"); } else if (!shouldRecordBody(contentType)) { logBuffer.append(contentType.toString()).append(",不記錄body------------\n"); } else { bodyCharset = getMediaTypeCharset(contentType); logBuffer.append(contentType.toString()).append("------------\n"); } } } if (bodyCharset != null) { return doRecordReqBody(logBuffer, request.getBody(), bodyCharset, logNotes) .then(Mono.defer(() -> { logBuffer.append("\n------------ end ------------\n\n"); return Mono.empty(); })); } else { logBuffer.append("------------ end ------------\n\n"); return Mono.empty(); } } //日志輸出返回值 private Mono<Void> printLog(StringBuilder logBuilder, ServerHttpResponse response) { HttpStatus statusCode = response.getStatusCode(); assert statusCode != null; logBuilder.append("響應(yīng):").append(statusCode.value()).append(" ").append(statusCode.getReasonPhrase()).append('\n'); HttpHeaders headers = response.getHeaders(); logBuilder.append("------------響應(yīng)頭------------\n"); headers.forEach((name, values) -> { values.forEach(value -> { logBuilder.append(name).append(":").append(value).append('\n'); }); }); logBuilder.append("\n------------ end at ") .append(System.currentTimeMillis()) .append("------------\n\n"); logger.info(logBuilder.toString()); return Mono.empty(); } // @Override public int getOrder() { //在GatewayFilter之前執(zhí)行 return -1; } private boolean hasBody(HttpMethod method) { //只記錄這3種謂詞的body // if (method == HttpMethod.POST || method == HttpMethod.PUT || method == HttpMethod.PATCH) return true; // return false; } //記錄簡單的常見的文本類型的request的body和response的body private boolean shouldRecordBody(MediaType contentType) { String type = contentType.getType(); String subType = contentType.getSubtype(); if ("application".equals(type)) { return "json".equals(subType) || "x-www-form-urlencoded".equals(subType) || "xml".equals(subType) || "atom+xml".equals(subType) || "rss+xml".equals(subType); } else if ("text".equals(type)) { return true; } //暫時(shí)不記錄form return false; } // 獲取請(qǐng)求的參數(shù) private Mono<Void> doRecordReqBody(StringBuilder logBuffer, Flux<DataBuffer> body, Charset charset, LogNotes logNotes) { return DataBufferUtils.join(body).doOnNext(buffer -> { CharBuffer charBuffer = charset.decode(buffer.asByteBuffer()); //記錄我實(shí)體的請(qǐng)求體 logNotes.setParams(charBuffer.toString()); logBuffer.append(charBuffer.toString()); DataBufferUtils.release(buffer); }).then(); } private Charset getMediaTypeCharset(@Nullable MediaType mediaType) { if (mediaType != null && mediaType.getCharset() != null) { return mediaType.getCharset(); } else { return StandardCharsets.UTF_8; } } }
2.RecorderServerHttpRequestDecorator 繼承了ServerHttpRequestDecorator
package com.zc.gateway.filter; import com.zc.entity.LogNotes; import org.springframework.core.io.buffer.DataBuffer; 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 java.util.LinkedList; import java.util.List; /** * @author qiwenshuai * @note * @since 19-5-16 17:30 by jdk 1.8 */ // request public class RecorderServerHttpRequestDecorator extends ServerHttpRequestDecorator { private final List<DataBuffer> dataBuffers = new LinkedList<>(); private boolean bufferCached = false; private Mono<Void> progress = null; public RecorderServerHttpRequestDecorator(ServerHttpRequest delegate) { super(delegate); } //重寫request請(qǐng)求體 @Override public Flux<DataBuffer> getBody() { synchronized (dataBuffers) { if (bufferCached) return copy(); if (progress == null) { progress = cache(); } return progress.thenMany(Flux.defer(this::copy)); } } private Flux<DataBuffer> copy() { return Flux.fromIterable(dataBuffers) .map(buf -> buf.factory().wrap(buf.asByteBuffer())); } private Mono<Void> cache() { return super.getBody() .map(dataBuffers::add) .then(Mono.defer(()-> { bufferCached = true; progress = null; return Mono.empty(); })); } }
3.RecorderServerHttpResponseDecorator 繼承了 ServerHttpResponseDecorator
package com.zc.gateway.filter; import com.zc.entity.LogNotes; import com.zc.gateway.service.FilterService; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; 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 org.springframework.core.io.buffer.DataBuffer; import java.nio.charset.Charset; import java.util.LinkedList; import java.util.List; /** * @author qiwenshuai * @note * @since 19-5-16 17:32 by jdk 1.8 */ public class RecorderServerHttpResponseDecorator extends ServerHttpResponseDecorator { private Logger logger = LoggerFactory.getLogger(RecorderServerHttpResponseDecorator.class); private LogNotes logNotes; private FilterService filterService; RecorderServerHttpResponseDecorator(ServerHttpResponse delegate, LogNotes logNotes, FilterService filterService) { super(delegate); this.logNotes = logNotes; this.filterService = filterService; } /** * 基于netty,我這里需要顯示的釋放一次dataBuffer,但是slice出來的byte是不需要釋放的, * 與下層共享一個(gè)字符串緩沖池,gateway過濾器使用的是nettyWrite類,會(huì)發(fā)生response數(shù)據(jù)多次才能返回完全。 * 在 ServerHttpResponseDecorator 之后會(huì)釋放掉另外一個(gè)refCount. */ @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { DataBufferFactory bufferFactory = this.bufferFactory(); if (body instanceof Flux) { Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body; Publisher<? extends DataBuffer> re = fluxBody.map(dataBuffer -> { // probably should reuse buffers byte[] content = new byte[dataBuffer.readableByteCount()]; // 數(shù)據(jù)讀入數(shù)組 dataBuffer.read(content); // 釋放掉內(nèi)存 DataBufferUtils.release(dataBuffer); // 記錄返回值 String s = new String(content, Charset.forName("UTF-8")); logNotes.setAppendResponse(s); try { filterService.updateLog(logNotes); } catch (Exception e) { logger.error("Response值修改日志記錄出現(xiàn)錯(cuò)誤->{}", e); } byte[] uppedContent = new String(content, Charset.forName("UTF-8")).getBytes(); return bufferFactory.wrap(uppedContent); }); return super.writeWith(re); } return super.writeWith(body); } @Override public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) { return writeWith(Flux.from(body).flatMapSequential(p -> p)); } }
注意:
網(wǎng)關(guān)過濾返回值 底層用到了Netty服務(wù),在response返回的時(shí)候,有時(shí)候會(huì)寫的數(shù)據(jù)是不全的,于是我在實(shí)體類中新增了一個(gè)setAppendResponse方法進(jìn)行拼接, 再者,gateway的過濾器是鏈?zhǔn)浇Y(jié)構(gòu),需要定義order排序?yàn)樽钕?-1),然后和預(yù)置的gateway過濾器做一個(gè)combine.
代碼中用到的 dataBuffer 結(jié)構(gòu),底層其實(shí)也是類似netty的byteBuffer,用到了字節(jié)數(shù)組池,同時(shí)也用到了 引用計(jì)數(shù)器 (refInt).
為了讓jvm在gc的時(shí)候垃圾得到回收,避免內(nèi)存泄露,我們需要在轉(zhuǎn)換字節(jié)使用的地方,顯示的釋放一次
DataBufferUtils.release(dataBuffer);
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- SpringCloud-Gateway轉(zhuǎn)發(fā)WebSocket失敗問題及解決
- 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 記錄請(qǐng)求應(yīng)答數(shù)據(jù)日志操作
- 基于Spring-cloud-gateway實(shí)現(xiàn)全局日志記錄的方法
相關(guān)文章
Java中ArrayList與順序表的定義與實(shí)現(xiàn)方法
ArrayList是一個(gè)實(shí)現(xiàn)List接口的類,底層是動(dòng)態(tài)類型順序表,本質(zhì)也就是數(shù)組,動(dòng)態(tài)主要體現(xiàn)在它的擴(kuò)容機(jī)制,下面這篇文章主要給大家介紹了關(guān)于Java中ArrayList與順序表的定義與實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2022-07-07Java時(shí)間復(fù)雜度、空間復(fù)雜度的深入詳解
對(duì)于一個(gè)算法,其時(shí)間復(fù)雜度和空間復(fù)雜度往往是相互影響的,當(dāng)追求一個(gè)較好的時(shí)間復(fù)雜度時(shí),可能會(huì)使空間復(fù)雜度的性能變差,即可能導(dǎo)致占用較多的存儲(chǔ)空間,這篇文章主要給大家介紹了關(guān)于Java時(shí)間復(fù)雜度、空間復(fù)雜度的相關(guān)資料,需要的朋友可以參考下2021-11-11Android應(yīng)用開發(fā)的一般文件組織結(jié)構(gòu)講解
這篇文章主要介紹了Android應(yīng)用開發(fā)的一般文件組織結(jié)構(gòu)講解,同時(shí)附帶介紹了一個(gè)獲取Android的文件列表的方法,需要的朋友可以參考下2015-12-12SpringBoot實(shí)現(xiàn)定時(shí)任務(wù)和異步調(diào)用
這篇文章主要為大家詳細(xì)介紹了SpringBoot實(shí)現(xiàn)定時(shí)任務(wù)和異步調(diào)用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-04-04springboot使用redisRepository和redistemplate操作redis的過程解析
本文給大家介紹springboot整合redis/分別用redisRepository和redistemplate操作redis,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2022-05-05myeclipse導(dǎo)出可運(yùn)行jar包簡介
這篇文章主要介紹了myeclipse導(dǎo)出可運(yùn)行jar包簡介,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11