Spring 异常处理器:从混乱到有序,优雅处理所有异常

发布于:2025-09-09 ⋅ 阅读:(22) ⋅ 点赞:(0)

Spring 异常处理器:从混乱到有序,优雅处理所有异常

在 Java 开发中,异常处理是绕不开的话题。如果每个接口都用 try-catch 处理异常,不仅代码冗余,还可能导致异常处理逻辑分散、不一致(比如有的返回 500,有的返回自定义消息)。Spring 提供了一套强大的异常处理机制,能帮我们集中管理所有异常,让代码更简洁,异常响应更规范。

这篇文章从 “痛点分析” 到 “实战落地”,带你掌握 Spring 异常处理器的核心用法,让异常处理从 “混乱不堪” 变得 “井然有序”。

一、先看痛点:传统异常处理的问题

在没有统一异常处理时,我们通常会这样写代码:

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/user/{id}")
    public User getUserById(@PathVariable Long id) {
        try {
            // 业务逻辑
            return userService.getById(id);
        } catch (NullPointerException e) {
            // 处理空指针异常
            throw new RuntimeException("用户不存在");
        } catch (IllegalArgumentException e) {
            // 处理参数异常
            throw new RuntimeException("参数错误:" + e.getMessage());
        } catch (Exception e) {
            // 处理其他异常
            throw new RuntimeException("服务器出错了");
        }
    }
}

这种方式的问题很明显:

  • 代码冗余:每个接口都要写重复的 try-catch;

  • 风格不统一:不同开发者可能返回不同格式的错误信息;

  • 维护困难:需要修改异常处理逻辑时,要改所有接口;

  • 无法全局捕获:比如拦截器、过滤器中的异常可能漏处理。

二、Spring 异常处理的核心方案:@ControllerAdvice + @ExceptionHandler

Spring 提供了 全局异常处理器 机制,通过 @ControllerAdvice(控制器增强)和 @ExceptionHandler(异常处理器)注解,能将所有异常处理逻辑集中到一个类中,彻底解决上述问题。

核心原理:

  • @ControllerAdvice:标记一个类为 “全局异常处理类”,Spring 会自动扫描并生效;

  • @ExceptionHandler(异常类型.class):标记方法为 “特定异常的处理器”,当系统抛出该类型异常时,会自动调用该方法处理。

三、实战:实现全局异常处理器

以 “前后端分离项目” 为例,实现一个全局异常处理器,统一返回 JSON 格式的错误信息(包含状态码、错误消息、时间戳)。

步骤 1:定义统一的异常响应格式

先创建一个 “异常响应 DTO”,规范返回给前端的错误信息格式:

import lombok.Data;

import java.time.LocalDateTime;

// 统一异常响应格式
@Data
public class ErrorResponse {
    private Integer code; // 状态码(如 400、500)
    private String message; // 错误消息
    private LocalDateTime timestamp; // 发生时间

    public ErrorResponse(Integer code, String message) {
        this.code = code;
        this.message = message;
        this.timestamp = LocalDateTime.now();
    }
}

步骤 2:创建全局异常处理器类

用 @ControllerAdvice 和 @ExceptionHandler 实现全局异常处理:

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

// 全局异常处理器(会处理所有 @Controller 标注的类的异常)
@ControllerAdvice
public class GlobalExceptionHandler {

    // 1. 处理自定义业务异常(最常用)
    @ExceptionHandler(BusinessException.class) // 指定处理 BusinessException 类型的异常
    @ResponseBody // 返回 JSON 格式
    public ErrorResponse handleBusinessException(BusinessException e) {
        // 返回自定义状态码和异常消息
        return new ErrorResponse(400, e.getMessage());
    }

    // 2. 处理空指针异常(系统异常示例)
    @ExceptionHandler(NullPointerException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 指定 HTTP 状态码为 500
    public ErrorResponse handleNullPointerException(NullPointerException e) {
        // 生产环境中不建议返回具体异常信息,避免泄露细节
        return new ErrorResponse(500, "服务器内部错误:空指针异常");
    }

    // 3. 处理参数绑定异常(如请求参数格式错误)
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseBody
    public ErrorResponse handleIllegalArgumentException(IllegalArgumentException e) {
        return new ErrorResponse(400, "参数错误:" + e.getMessage());
    }

    // 4. 处理所有未捕获的异常(兜底处理)
    @ExceptionHandler(Exception.class) // 父类异常,会捕获所有未被上面方法处理的异常
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleAllUncaughtException(Exception e) {
        return new ErrorResponse(500, "服务器繁忙,请稍后再试");
    }
}

步骤 3:定义自定义业务异常(可选但推荐)

实际开发中,业务逻辑异常(如 “用户已存在”“余额不足”)建议用自定义异常,更便于分类处理:

// 自定义业务异常
public class BusinessException extends RuntimeException {
    // 构造方法,接收错误消息
    public BusinessException(String message) {
        super(message);
    }
}

步骤 4:在业务中抛出异常

在 Service 或 Controller 中直接抛出异常,无需手动 try-catch,全局异常处理器会自动捕获并处理:

@Service
public class UserService {
    public User getById(Long id) {
        if (id == null || id <= 0) {
            // 抛出自定义业务异常(会被 handleBusinessException 处理)
            throw new BusinessException("用户ID必须大于0");
        }
        
        // 模拟查询用户(若查询结果为 null,会抛出空指针异常)
        User user = userMapper.selectById(id);
        if (user == null) {
            throw new BusinessException("用户不存在,ID:" + id);
        }
        return user;
    }
}

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    // 接口中无需 try-catch,直接调用业务方法
    @GetMapping("/user/{id}")
    public User getUserById(@PathVariable Long id) {
        // 抛出的异常会被 GlobalExceptionHandler 自动处理
        return userService.getById(id);
    }
}

测试效果:

  • 访问 /user/-1 → 触发 BusinessException → 返回 {“code”:400,“message”:“用户ID必须大于0”,“timestamp”:“xxx”};

  • 访问 /user/999(不存在的 ID) → 触发 BusinessException → 返回 {“code”:400,“message”:“用户不存在,ID:999”,“timestamp”:“xxx”};

  • 若 Service 中出现 null.getXXX() → 触发 NullPointerException → 返回 {“code”:500,“message”:“服务器内部错误:空指针异常”,“timestamp”:“xxx”}。

四、不同场景的适配:传统项目与前后端分离

全局异常处理器可根据项目类型(前后端分离 / 传统 JSP)返回不同结果:

1. 前后端分离(返回 JSON)

如上面的示例,用 @ResponseBody 直接返回 ErrorResponse 对象(自动序列化为 JSON)。

2. 传统项目(返回错误页面)

若项目用 JSP/Thymeleaf 渲染页面,可返回错误视图名:

@ControllerAdvice
public class GlobalExceptionHandler {

    // 处理业务异常,返回错误页面
    @ExceptionHandler(BusinessException.class)
    public ModelAndView handleBusinessException(BusinessException e) {
        ModelAndView mv = new ModelAndView();
        mv.addObject("errorMsg", e.getMessage()); // 错误消息
        mv.setViewName("error"); // 错误页面(如 error.jsp)
        return mv;
    }
}

五、异常处理的优先级:精确匹配优先

当多个异常处理器都能处理某个异常时(如子类异常和父类异常),Spring 会遵循 “精确匹配优先” 原则:

  • 先执行最匹配的异常处理器(子类异常处理器);

  • 若没有,则执行父类异常处理器。

示例

  • 抛出 NullPointerException 时,会优先执行 @ExceptionHandler(NullPointerException.class) 标注的方法;

  • 若没有专门处理 NullPointerException 的方法,才会执行 @ExceptionHandler(Exception.class) 标注的兜底方法。

六、避坑指南:这些错误别犯

1. 异常处理器未生效?检查这 3 点

  • 确保类上标注了 @ControllerAdvice;

  • 确保异常处理器方法上标注了 @ExceptionHandler 并指定了正确的异常类型;

  • 确保 Spring 能扫描到异常处理器类(在 @ComponentScan 的扫描路径内)。

2. 不要在异常处理器中 “吞掉异常”

错误示例:

@ExceptionHandler(Exception.class)
public ErrorResponse handleException(Exception e) {
    // 错误:没有记录异常堆栈,难以排查问题
    return new ErrorResponse(500, "服务器错误");
}

正确做法:记录异常堆栈(至少在开发环境):

@ExceptionHandler(Exception.class)
public ErrorResponse handleException(Exception e) {
    // 记录异常详情(日志框架如 Logback/Log4j)
    log.error("未捕获异常", e); // 关键:打印堆栈信息
    return new ErrorResponse(500, "服务器繁忙,请稍后再试");
}

3. 自定义异常建议继承 RuntimeException

Spring 事务默认只对 RuntimeException 及其子类回滚。若自定义异常继承 Exception(受检异常),需手动配置 @Transactional(rollbackFor = 自定义异常.class) 才会回滚事务,增加复杂度。

七、总结:全局异常处理器的核心价值

  1. 代码简洁:消除重复的 try-catch,控制器和服务层只需专注业务逻辑;

  2. 统一规范:所有异常返回格式一致(状态码、消息结构),前端处理更简单;

  3. 易于维护:异常处理逻辑集中在一个类,修改时只需改一处;

  4. 覆盖全面:能捕获控制器、服务层、甚至拦截器中的异常(过滤器中的异常需额外处理)。

掌握 @ControllerAdvice + @ExceptionHandler 是 Spring 开发的必备技能,无论是小型项目还是大型企业应用,这套机制都能显著提升异常处理的效率和规范性。实际开发中,建议结合自定义业务异常,让异常分类更清晰,处理更精准。


网站公告

今日签到

点亮在社区的每一天
去签到