spring cloud gateway
为了记录访问记录,须要记录请求体里面的内容,可是 request body
是只能读取一次的,若是读取之后不封装回去,则会形成后面的服务没法读取body
数据. 在网关里添加一个过滤器RequestRecordFilter
类:java
@Slf4j @Component public class RequestRecordFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); URI requestUri = request.getURI(); //只记录 http 请求(包含 https) String schema = requestUri.getScheme(); if ((!"http".equals(schema) && !"https".equals(schema))){ return chain.filter(exchange); } AccessRecord accessRecord = new AccessRecord(); accessRecord.setPath(requestUri.getPath()); accessRecord.setQueryString(request.getQueryParams()); exchange.getAttributes().put("startTime", System.currentTimeMillis()); String method = request.getMethodValue(); String contentType = request.getHeaders().getFirst("Content-Type"); //此处要排除流文件类型,好比上传的文件 if ("POST".equals(method) && !contentType.startsWith("multipart/form-data")){ String bodyStr = resolveBodyFromRequest(request); //下面将请求体再次封装写回到 request 里,传到下一级. URI ex = UriComponentsBuilder.fromUri(requestUri).build(true).toUri(); ServerHttpRequest newRequest = request.mutate().uri(ex).build(); DataBuffer bodyDataBuffer = stringBuffer(bodyStr); Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer); newRequest = new ServerHttpRequestDecorator(newRequest) { @Override public Flux<DataBuffer> getBody() { return bodyFlux; } }; accessRecord.setBody(formatStr(bodyStr)); ServerWebExchange newExchange = exchange.mutate().request(newRequest).build(); return returnMono(chain, newExchange, accessRecord); } else { return returnMono(chain, exchange, accessRecord); } } private Mono<Void> returnMono(GatewayFilterChain chain,ServerWebExchange exchange, AccessRecord accessRecord){ return chain.filter(exchange).then(Mono.fromRunnable(()->{ Long startTime = exchange.getAttribute("startTime"); if (startTime != null){ long executeTime = (System.currentTimeMillis() - startTime); accessRecord.setExpendTime(executeTime); accessRecord.setHttpCode(Objects.requireNonNull(exchange.getResponse().getStatusCode()).value()); writeAccessLog(JSON.toJSONString(accessRecord) + "\r\n"); } })); } @Override public int getOrder() { return 1; } /** * 获取请求体中的字符串内容 * @param serverHttpRequest * @return */ private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest){ //获取请求体 Flux<DataBuffer> body = serverHttpRequest.getBody(); StringBuilder sb = new StringBuilder(); body.subscribe(buffer -> { byte[] bytes = new byte[buffer.readableByteCount()]; buffer.read(bytes); DataBufferUtils.release(buffer); String bodyString = new String(bytes, StandardCharsets.UTF_8); sb.append(bodyString); }); return sb.toString(); } /** * 去掉空格,换行和制表符 * @param str * @return */ private String formatStr(String str){ if (str != null && str.length() > 0) { Pattern p = Pattern.compile("\\s*|\t|\r|\n"); Matcher m = p.matcher(str); return m.replaceAll(""); } return str; } private DataBuffer stringBuffer(String value){ byte[] bytes = value.getBytes(StandardCharsets.UTF_8); NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT); DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length); buffer.write(bytes); return buffer; } /** * 访问记录对象 */ @Data private class AccessRecord{ private String path; private String body; private MultiValueMap<String,String> queryString; private long expendTime; private int httpCode; } private void writeAccessLog(String str){ File file = new File("access.log"); if (!file.exists()){ try { if (file.createNewFile()){ file.setWritable(true); } } catch (IOException e) { log.error("建立访问日志文件失败.{}",e.getMessage(),e); } } try(FileWriter fileWriter = new FileWriter(file.getName(),true)){ fileWriter.write(str); } catch (IOException e) { log.error("写访问日志到文件失败. {}", e.getMessage(),e); } } }
网上有个获取 body
的写法, 可是这种写法对请求体的字符串长度有限制,稍微长一点, 就会转换不完整,方法以下:spring
private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) { //获取请求体 Flux<DataBuffer> body = serverHttpRequest.getBody(); AtomicReference<String> bodyRef = new AtomicReference<>(); body.subscribe(buffer -> { CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer()); DataBufferUtils.release(buffer); bodyRef.set(charBuffer.toString()); }); //获取request body return bodyRef.get(); }
对于出现 Only one connection receive subscriber allowed
,或者 response is set
之类的信息,这个是springboot2.0.5以后的一个bug,须要在类里面添加如下方法去实现(这个是springboot开发成员提供的方法):springboot
@Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter() { return new HiddenHttpMethodFilter() { @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { return chain.filter(exchange); } }; }