You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring REST如何异步记录全部请求响应且不影响客户端?

Great question! I’ve run into this exact issue before, and overriding the MappingJackson2MessageConverter works but definitely isn’t the cleanest approach. Let’s break down why that’s not ideal, then walk through a better solution that keeps your code decoupled and avoids blocking client responses.

Why the MessageConverter approach isn’t great

Your current idea works, but it has some key drawbacks:

  • High coupling: You’re mixing logging logic with Spring’s core message conversion functionality. If Spring updates how its converters work, your code might break.
  • Configuration inconsistencies: You’ll have to manually sync your custom ObjectMapper with Spring’s default configuration (like date formatting, null handling, etc.)—easy to miss details here.
  • Limited context: Getting access to extra request details (HTTP method, URI, client IP, processing time) in the converter requires hacky workarounds like ThreadLocal, which adds unnecessary complexity.

A cleaner solution: Use Spring’s caching wrappers + async Filter

Spring provides ContentCachingRequestWrapper and ContentCachingResponseWrapper specifically for this scenario—they let you read the request/response body multiple times without breaking downstream processing. Pair this with an async thread pool to log without blocking the client.

Here’s a complete example:

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // Ensure this runs before other filters to cache content early
public class AsyncRequestResponseLoggerFilter implements Filter {

    // Use a dedicated thread pool for async logging to avoid blocking request threads
    private final ExecutorService asyncLoggerPool = Executors.newSingleThreadExecutor();
    private static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory.getLogger(AsyncRequestResponseLoggerFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        long startTime = System.currentTimeMillis();
        
        // Wrap the request/response to cache their bodies
        ContentCachingRequestWrapper cachedRequest = new ContentCachingRequestWrapper((HttpServletRequest) request);
        ContentCachingResponseWrapper cachedResponse = new ContentCachingResponseWrapper((HttpServletResponse) response);

        try {
            // Let Spring process the request as normal
            chain.doFilter(cachedRequest, cachedResponse);
        } finally {
            // Collect all needed logging data
            String httpMethod = cachedRequest.getMethod();
            String requestUri = cachedRequest.getRequestURI();
            String clientIp = cachedRequest.getRemoteAddr();
            long processingTimeMs = System.currentTimeMillis() - startTime;

            // Gather non-default request headers (customize the filter logic as needed)
            StringBuilder nonDefaultHeaders = new StringBuilder();
            cachedRequest.getHeaderNames().forEach(headerName -> {
                String headerValue = cachedRequest.getHeader(headerName);
                if (!isDefaultHeader(headerName)) {
                    nonDefaultHeaders.append(headerName).append(": ").append(headerValue).append("\n");
                }
            });

            // Read cached request/response bodies
            String requestBody = StreamUtils.copyToString(cachedRequest.getContentAsByteArray(), StandardCharsets.UTF_8);
            String responseBody = StreamUtils.copyToString(cachedResponse.getContentAsByteArray(), StandardCharsets.UTF_8);

            // Submit logging task to async pool—won't block the client response
            asyncLoggerPool.submit(() -> {
                LOGGER.debug("Request: {} {}", httpMethod, requestUri);
                LOGGER.debug("Client IP: {}", clientIp);
                LOGGER.debug("Processing Time: {}ms", processingTimeMs);
                if (!nonDefaultHeaders.isEmpty()) {
                    LOGGER.debug("Non-default Headers:\n{}", nonDefaultHeaders);
                }
                LOGGER.debug("Request Body:\n{}", requestBody);
                LOGGER.debug("Response Body:\n{}", responseBody);
            });

            // Critical: Copy the cached response back to the original response so the client gets it
            cachedResponse.copyBodyToResponse();
        }
    }

    // Customize this method to define which headers you consider "default"
    private boolean isDefaultHeader(String headerName) {
        return switch (headerName.toLowerCase()) {
            case "host", "user-agent", "accept", "accept-encoding", "connection", "content-length" -> true;
            default -> false;
        };
    }

    @Override
    public void destroy() {
        asyncLoggerPool.shutdown();
    }
}

Key advantages of this approach

  • Decoupled logging: Logging logic lives in a Filter, separate from Spring’s message conversion system.
  • Full context access: Easily get all the details you need (method, URI, IP, processing time) without workarounds.
  • Async safe: Logging runs in a separate thread, so your client gets a response immediately without waiting for I/O-bound logging operations.
  • Spring-native: Uses Spring’s built-in caching wrappers, which are well-tested and compatible with all Spring MVC components.

Quick notes for production use

  • Large payloads: If you expect huge request/response bodies, add a check to limit cached content size (or log only a summary) to avoid memory issues.
  • Sensitive data: Add logic to sanitize sensitive fields (like passwords, tokens) before logging to avoid leaks.
  • Thread pool configuration: For production, use Spring’s ThreadPoolTaskExecutor instead of Executors.newSingleThreadExecutor()—it’s more configurable and integrates with Spring’s monitoring tools.

内容的提问来源于stack exchange,提问作者Nilesh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:32:44