自定义全局异常处理器优雅返回异常信息
在开发 Spring Boot RESTful API 时,散落在各处的 try/catch
会导致代码冗余,且难以统一维护;而默认的错误页面(Whitelabel)或 /error
返回的 JSON 也不够友好。为此,我们需要一种「优雅且可扩展」的全局异常捕获方案,既能统一处理参数校验、业务异常和系统异常,又能以规范的 JSON 结构反馈给前端。
-
分散的异常处理:业务代码中到处写
catch
,既影响可读性,也不利于集中日志和监控。 -
统一性需求:前端期望所有接口都返回
{ code, message, data?, timestamp, path }
这样的统一格式,方便解析与展示。 -
分层次拦截:校验异常、业务异常和系统异常需要不同等级的日志和不同的返回结构。
全局异常处理器:CustomGlobalExceptionHandler
@RestControllerAdvice @Slf4j public class CustomGlobalExceptionHandler { // 通用异常 @ExceptionHandler(Exception.class) public ResponseEntity<CustomJsonResult> handleAll(Exception ex) { log.error("系统异常,URI={},错误信息:", RequestUtil.getPathURI(), ex); CustomJsonResult result = CustomJsonResult.fail(ex.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result); } // 自定义业务异常 @ExceptionHandler(CustomException.class) public ResponseEntity<CustomJsonResult> handleCustomException(CustomException ex) { log.warn("业务异常,URI={},message={}", RequestUtil.getPathURI(), ex.getMessage()); CustomJsonResult result = CustomJsonResult.fail(ex.getMessage()); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result); } // 请求体参数校验失败 @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<CustomJsonResult> handleMethodArgNotValid(MethodArgumentNotValidException ex) { List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(FieldError::getDefaultMessage) .collect(Collectors.toList()); String msg = String.join("; ", errors); log.info("请求体校验失败,URI={},errors={}", RequestUtil.getPathURI(), errors); CustomJsonResult result = CustomJsonResult.fail(msg); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result); } // bean 对象参数(@ModelAttribute)校验失败 @ExceptionHandler(BindException.class) public ResponseEntity<CustomJsonResult> handleBindException(BindException ex) { List<String> errors = ex.getBindingResult() .getAllErrors() .stream() .map(ObjectError::getDefaultMessage) .collect(Collectors.toList()); String msg = String.join("; ", errors); log.info("Bean 参数校验失败,URI={},errors={}", RequestUtil.getPathURI(), errors); CustomJsonResult result = CustomJsonResult.fail(msg); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result); } // 单个参数校验失败(@RequestParam、@PathVariable) @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity<CustomJsonResult> handleConstraintViolation(ConstraintViolationException ex ) { List<String> errors = ex.getConstraintViolations() .stream() .map(ConstraintViolation::getMessage) .collect(Collectors.toList()); String msg = String.join("; ", errors); log.info("单参数校验失败,URI={},errors={}", RequestUtil.getPathURI(), errors); CustomJsonResult result = CustomJsonResult.fail(msg); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result); } }
自定义异常类:CustomException
public class CustomException extends RuntimeException { public CustomException(String message) { super(message); // 保留栈信息 } public CustomException(Exception e){ super(e); } }
全局 API 响应封装:CustomJsonResult
/** * 全局 API 响应封装 */ @Data @Builder @JsonInclude(JsonInclude.Include.NON_NULL) public class CustomJsonResult { private static final String CODE_SUCCESS = "0"; private static final String WAITING_CODE = "1"; private static final String CODE_FAIL = "-1"; public static final String MSG_SUCCESS = "成功"; /** 业务或系统错误码 */ private String code; /** 友好提示消息 */ private String message; /** 时间戳(毫秒) */ private long timestamp; /** 请求路径 */ private String path; /** 通用数据载体 */ private Object data; /** 成功:默认 code + 默认 message */ public static CustomJsonResult success() { return build(CODE_SUCCESS, MSG_SUCCESS, null); } /** 成功:默认 code + 默认 message + data */ public static CustomJsonResult success(Object data) { return build(CODE_SUCCESS, MSG_SUCCESS, data); } /** 成功:默认 code + 自定义 message */ public static CustomJsonResult success(String message) { return build(CODE_SUCCESS, message, null); } /** 成功:默认 code + 自定义 message + data */ public static CustomJsonResult success(Object data, String message) { return build(CODE_SUCCESS, message, data); } /** 失败:默认 code + 自定义 message */ public static CustomJsonResult fail(String message) { return build(CODE_FAIL, message, null); } /** 失败:自定义 code + 自定义 message */ public static CustomJsonResult fail(String code, String message) { return build(code, message, null); } /** 内部统一构造 */ private static CustomJsonResult build(String code, String message, Object data) { return CustomJsonResult.builder() .code(code) .message(message) .timestamp(System.currentTimeMillis()) .path(RequestUtil.getPathURI()) .data(data) .build(); } }
测试异常控制层类:ExceptionController
@Validated @RestController @RequestMapping("exception") public class ExceptionController { @GetMapping("{id}") public ResponseEntity<String> getUser( @PathVariable @Min(value = 1, message = "ID 必须大于等于 1") Long id, @RequestParam @NotBlank(message = "类型不能为空") String type) { // 业务逻辑… return ResponseEntity.ok("查询成功"); } }
测试结果: