新闻中心

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

2025-12-02
浏览次数:
返回列表

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 j*a.time.Duration;
import j*a.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的自定义异常类,并添加一个字段来存储执行时间。

灵感PPT 灵感PPT

AI灵感PPT - 免费一键PPT生成工具

灵感PPT 308 查看详情 灵感PPT
import j*a.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 j*a.time.Duration;
import j*a.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 j*a.time.Duration;
import j*a.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中优雅地记录方法执行时间并结合异常处理的详细内容,更多请关注其它相关文章!


# 处理器  # app  # red  # 执行时间  # 自定义  # 抛出  # 并结合  # java  # 英山网站建设预案  # seo百度支持 spa  # 建设网站教程app软件  # 外卖店如何搭建网站推广  # 王姣君 seo  # 武汉时代生物网站建设  # 学校网站建设主体  # 豆瓣网站优化工具  # 铜陵线上推广获客网站  # 厚街企业网站建设  # 如何使用  # 横切  # 情况下  # 两种  # 也能  # 你可以 


相关栏目: 【 科技资讯46185 】 【 网络学院92790


相关推荐: 押井守高度称赞《辐射4》:玩了八年都停不下来!  PySpark中从现有列右侧提取可变长度字符创建新列的教程  AO3最新官网入口公告_2025AO3镜像站实时查询方法  字由网在线版登录地址 字由网网页版安全入口  2026春节假期时间安排 2026春节假日查询  正确连接J*aScript到HTML实现可点击图片与自定义事件处理  C++20的source_location是什么_C++在编译期获取源码位置信息用于日志和断言  Golang指针如何与map组合使用_Golang map指针组合实践  电脑屏幕颜色不舒服怎么办_Windows夜间模式与色彩校准教程【护眼技巧】  如何使用CaptainHook和Composer管理Git钩子_在提交前自动运行代码检查的Composer配置  Adobe PDF表单中利用J*aScript解析与格式化日期组件的教程  Animex动漫社网入口地址 Animex动漫社网正版在线入口  mysql如何设置表访问权限_mysql表访问权限配置  Win11怎么修改默认浏览器_Windows 11设置Chrome为默认  在WordPress中通过REST API获取BasicAuth保护的远程文章  C++如何比较两个字符串_C++ string compare函数与操作符对比  创客贴用户入口官网登录 创客贴网页版电脑版系统  Mac怎么使用表情符号_Mac Emoji快捷键面板  Django AJAX 文件上传教程:解决图片无法保存到模型的常见问题  在J*a中如何开发简易博客标签推荐系统_博客标签推荐项目实战解析  TikTok网页版直接登录 TikTok网页端官方平台入口  没有大陆身份证/银行卡如何实名微信? 亲测有效的几种方法分享  神经网络二分类模型训练异常:高损失与完美验证准确率的排查与修正  深入理解Promise链:如何在catch后中断then的执行  优化LangChain文档加载与ChromaDB集成:解决多文档处理与分块问题  c++ 命名空间怎么用 c++ namespace使用指南  PyTorch模型训练效果不佳?深入剖析常见错误与调试技巧  内存检查:在VS Code中调试C++时的内存视图  AO3中文官网链接_AO3网页版稳定镜像站  age动漫网站入口 age动漫官网直接访问入口  sublime怎么覆盖插件的默认快捷键_sublime快捷键优先级与设置  Golang如何优雅处理error_Golang error处理最佳实践总结  厨房不锈钢水槽发黑生锈怎么处理_水槽用可乐+锡纸2分钟抛亮如新  Lar*el如何生成PDF或Excel文件_Lar*el文档导出工具与使用教程  UC浏览器官网入口2025最新 UC浏览器网页版正式地址  2306选座时如何选靠窗位置_12306选座靠窗座位查看方法解析  qq浏览器打开空白页怎么办 qq浏览器启动后显示白屏的解决教程  谷歌浏览器最新官方入口链接 谷歌浏览器网页版官网导航  小猿搜题在线学习页面在哪_小猿搜题在线学习中心入口  Windows电脑怎么截图最方便_系统自带截图工具的5种神仙用法【技巧】  极速漫画官方主页网址 极速漫画漫画在线浏览官网链接  Safari自带网页翻译功能怎么用 无需插件轻松看懂外文网站【方法】  QQ网页版官方账号入口 QQ网页版网页版登录指南  Win11截图该按哪些键 Win11截屏完整流程解析【教程】  极兔快递快件信息查询系统 极兔快递官网运单号追踪  Lar*el如何正确地在控制器和模型之间分配逻辑_Lar*el代码职责分离与架构建议  星露谷物语官网入口 星露谷物语游戏官网入口  基于动态规划的房屋花卉种植最小成本算法详解  Yandex浏览器官方网页版入口 Yandex浏览器最新版官网  限制HTML日期输入框的日期选择范围 

搜索