问题:每个接口各自写 try-catch 会带来什么麻烦
Controller 里到处写 try-catch,时间一长就会出现三个问题:返回格式不统一,前端要针对每种错误单独判断;参数校验失败时框架默认返回的错误体字段冗长,前端很难直接展示;业务校验和系统异常混在一起,出问题后既不好定位,也不好给用户一句能看懂的提示。适用场景很明确:对外提供 REST API、前后端分离、希望错误格式全站统一的 Spring Boot 项目。
第一步:先定一个统一返回结构
所有接口都返回同一个结构,成功与失败只在字段值上区分,HTTP 状态语义仍然保留。结构里一般有三个字段:业务码、提示信息、业务数据,并提供成功与失败两个静态构造方法。业务异常单独定义一个带业务码的运行时异常,继承 RuntimeException 即可,校验失败和参数问题则由框架异常承载,不需要为每一种情况都新建异常类。
第二步:用全局异常处理收口
一个标了 RestControllerAdvice 的类就能接管全站 Controller 抛出的异常,业务代码里只需要 throw,不用再写 try-catch。校验异常单独处理,业务异常返回业务码,未知异常返回通用提示:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResult<Void>> handleBusiness(BusinessException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResult.error(e.getCode(), e.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResult<Void>> handleValid(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.findFirst().map(f -> f.getDefaultMessage()).orElse("参数校验失败");
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResult.error(400, message));
}
}
参数校验本身不需要额外代码,入参上加 Valid 注解、字段上加 NotBlank、NotNull 这类校验注解即可,校验失败会自动走到上面的 handleValid 方法。Spring Boot 3 的校验包名是 jakarta.validation,写注解时注意不要混用旧的 javax 包名。
注意点
- 只处理可预期的异常:业务异常、参数校验异常按约定返回,未知异常一律走通用提示,不要为了省事把所有异常都映射成“成功”。
- 不要把异常堆栈、SQL 语句、内部文件路径返回给前端,完整信息只写进服务端日志,定位问题靠日志,不靠前端报错。
- 校验失败只返回第一条字段错误,避免一次返回十几条、前端难展示。
- 不要在全局处理里把未覆盖的异常包成成功返回:交给默认处理,比“假成功”更易发现问题。
- 业务码和 HTTP 状态各司其职:HTTP 状态表达请求层面的成败,业务码表达业务层面的原因,两边不要互相替代。
落地顺序就是:定结构、收口处理、接校验。后续新增接口只管抛异常,格式自然统一。