新闻中心

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

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

spring boot中优雅地记录异常处理时的方法执行时间

本文旨在探讨在Spring Boot应用中,如何在方法执行期间发生异常并被`ExceptionHandler`捕获时,依然能够准确记录其执行时间。我们将介绍两种主要策略:利用Spring AOP实现横切关注点的时间测量,以及通过自定义异常封装执行时间。这些方法能够帮助开发者在不修改业务逻辑代码的前提下,实现高效且非侵入式的性能监控与异常日志记录。

在现代企业级应用开发中,性能监控和异常处理是不可或缺的环节。当业务逻辑方法抛出异常并由Spring的@ExceptionHandler统一处理时,我们常常需要记录该方法的完整执行时间,包括异常发生和处理的时间。然而,由于异常处理器的性质,它通常无法直接获取到原始方法的起始时间,这给准确的时间测量带来了挑战。本教程将详细介绍两种有效策略来解决这一问题。

策略一:利用Spring AOP实现非侵入式时间测量

Spring AOP(面向切面编程)提供了一种强大的机制,允许开发者在不修改核心业务逻辑的情况下,为应用程序添加横切关注点,例如日志记录、事务管理和性能监控。通过定义一个切面,我们可以在方法执行前、执行后或抛出异常时插入自定义逻辑。

1. 定义一个性能监控切面

首先,创建一个Spring AOP切面来环绕目标方法的执行。在这个切面中,我们可以记录方法的开始时间,并在方法执行完毕或抛出异常时计算并记录总的执行时间。

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 logger = LoggerFactory.getLogger(PerformanceMonitorAspect.class);

    /**
     * 环绕通知,用于测量方法的执行时间。
     * 
     * @param joinPoint 连接点,代表被拦截的方法。
     * @return 目标方法的返回值。
     * @throws Throwable 如果目标方法抛出异常。
     */
    @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.DeleteMapping) || " +
            "execution(* com.example.service.*.*(..))") // 示例:可以根据实际情况调整切点表达式
    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        Instant start = Instant.now();
        Object result;
        try {
            result = joinPoint.proceed(); // 执行目标方法
        } catch (Exception ex) {
            // 捕获异常时,依然计算并记录执行时间
            Instant end = Instant.now();
            long executionTimeMillis = Duration.between(start, end).toMillis();
            logger.error("方法 '{}' 执行异常,耗时: {} ms. 异常信息: {}", 
                         joinPoint.getSignature().toShortString(), 
                         executionTimeMillis, 
                         ex.getMessage(), 
                         ex);
            throw ex; // 重新抛出异常,以便ExceptionHandler可以捕获
        }
        Instant end = Instant.now();
        long executionTimeMillis = Duration.between(start, end).toMillis();
        logger.info("方法 '{}' 执行成功,耗时: {} ms.", 
                    joinPoint.getSignature().toShortString(), 
                    executionTimeMillis);
        return result;
    }
}

在上述切面中:

  • @Aspect 标识这是一个切面。
  • @Component 将其注册为Spring Bean。
  • @Around 定义了一个环绕通知,它会在匹配的连接点(方法)执行前后执行。
  • joinPoint.proceed() 调用目标方法。
  • try-catch 块确保无论方法成功执行还是抛出异常,都能计算并记录执行时间。在捕获异常后,我们重新抛出它,以确保Spring的@ExceptionHandler能够继续处理。

2. 启用AOP

确保你的Spring Boot应用中启用了AOP。通常,如果添加了spring-boot-starter-aop依赖,AOP会自动启用。

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

3. 示例控制器和异常处理器

现在,我们的业务方法和ExceptionHandler可以保持简洁,无需关心时间测量逻辑。

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {

    @GetMapping("/test-aop-success/{id}")
    public String testAopSuccess(@PathVariable String id) throws InterruptedException {
        Thread.sleep(100); // 模拟耗时操作
        return "Success for ID: " + id;
    }

    @GetMapping("/test-aop-error/{id}")
    public String testAopError(@PathVariable String id) throws InterruptedException {
        Thread.sleep(150); // 模拟耗时操作
        if (id.equals("error")) {
            throw new RuntimeException("Something went wrong for ID: " + id);
        }
        return "Success for ID: " + id;
    }

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<String> handleRuntimeException(RuntimeException e) {
        // AOP切面已经记录了执行时间,这里只需处理异常信息
        return new ResponseEntity<>("Error occurred: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

当testAopError方法抛出异常时,PerformanceMonitorAspect会捕获它,记录执行时间,然后重新抛出。handleRuntimeException方法会接收到这个异常并进行处理,而日志中已经包含了该方法的执行时间。

策略二:通过自定义异常封装执行时间

如果由于某些原因不希望使用AOP,或者希望将执行时间与异常信息更紧密地绑定,可以考虑创建自定义异常来封装执行时间。这种方法要求在业务逻辑或其包装层显式地捕获异常并创建自定义异常。

TTSMaker TTSMaker

TTSMaker是一个免费的文本转语音工具,提供语音生成服务,支持多种语言。

TTSMaker 2275 查看详情 TTSMaker

1. 定义自定义异常

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

import j*a.time.Duration;

public class TimeMeasuredException extends RuntimeException {

    private final Duration executionDuration;

    public TimeMeasuredException(Duration executionDuration, Throwable cause) {
        super("Method execution failed with time: " + executionDuration.toMillis() + "ms", cause);
        this.executionDuration = executionDuration;
    }

    public TimeMeasuredException(Duration executionDuration, String message, Throwable cause) {
        super(message + " (Execution time: " + executionDuration.toMillis() + "ms)", cause);
        this.executionDuration = executionDuration;
    }

    public Duration getExecutionDuration() {
        return executionDuration;
    }
}

2. 在业务逻辑中抛出自定义异常

在业务逻辑方法内部(或更推荐的,在一个服务层或一个代理层),使用try-catch块来测量时间,并在发生异常时抛出TimeMeasuredException。

import org.springframework.stereotype.Service;
import j*a.time.Duration;
import j*a.time.Instant;

@Service
public class MyService {

    public String performTaskWithTimeMeasurement(String input) {
        Instant start = Instant.now();
        try {
            // 模拟耗时操作和潜在异常
            Thread.sleep(120); 
            if ("fail".equals(input)) {
                throw new IllegalArgumentException("Invalid input: " + input);
            }
            return "Task completed for: " + input;
        } catch (Exception e) {
            Instant end = Instant.now();
            Duration duration = Duration.between(start, end);
            // 捕获原始异常,并抛出包含执行时间的自定义异常
            throw new TimeMeasuredException(duration, "Failed to perform task", e);
        }
    }
}

3. 在ExceptionHandler中捕获并处理自定义异常

现在,你的ExceptionHandler需要捕获TimeMeasuredException,并从中提取执行时间。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyControllerWithCustomException {

    private static final Logger logger = LoggerFactory.getLogger(MyControllerWithCustomException.class);
    private final MyService myService;

    public MyControllerWithCustomException(MyService myService) {
        this.myService = myService;
    }

    @GetMapping("/test-custom-exception/{input}")
    public String testCustomException(@PathVariable String input) {
        return myService.performTaskWithTimeMeasurement(input);
    }

    @ExceptionHandler(TimeMeasuredException.class)
    public ResponseEntity<String> handleTimeMeasuredException(TimeMeasuredException e) {
        logger.error("方法执行失败,耗时: {} ms. 原始异常: {}", 
                     e.getExecutionDuration().toMillis(), 
                     e.getCause() != null ? e.getCause().getMessage() : "N/A", 
                     e);
        return new ResponseEntity<>("Error occurred with execution time: " + e.getExecutionDuration().toMillis() + "ms. " + e.getCause().getMessage(), 
                                    HttpStatus.INTERNAL_SERVER_ERROR);
    }

    // 可以保留一个通用的ExceptionHandler来捕获其他未被TimeMeasuredException包装的异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneralException(Exception e) {
        logger.error("发生未知错误: {}", e.getMessage(), e);
        return new ResponseEntity<>("An unexpected error occurred: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

总结与注意事项

  • Spring AOP策略

    • 优点:高度解耦,业务逻辑代码保持纯净,无需手动添加时间测量逻辑。适用于广泛的横切关注点。
    • 缺点:需要理解AOP概念和切点表达式。如果切点定义不当,可能影响性能或遗漏测量。
    • 推荐场景:作为通用的性能监控和异常日志记录方案,尤其适用于大型项目。
  • 自定义异常策略

    • 优点:逻辑更显式,执行时间与异常紧密关联,无需AOP配置。
    • 缺点:侵入性较高,需要在每个可能抛出异常并需要测量时间的业务方法中手动添加try-catch块。可能导致异常层次结构变得复杂。
    • 推荐场景:特定场景下,当需要将执行时间作为异常信息的一部分传递时,或者不希望引入AOP依赖时。

注意事项:

  1. 日志级别:在记录执行时间时,根据实际需求选择合适的日志级别(如INFO用于成功,ERROR用于异常)。
  2. 性能开销:AOP和Instant.now()的调用都会带来微小的性能开销。对于大多数应用而言,这种开销可以忽略不计,但在极端高并发或对延迟极其敏感的场景下,需要进行性能测试。
  3. 粒度选择:决定是在控制器层、服务层还是更深层次的方法上进行时间测量。通常,在服务层或控制器层进行测量,可以更好地反映用户请求的处理时间。
  4. 可观测性:除了简单的日志记录,还可以考虑将这些执行时间数据集成到更专业的监控系统(如Prometheus、Grafana)中,以便进行趋势分析和告警。

通过上述两种策略,开发者可以灵活地在Spring Boot应用中实现异常处理时的执行时间记录,从而提升系统的可观测性和维护性。在大多数情况下,Spring AOP是更推荐的解决方案,因为它提供了更好的代码分离和模块化。

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


# 我们可以  # 设置网站建设  # 巴雄天下推广员网站  # 找哪家建网站推广  # 29岁seo  # 大同seo站内优化公司  # 南京网站优化最新行情  # 房产网站建设如何运营  # 云南app关键词排名  # 龙岩抖音关键词排名软件  # 创新论文网站建设  # 横切  # 时计  # java  # 并在  # 适用于  # 两种  # 自定义  # 抛出  # 执行时间  # red  # 性能测试  # 应用开发  # ai  # app  # 处理器 


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


相关推荐: 小红书怎么解除第三方平台绑定_小红书多平台登录解绑方法介绍  C++如何解决segmentation fault_C++段错误调试与原因分析  探索高级语言到原生C/C++的转译:挑战与内存管理策略  MongoDB聚合管道:正确匹配对象数组中_id的方法  PHP中获取MongoDB服务器运行时间(Uptime)的专业指南  Angular中父组件异步更新子组件复选框状态的实践指南  mcjs网页版在线存档 mcjs云存档登录入口  铃兰之剑为这和平的世界希里技能组及加点推荐  印象笔记如何设离线包出差查阅_印象笔记设离线包出差查阅【离线阅读】  虫虫漫画精品漫画官网_虫虫漫画精品漫画官网进入精品漫画  J*aScript:在map操作中高效处理空数组  Win11怎么设置开机NumLock亮 Win11修改注册表InitialKeyboardIndicators值  J*aScript中向JSON对象添加新属性的正确姿势  J*a应用程序首次运行自动创建文件与目录的最佳实践  打开就能玩的植物大战僵尸 植物大战僵尸网页版传送门  抖音怎么赚钱_抖音创作者变现方法与途径指南  移动端XML文件怎么转换成Excel 手机和平板上的解决方案  PHP URL参数传递与500错误调试指南  React Router 嵌套组件中 URL 重定向问题的解决方案  Archive of Our Own官网直达 AO3最新可用地址一览  腾讯QQ邮箱登录入口_QQ邮箱官方网站使用地址  c++20的std::jthread是什么_c++可中断线程与RAII式管理  Python类型检查:优化关联可选属性的Mypy推断策略  qq游戏大厅官方下载_qq游戏免费下载安装入口  mc.js游戏直达 mc.js网页免下载版本秒进地址  c++如何实现单例设计模式_c++线程安全的单例模式写法  J*a编写用户注册与登录功能_掌握字符串与验证逻辑  《明末:渊虚之羽》设计师谈设计角色:那会刚毕业 充满激情  顺丰快件物流信息 官方网站查询入口  QQ邮箱网页版入口页面 QQ邮箱在线登录入口官网  sublime怎么进行远程开发编辑_配置rsub/rmate实现sublime编辑服务器文件  如何优雅地扩展SprykerGlue后端API授权逻辑,使用spryker/glue-backend-api-application-authorization-connector-extension  纯CSS与HTML网格布局的HTML精简策略:SVG与JS方案解析  将JSON对象数组转置为键值对列表的实用指南  12306选座如何查看座位示意图_12306座位示意图解读与使用  Python中如何避免重复条件判断:利用数据结构实现动态逻辑  Win11怎么修改默认浏览器_Windows 11设置Chrome为默认  创客贴用户入口官网登录 创客贴网页版电脑版系统  cad怎么合并重叠的线段_cad清理重复重叠线条的操作方法  动漫共和国防屏蔽稳定域名-动漫共和国官方正版直达通道  Golang切片为何属于引用类型_Golang slice底层结构与引用语义说明  百度浏览器字体显示异常偏小_百度浏览器字体渲染修复方案  微信网页版官方入口教程 微信网页版网页版快速登录步骤  实现分段式页面滚动导航:CSS与J*aScript教程  QQ邮箱官方网页版登录 QQ邮箱个人邮箱快速访问  如何在Promise链中优雅地中断后续then执行  12306选座怎么选到临时改签座_12306改签选座策略与步骤  C++ string find函数返回值npos详解_C++字符串查找失败的判断条件  提升屏幕阅读器对“m”时间单位的播报准确性:HTML与CSS组合解决方案  Django表单验证失败时保留用户输入数据的最佳实践 

搜索