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); } }; }