
本文旨在探讨在Spring Boot应用中,如何在方法执行期间发生异常并被`ExceptionHandler`捕获时,依然能够准确记录其执行时间。我们将介绍两种主要策略:利用Spring AOP实现横切关注点的时间测量,以及通过自定义异常封装执行时间。这些方法能够帮助开发者在不修改业务逻辑代码的前提下,实现高效且非侵入式的性能监控与异常日志记录。
在现代企业级应用开发中,性能监控和异常处理是不可或缺的环节。当业务逻辑方法抛出异常并由Spring的@ExceptionHandler统一处理时,我们常常需要记录该方法的完整执行时间,包括异常发生和处理的时间。然而,由于异常处理器的性质,它通常无法直接获取到原始方法的起始时间,这给准确的时间测量带来了挑战。本教程将详细介绍两种有效策略来解决这一问题。
Spring AOP(面向切面编程)提供了一种强大的机制,允许开发者在不修改核心业务逻辑的情况下,为应用程序添加横切关注点,例如日志记录、事务管理和性能监控。通过定义一个切面,我们可以在方法执行前、执行后或抛出异常时插入自定义逻辑。
首先,创建一个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
@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;
}
}在上述切面中:
确保你的Spring Boot应用中启用了AOP。通常,如果添加了spring-boot-starter-aop依赖,AOP会自动启用。
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>现在,我们的业务方法和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,或者希望将执行时间与异常信息更紧密地绑定,可以考虑创建自定义异常来封装执行时间。这种方法要求在业务逻辑或其包装层显式地捕获异常并创建自定义异常。
创建一个继承自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;
}
}在业务逻辑方法内部(或更推荐的,在一个服务层或一个代理层),使用try-catch块来测量时间,并在发生异常时抛出TimeMeasuredException。
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.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);
}
}
}现在,你的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策略:
自定义异常策略:
注意事项:
通过上述两种策略,开发者可以灵活地在Spring Boot应用中实现异常处理时的执行时间记录,从而提升系统的可观测性和维护性。在大多数情况下,Spring AOP是更推荐的解决方案,因为它提供了更好的代码分离和模块化。
以上就是Spring Boot中优雅地记录异常处理时的方法执行时间的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号