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
相关产品推荐
相关产品推荐

