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
ObjectMapperwith 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
ThreadPoolTaskExecutorinstead ofExecutors.newSingleThreadExecutor()—it’s more configurable and integrates with Spring’s monitoring tools.
内容的提问来源于stack exchange,提问作者Nilesh

