首页 > Java > java教程 > 正文

Spring Boot中优雅地记录方法执行时间并结合异常处理

心靈之曲
发布: 2025-12-02 19:02:27
原创
291人浏览过

Spring Boot中优雅地记录方法执行时间并结合异常处理

本文探讨了在spring boot应用中,如何在方法执行过程中,即使发生异常并由全局异常处理器捕获时,也能准确记录方法执行时间。文章提出了两种主要解决方案:利用spring aop实现横切关注点,在切面中统一测量时间并处理异常;或者通过自定义异常类,在其中封装执行时间信息,供异常处理器获取。这两种方法都能帮助开发者实现更完善的性能监控和异常日志记录。

在Spring Boot应用程序开发中,记录方法的执行时间是性能监控和问题诊断的关键环节。然而,当业务逻辑中出现异常,并且这些异常被全局的@ExceptionHandler方法统一处理时,如何在异常处理流程中获取到该方法的完整执行时间,成为了一个常见挑战。传统的做法是在每个方法内部使用try-catch块来计算时间,但这会导致大量重复代码,降低代码可维护性。本文将介绍两种更优雅、更具通用性的解决方案。

1. 利用Spring AOP实现执行时间测量与异常统一处理

Spring AOP(面向切面编程)是解决这类横切关注点(如日志记录、性能监控、事务管理等)的理想方案。通过AOP,我们可以在不修改原有业务逻辑代码的情况下,在方法执行前后织入计时和异常处理逻辑。

1.1 引入AOP依赖

首先,确保你的pom.xml中包含了Spring AOP的Starter依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
登录后复制

1.2 创建性能监控切面

接下来,创建一个切面(Aspect)来定义计时逻辑。我们使用@Around通知,因为它允许我们在目标方法执行前后执行自定义逻辑,并且可以捕获目标方法抛出的异常。

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.time.Instant;

@Aspect
@Component
public class PerformanceMonitorAspect {

    private static final Logger log = LoggerFactory.getLogger(PerformanceMonitorAspect.class);

    // 定义切点,这里以所有Service层方法为例
    // 你可以根据需要调整切点表达式,例如特定注解、特定包下的所有方法等
    @Around("execution(* com.example.app.service.*.*(..))")
    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        Instant start = Instant.now();
        Object result;
        String methodName = joinPoint.getSignature().toShortString();

        try {
            result = joinPoint.proceed(); // 执行目标方法
        } catch (Throwable e) {
            Instant end = Instant.now();
            long executionTime = Duration.between(start, end).toMillis();
            log.error("方法 {} 执行异常,耗时 {} ms. 异常信息: {}", methodName, executionTime, e.getMessage(), e);
            throw e; // 重新抛出异常,以便ExceptionHandler能够捕获
        }

        Instant end = Instant.now();
        long executionTime = Duration.between(start, end).toMillis();
        log.info("方法 {} 正常执行完成,耗时 {} ms", methodName, executionTime);
        return result;
    }
}
登录后复制

代码解释:

  • @Aspect和@Component:将该类声明为一个Spring管理的切面。
  • @Around("execution(* com.example.app.service.*.*(..))"):定义了一个环绕通知,它将应用于com.example.app.service包下所有类的所有方法。你可以根据实际需求调整切点表达式,例如:
    • @Around("@annotation(com.example.app.annotation.LogExecutionTime)"):只对带有@LogExecutionTime注解的方法生效。
    • @Around("bean(*Controller)"):对所有名为XXXController的Bean中的方法生效。
  • ProceedingJoinPoint joinPoint:提供了访问目标方法信息和控制目标方法执行的能力。
  • joinPoint.proceed():这是执行目标方法的关键。
  • try-catch块:在joinPoint.proceed()外部包裹try-catch,无论方法正常完成还是抛出异常,我们都能在finally语义上(这里是catch块和try块的后续部分)计算执行时间。
  • log.error(...)和log.info(...):分别记录异常情况下的执行时间和正常情况下的执行时间。
  • throw e;:在catch块中,捕获异常后必须重新抛出,这样全局@ExceptionHandler才能捕获到并进行统一处理。

1.3 结合全局异常处理器

有了上述切面,你的全局@ExceptionHandler将继续正常工作,因为它会捕获到由切面重新抛出的异常。

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleAllExceptions(Exception e) {
        // 这里的e就是由PerformanceMonitorAspect重新抛出的原始异常
        // 在切面中已经记录了执行时间,这里可以专注于异常响应的构建
        log.error("全局异常处理器捕获到异常: {}", e.getMessage(), e);
        return new ResponseEntity<>("服务器内部错误:" + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
登录后复制

优点:

  • 解耦性强: 业务逻辑与计时、异常处理逻辑完全分离。
  • 可维护性高: 计时逻辑集中在一个地方,修改方便。
  • 侵入性低: 无需修改业务方法代码。

2. 利用自定义异常传递执行时间信息

如果因为某些特定原因不希望使用AOP,或者需要将执行时间信息直接传递给ExceptionHandler,可以考虑自定义一个异常类来封装执行时间。

2.1 定义自定义异常类

创建一个继承自RuntimeException的自定义异常类,并添加一个字段来存储执行时间。

网易人工智能
网易人工智能

网易数帆多媒体智能生产力平台

网易人工智能 206
查看详情 网易人工智能
import java.time.Duration;

public class TimeMeasuredException extends RuntimeException {

    private final Duration executionDuration;
    private final Throwable originalCause; // 存储原始异常

    public TimeMeasuredException(Duration executionDuration, Throwable originalCause) {
        super("方法执行异常,耗时 " + executionDuration.toMillis() + " ms", originalCause);
        this.executionDuration = executionDuration;
        this.originalCause = originalCause;
    }

    public Duration getExecutionDuration() {
        return executionDuration;
    }

    public Throwable getOriginalCause() {
        return originalCause;
    }
}
登录后复制

2.2 在业务逻辑或前置AOP中抛出自定义异常

在需要测量时间的方法内部,使用try-catch块来捕获异常,计算时间,然后抛出TimeMeasuredException。

import java.time.Duration;
import java.time.Instant;

public class MyService {

    public void doSomeFancyStuff() {
        Instant start = Instant.now();
        try {
            // ... 你的业务逻辑代码 ...
            if (Math.random() > 0.5) {
                throw new RuntimeException("模拟业务异常");
            }
            // ...
        } catch (Exception e) {
            Instant end = Instant.now();
            Duration executionTime = Duration.between(start, end);
            // 捕获到原始异常后,计算时间并抛出自定义异常
            throw new TimeMeasuredException(executionTime, e);
        }
    }
}
登录后复制

注意: 这种方法要求在每个可能抛出异常并需要记录时间的地方都进行这样的try-catch封装。如果方法数量很多,这仍然会引入重复代码。更推荐的做法是,在AOP切面内部执行此逻辑,而不是在每个业务方法中。

示例:在AOP中抛出TimeMeasuredException 如果你想结合AOP的非侵入性,但又想利用自定义异常传递时间,可以这样做:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.time.Instant;

@Aspect
@Component
public class TimeMeasuringAspectWithCustomException {

    @Around("execution(* com.example.app.service.*.*(..))")
    public Object measureAndWrapException(ProceedingJoinPoint joinPoint) throws Throwable {
        Instant start = Instant.now();
        Object result;
        try {
            result = joinPoint.proceed(); // 执行目标方法
        } catch (Throwable e) {
            Instant end = Instant.now();
            Duration executionTime = Duration.between(start, end);
            // 捕获到原始异常后,计算时间并抛出自定义异常
            throw new TimeMeasuredException(executionTime, e);
        }
        return result;
    }
}
登录后复制

2.3 ExceptionHandler捕获自定义异常

全局异常处理器现在可以专门捕获TimeMeasuredException,从而获取执行时间信息。

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(TimeMeasuredException.class)
    public ResponseEntity<String> handleTimeMeasuredException(TimeMeasuredException e) {
        log.error("捕获到带有执行时间的异常,耗时 {} ms. 原始异常信息: {}", 
                  e.getExecutionDuration().toMillis(), e.getOriginalCause().getMessage(), e.getOriginalCause());
        return new ResponseEntity<>("业务处理失败,耗时 " + e.getExecutionDuration().toMillis() + " ms. 错误详情: " + e.getOriginalCause().getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }

    // 仍然可以保留一个通用的异常处理器来捕获其他未被TimeMeasuredException包装的异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleAllOtherExceptions(Exception e) {
        log.error("捕获到其他未处理的异常: {}", e.getMessage(), e);
        return new ResponseEntity<>("服务器内部错误:" + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
登录后复制

优点:

  • 直接将执行时间信息封装在异常对象中,方便异常处理器获取。
  • 适用于需要将特定上下文信息与异常一同传递的场景。

缺点:

  • 如果不在AOP中实现,会在业务代码中引入更多的try-catch块。
  • 引入了额外的异常类,可能增加代码复杂性。
  • 对于横切关注点,不如AOP直接在切面中处理来得简洁。

总结与建议

在Spring Boot中记录方法执行时间并结合异常处理,最佳实践是采用Spring AOP。它能够以非侵入的方式,在应用层面统一管理性能监控和异常日志,极大地提高了代码的整洁度和可维护性。通过在@Around通知中捕获异常并重新抛出,可以确保全局@ExceptionHandler仍然能够正常工作,同时在切面中完成计时和日志记录。

自定义异常类的方式虽然也能达到目的,但更适合于需要将特定业务上下文(而不仅仅是执行时间)与异常一同传递的场景。如果仅仅是为了记录执行时间,AOP是更优的选择。

无论选择哪种方案,关键都在于将计时逻辑从业务方法中分离出来,使其成为一个独立的、可复用的组件,从而构建出更健壮、更易于维护的Spring Boot应用程序。

以上就是Spring Boot中优雅地记录方法执行时间并结合异常处理的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号