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

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时无此问题,现提出两个问题:

  1. 能否优化AOP实现日志记录需求?
  2. 优化后的方案是否支持通过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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 23:50:28