2. 引言

在开发中,我们经常需要对 HTTP 请求体进行额外处理,比如日志记录记录请求日志是调试应用的关键手段

本文将介绍如何使用 Spring Boot 的日志过滤器记录请求日志的两种方案:

✅ 自定义过滤器方案(灵活但需手动实现)
✅ Spring Boot 内置过滤器方案(开箱即用)

3. Maven 依赖

首先在 pom.xml 添加核心依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.2.2</version>
</dependency>

这个依赖提供了 Spring Boot 应用记录请求日志所需的所有基础组件。

4. 基础 Web 控制器

先定义一个测试用控制器:

@RestController
public class TaxiFareController {

    @GetMapping("/taxifare/get/")
    public RateCard getTaxiFare() {
        return new RateCard();
    }
    
    @PostMapping("/taxifare/calculate/")
    public String calculateTaxiFare(
      @RequestBody @Valid TaxiRide taxiRide) {
 
        // 返回计算结果
    }
}

接下来我们将用两种方式记录请求到该控制器的日志。

5. 自定义请求日志方案

通过自定义过滤器在请求到达控制器前捕获并记录请求体

5.1 封装 HTTP 请求

需要用 HttpServletRequestWrapper 包装请求对象:

public class CachedHttpServletRequest extends HttpServletRequestWrapper {

    private byte[] cachedPayload;

    public CachedHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        InputStream requestInputStream = request.getInputStream();
        this.cachedPayload = StreamUtils.copyToByteArray(requestInputStream);
    }
}

⚠️ 关键点:在构造函数中读取请求体并缓存到字节数组 cachedPayload

接着重写 getInputStream()getReader() 方法:

@Override
public ServletInputStream getInputStream() {
    return new CachedServletInputStream(this.cachedPayload);
}

@Override
public BufferedReader getReader() {
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedPayload);
    return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}

5.2 扩展 ServletInputStream

创建自定义输入流类:

public class CachedServletInputStream extends ServletInputStream {

    private final static Logger LOGGER = LoggerFactory.getLogger(CachedServletInputStream.class);
    private InputStream cachedInputStream;

    public CachedServletInputStream(byte[] cachedBody) {
        this.cachedInputStream = new ByteArrayInputStream(cachedBody);
    }
}

重写核心方法:

@Override
public boolean isFinished() {
    try {
        return cachedInputStream.available() == 0;
    } catch (IOException exp) {
        LOGGER.error(exp.getMessage());
    }
    return false;
}

@Override
public boolean isReady() {
    return true;
}

@Override
public void setReadListener(ReadListener readListener) {
    throw new UnsupportedOperationException();
}

@Override
public int read() throws IOException {
    return cachedInputStream.read();
}

5.3 自定义过滤器

创建过滤器类:

@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
@WebFilter(filterName = "RequestCachingFilter", urlPatterns = "/*")
public class RequestCachingFilter extends OncePerRequestFilter {

    private final static Logger LOGGER = LoggerFactory.getLogger(RequestCachingFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
        CachedHttpServletRequest cachedHttpServletRequest = new CachedHttpServletRequest(request);
        LOGGER.info("REQUEST DATA: " + IOUtils.toString(cachedHttpServletRequest.getInputStream(), StandardCharsets.UTF_8));
        filterChain.doFilter(cachedHttpServletRequest, response);
    }
}

📌 说明:

  • @Order 确保过滤器优先级最高
  • urlPatterns="/*" 匹配所有请求
  • 核心逻辑:用包装类读取请求体并记录日志

6. Spring Boot 内置日志方案

Spring Boot 提供了现成的请求日志过滤器,通过配置即可使用。

AbstractRequestLoggingFilter 是基础类,其子类需重写 beforeRequest()afterRequest() 方法。Spring Boot 提供了三个实现:

  • CommonsRequestLoggingFilter(推荐)
  • Log4jNestedDiagnosticContextFilter(已废弃)
  • ServletContextRequestLoggingFilter

6.1 配置 CommonsRequestLoggingFilter

添加配置类:

@Configuration
public class RequestLoggingFilterConfig {

    @Bean
    public CommonsRequestLoggingFilter logFilter() {
        CommonsRequestLoggingFilter filter
          = new CommonsRequestLoggingFilter();
        filter.setIncludeQueryString(true);
        filter.setIncludePayload(true);
        filter.setMaxPayloadLength(10000);
        filter.setIncludeHeaders(false);
        filter.setAfterMessagePrefix("REQUEST DATA: ");
        return filter;
    }
}

配置项说明:

  • includeQueryString:记录查询参数
  • includePayload:记录请求体
  • maxPayloadLength:请求体最大长度
  • includeHeaders:是否记录请求头

6.2 配置日志级别

必须将过滤器日志级别设为 DEBUG

方式一:logback.xml

<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter">
    <level value="DEBUG" />
</logger>

方式二:application.properties

logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG

⚠️ 踩坑提示:未设置 DEBUG 级别将不会输出日志!

7. 实战演示

编写测试用例验证:

@Test
public void givenRequest_whenFetchTaxiFareRateCard_thanOK() {
    TestRestTemplate testRestTemplate = new TestRestTemplate();
    TaxiRide taxiRide = new TaxiRide(true, 10l);
    String fare = testRestTemplate.postForObject(
      URL + "calculate/", 
      taxiRide, String.class);
 
    assertThat(fare, equalTo("200"));
}

执行测试后控制台输出:

18:24:04.318 [] INFO  c.b.web.log.app.RequestCachingFilter - REQUEST DATA: {"isNightSurcharge":true,"distanceInMile":10}
18:24:04.321 [] DEBUG o.s.w.f.CommonsRequestLoggingFilter - Before request [POST /spring-rest/taxifare/calculate/]
18:24:04.429 [] DEBUG o.s.w.f.CommonsRequestLoggingFilter - REQUEST DATA: POST /spring-rest/taxifare/calculate/, payload={"isNightSurcharge":true,"distanceInMile":10}]

两种方案都成功记录了请求体数据。

8. 总结

本文介绍了两种 Spring Boot 记录请求日志的方案:

方案 优点 缺点
自定义过滤器 灵活可控 需手动实现
内置过滤器 开箱即用 配置项有限

推荐场景

  • 需要深度定制日志格式 → 用自定义过滤器
  • 快速实现基础日志 → 用 CommonsRequestLoggingFilter

完整代码示例见 GitHub 仓库


原始标题:Spring - Log Incoming Requests