springboot项目全局异常处理的三种方式
背景
通过多次代码重构的经验积累,在最近一次从零开始的功能开发中,我决定完善项目的全局异常处理机制。项目中的主要应用场景分为两类:一是面向服务的控制层(如各类控制器),这类问题可以通过简单的 @ControllerAdvice
和 @ExceptionHandler
注解来解决;二是各种异步定时任务,对于这类场景,上述方法不再适用,因此需要采用自定义切面的方式来实现全局异常处理。经过测试和总结,找到了三种有效的全局异常处理方法。
实现
控制层的全局异常@ControllerAdvice和@ExceptionHandler
package com.exception.handler.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.servlet.http.HttpServletRequest;
/**
* 统一处理控制器中的异常, 返回一个带有HTTP状态码和响应体的对象
*/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = {Exception.class})
public ResponseEntity<Object> handleAllUncaughtExceptions(Exception ex, HttpServletRequest request) {
// 可以在这里添加日志记录或其他处理逻辑
log.error("Caught an exception: " + ex.getMessage());
// 返回一个自定义的错误消息和HTTP状态码
return ResponseEntity.status(500).body("An unexpected error occurred: " + ex.getMessage());
}
@ExceptionHandler(value = {NullPointerException.class})
public ResponseEntity<Object> handleNullPointerException(NullPointerException ex, WebRequest request) {
// 特定于NullPointerException的处理逻辑
return ResponseEntity.status(400).body("Null pointer exception occurred: " + ex.getMessage());
}
// 可以添加更多的异常处理器方法...
}
异步定时任务的全局异常
1.@AfterThrowing
package com.exception.handler.config;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class ExceptionHandlingAspect {
// 匹配com.exception.handler包及其所有子包下的所有类中的所有方法的执行
@AfterThrowing(pointcut = "execution(* com.exception.handler..*(..))", throwing = "e")
public void handleException(JoinPoint joinPoint, Exception e) {
// 在这里处理异常
// 可以在这里记录日志或者发送邮件通知等
log.error("[{}] occurs exception, error is {}, location is {}",
joinPoint.getSignature().toShortString(), e.getMessage(), e.getStackTrace()[0]);
}
}
2.自定义注解 MyExceptionGlobalCatch
package com.exception.handler.aspect;
import java.lang.annotation.*;
/**
* 可作用类、方法的全局异常捕捉处理注解
*/
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyExceptionGlobalCatch {
}
package com.exception.handler.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Slf4j
@Component
public class MyExceptionGlobalCatchAspect {
@Around("@annotation(com.exception.handler.aspect.MyExceptionGlobalCatch) || @within(com.exception.handler.aspect.MyExceptionGlobalCatch)")
public Object exceptionHandler(ProceedingJoinPoint point) {
Object result = null;
String signature = point.getSignature().toShortString();
try {
result = point.proceed();
} catch (Throwable e) {
log.error("[{}] occurs exception, error is {}, location is {}", signature, e.getMessage(), e.getStackTrace()[0]);
} finally {
// 这里面可以写业务提交
// 比如之前写的kafka的ack,通过point.getArgs()然后通过 instanceof 找到对应的参数调用提交方法
// 时刻注意生产环境里高流量下打印日志的级别
log.debug("后续处理完成");
}
return result;
}
}
测试
对于控制层异常的全局处理测试
对于异步定时任务异常的全局处理测试
AfterThrowing测试
自定义注解测试
总结
-
使用
@ControllerAdvice
: 这种方式适用于控制器层的全局异常捕获与处理。它可以拦截控制器中的异常,并返回一个包含特定HTTP状态码及响应体的对象。 -
针对异步定时任务的全局异常处理:
- 使用
@AfterThrowing
和自定义注解是一种可行的方法。然而,直接使用@AfterThrowing
只能用于记录异常信息,但不会阻止异常在其他地方被重复记录,因此不太理想。 - 更推荐的做法是利用自定义注解结合切面编程。这种方式通过将自定义注解应用于类级别,可以在实现的切面类中集中处理异常,确保异常只被记录一次。
此外,如果项目中涉及到 Kafka 的主动监听功能,可以考虑将消息确认(acknowledgment)的提交逻辑纳入全局异常处理机制中。
- 使用