Spring Cloud Gateway 2.x 打印 Log

场景

在服务网关层面,需要打印出用户每次的请求body和其他的参数,gateway使用的是Reactor响应式编程,和Zuul网关获取流的写法还有些不同,

不过基本的思路是一样的,都是在filter中读取body流,然后缓存回去,因为body流,框架默认只允许读取一次。

思路

1. 添加一个filter做一次请求的拦截

GatewayConfig.java

添加一个配置类,配置一个高优先级的filter,并且注入一个PayloadServerWebExchangeDecorator 对request和response做包装的类。

package com.demo.gateway2x.config;

import com.demo.gateway2x.decorator.PayloadServerWebExchangeDecorator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.server.WebFilter;

@Configuration
public class GatewayConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE) //过滤器顺序
    public WebFilter webFilter() {
        return (exchange, chain) -> chain.filter(new PayloadServerWebExchangeDecorator(exchange));
    }

}

PayloadServerWebExchangeDecorator.java

这个类中,我们实现了框架的ServerWebExchangeDecorator类,同时注入了自定义的两个类,PartnerServerHttpRequestDecoratorPartnerServerHttpResponseDecorator ,

这两个类用于后面对请求与响应的拦截。

package com.demo.gateway2x.decorator;

import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebExchangeDecorator;

public class PayloadServerWebExchangeDecorator extends ServerWebExchangeDecorator {

    private PartnerServerHttpRequestDecorator requestDecorator;

    private PartnerServerHttpResponseDecorator responseDecorator;

    public PayloadServerWebExchangeDecorator(ServerWebExchange delegate) {
        super(delegate);
        requestDecorator = new PartnerServerHttpRequestDecorator(delegate.getRequest());
        responseDecorator = new PartnerServerHttpResponseDecorator(delegate.getResponse());
    }

    @Override
    public ServerHttpRequest getRequest() {
        return requestDecorator;
    }

    @Override
    public ServerHttpResponse getResponse() {
        return responseDecorator;
    }

}

2. 在请求进入时,对request做一次拦截

PartnerServerHttpRequestDecorator.java

这个类实现了 ServerHttpRequestDecorator , 并在构造函数中,使用响应式编程,调用了打印log的方法,注意关注 Mono<DataBuffer> mono = DataBufferUtils.join(flux);

这里将Flux合并成了一个Mono,因为如果不这么做,body内容过多,将会被分段打印,这里是一个恒重要的点,

在打印RequestParamsHandle.chain打印过日志后,我们又返回了一个dataBuffer,用作向下传递,否则dataBuffer被读取过一次后就不能继续使用了。

package com.demo.gateway2x.decorator;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import static reactor.core.scheduler.Schedulers.single;

@Slf4j
public class PartnerServerHttpRequestDecorator extends ServerHttpRequestDecorator {

    private Flux<DataBuffer> body;

    public PartnerServerHttpRequestDecorator(ServerHttpRequest delegate) {
        super(delegate);
        Flux<DataBuffer> flux = super.getBody();
        if (ParamsUtils.CHAIN_MEDIA_TYPE.contains(delegate.getHeaders().getContentType())) {
            Mono<DataBuffer> mono = DataBufferUtils.join(flux);
            body = mono.publishOn(single()).map(dataBuffer -> RequestParamsHandle.chain(delegate, log, dataBuffer)).flux();
        } else {
            body = flux;
        }
    }

    @Override
    public Flux<DataBuffer> getBody() {
        return body;
    }

}

RequestParamsHandle.java

这个类主要用来读取dataBuffer并做了日志打印处理,也可以做一些其他的例如参数校验等使用。

package com.demo.gateway2x.decorator;

import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.Map;

public class RequestParamsHandle {

    public static <T extends DataBuffer> T chain(ServerHttpRequest delegate, Logger log, T buffer) {
        ParamsUtils.BodyDecorator bodyDecorator = ParamsUtils.buildBodyDecorator(buffer);
        // 参数校验 和 参数打印
        log.info("Payload: {}", JSON.toJSONString(validParams(getParams(delegate, bodyDecorator.getBody()))));
        return (T) bodyDecorator.getDataBuffer();
    }

    public static Map<String,Object> getParams(ServerHttpRequest delegate, String body) {
        // 整理参数
        Map<String,Object> params = new HashMap<>();
        if (delegate.getQueryParams() != null) {
            params.putAll(delegate.getQueryParams());
        }
        if (!StringUtils.isEmpty(body)) {
            params.putAll(JSON.parseObject(body));
        }
        return params;
    }

    public static Map<String,Object> validParams(Map<String,Object> params) {
        // todo 参数校验
        return params;
    }

}

3. 在结果返回时,对response做一次拦截

PartnerServerHttpResponseDecorator.java

这个类和上面的request的异曲同工,拦截响应流,并做记录入处理。

package com.demo.gateway2x.decorator;

import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import static reactor.core.scheduler.Schedulers.single;

@Slf4j
public class PartnerServerHttpResponseDecorator extends ServerHttpResponseDecorator {

    PartnerServerHttpResponseDecorator(ServerHttpResponse delegate) {
        super(delegate);
    }

    @Override
    public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
        return super.writeAndFlushWith(body);
    }

    @Override
    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
        final MediaType contentType = super.getHeaders().getContentType();
        if (ParamsUtils.CHAIN_MEDIA_TYPE.contains(contentType)) {
            if (body instanceof Mono) {
                final Mono<DataBuffer> monoBody = (Mono<DataBuffer>) body;
                return super.writeWith(monoBody.publishOn(single()).map(dataBuffer -> ResponseParamsHandle.chain(log, dataBuffer)));
            } else if (body instanceof Flux) {
                Mono<DataBuffer> mono = DataBufferUtils.join(body);
                final Flux<DataBuffer> monoBody = mono.publishOn(single()).map(dataBuffer -> ResponseParamsHandle.chain(log, dataBuffer)).flux();
                return super.writeWith(monoBody);
            }
        }
        return super.writeWith(body);
    }

}

ResponseParamsHandle.java

响应流的日志打印

package com.demo.gateway2x.decorator;

import org.slf4j.Logger;
import org.springframework.core.io.buffer.DataBuffer;

public class ResponseParamsHandle {

    public static <T extends DataBuffer> T chain(Logger log, T buffer) {
        ParamsUtils.BodyDecorator bodyDecorator = ParamsUtils.buildBodyDecorator(buffer);
        // 参数校验 和 参数打印
        log.info("Payload: {}", bodyDecorator.getBody());
        return (T) bodyDecorator.getDataBuffer();
    }

}

下面是实际操作,发送一次http请求:

控制台log结果:

github源码地址:https://github.com/qiaomengnan16/gateway-2x-log-demo

总结

gateway和zuul打印参数的方式思路是一致的,只是gateway采用的是reactor,写法上与zuul的直接读取流有些不同,这里需要知道的是Flux需要转换为Mono这个地方,如果不转换容易分多批打印。

参考学习了以下的博客:

自定义Spring Webflux 过滤器,解决请求body只能获取一次的问题 :https://my.oschina.net/junjunyuanyuankeke/blog/2253493

SpringCloud Gateway获取post请求体(request body):https://blog.51cto.com/thinklili/2329184

如何在Reactive Java中从Mono流获取字符串/对象?:https://www.bilibili.com/read/cv5787745

How to correctly read Flux and convert it to a single inputStream:
https://stackoverflow.com/questions/46460599/how-to-correctly-read-fluxdatabuffer-and-convert-it-to-a-single-inputstream

Only one connection receive subscriber allowed解决思路: https://blog.csdn.net/weixin_40899682/article/details/82784242

猜你喜欢

转载自blog.csdn.net/cainiao1412/article/details/108847792