全局异常处理+封装返回对象

全局异常处理流程

  • 自定义业务状态码枚举类,规范状态码
  • 封装返回对象例如Result
  • 自定义异常类BusinessException,这类异常是可预见性的,直接手动throw异常抛出就可以捕获到,优先级最高
  • 全局异常处理器@RestControllerAdvice
  • 这套方案能够在异常处理器里面精确处理各种异常的返回情况,同时不用大量的try-catch代码,可预见的直接抛异常,不可预见的也可以被拦截进行处理

ResponseEntity

  • 能封装完整的响应信息包括响应头,响应体,响应状态码。
  • return ResponseEntity.badRequest().body(Map.of("code", 400, "message", "Username and password cannot be empty"));
   这个badRequest中就封装了一个HttpStatus的状态码
    public static BodyBuilder badRequest() {
        return status(HttpStatus.BAD_REQUEST);
    }
  • return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("code", 500, "message", "Internal server error"));

  • ResponseEntity.status(HttpStatus status) 静态方法,获取构建器,HttpStatus是一个枚举类,里面枚举了许多状态码。

  • 常用状态码:

    • 2xx,成功状态码。如OK(200)请求完全成功,create(201)资源创建成功
    • 3xx,重定向状态码。MOVED_PERMANENTLY(301)永久重定向,访问旧域名返回301重定向到新域名。302临时重定向,用户未登录需要访问需要权限的页面就会重定向到登录页)
    • 4xx,客户端异常码,400请求参数/地址错误,401用户未认证/未登录/认证失效,403已认证但是无权限,404未找到请求资源,405请求方法不支持,比如get请求使用了post请求
    • 5xx,服务端错误状态码,500服务器内部报错,503服务器不可用
      image
  • body() 方法会创建 ResponseEntity 实例,将传入的 Map 作为响应体(body), 传入的 HttpStatus.INTERNAL_SERVER_ERROR 作为状态码(status),响应头默认为空(父类 HttpEntity 初始化空响应头),响应头也可以设置

  • 相关源码

    //BodyBuilder接口去继承HeadersBuilder
    public interface BodyBuilder extends HeadersBuilder<BodyBuilder> {
        BodyBuilder contentLength(long contentLength);

        BodyBuilder contentType(MediaType contentType);

        <T> ResponseEntity<T> body(@Nullable T body);
    }

   //这个静态类去实现BodyBuilder以及他的父类里面所有的方法
    private static class DefaultBuilder implements BodyBuilder{
        //在这个接口里面的一些方法返回值都是BodyBuilder,最后通过return this返回相同的对象,这个是实现函数链式调用的核心
        //我们最后使用这个body函数他对响应头,响应体,响应状态码进行封装返回一个ResponseEntity结束这个链式调用
        public BodyBuilder header(String headerName, String... headerValues) {
            for(String headerValue : headerValues) {
                this.headers.add(headerName, headerValue);
            }

            return this;
        }

        public BodyBuilder headers(@Nullable HttpHeaders headers) {
            if (headers != null) {
                this.headers.putAll(headers);
            }

            return this;
        }

        public BodyBuilder headers(Consumer<HttpHeaders> headersConsumer) {
            headersConsumer.accept(this.headers);
            return this;
        }
  }

Result

  • 本质是一个普通的 POJO,主要封装了业务状态码(code),业务提示信息(message),业务数据(data),请求时间戳(便于问题排查,大厂必加)timestamp,静态工具类分为成功返回和失败返回两种;
public class Result<T> {
    // 业务状态码
    private Integer code;
    // 业务提示信息
    private String msg;
    // 业务数据
    private T data;

    // 静态工具方法:成功响应(带数据)
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(0);
        result.setMsg("操作成功");
        result.setData(data);
        return result;
    }

    // 静态工具方法:失败响应
    public static <T> Result<T> fail(String msg) {
        Result<T> result = new Result<>();
        result.setCode(1);
        result.setMsg(msg);
        result.setData(null);
        return result;
    }
}

@RestControllerAdvice全局处理器

  • 在Spring异常处理体系中HandlerExceptionResolver是顶级接口,而RestControllerAdvice是对这个顶级接口的高级封装实现,里面拦截的顺序是子类优先级 > 父类优先级。 如果都是子类那么谁先抛出异常谁先执行拦截。

/**
 * 全局异常处理器(大厂标准:统一拦截所有控制器异常)
 * @RestControllerAdvice = @ControllerAdvice + @ResponseBody,自动返回JSON格式
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    // ==================== 1. 捕获自定义业务异常(优先级最高,可预见异常) ====================
    @ExceptionHandler(BusinessException.class)
    public GlobalResult<?> handleBusinessException(BusinessException e) {
        // 业务异常:只打印普通日志(无需堆栈,因为是主动抛出的可预见异常)
        log.warn("业务异常:{}", e.getMessage());
        // 返回业务异常信息给前端(友好提示)
        return GlobalResult.fail(e);
    }

    // ==================== 2. 捕获参数校验异常(高频场景,如@NotBlank、@NotNull校验失败) ====================
    // 处理 @Valid 注解的请求体参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public GlobalResult<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        // 拼接所有参数错误信息(返回给前端,明确提示哪个参数有误)
        String errorMsg = e.getBindingResult().getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining(";"));
        // 打印日志
        log.warn("参数校验异常:{}", errorMsg);
        // 返回参数校验失败状态码和详细错误信息
        return GlobalResult.fail(ResultCodeEnum.PARAM_VALID_ERROR.getCode(), errorMsg);
    }

    // 处理 @Valid 注解的请求参数(非请求体)校验异常
    @ExceptionHandler(BindException.class)
    public GlobalResult<?> handleBindException(BindException e) {
        String errorMsg = e.getBindingResult().getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining(";"));
        log.warn("参数绑定异常:{}", errorMsg);
        return GlobalResult.fail(ResultCodeEnum.PARAM_VALID_ERROR.getCode(), errorMsg);
    }

    // ==================== 3. 捕获特定系统异常(精准处理,如404) ====================
    // 处理接口不存在异常(404)
    @ExceptionHandler(NoHandlerFoundException.class)
    public GlobalResult<?> handleNoHandlerFoundException(NoHandlerFoundException e) {
        String errorMsg = "请求接口不存在:" + e.getRequestURL();
        // 打印日志
        log.warn(errorMsg);
        return GlobalResult.fail(ResultCodeEnum.RESOURCE_NOT_FOUND.getCode(), errorMsg);
    }

    // ==================== 4. 兜底捕获所有未处理异常(优先级最低,不可预见系统异常) ====================
    @ExceptionHandler(Exception.class)
    public GlobalResult<?> handleException(Exception e) {
        // 系统异常:打印完整堆栈日志(便于排查问题,大厂必做)
        log.error("系统内部异常", e);
        // 对外返回友好提示,不暴露具体异常信息(安全考虑)
        return GlobalResult.fail(ResultCodeEnum.SYSTEM_ERROR);
    }
}
posted @ 2025-12-29 23:06  Huangyien  阅读(14)  评论(0)    收藏  举报