自定义全局异常处理器优雅返回异常信息

在开发 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("查询成功");
    }
}

测试结果:

 

posted @ 2025-04-18 09:21  ~落辰~  阅读(58)  评论(0)    收藏  举报