springbootgateway记录请求和响应日志
springboot gateway 记录请求和响应日志
spring cloud gateway是基于webflux的项目,因而不能跟使用spring mvc一样直接获取request body,因此需要重新构造再转发。
如果我们在spring cloud gateway 封装之前读取了一次request body,比如打印request body日志,在下游获取数据的时候会出现错误:[spring cloud] [error] java.lang.IllegalStateException: Only one connection receive subscriber allowed. 因为request body只能读取一次,它是属于消费类型的。
出现这样的原因是InputStream的read()方法内部有一个postion,标志当前流被读取到的位置,每读取一次,该标志就会移动一次,如果读到最后,read()会返回-1,表示已经读取完了。如果想要重新读取则需要调用reset()方法,position就会移动到上次调用mark的位置,mark默认是0,所以就能从头再读了。调用reset()方法的前提是已经重写了reset()方法,当然能否reset也是有条件的,它取决于markSupported()方法是否返回true,InputStream默认不实现reset(),并且markSupported()默认也是返回false。综上,InputStream默认不实现reset的相关方法,而ServletInputStream也没有重写reset的相关方法,这样就无法重复读取流,这就是我们从request对象中获取的输入流就只能读取一次的原因。
直接上代码 import lombok.Data; import java.io.Serializable; import java.util.Date; @Data public class GatewayLog implements Serializable { private static final long serialVersionUID = 1983879536575766072L; /**访问实例*/ private String targetServer; /**请求路径*/ private String requestPath; /**请求方法*/ private String requestMethod; /**协议 */ private String schema; /**请求体*/ private String requestBody; /**响应体*/ private String responseData; /**请求ip*/ private String ip; /**请求时间*/ private Date requestTime; /**响应时间*/ private Date responseTime; /**执行时间*/ private long executeTime; /**返回码*/ private long code; /**返回数据类型*/ private String responseContentType; /**请求数据类型*/ private String requestContentType; /**请求用户id*/ private String userId; }
public interface AccessLogService { void saveAccessLog(GatewayLog gatewayLog); }
import cn.hutool.core.collection.CollectionUtil; import com.shouwei.gateway.entity.GatewayLog; import com.shouwei.gateway.service.AccessLogService; import com.shouwei.gateway.utils.IpUtils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.reactivestreams.Publisher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage; import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.support.BodyInserterContext; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.*; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.multipart.FormFieldPart; import org.springframework.http.codec.multipart.Part; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.HandlerStrategies; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * /** * 全局拦截器,作用所有的微服务 * 1. 对请求的API调用过滤,记录接口的请求时间,方便日志审计、告警、分析等运维操作 * 2. 后期可以扩展对接其他日志系统 */ @Slf4j @Component public class AccessLogFilter implements GlobalFilter, Ordered { /** * default HttpMessageReader. */ private static final List> MESSAGE_READERS = HandlerStrategies.withDefaults().messageReaders(); @Autowired private AccessLogService accessLogService; private final List> messageReaders = HandlerStrategies.withDefaults().messageReaders(); /** * 顺序必须是<-1,否则标准的NettyWriteResponseFilter将在您的过滤器得到一个被调用的机会之前发送响应 * 也就是说如果不小于 -1 ,将不会执行获取后端响应的逻辑 * * @return */ @Override public int getOrder() { return -100; } @Override @SuppressWarnings("unchecked") public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); // 请求路径 String requestPath = request.getPath().pathWithinApplication().value(); Route route = getGatewayRoute(exchange); String ipAddress = IpUtils.getIpAddress(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(); gatewayLog.setRequestContentType(mediaType.getType() + "/" + mediaType.getSubtype()); //json格式 if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) { return writeBodyLog(exchange, chain, gatewayLog); } //form-data格式 else if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType) || MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)) { return readFormData(exchange, chain, gatewayLog); } //其他格式 else { return writeBasicLog(exchange, chain, gatewayLog); } } private Mono writeBasicLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog accessLog) { return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> { DataBufferUtils.retain(dataBuffer); final Flux cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount()))); final ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) { @Override public Flux getBody() { return cachedFlux; } @Override public MultiValueMap getQueryParams() { return UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).build().getQueryParams(); } }; StringBuilder builder = new StringBuilder(); MultiValueMap queryParams = exchange.getRequest().getQueryParams(); if (CollectionUtil.isNotEmpty(queryParams)) { for (Map.Entry> entry : queryParams.entrySet()) { builder.append(entry.getKey()).append("=").append(StringUtils.join(entry.getValue(), ",")); } } accessLog.setRequestBody(builder.toString()); //获取响应体 ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog); return chain.filter(exchange.mutate().request(mutatedRequest).response(decoratedResponse).build()) .then(Mono.fromRunnable(() -> { // 打印日志 writeAccessLog(accessLog); })); }); } /** * 读取form-data数据 * * @param exchange * @param chain * @param accessLog * @return */ private Mono readFormData(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog accessLog) { return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> { DataBufferUtils.retain(dataBuffer); final Flux cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount()))); final ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) { @Override public Flux getBody() { return cachedFlux; } @Override public MultiValueMap getQueryParams() { return UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).build().getQueryParams(); } }; final HttpHeaders headers = exchange.getRequest().getHeaders(); if (headers.getContentLength() == 0) { return chain.filter(exchange); } ResolvableType resolvableType; if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(headers.getContentType())) { resolvableType = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class); } else { //解析 application/x-www-form-urlencoded resolvableType = ResolvableType.forClass(String.class); } return MESSAGE_READERS.stream().filter(reader -> reader.canRead(resolvableType, mutatedRequest.getHeaders().getContentType())).findFirst() .orElseThrow(() -> new IllegalStateException("no suitable HttpMessageReader.")).readMono(resolvableType, mutatedRequest, Collections.emptyMap()).flatMap(resolvedBody -> { if (resolvedBody instanceof MultiValueMap) { LinkedMultiValueMap map = (LinkedMultiValueMap) resolvedBody; if (CollectionUtil.isNotEmpty(map)) { StringBuilder builder = new StringBuilder(); final Part bodyPartInfo = (Part) ((MultiValueMap) resolvedBody).getFirst("body"); if (bodyPartInfo instanceof FormFieldPart) { String body = ((FormFieldPart) bodyPartInfo).value(); // log.info("body ==== " + body); builder.append("body=").append(body); } final Part uidPartInfo = (Part) ((MultiValueMap) resolvedBody).getFirst("uid"); if (uidPartInfo instanceof FormFieldPart) { String uid = ((FormFieldPart) uidPartInfo).value(); // log.info("uid ==== " + uid); accessLog.setUserId(uid); if (builder.length() > 0) { builder.append("&uid=").append(uid); } else { builder.append("uid=").append(uid); } } final Part timeStampPartInfo = (Part) ((MultiValueMap) resolvedBody).getFirst("timeStamp"); if (timeStampPartInfo instanceof FormFieldPart) { String timeStamp = ((FormFieldPart) timeStampPartInfo).value(); // log.info("timeStamp ==== " + timeStamp ); if (builder.length() > 0) { builder.append("&timeStamp=").append(timeStamp); } else { builder.append("timeStamp=").append(timeStamp); } } final Part tokenPartInfo = (Part) ((MultiValueMap) resolvedBody).getFirst("token"); if (tokenPartInfo instanceof FormFieldPart) { String token = ((FormFieldPart) tokenPartInfo).value(); // log.info("token ==== " + token); if (builder.length() > 0) { builder.append("&token=").append(token); } else { builder.append("token=").append(token); } } final Part signPartInfo = (Part) ((MultiValueMap) resolvedBody).getFirst("sign"); if (signPartInfo instanceof FormFieldPart) { String sign = ((FormFieldPart) signPartInfo).value(); // log.info("sign ==== " + sign); if (builder.length() > 0) { builder.append("&sign=").append(sign); } else { builder.append("sign=").append(sign); } } accessLog.setRequestBody(builder.toString()); } } else { accessLog.setRequestBody((String) resolvedBody); } //获取响应体 ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog); return chain.filter(exchange.mutate().request(mutatedRequest).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(() -> { // 重新封装请求 ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage); // 记录响应日志 ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog); // 记录普通的 return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build()) .then(Mono.fromRunnable(() -> { // 打印日志 writeAccessLog(gatewayLog); })); })); } /** * 打印日志 * * @param gatewayLog 网关日志 * @author javadaily * @date 2021/3/24 14:53 */ private void writeAccessLog(GatewayLog gatewayLog) { accessLogService.saveAccessLog(gatewayLog); } private Route getGatewayRoute(ServerWebExchange exchange) { return exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); } /** * 请求装饰器,重新计算 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(); } }; } /** * 记录响应日志 * 通过 DataBufferFactory 解决响应体分段传输问题。 */ 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); // 计算执行时间 long executeTime = (responseTime.getTime() - gatewayLog.getRequestTime().getTime()); gatewayLog.setExecuteTime(executeTime); // 获取响应类型,如果是 json 就打印 String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR); // log.info("originalResponseContentType =========== " + originalResponseContentType); gatewayLog.setResponseContentType(originalResponseContentType); gatewayLog.setCode(this.getStatusCode().value()); // if (ObjectUtils.equals(this.getStatusCode(), HttpStatus.OK) // && !StringUtil.isNullOrEmpty(originalResponseContentType) // && originalResponseContentType.contains("application/json")) { Flux<? extends DataBuffer> fluxBody = Flux.from(body); return super.writeWith(fluxBody.buffer().map(dataBuffers -> { // 合并多个流集合,解决返回体分段传输 DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); DataBuffer join = dataBufferFactory.join(dataBuffers); byte[] content = new byte[join.readableByteCount()]; join.read(content); // 释放掉内存 DataBufferUtils.release(join); String responseResult = new String(content, StandardCharsets.UTF_8); // log.info("responseResult =========== " + responseResult); gatewayLog.setResponseData(responseResult); return bufferFactory.wrap(content); })); } // } // if body is not a flux. never got there. return super.writeWith(body); } }; } }
用到的依赖 org.springframework.cloud spring-cloud-starter-gateway 2.2.5.RELEASE org.projectlombok lombok 1.18.22 cn.hutool hutool-all 5.2.5
安卓既然是开源的,为什么还需要谷歌授权?感谢邀请!要回答这个问题,只需了解安卓系统的来龙去脉,就会非常清晰了!(1)首先,必须明确安卓系统(Android)并不是谷歌公司自己开发的,是谷歌公司收购过来的。(2)Andro
特斯拉要求多辆订单客户承诺不转卖近日,特斯拉要求消费者签署不转卖承诺函,文件指出,一次或多次累计下单购买多台特斯拉车辆的车主需签署这份承诺函,承诺一年内不得向第三方转售,违者按车辆开票价20支付违约金,否则特斯拉
头条新闻之热点大事件头条新闻之热点大事件王兴称美团每送一单亏损超1元美团CEO王兴表示,第四季度公司配送服务营收143亿元,远低于183亿的相关成本,意味着每单亏超过1元。尽管配送服务业务尚未实现盈利
突发!巨头被曝裁员,紧急回应!更有裁员工牌堆满一大箱,半层楼的人都空了中国基金报安曼互联网大厂的裁员潮愈演愈烈。近日,社交平台上一份致京东员工的毕业须知引起热议。有多名认证为京东员工的网友发声,京东多条业务线正在裁员,并分享被裁经历。更有网友爆料,有
特斯拉在欧洲首座超级工厂开工并交付新车新华社柏林3月23日电(记者张毅荣)美国特斯拉公司设在德国首都柏林附近的超级工厂22日正式开工,并交付了首批30辆新车。这是欧洲首座也是全球第四座特斯拉超级工厂。据当地媒体报道,特
一个月内近40款新能源汽车涨价最高超3万元近日,大家热议的话题就是新能源车涨价。一度掀起了涨价潮。据统计,3月以来共有近20个品牌40个车型涨价,幅度从2000元到30000元不等。从目前的涨价情况来看,特斯拉以及小鹏理想
恒大布局新能源汽车是机会,还是噱头?据天眼查App显示,3月24日,恒驰国瑞新能源汽车销售(上海)有限公司成立,注册资本1亿元,法定代表人为唐琳,经营范围包括新能源汽车整车销售充电桩销售二手车经销集中式快速充电站等。
长安C385实车曝光,对飚特斯拉Model3?随着北京车展的临近,这段时间也有越来越多的新车浮出水面。近日,长安曝光了旗下新车C385的官图,并且在路上也看到了该车的无伪装路试照片。作为长安全新专用电动车打造的纯电新车,新车发
博敏电子股份有限公司关于全资子公司收到小鹏汽车定点开发通知书的公告证券代码603936证券简称博敏电子公告编号临2022018本公司董事会及全体董事保证本公告内容不存在任何虚假记载误导性陈述或者重大遗漏,并对其内容的真实性准确性和完整性承担个别及
汽车股普跌,大摩称锂价飙升或将打击对新能源车需求格隆汇3月25日丨汽车股普跌,长城汽车跌逾7,比亚迪股份跌逾6,蔚来SW广汽集团理想汽车W雅迪控股跌逾4。5,小鹏汽车吉利汽车跌超3。最近,多家企业上调新能源汽车销售价格,以应对原
富途获颁最佳数字金融服务奖项财富管理平台能力受认可中证网讯(记者林倩)日前由香港中资基金业协会彭博联合举办的第七届离岸中资基金奖揭晓,富途控股有限公司(NasdaqFUTU)旗下子公司富途证券国际(香港)有限公司获颁最佳数字金融服