第 47 章:统一返回与异常处理
学习目标
- 自定义统一返回
Result<T> - 全局异常处理器
- 业务异常统一封装
一、统一返回 Result<T>
问题:每个接口返回不同结构,前端要写一堆 if-else。
解决:所有接口统一返回 Result<T>。
java
@Data
public class Result<T> {
private int code; // 200 = 成功,其他 = 失败
private String message;
private T data;
private String traceId; // 链路 ID,方便排查
public static <T> Result<T> ok(T data) {
Result<T> r = new Result<>();
r.setCode(200);
r.setMessage("OK");
r.setData(data);
return r;
}
public static <T> Result<T> error(int code, String message) {
Result<T> r = new Result<>();
r.setCode(code);
r.setMessage(message);
return r;
}
}Controller:
java
@GetMapping("/{id}")
public Result<UserVO> getById(@PathVariable Long id) {
return Result.ok(userService.getById(id));
}二、错误码枚举
java
public enum ErrorCode {
SUCCESS(200, "OK"),
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "未登录"),
FORBIDDEN(403, "无权限"),
NOT_FOUND(404, "资源不存在"),
INTERNAL_ERROR(500, "服务器内部错误"),
// 业务错误(10000+)
USER_NOT_FOUND(10001, "用户不存在"),
USER_PASSWORD_ERROR(10002, "密码错误"),
ORDER_ALREADY_PAID(20001, "订单已支付");
private final int code;
private final String message;
ErrorCode(int code, String message) { this.code = code; this.message = message; }
public int getCode() { return code; }
public String getMessage() { return message; }
}三、业务异常
java
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.code = errorCode.getCode();
}
public int getCode() { return code; }
}用法:
java
public User getById(Long id) {
return userMapper.selectById(id)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
}⚠️ 坑 1:业务异常用
RuntimeException,不要继承Exception(受检异常,会强制 try-catch,污染代码)。
四、@RestControllerAdvice 全局异常处理
java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// ① 业务异常
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusiness(BusinessException e) {
log.warn("业务异常: code={}, msg={}", e.getCode(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
// ② 参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.joining("; "));
return Result.error(400, msg);
}
// ③ 兜底:未知异常
@ExceptionHandler(Exception.class)
public Result<Void> handleUnknown(Exception e) {
log.error("系统异常", e); // 详细日志
return Result.error(500, "服务器开小差,请稍后再试"); // 友好提示
}
}⚠️ 坑 2:⚠️ 兜底里千万别把异常 message 直接返回给前端!会泄露 SQL、文件路径等内部信息。
五、404 也要走 Result
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NoHandlerFoundException.class)
public Result<Void> handle404(NoHandlerFoundException e) {
return Result.error(404, "接口不存在: " + e.getRequestURL());
}
}需要在 application.yml 开启:
yaml
spring:
mvc:
throw-exception-if-no-handler-found: true
web:
resources:
add-mappings: false # 禁用默认静态资源处理六、异常处理顺序
Spring 按最具体匹配:
BusinessException → 走 handler1
MethodArgumentNotValidException → 走 handler2
Exception → 走 handler3(兜底)⚠️ 坑 3:兜底 handler 里千万不要
throw new RuntimeException(e)——会再走一遍,然后死循环。
七、自定义错误码的国际化
如果需要中英文切换,把 message 放 messages.properties:
properties
# messages_zh_CN.properties
error.user.not_found=用户不存在
error.user.password_error=密码错误java
public BusinessException(ErrorCode code, Object... args) {
super(messageSource.getMessage(code.getMessageKey(), args, LocaleContextHolder.getLocale()));
}简单项目不用做国际化,直接把中文 message 放在枚举里就行。
八、本章小结
| 要点 | 关键 |
|---|---|
| 统一返回 | Result<T> + 静态工厂方法 |
| 错误码 | enum 集中管理 |
| 业务异常 | extends RuntimeException |
| 全局处理 | @RestControllerAdvice + @ExceptionHandler |
| 兜底 | 详细日志 + 友好提示,不泄露内部信息 |
动手练习
- 实现
Result<T>、ErrorCode、BusinessException、GlobalExceptionHandler - 写个 controller 抛
BusinessException,看返回结构 - 故意写个除零异常,看兜底 handler 是否安全
下一章:第 48 章:参数校验 →