在开发Web应用时,请求和响应的日志记录对于调试、监控和审计至关重要。今天我将分享一个在生产环境中经过验证的请求日志拦截方案,基于Spring Boot的OncePerRequestFilter实现。

为什么需要请求日志拦截?

在Web应用中,记录请求和响应信息可以帮助我们:

  1. 快速定位和排查问题
  2. 监控API性能
  3. 进行安全审计
  4. 分析用户行为

核心实现解析

下面是我们今天要讨论的LoggingFilter实现代码:

@Slf4j
@Component
public class LoggingFilter extends OncePerRequestFilter {
    private String getStringValue(byte[] contentAsByteArray) {
        return new String(contentAsByteArray, StandardCharsets.UTF_8);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 请求头处理
        Enumeration<String> enumHeaderNames = request.getHeaderNames();
        TreeMap<String, String> headerTreeMap = new TreeMap<>();
        while (enumHeaderNames.hasMoreElements()) {
            String name = enumHeaderNames.nextElement();
            String value = request.getHeader(name);
            // 筛选出自定义头
            if (name.toLowerCase().startsWith("xxxx") || name.equalsIgnoreCase(IdeConstant.AUTHORIZATION_HEADER)) {
                headerTreeMap.put(name, value);
            }
        }
        
        // 请求参数
        Map<String, String> requestParams = ServletUtils.getParamMap(request);
        // 客户端IP
        String clientIp = IpUtils.getIpAddr(request);

        // 包装请求和响应以支持多次读取
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);

        long startTime = System.currentTimeMillis();
        filterChain.doFilter(requestWrapper, responseWrapper);
        long timeTaken = System.currentTimeMillis() - startTime;

        // 获取请求和响应体
        String requestBody = getStringValue(requestWrapper.getContentAsByteArray());
        String responseBody = getStringValue(responseWrapper.getContentAsByteArray());

        log.info(
                "请求拦截: 耗时={}ms; 方法={}; IP={}; 地址={}; 请求参={}; 请求体={}; 请求头={}; 返回码={}; 返回体={}",
                timeTaken, request.getMethod(), clientIp, request.getRequestURI(), 
                requestParams, requestBody, headerTreeMap, response.getStatus(), responseBody);
                
        responseWrapper.copyBodyToResponse();
    }
}

关键实现细节

1. 继承OncePerRequestFilter

我们选择继承OncePerRequestFilter而不是直接实现Filter接口,因为:

  • 确保每个请求只被过滤一次
  • Spring提供了更好的集成支持
  • 简化了异步请求处理

2. 使用内容缓存包装器

ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);

这两个包装器类允许我们多次读取请求和响应体内容,而标准的HttpServletRequestHttpServletResponse只能读取一次。

3. 性能监控

long startTime = System.currentTimeMillis();
filterChain.doFilter(requestWrapper, responseWrapper);
long timeTaken = System.currentTimeMillis() - startTime;

通过记录过滤器前后的时间戳,我们可以精确计算请求处理耗时。

4. 选择性记录请求头

if (name.toLowerCase().startsWith("xxxx") || name.equalsIgnoreCase(IdeConstant.AUTHORIZATION_HEADER)) {
    headerTreeMap.put(name, value);
}

这里我们只记录以"xxxx"开头的自定义头和授权头,避免记录不必要的标准头信息,减少日志体积。

5. 响应体复制

responseWrapper.copyBodyToResponse();

这一步至关重要,确保响应内容被正确写回客户端。

生产环境优化建议

  1. 敏感信息过滤:添加对敏感数据(如密码、token)的脱敏处理
  2. 日志级别控制:根据环境动态调整日志级别
  3. 大文件处理:添加对文件上传请求的特殊处理
  4. 异步记录:考虑使用异步方式记录日志以减少对主流程的影响
  5. 采样率控制:在高流量环境下,可以按比例采样记录

完整日志示例

启用这个过滤器后,你将会看到类似以下的日志输出:

请求拦截: 耗时=45ms; 方法=POST; IP=192.168.1.100; 地址=/api/users; 
请求参={name=张三, age=30}; 
请求体={"email":"zhangsan@example.com","phone":"13800138000"}; 
请求头={Authorization=Bearer xyz123, X-Reqm-ID=req123}; 
返回码=200; 
返回体={"code":0,"data":{"userId":123}}

总结

这个LoggingFilter实现提供了全面的请求/响应日志记录功能,具有以下优点:

  1. 完整的请求信息捕获
  2. 精确的性能监控
  3. 灵活的头部过滤
  4. 对应用无侵入性
  5. 易于扩展和定制

你可以直接在生产环境中使用这个过滤器,或者根据具体需求进行进一步定制。希望这篇文章对你的开发工作有所帮助!

标签: java, 日志

评论已关闭