Spring Boot中AOP日志与异常处理器适配问题及通用方案咨询
问题背景
我正在开发一个返回ResponseEntity<StreamingResponseBody>的Spring Boot REST服务,同时使用@ControllerAdvice做异常处理,正常场景运行正常。
为记录端点调用、执行时间、HTTP状态等日志,我用了带@Around切点的AOP实现,正常场景工作正常:
@Around(value = "@annotation(requestMapping)", argNames = "pjp,requestMapping") public Object trackExecutionTime(ProceedingJoinPoint pjp, RequestMapping requestMapping) throws Throwable { return trackExecutionTime(pjp, requestMapping.method()[0], requestMapping.value()[0]); }
但异常场景下,把异常处理器注入AOP的方式既脆弱又失效,简化后的AOP代码如下:
// Simplified private Object trackExecutionTime(ProceedingJoinPoint pjp) throws Throwable { StopWatch stopWatch = new StopWatch(); stopWatch.start(); Object result = null; try { result = pjp.proceed(); } catch (MyException1 e) { result = exceptionHandler.handleMyException1(e); } catch (Exception e) { result = exceptionHandler.handleException(e, webRequest); } finally { stopWatch.stop(); doLog(result, stopWatch.getTotalTimeMillis(), ((ResponseEntity) result).getStatus()); } return result; }
存在两个核心问题:
- 脆弱性:异常处理器新增异常处理逻辑时,必须同步在AOP中添加对应catch块,维护成本高;
- 失效问题:Spring期望返回
StreamingResponseBody,但AOP返回了ErrorDto,抛出IllegalArgumentException,接口返回Spring通用500错误而非自定义ErrorDto及对应HTTP状态码,报错信息:
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.IllegalArgumentException: StreamingResponseBody expected: lu.pgd.dri.bidas.nist.rest.dto.NistRestApiErrorDto at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-5.3.24.jar:5.3.24] at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.24.jar:5.3.24] at javax.servlet.http.HttpServlet.service(HttpServlet.java:517) ~[jakarta.servlet-api-4.0.4.jar:4.0.4] ... Caused by: java.lang.IllegalArgumentException: StreamingResponseBody expected: lu.pgd.dri.bidas.nist.rest.dto.NistRestApiErrorDto at org.springframework.util.Assert.instanceCheckFailed(Assert.java:702) ~[spring-core-5.3.24.jar:5.3.24] at org.springframework.util.Assert.isInstanceOf(Assert.java:602) ~[spring-core-5.3.24.jar:5.3.24] at org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBodyReturnValueHandler.handleReturnValue(StreamingResponseBodyReturnValueHandler.java:90) ~[spring-webmvc-5.3.24.jar:5.3.24] ...
此前用ResponseEntity<String>做POC时无此问题,现提出两个问题:
- 能否优化AOP实现日志记录需求?
- 优化后的方案是否支持通过Spring上下文调度器等方式通用处理异常?
曾考虑不捕获异常,让ExceptionHandler自行调用日志服务,但不知如何传递StopWatch记录的执行时间。
解决方案
1. 优化AOP实现日志记录
核心思路:不手动捕获异常,让Spring的@ControllerAdvice自然处理异常,同时通过请求上下文传递StopWatch的执行时间,拆分正常/异常场景的日志记录逻辑。
优化后的AOP代码
@Aspect @Component public class RestEndpointLoggingAspect { private static final Logger log = LoggerFactory.getLogger(RestEndpointLoggingAspect.class); @Around("@annotation(requestMapping)") public Object trackExecutionTime(ProceedingJoinPoint pjp, RequestMapping requestMapping) throws Throwable { StopWatch stopWatch = new StopWatch(); stopWatch.start(); String endpoint = Arrays.toString(requestMapping.value()); HttpMethod method = requestMapping.method()[0]; // 将StopWatch存入请求上下文,供异常处理器获取 RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); if (attributes != null) { ((ServletRequestAttributes) attributes).getRequest() .setAttribute("EXECUTION_STOPWATCH", stopWatch); } Object result = null; HttpStatus status = HttpStatus.OK; try { result = pjp.proceed(); // 正常场景读取响应状态 if (result instanceof ResponseEntity) { status = ((ResponseEntity<?>) result).getStatusCode(); } return result; } catch (Throwable throwable) { // 不拦截异常,直接交给@ControllerAdvice处理 throw throwable; } finally { stopWatch.stop(); long executionTime = stopWatch.getTotalTimeMillis(); // 记录正常场景日志,异常场景由处理器补充 log.info("Endpoint {} {} executed in {}ms, status: {}", method, endpoint, executionTime, status); } } }
配合@ControllerAdvice完成异常日志
修改异常处理器,从请求上下文获取StopWatch,记录异常场景的执行时间:
@ControllerAdvice public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(MyException1.class) public ResponseEntity<NistRestApiErrorDto> handleMyException1(MyException1 e, HttpServletRequest request) { NistRestApiErrorDto errorDto = new NistRestApiErrorDto(HttpStatus.BAD_REQUEST.value(), e.getMessage()); // 读取AOP存入的执行时间 StopWatch stopWatch = (StopWatch) request.getAttribute("EXECUTION_STOPWATCH"); if (stopWatch != null) { log.error("Endpoint {} {} failed in {}ms, status: {}, error: {}", request.getMethod(), request.getRequestURI(), stopWatch.getTotalTimeMillis(), HttpStatus.BAD_REQUEST, e.getMessage()); } return new ResponseEntity<>(errorDto, HttpStatus.BAD_REQUEST); } @ExceptionHandler(Exception.class) public ResponseEntity<NistRestApiErrorDto> handleException(Exception e, HttpServletRequest request) { NistRestApiErrorDto errorDto = new NistRestApiErrorDto(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Internal server error"); StopWatch stopWatch = (StopWatch) request.getAttribute("EXECUTION_STOPWATCH"); if (stopWatch != null) { log.error("Endpoint {} {} failed in {}ms, status: {}, error: {}", request.getMethod(), request.getRequestURI(), stopWatch.getTotalTimeMillis(), HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); } return new ResponseEntity<>(errorDto, HttpStatus.INTERNAL_SERVER_ERROR); } }
解决StreamingResponseBody类型不匹配问题
之前的报错是因为AOP手动调用异常处理器返回了ResponseEntity<ErrorDto>,导致Spring的StreamingResponseBodyReturnValueHandler类型校验失败。现在让异常处理器自然接管,Spring会自动根据返回值选择对应处理器,不会再出现类型不匹配问题。
2. 支持Spring上下文调度器的通用异常处理
优化后的方案完全支持Spring上下文的通用异常处理,原因如下:
- 异常依然通过Spring标准的
@ControllerAdvice机制处理,符合Spring上下文调度逻辑; - 请求上下文(
RequestAttributes)是Spring Web的标准组件,在所有Web相关组件(拦截器、异常处理器、异步线程)中均可访问; - 如果需要支持异步场景(如
@Async),只需配置线程池的任务装饰器,让子线程继承请求上下文:
@Configuration public class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setTaskDecorator(runnable -> { RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); return () -> { try { // 设置上下文可继承,供异步线程访问 RequestContextHolder.setRequestAttributes(attributes, true); runnable.run(); } finally { RequestContextHolder.resetRequestAttributes(); } }; }); executor.initialize(); return executor; } }
内容的提问来源于stack exchange,提问作者Chop
相关产品推荐
相关产品推荐

