從api請求中獲取訪問的具體信息,是一個很常見的功能,這幾天在研究springcloud,使用到了其中的gateway,剛好將研究的過程結果都記錄下來
0. Version
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<spring-cloud.version>Greenwich.M3</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
1. GET請求
對於記錄get的請求,gateway中過濾器的exchange.getRequest().getQueryParams()方法就可以獲取的到了,關鍵的代碼如下
// 記錄請求的參數信息 針對GET 請求
MultiValueMap<String, String> queryParams = request.getQueryParams();
for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
builder.append(entry.getKey()).append("=").append(StringUtils.join(entry.getValue(), ",")).append(",");
}
2. POST請求
對於將請求的參數,存放在body這類的請求(如post),網上的很多方法是從ServerHttpRequest對象的getBody()方法返回的Flux<DataBuffer>進行讀取的,依靠響應式編程來進行讀取,但在自己demo中都沒有辦法真正獲取到
在參考一遍網友的文章后,可以參照ModifyRequestBodyGatewayFilterFactory提供的類的做法來進行,自己的實現,需要注意的是因為從body中讀取出來的內容,是依靠響應式編程的,也就是subscribe()被調用過一次后,不能被springboot內部再調用一次,所以我們需要重新返回一個新的request回去,以下是比較核心的代碼
/**
* 過濾器的內部類
*/
private class InnerFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 獲取用戶傳來的數據類型
MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
ServerRequest serverRequest = new DefaultServerRequest(exchange);
// 如果是json格式,將body內容轉化為object or map 都可
if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)){
Mono<Object> modifiedBody = serverRequest.bodyToMono(Object.class)
.flatMap(body -> {
recordLog(exchange.getRequest(), body);
return Mono.just(body);
});
return getVoidMono(exchange, chain, Object.class, modifiedBody);
}
// 如果是表單請求
else if(MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)){
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
// .log("modify_request_mono", Level.INFO)
.flatMap(body -> {
recordLog(exchange.getRequest(), body);
return Mono.just(body);
});
return getVoidMono(exchange, chain, String.class, modifiedBody);
}
// TODO 這里未來還可以限制一些格式
// 無法兼容的請求,則不讀取body,像Get請求這種
recordLog(exchange.getRequest(), "");
return chain.filter(exchange.mutate().request(exchange.getRequest()).build());
}
/**
* 優先級默認設置為最高
* @return
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
/**
* 參照 ModifyRequestBodyGatewayFilterFactory.java 截取的方法
* @param exchange
* @param chain
* @param outClass
* @param modifiedBody
* @return
*/
private Mono<Void> getVoidMono(ServerWebExchange exchange, GatewayFilterChain chain, Class outClass, Mono<?> modifiedBody) {
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass);
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())
// .log("modify_request", Level.INFO)
.then(Mono.defer(() -> {
ServerHttpRequestDecorator decorator = 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<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
return chain.filter(exchange.mutate().request(decorator).build());
}));
}
/**
* 記錄到請求日志中去
* @param request request
* @param body 請求的body內容
*/
private void recordLog(ServerHttpRequest request, Object body) {
// 記錄要訪問的url
StringBuilder builder = new StringBuilder(" request url: ");
builder.append(request.getURI().getRawPath());
// 記錄訪問的方法
HttpMethod method = request.getMethod();
if (null != method){
builder.append(", method: ").append(method.name());
}
// 記錄頭部信息
builder.append(", header { ");
for (Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
builder.append(entry.getKey()).append(":").append(StringUtils.join(entry.getValue(), ",")).append(",");
}
// 記錄參數
builder.append("} param: ");
// 處理get的請求
if (null != method && HttpMethod.GET.matches(method.name())) {
// 記錄請求的參數信息 針對GET 請求
MultiValueMap<String, String> queryParams = request.getQueryParams();
for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
builder.append(entry.getKey()).append("=").append(StringUtils.join(entry.getValue(), ",")).append(",");
}
}
else {
// 從body中讀取參數
builder.append(body);
}
LogUtil.info(builder.toString());
}
}
關於項目的完整代碼,在我的 github上
運行情況如下

在日志中的打印為
2019-06-01 16:47:30.442 [reactor-http-nio-2] INFO - request url: /open/check, method: POST, header { Accept:/,Content-type:application/json,User-Agent:curl/7.58.0,Host:localhost:8888,Content-Length:68,} param: {name=zhangsan, address=3678921789378217397128973982189321}
