SpringBoot正确打日志的姿势

本文详细介绍SpringBoot中使用Log4j2进行统一日志处理的方法,包括HTTP请求日志和Exception异常日志的配置与实践,通过MDCFilter全局拦截实现日志信息的丰富与标准化。

前篇

Spring Boot 日志处理你还在用Logback?

本文简介

前篇侧重 Log4j2 的配置,本篇侧重统一日志处理的应用,以下包含 HTTP 请求的日志处理、Exception 异常日志处理。

HTTP 请求日志

imgimg

1、明确日志记录的内容

示例:用户、IP地址、Method、URI、请求参数、请求体

2、全局拦截 MDCFilter.java

package com.anoyi.config.server;

import lombok.extern.log4j.Log4j2;
import org.slf4j.MDC;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.*;

/**
 * 拦截请求信息,添加到日志
 */
@Component
@Log4j2
public class MDCFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        try {
            MDC.put("user", request.getRemoteUser());
            String query = request.getQueryString() != null ? "?"   request.getQueryString() : "";
            if (request.getMethod().equals(HttpMethod.POST.name())) {
                MultiReadHttpServletRequest multiReadHttpServletRequest = new MultiReadHttpServletRequest(request);
                log.info("IP:{}, Method:{}, URI:{} Body:{}", request.getRemoteAddr(), request.getMethod(), request.getRequestURI()   query, multiReadHttpServletRequest.getRequestBody());
                chain.doFilter(multiReadHttpServletRequest, response);
            } else {
                log.info("IP:{}, Method:{}, URI:{}", request.getRemoteAddr(), request.getMethod(), request.getRequestURI()   query);
                chain.doFilter(request, response);
            }
        } finally {
            MDC.clear();
        }
    }

    /**
     * HttpServletRequest 请求体多读
     */
    class MultiReadHttpServletRequest extends HttpServletRequestWrapper {

        // 缓存 RequestBody
        private String requestBody;

        MultiReadHttpServletRequest(HttpServletRequest request) {
            super(request);
            requestBody = "";
            try {
                StringBuilder stringBuilder = new StringBuilder();
                InputStream inputStream = request.getInputStream();
                byte[] bs = new byte[1024];
                int len;
                while ((len = inputStream.read(bs)) != -1) {
                    stringBuilder.append(new String(bs, 0, len));
                }
                requestBody = stringBuilder.toString();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody.getBytes());

            return new ServletInputStream() {
                public int read() throws IOException {
                    return byteArrayInputStream.read();
                }

                @Override
                public boolean isFinished() {
                    return byteArrayInputStream.available() == 0;
                }

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

                @Override
                public void setReadListener(ReadListener readListener) {

                }
            };
        }

        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(this.getInputStream()));
        }

        String getRequestBody() {
            return requestBody.replaceAll("\n", "");
        }
    }

}

3、配置日志 Pattern

logging:
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [.15t] %-40.40c{1.} [%X{user}] : %m%n%xwEx"

txt

说明: MDC.put("user", request.getRemoteUser()); => %X{user}

© 著作权归作者所有,转载或内容合作请联系作者

img

Spring Boot 定制 parent 快速构建应用

Spring Boot 容器化部署 - Docker

SpringBot中教你手把手配置 https

Spring Boot 日志处理你还在用Logback?

【双11狂欢的背后】微服务注册中心如何承载大型系统的千万级访问?

Spring Boot 新一代监控你该这么玩

Spring Boot 异常处理

Spring Boot 配置 - 配置信息加密

拒绝黑盒应用-Spring Boot 应用可视化监控

并发Bug之源有三,请睁大眼睛看清它们

img

本文由博客一文多发平台 OpenWrite 发布!

<think>我们之前讨论的是在拦截器中读取请求体导致后续`@RequestBody`解析失败的问题,并提出了使用自定义过滤器包装请求的方案。现在用户询问Spring Boot内置的`ContentCachingRequestWrapper`是否可以在拦截器中规避这个问题。 ### 核心问题回顾 在拦截器中直接通过`request.getInputStream()`或`request.getReader()`读取请求体后,输入流会被消费。后续Spring MVC的`@RequestBody`参数解析器将无法再次读取请求体,导致解析失败。 ### Spring Boot内置的`ContentCachingRequestWrapper`解决方案 Spring Boot内置的`ContentCachingRequestWrapper`(位于`org.springframework.web.util`包)正是为解决重复读取请求体而设计。其原理是**在首次读取时缓存请求体内容**,后续读取直接返回缓存数据。 #### 使用步骤 1. **创建过滤器包装请求** 在过滤器中,将原始`HttpServletRequest`替换为`ContentCachingRequestWrapper`: ```java import org.springframework.web.util.ContentCachingRequestWrapper; @WebFilter("/*") public class CachingRequestFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 包装原始请求 HttpServletRequest wrappedRequest = new ContentCachingRequestWrapper((HttpServletRequest) request); chain.doFilter(wrappedRequest, response); } } ``` 2. **在拦截器中安全读取缓存内容** ```java import org.springframework.web.util.ContentCachingRequestWrapper; public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 确认请求已被包装 if (request instanceof ContentCachingRequestWrapper) { ContentCachingRequestWrapper wrappedRequest = (ContentCachingRequestWrapper) request; // 获取缓存内容(字节数组) byte[] contentBytes = wrappedRequest.getContentAsByteArray(); String requestBody = new String(contentBytes, request.getCharacterEncoding()); // 执行业务逻辑(如验签) verifySignature(requestBody); } return true; } } ``` ### 关键机制说明 - **缓存触发条件**:`ContentCachingRequestWrapper`仅在输入流被读取时才会缓存内容。拦截器中直接调用`getContentAsByteArray()`不会触发缓存(此时返回空数组),需确保在拦截器执行前缓存已被填充。 - **缓存时机**:Spring MVC的参数解析器(如`@RequestBody`)会读取输入流,此时`ContentCachingRequestWrapper`自动缓存内容。因此**拦截器必须在参数解析之后执行**才能读取到缓存。 ### 执行顺序问题与解决方案 默认拦截器执行顺序早于参数解析器,导致拦截器调用`getContentAsByteArray()`时缓存仍为空[^1]。需调整顺序: ```mermaid sequenceDiagram participant Filter participant Interceptor participant ParameterResolver participant Controller Filter->>Interceptor: 传递包装请求 Interceptor->>ParameterResolver: 调用chain.doFilter() ParameterResolver->>ContentCachingRequestWrapper: 读取输入流(触发缓存) ContentCachingRequestWrapper-->>ParameterResolver: 返回缓存内容 ParameterResolver->>Controller: 注入@RequestBody参数 Interceptor<-ParameterResolver: 返回到拦截器(postHandle等) ``` 解决方法:**将缓存读取逻辑移至`HandlerInterceptor.afterCompletion()`**(请求处理完成后执行): ```java @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if (request instanceof ContentCachingRequestWrapper) { ContentCachingRequestWrapper wrappedRequest = (ContentCachingRequestWrapper) request; byte[] content = wrappedRequest.getContentAsByteArray(); // 此时缓存已被填充 } } ``` > ⚠️ 警告:`afterCompletion()`在视图渲染后执行,此时响应已提交,不能再修改响应状态或头信息。仅适合日志记录、审计等场景。 ### 替代方案:自定义过滤器优先缓存 若需在拦截器中即时访问请求体(如验签),可结合自定义缓存逻辑: ```java public class EarlyBodyCachingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // 自定义缓存(提前读取) ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); // 主动触发缓存:读取并存储请求体 wrappedRequest.getInputStream().readAllBytes(); // 触发缓存 chain.doFilter(wrappedRequest, response); } } ``` 此方法强制在过滤器中缓存内容,确保拦截器中可直接读取,但会**破坏流式处理优势**,且消耗内存。 ### 性能与限制 - **内存开销**:缓存整个请求体,不适合大文件上传(需结合`Multipart`请求判断)[^2]。 - **默认限制**:`ContentCachingRequestWrapper`的缓存大小默认为8KB。超出部分将被截断,可通过`setMaxPayloadSize()`调整。 ### 结论 **可以规避,但有条件**: 1. 使用`ContentCachingRequestWrapper`包装请求 2. 在拦截器中通过`getContentAsByteArray()`读取缓存 3. 注意执行顺序: - 若在`preHandle`中读取,需确保缓存已被填充(如参数解析器已执行) - 或在`afterCompletion`中读取(此时缓存一定存在) --- ### 相关问题 1. **如何在Spring Boot中调整拦截器的执行顺序?** 通过`WebMvcConfigurer.addInterceptors()`注册时指定`order`值[^3]。 2. **`ContentCachingRequestWrapper`对文件上传请求有效吗?** 无效。文件上传请求(`multipart/form-data`)的输入流被Spring的`MultipartResolver`解析后,`ContentCachingRequestWrapper`无法捕获完整内容[^2]。 3. **如何避免缓存大请求体导致内存溢出?** 可在过滤器中检查`Content-Length`请求头,超过阈值则不包装(直接传递原始请求)[^4]。 4. **Spring Security的过滤器链会影响`ContentCachingRequestWrapper`的使用吗?** 会。需确保缓存过滤器位于Spring Security过滤器之前(通过`@Order`控制)[^5]。 [^1]: Spring MVC处理流程中,参数解析器在拦截器`preHandle`之后执行 [^2]: `ContentCachingRequestWrapper`仅缓存非`multipart`请求 [^3]: `InterceptorRegistry.addInterceptor().order(int)` [^4]: 可设置最大缓存大小(如10MB) [^5]: Spring Security过滤器链默认顺序为`Ordered.HIGHEST_PRECEDENCE` </think>在 Spring Boot 中使用内置的 `ContentCachingRequestWrapper` **可以部分解决拦截器中读取请求体(body)导致后续 `@RequestBody` 解析失败的问题**,但需注意其限制条件和正确用法。 --- ### 解决方案:使用 `ContentCachingRequestWrapper` 的正确姿势 #### 1. 通过过滤器包装原始请求 在过滤器中创建 `ContentCachingRequestWrapper` 替换原始请求: ```java import org.springframework.web.util.ContentCachingRequestWrapper; @WebFilter("/*") public class CachingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 包装原始请求 ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper( (HttpServletRequest) request ); chain.doFilter(wrappedRequest, response); // 传递包装后的请求 } } ``` #### 2. 在拦截器中安全读取缓存内容 ```java public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (request instanceof ContentCachingRequestWrapper) { ContentCachingRequestWrapper wrappedRequest = (ContentCachingRequestWrapper) request; // 获取缓存的请求体内容 byte[] contentBytes = wrappedRequest.getContentAsByteArray(); String requestBody = new String(contentBytes, StandardCharsets.UTF_8); // 执行业务逻辑(如验签) verifySignature(requestBody); } return true; } } ``` --- ### 关键机制与注意事项 #### ✅ 工作原理 1. **缓存触发**:当后续组件(如 `@RequestBody` 解析器)首次读取输入流时,`ContentCachingRequestWrapper` 会**自动缓存请求体**到内存 2. **重复读取**:通过 `getContentAsByteArray()` 直接访问缓存内容,避免重复消费输入流 3. **兼容性**:与 Spring MVC 的参数解析器无缝协作 #### ⚠️ 重要限制 1. **拦截器执行顺序问题**: - 如果拦截器在 `preHandle` 中调用 `getContentAsByteArray()`,需确保**参数解析器已触发缓存** - 解决方案:在拦截器的 `afterCompletion()` 中读取(此时缓存一定存在): ```java @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { byte[] content = ((ContentCachingRequestWrapper)request).getContentAsByteArray(); // 处理内容 } ``` 2. **大文件请求风险**: - 默认缓存上限为 **8KB**(超出部分被截断) - 解决方案:设置更大的缓存空间 ```java // 在过滤器中设置缓存大小(例如10MB) ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper( (HttpServletRequest) request, 10 * 1024 * 1024 // 10MB ); ``` 3. **文件上传无效**: - 对 `multipart/form-data` 请求无效(需用 `MultipartHttpServletRequest` 处理)[^1] - 解决方案:跳过非文本类型请求的缓存: ```java if (!request.getContentType().startsWith("multipart/")) { // 执行包装 } ``` --- ### 方案对比(自定义包装器 vs 内置包装器) | **特性** | 自定义 `BodyCachingRequestWrapper` | 内置 `ContentCachingRequestWrapper` | |------------------------|-------------------------------------------|------------------------------------------| | **缓存触发时机** | 过滤器阶段主动读取并缓存 | 首次输入流读取时被动缓存 | | **内存控制** | 完全自定义 | 默认8KB,可手动设置上限 | | **文件上传支持** | 不适用 | 不适用 | | **与Spring MVC兼容性** | 高 | 原生支持(无需额外配置) | | **性能开销** | 所有请求立即缓存 | 按需缓存(延迟初始化) | --- ### 最佳实践建议 1. **适用场景**:优先用于文本型请求(JSON/XML)的验签、日志记录 2. **性能敏感场景**:添加内容类型过滤,仅缓存必要请求 ```java // 只缓存JSON请求 if (request.getContentType() != null && request.getContentType().contains("application/json")) { wrappedRequest = new ContentCachingRequestWrapper(request); } ``` 3. **大文件解决方案**: - 对于超过 10MB 的请求,改用流式处理 - 结合 `Content-Length` 请求头动态决策是否包装 --- ### 相关问题 1. **为什么在拦截器的 `preHandle` 中调用 `getContentAsByteArray()` 可能返回空数组?** 因为此时请求体尚未被后续组件读取,缓存未被初始化。需确保参数解析器先执行[^2]。 2. **如何确保 `ContentCachingRequestWrapper` 在 Spring Security 过滤器之前生效?** 通过 `@Order(Ordered.HIGHEST_PRECEDENCE)` 提升过滤器优先级[^3]。 3. **文件上传请求如何实现参数验签?** 需改用 `MultipartHttpServletRequest` 解析表单字段,结合 `@RequestPart` 注解处理[^1]。 4. **`ContentCachingRequestWrapper` 是否影响请求流读取性能?** 对小型请求无显著影响,大型请求建议限制最大缓存大小(如 10MB)[^4]。 [^1]: `multipart/form-data` 请求需要特殊处理 [^2]: 参数解析器在拦截器之后执行 [^3]: Spring Security 过滤器链默认优先级最高 [^4]: 大文件缓存可能导致内存压力
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值