欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring Cloud Gateway 2.x 打印 Log

程序员文章站 2022-03-14 10:14:12
...

场景

在服务网关层面,需要打印出用户每次的请求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请求:

Spring Cloud Gateway 2.x 打印 Log

Spring Cloud Gateway 2.x 打印 Log

控制台log结果:

Spring Cloud Gateway 2.x 打印 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://*.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

相关标签: gateway