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

Spring中如何通过AOP/过滤器/拦截器统一记录控制器错误请求

统一记录控制器错误日志与请求信息的优化方案

问题背景

当前控制器中每个接口都通过手动try-catch记录错误日志和请求信息,代码冗余度高:

@RestController 
class Controller {

    @GetMapping()
    public ResponseEntity<?> getAPI(@Valid @RequestBody Request request) {
        try {
            // success response.
        } catch (RequestParamsException e) {
            // log.error(e + request) log error and request.
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
        } catch (Exception e) {
            // log.error(e + request) log error and request
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

}

我们可以通过Spring提供的几种机制实现统一处理,避免重复编码。


方案1:@ControllerAdvice + @ExceptionHandler(最简洁的控制器层统一处理)

这是Spring官方推荐的全局异常处理方案,直接拦截控制器抛出的异常,同时可获取请求上下文信息。

实现代码

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    // 处理自定义参数校验异常
    @ExceptionHandler(RequestParamsException.class)
    public ResponseEntity<?> handleRequestParamsException(RequestParamsException e, WebRequest request) {
        log.error("请求参数异常: {}, 请求详情: {}", e.getMessage(), request.getDescription(false), e);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }

    // 兜底处理所有未捕获的异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleGenericException(Exception e, WebRequest request) {
        log.error("系统内部异常: {}, 请求详情: {}", e.getMessage(), request.getDescription(false), e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}

说明:request.getDescription(false)会返回请求URI、参数等基础信息;如果需要读取请求体,需结合过滤器提前缓存请求内容(因为原生请求体仅能读取一次)。


方案2:Spring AOP(面向切面拦截控制器方法)

通过AOP切面拦截所有控制器方法,在方法抛出异常时统一记录日志,适合需要更细粒度控制的场景。

实现代码

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ControllerErrorLogAspect {
    private static final Logger log = LoggerFactory.getLogger(ControllerErrorLogAspect.class);

    // 切点:匹配所有@RestController和@Controller下的方法
    @Pointcut("within(@org.springframework.web.bind.annotation.RestController *) || within(@org.springframework.web.bind.annotation.Controller *)")
    public void controllerMethods() {}

    // 异常抛出后的通知逻辑
    @AfterThrowing(pointcut = "controllerMethods()", throwing = "ex")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) {
        // 获取方法参数(假设第一个参数为请求对象,可根据实际场景调整)
        Object[] args = joinPoint.getArgs();
        String requestInfo = args != null && args.length > 0 ? args[0].toString() : "无请求参数";
        
        log.error("控制器方法执行异常: 方法名={}, 请求信息={}", 
                joinPoint.getSignature().toShortString(), requestInfo, ex);
    }
}

说明:如果需要获取请求Header、URI等信息,可在切面类中注入HttpServletRequest直接获取。


方案3:过滤器+全局异常处理(获取完整请求体)

通过过滤器缓存请求体,结合全局异常处理器读取缓存内容,适合需要记录完整请求Body的场景。

步骤1:实现请求体缓存过滤器

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestBodyCachingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 将请求包装为可重复读取的ContentCachingRequestWrapper
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
        filterChain.doFilter(wrappedRequest, response);
    }
}

步骤2:在全局异常处理器中读取缓存的请求体

import org.springframework.web.util.ContentCachingRequestWrapper;

// 修改全局异常处理器的兜底方法
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleGenericException(Exception e, HttpServletRequest request) {
    String requestBody = "";
    if (request instanceof ContentCachingRequestWrapper) {
        ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request;
        requestBody = new String(wrapper.getContentAsByteArray(), wrapper.getCharacterEncoding());
    }
    
    log.error("系统内部异常: URI={}, 请求参数={}, 请求体={}",
            request.getRequestURI(), request.getQueryString(), requestBody, e);
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

方案对比与选择

方案优势适用场景
@ControllerAdvice代码简洁、Spring官方推荐、直接关联异常仅需处理控制器层异常,逻辑简单
Spring AOP细粒度控制、可拦截任意方法需要对控制器方法做额外扩展操作
过滤器+全局异常处理能获取完整请求体、Header等信息需要记录完整请求内容的场景

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 17:30:46