SpringBoot WebFlux 记录接口访问日志
前言
最近项目要做一个记录接口日志访问的需求,但是项目采用的SpringBoot WebFlux进行开发的,在获取请求的内容时,拿不到RequestBody中的内容。debug看的时候是空的。或者就是出现重复读取的异常。在网上找了一些解决方案,最终还是解决了,在此记录一下。
对已有的系统加一个加这个记录接口访问日志功能,不用说,AOP是比较适合的,除了用切点控制能进入切面的类,还可以通过注解来控制。aop就不多介绍了。
执行顺序:请求Request -> 网关Gateway -> 本服务WebFlux(过滤器Filter -> 切面方法Aspect -> Controller方法) -> 返回接口结果
1. 网关
请求到网关GateWay的时候是可以进行一些操作的,比如鉴权,比如可以把用户信息放入请求中传到WebFlux中,然后打印接口日志的时候可以加上用户的信息等等。这里不多说了。
2. WebFlux服务
2.1 过滤器Filter
创建上下文RequestInfoContext
@Getter
@Setter
public class RequestInfoContext {
public static final String CONTEXT_KEY = "REQUEST_INFO";
private String url;
private String method;
private HttpHeaders headers;
private String requestBody;
private ServerHttpRequest request;
}
上线文用于存放请求的一些信息,然后在切面中去拿上线文的信息来用。
创建过滤器RequestInfoWebFilter
@Slf4j
public class RequestInfoWebFilter implements WebFilter,Ordered {
/**
* Ordered 排序设为 Ordered.HIGHEST_PRECEDENCE 最高级别
* 防止在 WebFlux 内部 Filter 过滤器处理链路上读取过 RequstBody,如:HiddenHttpMethodFilter 中,
* 导制后面控制器中再一次读取该 RequstBody 就会出现异或者为空的情况。
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 创建一个RequestInfoContext对象,用于存储请求相关的信息
RequestInfoContext requestInfoContext = new RequestInfoContext();
requestInfoContext.setRequest(request);
// 从请求头中获取Content-Type
String contentType = request.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
// 只对contentType=application/json的数据进行重写RequesetBody
if (MediaType.APPLICATION_JSON_VALUE.equals(contentType)) {
AtomicReference<String> bodyRef = new AtomicReference<>();
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer());
// 保留DataBuffer,以便后续使用
DataBufferUtils.retain(dataBuffer);
bodyRef.set(charBuffer.toString());
String bodyStr = bodyRef.get();
requestInfoContext.setRequestBody(bodyStr);
// 缓存请求体的数据
Flux<DataBuffer> cachedFlux = Flux
.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
// 创建一个装饰后的ServerHttpRequest,重写其getBody方法,使其返回缓存的Flux<DataBuffer>
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
return chain.filter(exchange.mutate().request(mutatedRequest).build())
.contextWrite(context -> context.putAll(
Context.of(RequestInfoContext.CONTEXT_KEY, Mono.just(requestInfoContext))
.readOnly()));
});
}
return chain.filter(exchange)
.contextWrite(context -> context.putAll(Context.of(RequestInfoContext.CONTEXT_KEY, Mono.just(requestInfoContext))
.readOnly()));
}
}
bodyRef
:使用AtomicReference
来存储请求体。
DataBufferUtils.join(exchange.getRequest().getBody())
:读取请求体的数据并将其聚合成一个DataBuffer
。
charBuffer
:将DataBuffer
中的字节数据解码成CharBuffer
。
DataBufferUtils.retain(dataBuffer)
:保留DataBuffer
,以便后续使用。
bodyStr
:将CharBuffer
转换为字符串。
requestInfoContext.setRequestBody(...)
:将请求体设置到RequestInfoContext
中。
cachedFlux
:创建一个Flux<DataBuffer>
,用于缓存请求体的数据。
mutatedRequest
:创建一个装饰后的ServerHttpRequest
,重写其getBody
方法,使其返回缓存的Flux<DataBuffer>
。
返回调用chain.filter
,继续处理过滤链,并将RequestInfoContext
放入Context
中。
2.2 切面Aspect
创建注解
@Target({
ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemLog {
// 描述
String methodDesc() default "";
// 模块
String module() default "";
//事件类型:LOGIN;LOGINOUT;ADD;DELETE;UPDATE;SELETE;UPLOAD;DOWNLOAD;OTHER
OperateType operateType() default OperateType.OTHER;
//日志类型:0:系统日志;1:业务日志
String logType() default "0";
}
public enum OperateType {
LOGIN, LOGINOUT, ADD, DELETE, UPDATE, SELETE, UPLOAD, DOWNLOAD, OTHER
}
创建切面
@Aspect
@Order(1)
public class LoggingAspect {
private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
private final Environment env;
public LoggingAspect(Environment env) {