@RestControllerAdvice
@RestControllerAdvice 是 Spring Boot 专为 RESTful 接口设计的全局增强注解,它是 @ControllerAdvice + @ResponseBody 的组合注解,核心作用是全局统一处理 Controller 层的异常、全局绑定数据、全局预处理请求参数,是企业级项目中统一接口响应、统一异常处理的标准方案。
一、核心作用(3大场景)
- 全局统一异常处理(最常用,90%的使用场景)
- 全局数据绑定(所有接口共享公共参数)
- 全局请求参数预处理(参数格式化、去空格、类型转换)
二、环境依赖
只需引入 Spring Web 依赖(Spring Boot 项目默认已包含):
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>2
3
4
5
三、最常用:全局统一异常处理(实战必用)
步骤1:定义统一响应体
接口需要固定返回格式,先封装通用结果类:
import lombok.Data;
/**
* 统一API响应结果
*/
@Data
public class Result<T> {
private int code; // 响应码
private String msg; // 响应消息
private T data; // 响应数据
// 成功响应
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("操作成功");
result.setData(data);
return result;
}
// 失败响应
public static <T> Result<T> fail(int code, String msg) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMsg(msg);
return result;
}
}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
步骤2:创建全局异常处理器
使用 @RestControllerAdvice + @ExceptionHandler 捕获所有接口异常:
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
/**
* 全局异常处理器(所有REST接口生效)
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
// ====================== 1. 捕获自定义业务异常 ======================
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
return Result.fail(e.getCode(), e.getMessage());
}
// ====================== 2. 捕获参数校验异常(@Valid) ======================
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidException(MethodArgumentNotValidException e) {
// 获取第一个校验错误信息
String message = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.fail(400, message);
}
// ====================== 3. 捕获404资源不存在异常 ======================
@ExceptionHandler(NoResourceFoundException.class)
public Result<Void> handle404Exception() {
return Result.fail(404, "请求资源不存在");
}
// ====================== 4. 捕获空指针异常 ======================
@ExceptionHandler(NullPointerException.class)
public Result<Void> handleNullPointerException() {
return Result.fail(500, "系统异常:空指针错误");
}
// ====================== 5. 捕获所有未匹配的异常(兜底) ======================
@ExceptionHandler(Exception.class)
public Result<Void> handleGlobalException(Exception e) {
e.printStackTrace(); // 打印日志
return Result.fail(500, "服务器内部错误");
}
}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
步骤3:自定义业务异常(可选)
import lombok.Data;
/**
* 自定义业务异常
*/
@Data
public class BusinessException extends RuntimeException {
private int code;
private String msg;
public BusinessException(int code, String msg) {
this.code = code;
this.msg = msg;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
步骤4:测试接口
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
// 测试业务异常
@GetMapping("/test")
public Result<String> test() {
throw new BusinessException(401, "用户未登录");
}
// 测试空指针
@GetMapping("/null")
public Result<String> testNull() {
String str = null;
str.length();
return Result.success("");
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
效果
所有接口的异常都会被统一捕获,返回标准JSON格式,前端无需处理各种混乱的异常响应。
四、精准控制生效范围(进阶)
默认 @RestControllerAdvice 对所有Controller生效,可通过属性指定生效包/类:
// 1. 只对指定包下的Controller生效(推荐)
@RestControllerAdvice(basePackages = "com.example.project.controller")
// 2. 只对指定类生效
@RestControllerAdvice(assignableTypes = {UserController.class, OrderController.class})
// 3. 按类所在包生效
@RestControllerAdvice(basePackageClasses = UserController.class)2
3
4
5
6
7
8
五、其他扩展用法
全局数据绑定(所有接口共享公共数据)
使用 @ModelAttribute,所有接口都能获取到绑定的数据:
@RestControllerAdvice
public class GlobalDataConfig {
// 所有接口都能获取到 appName 字段
@ModelAttribute("appName")
public String appName() {
return "SpringBoot 项目";
}
}2
3
4
5
6
7
8
全局参数预处理(格式化参数)
使用 @InitBinder,统一处理请求参数(如去空格、日期格式化):
@RestControllerAdvice
public class GlobalParamHandler {
@InitBinder
public void initBinder(WebDataBinder binder) {
// 字符串参数自动去空格
binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
}
}2
3
4
5
6
7
8
六、关键注意事项
匹配优先级:
@ExceptionHandler会优先匹配精确异常,再匹配父类异常(兜底用Exception.class)。扫描生效:全局异常处理器必须被 Spring 扫描到(放在主启动类同级/子包下)。
与
@ControllerAdvice区别:@RestControllerAdvice:返回 JSON 数据(REST接口专用);@ControllerAdvice:返回 页面/视图(传统Web项目用)。
无侵入性:无需修改原有Controller代码,全局统一处理。
总结
@RestControllerAdvice是 Spring Boot REST 接口全局增强神器,核心用于统一异常处理;- 搭配
@ExceptionHandler可捕获所有接口异常,返回标准化响应; - 支持精准控制生效范围,无侵入、易维护,是企业级项目必备配置。
Spring Boot异常处理:别被
@RestControllerAdvice“坑”了!
事故引入:支付异常却显示成功
我正在工位上悠哉悠哉地喝着咖啡,享受着片刻的宁静。突然,手机铃声大作,打破了这份惬意。电话那头传来财务小姐姐焦急的声音:“不好啦!有用户反馈支付成功了,但钱却没到账,已经有好几个用户都这么说了!” 我心里 “咯噔” 一下,暗叫不好,赶紧打开后台日志查看。
经过一番排查,很快定位到了问题所在。原来是支付回调接口出了岔子,代码是这样写的:
@PostMapping("/pay/callback")
public String handlePayCallback(@RequestBody PayRequest request) {
payService.process(request); // 这里抛了异常!
return "success"; // 但前端收到这个!
}2
3
4
5
从代码里可以看出,payService.process(request) 这行代码抛出了异常,可由于使用了 @RestControllerAdvice 全局捕获异常,返回给前端的竟然是 “success”,支付平台也以为支付成功了,结果用户被扣了钱,订单却没生成。这可真是个大乌龙!这次事故给公司带来了不小的损失,也让我深刻认识到 Spring Boot 异常处理可不能掉以轻心。今天,我就把自己总结的 3 个 Spring Boot 异常处理的隐藏陷阱分享给大家,希望能帮助大家在开发中少踩坑。
陷阱 1:@RestControllerAdvice 一锅端,协议不兼容
错误示范
在 Spring Boot 开发中,很多人习惯使用@RestControllerAdvice进行全局异常处理,代码类似这样:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error("系统异常", e);
return Result.error("系统繁忙");
}
}2
3
4
5
6
7
8
看起来很完美,所有异常都被统一处理了。但问题在于,这种 “一锅端” 的方式没有考虑到不同接口协议的特殊性。比如,支付回调接口要求失败时返回特定字符串,像支付宝回调失败需返回fail字符串;微信支付失败要返回 XML 格式,如 <return_code>FAIL</return_code>;邮件 Webhook 失败则需返回 HTTP 5xx 状态码 。而上述全局异常处理器统一返回 JSON 格式数据和 HTTP 200 状态码,完全不符合第三方平台要求。
产生后果
在实际业务中,这种错误处理方式会引发严重问题。就拿开篇提到的支付回调事故来说,由于异常被全局处理器捕获后返回了 HTTP 200 和错误 JSON,支付平台误判支付成功,用户被扣钱但订单却未生成,给公司造成直接经济损失。再比如短信回调接口,如果因为异常处理不当返回错误信息,可能导致验证码失效,影响用户正常注册登录 。Webhook 接口若返回错误格式,也会导致数据传输异常,影响业务流程的正常运转。
解决方案
为了避免这种情况,我们需要采用按路径 / 协议分治的生产级方案。针对不同类型接口,创建不同的异常处理器:
// 1. 普通API异常处理器(返回JSON)
@RestControllerAdvice(basePackages = "com.example.api")
public class ApiExceptionHandler {
@ExceptionHandler(ServiceException.class)
public Result<?> handleServiceException(ServiceException e) {
return Result.error(e.getCode(), e.getMessage());
}
}
// 2. 支付回调专用处理器(返回字符串)
@RestControllerAdvice(assignableTypes = PayCallbackController.class)
public class PayCallbackExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handlePayException(Exception e) {
log.error("支付回调异常", e);
// 支付宝/微信要求:失败返回"fail"
return ResponseEntity.status(200).body("fail");
}
}
// 3. Webhook专用处理器(返回5xx)
@RestControllerAdvice(assignableTypes = WebhookController.class)
public class WebhookExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<Void> handleWebhookException() {
// 明确返回500,触发第三方重试
return ResponseEntity.status(500).build();
}
}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
这里用到两个关键注解:basePackages 按包路径隔离,适用于对某一路径下的接口进行统一异常处理;assignableTypes 按 Controller 类隔离,更加精准地针对特定 Controller 处理异常 。验证时,可以用 Postman 调支付回调接口,故意传错参数,检查是否返回fail而非 JSON,以此确保异常处理器按预期工作。
陷阱 2:吞掉异常不记录,排查困难
错误示范
再来看第二个陷阱,捕获异常后不记录日志。很多人在处理异常时,只想着返回给前端一个友好的错误提示,却忽略了记录日志的重要性。比如这样的代码:
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusiness(BusinessException e) {
// 只返回错误,不记日志!
return Result.error(e.getMessage());
}2
3
4
5
这段代码捕获了业务异常 BusinessException,然后直接返回错误信息给前端,没有记录任何日志。看似简单方便,实则隐藏着巨大的隐患。
产生后果
在实际业务中,这种做法会让排查问题变得异常困难。想象一下,用户反馈 “下单失败”,但运维去查看日志时,却发现没有任何相关记录,CPU、内存、DB 全正常。最后只能靠用户截图才发现是库存不足异常。这时候老板就会灵魂拷问:“为什么没告警?为什么没日志?” 你只能哑口无言,因为异常被 “静默” 处理了。没有日志,就像在黑暗中摸索,根本不知道问题出在哪里,更别说快速解决问题了。
解决方案
为了解决这个问题,我们需要采用分级日志 + 关键字段脱敏的生产级方案。具体代码如下:
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusiness(BusinessException e, HttpServletRequest request) {
//1. 记录WARN日志(业务异常需关注)
log.warn("业务异常 | URI:{} | User:{} | Error:{}",
request.getRequestURI(),
getCurrentUserId(), // 从Token解析
e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<?> handleSystem(Exception e, HttpServletRequest request) {
//2. 记录ERROR日志(系统异常需告警)
String traceId = MDC.get("TRACE_ID"); // 链路ID
log.error("系统异常 | TraceId:{} | URI:{} | Params:{}",
traceId,
request.getRequestURI(),
maskSensitiveParams(request.getParameterMap()) // 脱敏!
, e);
//3. 触发告警(可选)
alertService.send("系统异常", e.getMessage());
return Result.error("系统繁忙,请稍后重试");
}
// 敏感参数脱敏工具
private String maskSensitiveParams(Map<String, String[]> params) {
Map<String, String> safeParams = new HashMap<>();
for (String key : params.keySet()) {
if (key.contains("password") || key.contains("card")) {
safeParams.put(key, "******");
} else {
safeParams.put(key, String.join(",", params.get(key)));
}
}
return JsonUtil.toJson(safeParams);
}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
这里对业务异常和系统异常进行了分级处理:业务异常记录 WARN 日志,因为业务异常通常是可预期的,如库存不足、参数错误等,只需要关注即可;系统异常记录 ERROR 日志,并触发告警,因为系统异常往往是严重的问题,如数据库连接失败、空指针异常等,需要及时处理 。同时,记录了关键信息,如用户 ID(从 Token 解析获取,用于快速定位问题用户)、链路 ID(通过 MDC 获取,用于全链路追踪),并对敏感参数进行了脱敏处理,防止密码、银行卡号等敏感信息泄露 。在实际应用中,一定要注意日志的配置,确保日志能够正确记录并保存,同时要定期清理日志,防止日志文件过大影响系统性能。
陷阱 3:返回码混乱,前端痛苦
错误示范
在处理异常返回码时,很多开发者没有统一的规范,导致返回码混乱不堪。比如下面这段代码:
@ExceptionHandler(NotFoundException.class)
public Result<?> handleNotFound() {
return Result.error(404, "资源未找到");
}
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusiness(BusinessException e) {
// 这里业务异常也返回404?
return Result.error(404, e.getMessage());
}
@ExceptionHandler(AnotherException.class)
public Result<?> handleAnother() {
// 又返回个无规则的1001
return Result.error(1001, "其他错误");
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在这个例子中,NotFoundException 和 BusinessException 都返回了 404 错误码,这显然是不合理的。而且 AnotherException 返回的 1001 错误码毫无规律,让人摸不着头脑。
产生后果
返回码混乱会给前端开发带来极大的困扰。前端需要根据返回码来进行不同的处理,比如显示不同的错误提示、进行页面跳转等 。但如果返回码没有规律,前端就难以统一处理错误,增加了开发和维护的成本。更严重的是,这会影响用户体验,导致用户在使用产品时遇到问题却得不到准确的提示,降低用户对产品的信任度。
解决方案
为了解决这个问题,我们需要制定统一的错误码规范。可以参考 HTTP 状态码的设计思路,对错误码进行分段,比如:
1XX:信息,一般用于临时响应,在 Web 开发中较少使用自定义此范围错误码。
2XX:成功,200 表示成功,这是大家最熟悉的。
3XX:重定向,如 301 永久重定向、302 临时重定向 ,在微服务内部调用中不常用来自定义错误码。
4XX:客户端错误,400 表示参数错误,401 表示未认证,403 表示禁止访问,404 表示资源未找到 。
5XX:服务器错误,500 表示服务器内部错误,502 表示网关错误,503 表示服务不可用 。
同时,针对业务异常,可以在 HTTP 状态码的基础上进行扩展,比如 10001 表示用户模块的用户不存在,20001 表示订单模块的订单不存在等 。
后端在返回错误时,严格按照这个规范来返回,前端就可以根据返回码进行针对性的处理。例如:
// 统一错误码定义
public interface ErrorCode {
int SUCCESS = 200;
int BAD_REQUEST = 400;
int UNAUTHORIZED = 401;
int FORBIDDEN = 403;
int NOT_FOUND = 404;
int INTERNAL_SERVER_ERROR = 500;
// 业务异常扩展
int USER_NOT_FOUND = 10001;
int ORDER_NOT_FOUND = 20001;
}
@ExceptionHandler(NotFoundException.class)
public Result<?> handleNotFound() {
return Result.error(ErrorCode.NOT_FOUND, "资源未找到");
}
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusiness(BusinessException e) {
// 根据业务异常类型返回对应的错误码
if (e instanceof UserNotFoundException) {
return Result.error(ErrorCode.USER_NOT_FOUND, "用户不存在");
} else if (e instanceof OrderNotFoundException) {
return Result.error(ErrorCode.ORDER_NOT_FOUND, "订单不存在");
}
return Result.error(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage());
}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
这样,前端只需要根据返回的错误码,就可以知道是哪类错误,从而进行相应的处理,大大提高了开发效率和用户体验。
总结
在 Spring Boot 开发中,异常处理是保障系统稳定运行的关键环节。本文通过支付回调异常的实际案例,深入剖析了使用@RestControllerAdvice进行全局异常处理时可能遇到的 3 个隐藏陷阱:协议不兼容、吞掉异常不记录、返回码混乱 。这些陷阱看似微小,却可能引发严重的生产事故,给公司带来经济损失,影响用户体验 。
针对这些陷阱,我们提出了相应的生产级解决方案:按路径 / 协议分治处理异常,确保不同接口协议得到正确处理;采用分级日志 + 关键字段脱敏,准确记录异常信息,便于排查问题;制定统一的错误码规范,避免返回码混乱,降低前端开发难度 。
