全局异常捕获

导言

image-20260105170250771

上图展示的是杭州市民卡APP在2026年1月1日出现的一次典型错误案例:由于埋点功能的异常,不仅导致了主流程中断,还将详细的异常堆栈信息暴露给了用户。这种现象不仅影响了用户体验,也存在安全隐患。如何规范地进行全局异常处理,将在下文详细介绍。

全局异常捕获是现代 Java Web 项目架构不可或缺的一环。它能够集中管理和处理系统中可能出现的各类异常,提升 API 的健壮性、可维护性和用户友好性。通过合理设计全局异常处理器,不仅可以保证前后端接口的响应一致性,还能方便排查和定位问题。

在日常开发中,直接使用 throw e 等方式抛出异常,容易导致大量堆栈信息暴露给调用方,既影响API友好度,也可能带来安全隐患。通过统一的全局异常捕获机制,可以避免异常堆栈泄露,将异常拦截后转化为一致、规范的前端响应。同时,全局异常捕获器还可以作为系统兜底保护——当代码中未被显式捕获的异常发生时,拦截这些异常,返回规范化错误信息,防止系统崩溃或异常信息外泄。

本篇笔记将系统梳理 Spring Boot/Web 项目中常见异常的类别、触发场景和最佳捕获实践。从基础的 @RestControllerAdvice@ExceptionHandler 的应用,到进阶的各类异常类型归纳,以及推荐的全局异常处理器骨架代码,让项目异常处理更规范、更易扩展。

全局异常捕获

核心注解@RestControllerAdvice@ExceptionHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestControllerAdvice  // 或 @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {

// 捕获自定义业务异常
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(BusinessException ex) {
return ResponseEntity.badRequest()
.body(new ErrorResponse("BUSINESS_ERROR", ex.getMessage()));
}
// 注意此处不能使用throw exception抛出所有异常,不然就会抛出所有堆栈信息给前端

// 捕获所有未处理异常(兜底)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
log.error("Unexpected error", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("SYSTEM_ERROR", "服务器内部错误"));
}
}

最佳实践

一般统一的全局异常捕获作为一个公共类BaseExceptionHandler,具体微服务中需要引入这个公共类,并定义一个GlobalExceptionHandler继承他。

BaseExceptionHandler 公共父类示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class BaseExceptionHandler {

@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(BusinessException ex) {
return ResponseEntity.badRequest()
.body(new ErrorResponse("BUSINESS_ERROR", ex.getMessage()));
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
// 这里假设有日志工具
log.error("Unexpected system exception", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("SYSTEM_ERROR", "系统异常,请联系管理员"));
}
}

各微服务中的继承实现

1
2
3
4
5
@RestControllerAdvice
public class GlobalExceptionHandler extends BaseExceptionHandler {
// 可根据每个微服务自己的实际情况,添加新的业务异常处理方法
// 若无特殊需求,可以不写代码,完全继承父类行为
}

RestControllerAdvice

@RestControllerAdvice 是 Spring 中用于全局异常处理的注解。它是 @ControllerAdvice@ResponseBody 的组合注解,作用如下:

  • 拦截所有被 @RestController 注解的控制器中的异常,实现统一的异常处理逻辑。
  • 标注在类上后,该类中用 @ExceptionHandler 注解的方法会自动处理全局范围内的异常,并返回 JSON 数据(即返回体会被自动序列化为 JSON)。
  • 通常用于RESTful 接口项目,提升异常处理的统一性与规范性。

总结
@RestControllerAdvice 可以让你把所有 Controller 层的异常集中处理,并且方便地返回统一的数据结构(如通用的错误响应对象)。

ExceptionHandler

@ExceptionHandler 是 Spring 框架提供的异常处理注解,用于定义在某个方法上,当指定类型的异常在 Controller 层被抛出时,由被注解的方法进行处理。

  • 使用方式:注解在方法上,参数为需要捕获的异常类型。可以指定一个或多个异常类。
  • 作用:捕获并处理指定异常,通常结合全局异常处理类(如上文中的 @RestControllerAdvice)一起使用,实现异常捕获和统一返回响应。

示例说明:

1
2
3
4
5
6
@ExceptionHandler(NullPointerException.class)
public ResponseEntity<String> handleNullPointer(NullPointerException ex) {
// 可以自行处理异常,例如打印日志、组装响应等
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("空指针异常:" + ex.getMessage());
}
  • 当控制器方法执行发生 NullPointerException,该方法会被自动调用进行处理。
  • 此方法统一包装 HTTP 状态码与异常信息返回给客户端。

总结
@ExceptionHandler 结合 @RestControllerAdvice 使用,实现了捕获全局 Controller 异常、集中处理和统一返回格式的目的。

常用异常类型

一般来说,在公共类中尽可能抽象地通用捕获一些常用异常类型,例如HttpRequestMethodNotSupportedException,BindException,MethodArgumentNotValidException,HandlerMethodValidationException

常用异常类型

异常类 触发场景 建议 HTTP 状态码
MethodArgumentNotValidException JSR-303/380 校验失败(@Valid + 对象参数) 400 Bad Request
BindException 表单提交(application/x-www-form-urlencoded)校验失败 400 Bad Request
ConstraintViolationException 手动调用 Validator.validate() 或路径变量/请求参数校验失败 400 Bad Request
MissingServletRequestParameterException 缺少必需的请求参数(@RequestParam(required = true) 400 Bad Request
ServletRequestBindingException 请求参数绑定失败(如类型不匹配) 400 Bad Request
HandlerMethodValidationException (Spring6) 方法级 @Validated 校验失败 400 Bad Request
HttpRequestMethodNotSupportedException 请求方法不支持(如 POST 访问只允许 GET 的接口) 405 Method Not Allowed
NoHandlerFoundException 404 路径未找到(需开启 spring.mvc.throw-exception-if-no-handler-found=true 404 Not Found
HttpMediaTypeNotSupportedException 请求头 Content-Type 不支持(如接口要求 application/json,但传了 text/plain) 415 Unsupported Media Type
HttpMessageNotReadableException JSON 解析失败(如格式错误、字段类型不匹配) 400 Bad Request
HttpMessageNotWritableException 响应序列化失败(通常为服务器编码问题,较少见) 500 Internal Server Error
MissingPathVariableException @PathVariable 变量缺失(路径模板与注解不匹配) 500(开发问题)或 400
MissingRequestHeaderException 缺少必需的请求头(@RequestHeader(required = true) 400 Bad Request
TypeMismatchException 路径变量/请求参数类型转换失败(如字符串转数字失败) 400 Bad Request
AsyncRequestTimeoutException 异步请求超时(如 DeferredResultCallable 使用时) 503 Service Unavailable (推荐)

💡 说明

  • 部分异常(如 HandlerMethodValidationException)为 Spring6+ 新增,用于方法级参数校验。
  • 默认 404 不会抛异常,要在 application.yml 中开启 spring.mvc.throw-exception-if-no-handler-found=true 才能被全局异常处理器捕获。
  • 500 主要用于服务器端开发失误产生的异常,一般应避免暴露给前端用户。

附配置示例(可捕获 404 路由异常):

1
2
3
4
5
6
spring:
mvc:
throw-exception-if-no-handler-found: true
web:
resources:
add-resources: false # 避免静态资源干扰

一、参数校验 & 数据绑定类异常

异常类 触发场景 建议 HTTP 状态码
MethodArgumentNotValidException JSR-303/380 校验失败(@Valid + 对象参数) 400 Bad Request
BindException 表单提交(application/x-www-form-urlencoded)校验失败 400 Bad Request
ConstraintViolationException 手动调用 Validator.validate() 或路径变量/请求参数校验失败 400 Bad Request
MissingServletRequestParameterException 缺少必需的请求参数(@RequestParam(required = true) 400 Bad Request
ServletRequestBindingException 请求参数绑定失败(如类型不匹配) 400 Bad Request

💡 注意

  • MethodArgumentNotValidExceptionBindException 都继承自 BindException,但通常分开处理以获取不同信息。
  • Spring 6 引入了 HandlerMethodValidationException(用于方法级 @Validated),可作为补充。

二、HTTP 方法 & 路由类异常

异常类 触发场景 建议状态码
HttpRequestMethodNotSupportedException 请求方法不支持(如 POST 访问只允许 GET 的接口) 405 Method Not Allowed
NoHandlerFoundException 404 路径未找到(需开启 spring.mvc.throw-exception-if-no-handler-found=true 404 Not Found

⚠️ 默认情况下,Spring Boot 对 404 返回 Whitelabel Error Page,不会抛出异常。

若想在 @ControllerAdvice 中捕获 404,必须配置:

1
2
3
4
5
6
spring:
mvc:
throw-exception-if-no-handler-found: true
web:
resources:
add-resources: false # 避免静态资源干扰

三、消息转换 & 内容协商异常

异常类 触发场景 建议状态码
HttpMediaTypeNotSupportedException 请求头 Content-Type 不支持(如接口要求 application/json,但传 text/plain) 415 Unsupported Media Type
HttpMessageNotReadableException JSON 解析失败(如格式错误、字段类型不匹配) 400 Bad Request
HttpMessageNotWritableException 序列化响应失败(较少见) 500 Internal Server Error

四、路径变量 & 请求头异常

异常类 触发场景 建议状态码
MissingPathVariableException @PathVariable 变量缺失(路径模板与注解不匹配) 500(开发问题)或 400
MissingRequestHeaderException 缺少必需的请求头(@RequestHeader(required = true) 400 Bad Request
TypeMismatchException 路径变量/请求参数类型转换失败(如字符串转数字失败) 400 Bad Request

五、异步 & 超时异常(高级)

异常类 触发场景 建议状态码
AsyncRequestTimeoutException 异步请求超时(使用 DeferredResultCallable 503 Service Unavailable

六、推荐的全局异常处理器骨架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
@RestControllerAdvice
@Slf4j
public class GlobalWebExceptionHandler {

// 1. 参数校验失败(@Valid 对象参数)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleValidation(MethodArgumentNotValidException ex) {
// 提取校验失败信息
String errorMsg = ex.getBindingResult().getFieldErrors()
.stream().map(e -> e.getField() + ": " + e.getDefaultMessage())
.reduce((a, b) -> a + "; " + b).orElse("参数校验失败");
log.warn("参数校验异常: {}", errorMsg);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMsg);
}

// 2. 表单参数校验
@ExceptionHandler(BindException.class)
public ResponseEntity<String> handleBind(BindException ex) {
String errorMsg = ex.getBindingResult().getFieldErrors()
.stream().map(e -> e.getField() + ": " + e.getDefaultMessage())
.reduce((a, b) -> a + "; " + b).orElse("参数绑定异常");
log.warn("参数绑定异常: {}", errorMsg);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMsg);
}

// 3. 方法参数校验(如 @Validated + @PathVariable)
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<String> handleConstraint(ConstraintViolationException ex) {
String errorMsg = ex.getConstraintViolations()
.stream().map(v -> v.getPropertyPath() + ": " + v.getMessage())
.reduce((a, b) -> a + "; " + b).orElse("约束校验异常");
log.warn("约束校验异常: {}", errorMsg);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMsg);
}

// 4. 缺少请求参数
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<String> handleMissingParam(MissingServletRequestParameterException ex) {
String errorMsg = "缺少请求参数: " + ex.getParameterName();
log.warn(errorMsg);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMsg);
}

// 5. HTTP 请求方法不允许
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<String> handleMethodNotSupported(HttpRequestMethodNotSupportedException ex) {
String errorMsg = "请求方法不支持: " + ex.getMethod();
log.warn(errorMsg);
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(errorMsg);
}

// 6. NoHandlerFoundException(即 404)
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<String> handleNoHandler(NoHandlerFoundException ex) {
String errorMsg = "请求路径不存在: " + ex.getRequestURL();
log.warn(errorMsg);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorMsg);
}

// 7. JSON解析失败
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<String> handleNotReadable(HttpMessageNotReadableException ex) {
log.warn("消息不可读: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("请求体JSON格式错误");
}

// 8. Content-Type不支持
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ResponseEntity<String> handleMediaType(HttpMediaTypeNotSupportedException ex) {
log.warn("不支持的Content-Type: {}", ex.getContentType());
return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).body("不支持的Content-Type");
}

// 9. 统一处理(兜底)
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleDefault(Exception ex) {
log.error("服务器内部异常", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("服务器异常,请联系管理员");
}
}

实际Feign调用实践


全局异常捕获
https://yicizhang00.github.io/posts/编程语言/Java/工程实践/全局异常捕获/
作者
Yici Zhang
发布于
2026年1月5日
许可协议