Spring|Spring Cloud gateway 网关如何拦截Post请求日志

gateway版本是 2.0.1
1.pom结构 (部分内部项目依赖已经隐藏)

org.springframework.cloudspring-cloud-starter-netflix-eureka-clientorg.springframework.cloudspring-cloud-starter-gatewayorg.springframework.bootspring-boot-starter-actuatororg.springframework.bootspring-boot-starter-testtestch.qos.logbacklogback-core1.1.11ch.qos.logbacklogback-classic1.1.11org.apache.httpcomponentshttpclient4.5.6org.crazycakejdbctemplatetool1.0.4-RELEASEmysqlmysql-connector-javacom.alibabadruid

2.表结构
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 '一级菜单',`bmenu` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '二级菜单',`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 '请求值',`response` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '返回值',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',`create_user` int(11) DEFAULT NULL COMMENT '操作人id',`end_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '响应时间',`status` int(1) NOT NULL DEFAULT '1' COMMENT '响应结果1成功0失败',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=103 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='日志信息记录表';

3.实体结构
@Table(catalog = "zhiche", name = "zc_log_notes")public class LogNotes {/*** 日志信息记录表主键id*/private Integer id; /*** 操作记录信息*/private String notes; /*** 一级菜单*/private String amenu; /*** 二级菜单*/private String bmenu; /*** 操作人ip地址,先用varchar存*/private String ip; /*** 请求参数记录*/private String params; /*** 返回结果记录*/private String response; /*** 操作时间*/private Date createTime; /*** 操作人id*/private Integer createUser; /*** 响应时间*/private Date endTime; /*** 响应结果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 实现了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请求的方式 * @since 19-5-16 17:29 by jdk 1.8 */@Componentpublic class RequestRecorderGlobalFilter implements GlobalFilter, Ordered {@AutowiredFilterService filterService; private Logger logger = LoggerFactory.getLogger(RequestRecorderGlobalFilter.class); @Overridepublic Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest originalRequest = exchange.getRequest(); URI originalRequestUrl = originalRequest.getURI(); //只记录http的请求String scheme = originalRequestUrl.getScheme(); if ((!"http".equals(scheme) && !"https".equals(scheme))) {return chain.filter(exchange); }//这是我要打印的log-StringBuilderStringBuilder logbuilder = new StringBuilder(); //我自己的log实体LogNotes logNotes = new LogNotes(); // 返回解码RecorderServerHttpResponseDecorator response = new RecorderServerHttpResponseDecorator(exchange.getResponse(), logNotes, filterService); //请求解码RecorderServerHttpRequestDecorator recorderServerHttpRequestDecorator = new RecorderServerHttpRequestDecorator(exchange.getRequest()); //增加过滤拦截吧ServerWebExchange ex = exchange.mutate().request(recorderServerHttpRequestDecorator).response(response).build(); //观察者模式 打印一下请求log// 这里可以在 配置文件中我进行配置//if (logger.isDebugEnabled()) {response.beforeCommit(() -> Mono.defer(() -> printLog(logbuilder, response))); //}return recorderOriginalRequest(logbuilder, ex, logNotes).then(chain.filter(ex)).then(); }private Mono recorderOriginalRequest(StringBuilder logBuffer, ServerWebExchange exchange, LogNotes logNotes) {logBuffer.append(System.currentTimeMillis()).append("------------"); ServerHttpRequest request = exchange.getRequest(); Mono result = recorderRequest(request, logBuffer.append("\n原始请求:\n"), logNotes); try {filterService.addLog(logNotes); } catch (Exception e) {logger.error("保存请求参数出现错误, e->{}", e.getMessage()); }return result; }/*** 记录原始请求逻辑*/private Mono 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("一级菜单"); logNotes.setBmenu("二级菜单"); logNotes.setNotes("操作记录"); logBuffer.append(method.toString()).append(' ').append(uri.toString()).append('\n'); logBuffer.append("------------请求头------------\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 printLog(StringBuilder logBuilder, ServerHttpResponse response) {HttpStatus statusCode = response.getStatusCode(); assert statusCode != null; logBuilder.append("响应:").append(statusCode.value()).append(" ").append(statusCode.getReasonPhrase()).append('\n'); HttpHeaders headers = response.getHeaders(); logBuilder.append("------------响应头------------\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(); }//@Overridepublic int getOrder() {//在GatewayFilter之前执行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的bodyprivate 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; }//暂时不记录formreturn false; }// 获取请求的参数private Mono doRecordReqBody(StringBuilder logBuffer, Flux body, Charset charset, LogNotes logNotes) {return DataBufferUtils.join(body).doOnNext(buffer -> {CharBuffer charBuffer = charset.decode(buffer.asByteBuffer()); //记录我实体的请求体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 */// requestpublic class RecorderServerHttpRequestDecorator extends ServerHttpRequestDecorator {private final List dataBuffers = new LinkedList<>(); private boolean bufferCached = false; private Mono progress = null; public RecorderServerHttpRequestDecorator(ServerHttpRequest delegate) {super(delegate); }//重写request请求体@Overridepublic Flux getBody() {synchronized (dataBuffers) {if (bufferCached)return copy(); if (progress == null) {progress = cache(); }return progress.thenMany(Flux.defer(this::copy)); }}private Flux copy() {return Flux.fromIterable(dataBuffers).map(buf -> buf.factory().wrap(buf.asByteBuffer())); }private Mono 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是不需要释放的,* 与下层共享一个字符串缓冲池,gateway过滤器使用的是nettyWrite类,会发生response数据多次才能返回完全。* 在 ServerHttpResponseDecorator 之后会释放掉另外一个refCount.*/@Overridepublic Mono writeWith(Publisher body) {DataBufferFactory bufferFactory = this.bufferFactory(); if (body instanceof Flux) {Flux fluxBody = (Flux) body; Publisher re = fluxBody.map(dataBuffer -> {// probably should reuse buffersbyte[] content = new byte[dataBuffer.readableByteCount()]; // 数据读入数组dataBuffer.read(content); // 释放掉内存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值修改日志记录出现错误->{}", e); }byte[] uppedContent = new String(content, Charset.forName("UTF-8")).getBytes(); return bufferFactory.wrap(uppedContent); }); return super.writeWith(re); }return super.writeWith(body); }@Overridepublic Mono writeAndFlushWith(Publisher> body) {return writeWith(Flux.from(body).flatMapSequential(p -> p)); }}

注意: 网关过滤返回值 底层用到了Netty服务,在response返回的时候,有时候会写的数据是不全的,于是我在实体类中新增了一个setAppendResponse方法进行拼接, 再者,gateway的过滤器是链式结构,需要定义order排序为最先(-1),然后和预置的gateway过滤器做一个combine.
代码中用到的 dataBuffer 结构,底层其实也是类似netty的byteBuffer,用到了字节数组池,同时也用到了 引用计数器 (refInt).
为了让jvm在gc的时候垃圾得到回收,避免内存泄露,我们需要在转换字节使用的地方,显示的释放一次
DataBufferUtils.release(dataBuffer);

【Spring|Spring Cloud gateway 网关如何拦截Post请求日志】以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

    推荐阅读