Spring Boot 利用Filter将请求数据、响应数据写入日志

发布时间:2022年05月04日 // 分类:代码 // 暂无评论

SpringBoot中Servlet提供的Filter与Gin的Middleware洋葱模型是很相似的,本质上可以认为是责任链设计模式。通过Filter,我们可以获取到请求对象ServletRequest、响应对象ServletResponse

利用Filter的特性,我们可以实现一个Filter,获取Request和Response,并整合写入到日志中去。

本文基于 Spring Boot 2.6.x 实现。

1、新建一个filter类

由于ServletRequest中的InputStream是一次性用品,所以我们需要使用Spring提供的ContentCachingRequestWrapper将request对象包装起来。ServletResponse也是类似的原理,需要用ContentCachingResponseWrapper包装起来。这样我们就可以在请求处理完毕后得到其中的数据。

package com.gentleltd.todo965.backend.infra.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
public class LogFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        // 包装 request 和 response
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) servletRequest);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) servletResponse);

        filterChain.doFilter(requestWrapper, responseWrapper);

        // 记录日志 (方案一,request和response整合到同一条日志中,截断超长文本)
        ObjectMapper mapper = new ObjectMapper();
        ObjectNode root = mapper.createObjectNode();
        root.putIfAbsent("request", mapper.readTree(requestWrapper.getContentAsByteArray()));
        root.putIfAbsent("response", mapper.readTree(responseWrapper.getContentAsByteArray()));
        log.info(Strings.left(mapper.writeValueAsString(root), 10240)); // 利用Jackson将请求响应数据整合到同一条json结构中,截断超长文本

        // 记录日志(方案二,request和response分别作为单独的span整合到日志中,截断超长文本)
        var span = GlobalTracer.get().activeSpan();
        if (span != null) {
            GlobalTracer.get().buildSpan("request").addReference("", span.context())
                    .start().log(Strings.left(requestWrapper.getContentAsByteArray()), 10240).finish();
            GlobalTracer.get().buildSpan("response").addReference("", span.context())
                    .start().log(Strings.left(responseWrapper.getContentAsByteArray()), 10240).finish();
        }

        // 注:以上两个方案任选其一即可

        // 回写响应数据
        responseWrapper.copyBodyToResponse();
    }
}

添加一个配置类,将我们的Filter加载起来

Spring Boot 加载 Filter 有两种方法。一种是通过注解实现,一种是通过配置类实现。个人偏好使用后者,方便统一管理。注解实现可参考网上其他资料,这里不再赘述。

package com.gentleltd.todo965.backend.config;

import com.gentleltd.todo965.backend.infra.filter.LogFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<LogFilter> filterRegistration() {
        return new FilterRegistrationBean<>(new LogFilter()) {{
            addUrlPatterns("/*"); // 对所有URL生效
            setEnabled(true);
            setName("LogFilter");
            setOrder(1); // 有多个Filter时,数字越小执行越早
        }};
    }
}

实现效果

简单两步,我们就完成了日志打印。在控制台我们看到了包含请求数据和响应数据的日志。
filter-log-0.png

扩展

  1. 配合Jaeger,可以更方便地查看日志,Jaeger非常贴心地格式化了JSON。
  2. 方案一将request和response整合到同一个json里,如果服务涉及HTML等业务,会出现JSON出错的情况。所以可以考虑方案二将request、response的String分别打log。
  3. 由于使用Thrift协议将数据发送给Jaeger,存在长度限制,所以在Response或Request非常长的时候会发生错误导致日志无法被记录,因此需要对超长文本进行截断。
  4. 整合接入Jaeger可查看上一篇文章 《SpringBoot 利用Filter将请求数据、响应数据写入日志
  5. 如果需要将OperationName修改为请求的URI,可以查看下一篇文《Spring Boot 对接 Jaeger 时利用Filter修改 Span 的 OperationName

jaeger-0.png

本文固定链接
https://www.ywlib.com/archives/194.html

标签
Spring Boot, Filter, 日志, 请求数据, 响应数据

添加新评论 »

分类
随机文章
最新文章
最近回复