SpringBoot WebFlux记录接口访问日志

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) {
   
   
    
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值