文档站点
Skip to content

异常处理 ​

Laravel 的 app/Exceptions/Handler.php 统一处理异常;Spring Boot 的对应物是 @RestControllerAdvice + @ExceptionHandler,代码就写在普通类里,比 Laravel 还直观。

为什么需要全局异常处理 ​

Java 是强类型语言,方法签名都声明「可能抛什么异常」。如果不管,异常会被框架转成默认错误响应(HTML 或简单 JSON),对前端不友好。统一处理可以做到:

  • 返回统一 {code, message, data} 结构
  • 业务异常返回对应业务码,不暴露堆栈
  • 未预期异常返回 500 并记录日志

最小可用:@RestControllerAdvice ​

java
package com.example.demo.common;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ApiResponse<Void> handleBusiness(BusinessException ex) {
        log.warn("业务异常: {}", ex.getMessage());
        return ApiResponse.of(ex.getCode(), ex.getMessage(), null);
    }

    @ExceptionHandler(Exception.class)
    public ApiResponse<Void> handleOther(Exception ex) {
        log.error("未预期异常", ex);
        return ApiResponse.of(500, "服务器开小差了", null);
    }
}
  • @RestControllerAdvice = 全局的「控制器增强」,拦截所有控制器抛出的异常
  • 每个 @ExceptionHandler(某异常.class) 处理一类异常
  • 返回统一结构给前端

类比 Laravel

@RestControllerAdvice ≈ Handler::render() + Handler::report()。不同之处:Spring 按异常类型分类处理,Laravel 靠 instanceof 判断 —— 思想一致,Spring 更「路由化」。

业务异常:自己定义 ​

Laravel 里你 abort(403, '没有权限') 或抛 HttpException;Spring 里定义自己的业务异常,携带业务码:

java
package com.example.demo.common;

import lombok.Getter;

@Getter
public class BusinessException extends RuntimeException {

    private final int code;

    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }

    public static BusinessException notFound(String msg) {
        return new BusinessException(404, msg);
    }

    public static BusinessException badRequest(String msg) {
        return new BusinessException(400, msg);
    }
}

使用:

java
@Service
public class PostService {

    public Post get(Long id) {
        return repository.findById(id)
                .orElseThrow(() -> BusinessException.notFound("帖子不存在: " + id));
    }
}

TIP

Laravel 的 ModelNotFoundException 自动 404;Spring 的 Optional.orElseThrow 手动抛异常,语义完全一样,只是要自己声明。

常用异常类型对照 ​

场景异常(异常处理器可拦截)HTTP
校验失败MethodArgumentNotValidException400
参数类型错误MethodArgumentTypeMismatchException400
路径不存在NoResourceFoundException404
请求体缺失/格式错HttpMessageNotReadableException400
方法不允许HttpRequestMethodNotSupportedException405
自己抛业务异常自定义 BusinessException自定义

配合 @ResponseStatus ​

简单场景可以不写处理器,直接在异常上标注:

java
@ResponseStatus(HttpStatus.NOT_FOUND)
public class PostNotFoundException extends RuntimeException {
    public PostNotFoundException(String message) {
        super(message);
    }
}

抛出去自动 404。适合「只关心状态码、不关心响应结构」的场景。

精确匹配 vs 兜底 ​

@ExceptionHandler 的查找规则:越具体的异常类型越优先。

java
@ExceptionHandler(BusinessException.class)   // 先匹配这个
public ApiResponse<Void> handleBusiness(BusinessException ex) { ... }

@ExceptionHandler(Exception.class)           // 其他所有异常兜底
public ApiResponse<Void> handleOther(Exception ex) { ... }

兜底要写日志

Exception.class 兜底处理器一定要 log.error("...", ex) 记录完整堆栈,否则生产环境出事连日志都没有。Laravel 的 report() 同理。

校验错误详解(返回 422 风格) ​

统一校验错误响应,让前端好解析:

java
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Map<String, String>> handleValidation(MethodArgumentNotValidException ex) {
    Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream()
            .collect(Collectors.toMap(
                    FieldError::getField,
                    FieldError::getDefaultMessage,
                    (a, b) -> a));

    return ApiResponse.of(422, "参数校验失败", errors);
}

输出类似:

json
{
  "code": 422,
  "message": "参数校验失败",
  "data": {
    "title": "标题不能为空",
    "status": "状态最小为 1"
  }
}

对应 Laravel 的 {"errors": {"title": ["..."]}} 结构,字段名到错误信息的映射。

异常链路上的两层处理 ​

还记得 请求生命周期 吗?异常处理有两层:

Controller → @RestControllerAdvice(Spring MVC 层)
          → @ControllerAdvice 没兜住 → Servlet 容器默认错误页(Filter 层)

@RestControllerAdvice 只在 Spring MVC 内生效。Filter 抛的异常(比如 CORS Filter 内部出错)它管不到,需要在 ErrorController 兜底。日常开发,把精力放在 @RestControllerAdvice 即可。

对照 Laravel Handler ​

LaravelSpring Boot
Handler::render($request, $e)@ExceptionHandler(XxxException.class) 方法
Handler::report($e)处理器方法里的日志调用
abort(404)抛自定义异常(带 @ResponseStatus 或处理器)
ModelNotFoundException 自动 404Optional.orElseThrow 手动抛
自定义异常类 + 判断类型自定义异常类 + 按类型分类处理器

实践建议 ​

  1. 业务异常必须统一:定义 BusinessException,业务代码里只抛它
  2. 控制器不 try-catch:除非确实需要局部处理,否则让异常冒泡到全局处理器(和 Laravel「别到处 try-catch」的忠告一致)
  3. 外部依赖异常转换:在 Service 层把第三方/数据层异常转成业务异常再抛
  4. 别吞异常:catch (Exception e) { /* 空 */ } 是 Java 圈最臭的代码之一

接下来,日志:日志。

面向 PHP / Laravel 开发者的 Spring Boot 中文文档