全局异常处理器怎么写?

wen python案例 4

本文目录导读:

全局异常处理器怎么写?

  1. 核心思想
  2. 完整代码实战
  3. 如何使用(示例 Controller)
  4. 进阶与优化建议

我们来详细讲解一下如何编写一个全局异常处理器,这通常是在使用 Spring Boot(或其他 Java Web 框架)时进行统一错误处理的标准做法。

我将以 Spring Boot 中最常用的 @RestControllerAdvice@ExceptionHandler 注解组合为例,为你提供一套完整、规范的写法。

核心思想

全局异常处理器的主要目标是:

  1. 统一捕获:捕获所有 Controller 层抛出的未处理异常。
  2. 统一格式:将异常信息转换为统一的、对前端友好的 JSON 响应体({ "code": 500, "message": "服务器内部错误", "data": null })。
  3. 解耦逻辑:业务代码中只抛出特定异常(如 BusinessException),处理器负责将其翻译成对应的 HTTP 状态码和错误信息。

完整代码实战

定义统一的响应结果类(非常重要)

你需要一个标准的 API 响应格式,这样前端可以统一解析。

import lombok.Data;
@Data
public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;
    // 成功的静态方法
    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(200);
        response.setMessage("success");
        response.setData(data);
        return response;
    }
    // 失败的静态方法
    public static <T> ApiResponse<T> fail(int code, String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(code);
        response.setMessage(message);
        response.setData(null);
        return response;
    }
}

定义自定义业务异常类

为了让异常分类更清晰,建议创建自己的业务异常。

import lombok.Getter;
@Getter
public class BusinessException extends RuntimeException {
    private int code;
    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }
    // 可以预定义一些常用的构造函数
    public BusinessException(String message) {
        super(message);
        this.code = 400; // 默认业务异常为 400 Bad Request
    }
}

编写核心的全局异常处理器(GlobalExceptionHandler)

这是核心部分,使用 @RestControllerAdvice 注解,它结合了 @ControllerAdvice + @ResponseBody

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;
@Slf4j // 使用Lombok记录日志
@RestControllerAdvice // 拦截所有 @RestController 的异常
public class GlobalExceptionHandler {
    // ==================== 1. 捕获自定义的业务异常 ====================
    @ExceptionHandler(BusinessException.class)
    public ApiResponse<Void> handleBusinessException(BusinessException e) {
        log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
        // 使用自定义的 code 和 message 返回
        return ApiResponse.fail(e.getCode(), e.getMessage());
    }
    // ==================== 2. 捕获参数校验异常(@Valid / @Validated) ====================
    // 处理 @RequestBody 上的参数校验失败
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST) // 返回 400 状态码
    public ApiResponse<Void> handleValidationException(MethodArgumentNotValidException e) {
        // 提取所有字段的错误信息,用逗号拼接
        String errorMsg = e.getBindingResult().getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining(", "));
        log.warn("参数校验失败: {}", errorMsg);
        return ApiResponse.fail(400, errorMsg);
    }
    // 处理 @RequestParam / @PathVariable 上的参数校验失败
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Void> handleConstraintViolationException(ConstraintViolationException e) {
        String errorMsg = e.getConstraintViolations().stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.joining(", "));
        log.warn("参数校验失败: {}", errorMsg);
        return ApiResponse.fail(400, errorMsg);
    }
    // ==================== 3. 捕获资源未找到异常 ====================
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND) // 返回 404
    public ApiResponse<Void> handleNotFoundException(NoHandlerFoundException e) {
        log.warn("资源未找到: {}", e.getRequestURL());
        return ApiResponse.fail(404, "请求的资源不存在");
    }
    // ==================== 4. 捕获最顶级的通用异常(兜底) ====================
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 返回 500
    public ApiResponse<Void> handleException(Exception e) {
        // 这里一定要记错误日志,方便排查问题
        log.error("系统内部异常: ", e);
        // 避免把具体的异常堆栈暴露给前端
        return ApiResponse.fail(500, "服务器内部错误,请联系管理员");
    }
}

如何使用(示例 Controller)

import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@RestController
@RequestMapping("/user")
public class UserController {
    // 1. 业务异常示例
    @GetMapping("/{id}")
    public ApiResponse<User> getUser(@PathVariable Long id) {
        if (id <= 0) {
            throw new BusinessException(1001, "用户ID不能为负数");
        }
        // 正常查询...
        User user = new User(); // 假设查到了
        return ApiResponse.success(user);
    }
    // 2. 参数校验示例(配合 @Valid)
    @PostMapping("/create")
    public ApiResponse<Void> createUser(@Valid @RequestBody UserCreateRequest request) {
        // request 的 name 为空,会触发 MethodArgumentNotValidException
        System.out.println("创建用户: " + request.getName());
        return ApiResponse.success(null);
    }
    // 3. 未捕获的异常示例(会触发兜底的 handleException)
    @GetMapping("/error")
    public ApiResponse<Void> triggerError() {
        int result = 1 / 0; // 抛出 ArithmeticException
        return ApiResponse.success(null);
    }
}
// 请求体 DTO
class UserCreateRequest {
    @NotBlank(message = "用户名不能为空")
    private String name;
    @NotNull(message = "年龄不能为空")
    private Integer age;
    // getters and setters...
}

进阶与优化建议

  1. 异常来源区分

    • 业务异常:用户操作不当(如余额不足),直接返回给用户提示。
    • 系统异常:代码 Bug、数据库连接失败,记录详细日志,但只返回“系统繁忙”给用户。
  2. 日志级别

    • BusinessException:用 log.warn,因为这是预期内的问题。
    • Exception(兜底):用 log.error,并打印完整堆栈 (log.error("...", e))。
  3. 安全问题

    • 不要在返回给前端的 message 中暴露数据库表名、SQL 错误、文件路径等敏感信息。
    • DataIntegrityViolationException(数据库约束违反)应该被转换为 “数据操作失败”,而不是 “Duplicate entry 'xxx' for key 'user.email'”
  4. 使用枚举管理错误码:创建一个 ErrorCode 枚举,避免在代码中写死数字 400500

    public enum ErrorCode {
        SUCCESS(200, "success"),
        BAD_REQUEST(400, "请求参数错误"),
        UNAUTHORIZED(401, "未登录"),
        NOT_FOUND(404, "资源不存在"),
        INTERNAL_ERROR(500, "服务器内部错误"),
        BUSINESS_ERROR(1001, "业务逻辑错误");
        private int code;
        private String message;
        // 构造方法、getter...
    }
  5. 处理 HttpMessageNotReadableException:用户传了无法解析的 JSON 时,返回 400 和清晰的提示。

一个优秀的全局异常处理器应该做到:

  • 分层捕获:自定义异常 -> 框架异常(参数校验、404等) -> 通用异常。
  • 统一响应:所有异常都返回结构一致的 JSON。
  • 日志分级:业务异常 warn,系统异常 error。
  • 信息安全:不向用户暴露系统细节。

按照上面的模板,你就可以快速搭建一个健壮、可维护的全局异常处理机制了。

抱歉,评论功能暂时关闭!