
本文旨在探讨在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 java.time.Duration;import java.time.Instant;@Aspect@Componentpublic 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会自动启用。
org.springframework.boot spring-boot-starter-aop
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;@RestControllerpublic 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 handleRuntimeException(RuntimeException e) { // AOP切面已经记录了执行时间,这里只需处理异常信息 return new ResponseEntity("Error occurred: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); }}
当testAopError方法抛出异常时,PerformanceMonitorAspect会捕获它,记录执行时间,然后重新抛出。handleRuntimeException方法会接收到这个异常并进行处理,而日志中已经包含了该方法的执行时间。
策略二:通过自定义异常封装执行时间
如果由于某些原因不希望使用AOP,或者希望将执行时间与异常信息更紧密地绑定,可以考虑创建自定义异常来封装执行时间。这种方法要求在业务逻辑或其包装层显式地捕获异常并创建自定义异常。
千帆AppBuilder
百度推出的一站式的AI原生应用开发资源和工具平台,致力于实现人人都能开发自己的AI原生应用。
174 查看详情
1. 定义自定义异常
创建一个继承自RuntimeException的自定义异常,并添加一个字段来存储执行时间。
import java.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 java.time.Duration;import java.time.Instant;@Servicepublic 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;@RestControllerpublic 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 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 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依赖时。
注意事项:
日志级别:在记录执行时间时,根据实际需求选择合适的日志级别(如INFO用于成功,ERROR用于异常)。性能开销:AOP和Instant.now()的调用都会带来微小的性能开销。对于大多数应用而言,这种开销可以忽略不计,但在极端高并发或对延迟极其敏感的场景下,需要进行性能测试。粒度选择:决定是在控制器层、服务层还是更深层次的方法上进行时间测量。通常,在服务层或控制器层进行测量,可以更好地反映用户请求的处理时间。可观测性:除了简单的日志记录,还可以考虑将这些执行时间数据集成到更专业的监控系统(如Prometheus、Grafana)中,以便进行趋势分析和告警。
通过上述两种策略,开发者可以灵活地在Spring Boot应用中实现异常处理时的执行时间记录,从而提升系统的可观测性和维护性。在大多数情况下,Spring AOP是更推荐的解决方案,因为它提供了更好的代码分离和模块化。
以上就是Spring Boot中优雅地记录异常处理时的方法执行时间的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1098982.html
微信扫一扫
支付宝扫一扫