SpringBoot实现统一异常处理

SpringBoot实现统一异常处理

一、什么是统一异常处理?

统一异常处理是一种集中式异常管理机制,通过定义全局异常处理器,捕获项目中所有未被局部处理的异常,并以统一格式返回响应。它将分散在各处的异常处理逻辑集中管理,避免代码冗余,保证异常响应格式一致。

二、为什么需要统一异常处理?

未使用统一异常处理时,项目通常存在以下问题:

  • 代码冗余:每个接口都需try-catch,重复代码多。
  • 响应格式混乱:不同异常返回格式不一(字符串、Map、500 错误页等),前端解析困难。
  • 安全性问题:未处理的异常可能暴露敏感信息(如数据库表名、类路径)。
  • 调试困难:异常信息分散,难以快速定位问题。

统一异常处理的优势:

  • 代码简洁:消除重复try-catch,专注业务逻辑。
  • 格式统一:所有响应采用标准化结构(状态码、消息、时间戳等),便于前后端对接。
  • 信息安全:屏蔽敏感信息,返回用户友好提示。
  • 便于维护:异常处理逻辑集中,修改只需改一处。

三、为什么引入 Lombok 和 @Slf4j?

  • Lombok:通过注解简化 Java 代码(如@Data自动生成 getter/setter,@NoArgsConstructor生成无参构造等),减少模板代码。
  • @Slf4j:Lombok 提供的注解,自动为类生成SLF4J日志对象(log),无需手动声明private static final Logger log = ...,简化日志使用。

使用@Slf4j后,可直接通过log.info()log.error()等方法记录日志,避免重复的日志对象定义。

四、环境准备

  • JDK:1.8 及以上
  • Spring Boot:2.7.x(本文以此为例,3.x 兼容)
  • 开发工具:IDEA(需安装 Lombok 插件,否则注解可能报错)
  • 核心依赖:spring-boot-starter-web(Web 功能)、lombok(简化代码)

五、实现过程

image-20251024105450515

步骤 1:创建 Spring Boot 项目并添加依赖

通过 Spring Initializr 创建项目,修改pox.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!-- SpringBoot父依赖 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.4</version>
        <relativePath/>
    </parent>
    <groupId>com.yqd</groupId>
    <artifactId>SpringBoot-Exception-Demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SpringBoot-Exception-Demo</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- Spring Web:提供REST接口支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- Lombok:简化代码,提供@Data、@Slf4j等注解 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
            <optional>true</optional>
        </dependency>

        <!-- 测试依赖(可选) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

IDEA 配置:需安装 Lombok 插件(File → Settings → Plugins,搜索Lombok并安装,重启 IDEA),否则注解可能无法识别。

步骤 2:定义统一响应结果类(Result)

创建标准化响应实体,确保成功 / 失败响应格式一致。

package com.yqd.common;

import lombok.Data;
import java.time.LocalDateTime;

/**
 * 统一响应结果类
 * @param <T> 响应数据类型
 */
@Data  // Lombok注解:自动生成getter/setter/toString等
public class Result<T> {
    // 状态码:200成功,非200失败(如400参数错误、500系统错误)
    private Integer code;
    // 响应消息(成功/失败描述)
    private String message;
    // 响应数据(成功时返回,失败时为null)
    private T data;
    // 响应时间戳
    private LocalDateTime timestamp;

    // 私有构造:通过静态方法创建实例,避免直接new
    private Result() {
        this.timestamp = LocalDateTime.now(); // 自动填充当前时间
    }

    /**
     * 成功响应(带数据)
     */
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("操作成功");
        result.setData(data);
        return result;
    }

    /**
     * 成功响应(无数据)
     */
    public static <T> Result<T> success() {
        return success(null);
    }

    /**
     * 失败响应(自定义状态码和消息)
     */
    public static <T> Result<T> fail(Integer code, String message) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        result.setData(null);
        return result;
    }
}

说明

  • @Data注解替代了手动编写gettersettertoString等方法,简化代码。
  • 静态方法success()fail()用于快速创建响应实例,避免重复代码。

步骤 3:定义自定义异常类

区分业务异常(如 “用户不存在”)和系统异常(如空指针),便于针对性处理。

package com.yqd.exception;

import lombok.Getter;

/**
 * 业务异常:用于处理可预见的业务错误(如参数错误、权限不足等)
 */
@Getter  // Lombok注解:自动生成getter方法(获取code和message)
public class BusinessException extends RuntimeException {
    // 异常状态码(如400参数错误、403权限不足、404资源不存在)
    private final Integer code;

    /**
     * 构造方法:自定义状态码和消息
     */
    public BusinessException(Integer code, String message) {
        super(message); // 调用父类构造,保存异常消息
        this.code = code;
    }

    /**
     * 简化构造:默认状态码400(Bad Request)
     */
    public BusinessException(String message) {
        this(400, message);
    }
}

说明

  • @Getter注解自动生成code字段的getter方法,无需手动编写。
  • 继承RuntimeException(非受检异常),避免业务代码中强制try-catch

步骤 4:实现全局异常处理器(结合 @Slf4j)

使用@RestControllerAdvice定义全局异常处理器,通过@ExceptionHandler捕获不同类型的异常,并使用@Slf4j简化日志记录。

package com.yqd.exception;

import com.yqd.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import javax.servlet.http.HttpServletRequest;

/**
 * 全局异常处理器:捕获所有Controller层抛出的异常
 */
@RestControllerAdvice  // 组合@ControllerAdvice和@ResponseBody,返回JSON响应
@Slf4j  // Lombok注解:自动生成log日志对象(SLF4J)
public class GlobalExceptionHandler {

    /**
     * 处理自定义业务异常(BusinessException)
     */
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
        // 使用@Slf4j生成的log对象记录日志(warn级别:业务异常为可预见错误)
        log.warn("业务异常 - 路径:{},消息:{}", request.getRequestURI(), e.getMessage());
        // 返回统一失败响应(使用异常中定义的code和message)
        return Result.fail(e.getCode(), e.getMessage());
    }

    /**
     * 处理参数绑定异常(如@RequestParam必填参数缺失)
     */
    @ExceptionHandler(ServletRequestBindingException.class)
    public Result<Void> handleServletRequestBindingException(ServletRequestBindingException e, HttpServletRequest request) {
        log.warn("参数绑定异常 - 路径:{},消息:{}", request.getRequestURI(), e.getMessage());
        return Result.fail(400, "参数错误:" + e.getMessage().split(":")[0]); // 简化错误消息
    }

    /**
     * 处理参数类型不匹配异常(如URL参数应为数字却传字符串)
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public Result<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
        log.warn("参数类型异常 - 路径:{},参数:{},期望类型:{}",
                request.getRequestURI(),
                e.getName(),
                e.getRequiredType().getSimpleName()); // 打印参数名和期望类型
        return Result.fail(400, "参数类型错误:" + e.getName() + "应为" + e.getRequiredType().getSimpleName());
    }

    /**
     * 处理所有未捕获的异常(兜底处理)
     */
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e, HttpServletRequest request) {
        // 系统异常记录error级别日志,并打印堆栈信息(便于排查问题)
        log.error("系统异常 - 路径:{}", request.getRequestURI(), e); // 注意:e作为最后一个参数会打印堆栈
        // 生产环境返回通用消息,避免暴露敏感信息(如类名、数据库信息)
        return Result.fail(500, "服务器内部错误,请联系管理员");
    }

    @ExceptionHandler(BindException.class)
    public Result<Void> handleBindException(BindException e, HttpServletRequest request) {
        // 获取第一个校验失败的消息
        String errorMsg = e.getBindingResult().getFieldError().getDefaultMessage();
        log.warn("参数校验异常 - 路径:{},消息:{}", request.getRequestURI(), errorMsg);
        return Result.fail(400, "参数校验失败:" + errorMsg);
    }
}

核心注解与逻辑说明

  1. @RestControllerAdvice
    • 作用于所有标注@RestController的类,拦截其抛出的异常。
    • 结合@ResponseBody,确保异常处理方法的返回值自动转为 JSON。
  2. @Slf4j
    • 自动生成private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GlobalExceptionHandler.class);
    • 直接使用log.warn()log.error()等方法记录日志,无需手动声明日志对象。
  3. @ExceptionHandler
    • 注解参数指定处理的异常类型(如BusinessException.class)。
    • 方法参数可注入HttpServletRequest,用于获取请求路径等信息。

步骤 5:编写测试接口验证效果

创建 Controller 模拟不同异常场景,测试统一异常处理是否生效。

package com.yqd.controller;

import com.yqd.common.Result;
import com.yqd.dto.UserDTO;
import com.yqd.exception.BusinessException;
import com.yqd.exception.UserNotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequestMapping("/test")
@Slf4j  // 用于记录接口访问日志(示例)
public class ExceptionTestController {

    /**
     * 正常响应示例
     */
    @GetMapping("/success")
    public Result<String> success() {
        log.info("访问成功接口"); // 使用@Slf4j的log对象
        return Result.success("这是成功返回的数据");
    }

    /**
     * 抛出自定义业务异常(用户不存在)
     */
    @GetMapping("/user/{id}")
    public Result<String> getUser(@PathVariable Long id) {
        log.info("查询用户ID:{}", id);
        if (id <= 0) {
            // 模拟业务校验失败,抛出自定义异常
            throw new BusinessException(400, "用户ID必须为正数");
        }
        if (id == 999) {
            throw new UserNotFoundException(id); // 抛出自定义细分异常
        }
        return Result.success("用户信息:" + id);
    }

    /**
     * 模拟参数缺失异常(@RequestParam必填参数未传)
     */
    @GetMapping("/param-required")
    public Result<Void> paramRequired(
            @RequestParam(required = true) String username // required=true表示必填
    ) {
        return Result.success();
    }

    /**
     * 模拟参数类型不匹配(URL参数应为数字,实际传字符串)
     */
    @GetMapping("/param-type/{num}")
    public Result<Void> paramType(@PathVariable Integer num) {
        return Result.success();
    }

    /**
     * 模拟系统异常(空指针)
     */
    @GetMapping("/system-error")
    public Result<Void> systemError() {
        String str = null;
        str.length(); // 触发NullPointerException
        return Result.success();
    }

    @PostMapping("/user")
    public Result<UserDTO> addUser(@Valid @RequestBody UserDTO user) {
        return Result.success(user);
    }
}

步骤 6:启动类

package com.yqd;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootExceptionDemo {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootExceptionDemo.class, args);
    }
}

步骤 7:启动项目并测试

启动 Spring Boot 应用,通过浏览器或 Postman 访问以下接口,验证效果:

  1. 正常响应http://localhost:8080/test/success响应:

    {
      "code": 200,
      "message": "操作成功",
      "data": "这是成功返回的数据",
      "timestamp": "2024-10-24T16:30:00"
    }
    

    日志输出:INFO com.yqd.controller.ExceptionTestController : 访问成功接口

  2. 业务异常(用户 ID 为负数)http://localhost:8080/test/user/-1响应:

    {
      "code": 400,
      "message": "用户ID必须为正数",
      "data": null,
      "timestamp": "2024-10-24T16:31:00"
    }
    

    日志输出:WARN com.yqd.exception.GlobalExceptionHandler : 业务异常 - 路径:/test/user/-1,消息:用户ID必须为正数

  3. 参数缺失异常http://localhost:8080/test/param-required(不传递username)响应:

    {
      "code": 400,
      "message": "参数错误:Required String parameter 'username' is not present",
      "data": null,
      "timestamp": "2024-10-24T16:32:00"
    }
    

    日志输出:WARN com.yqd.exception.GlobalExceptionHandler : 参数绑定异常 - 路径:/test/param-required,消息:Required String parameter 'username' is not present

  4. 参数类型不匹配http://localhost:8080/test/param-type/abcabc不是数字)响应:

    {
      "code": 400,
      "message": "参数类型错误:num应为Integer",
      "data": null,
      "timestamp": "2024-10-24T16:33:00"
    }
    

    日志输出:WARN com.yqd.exception.GlobalExceptionHandler : 参数类型异常 - 路径:/test/param-type/abc,参数:num,期望类型:Integer

  5. 系统异常(空指针)http://localhost:8080/test/system-error响应:

    {
      "code": 500,
      "message": "服务器内部错误,请联系管理员",
      "data": null,
      "timestamp": "2024-10-24T16:34:00"
    }
    

    日志输出(包含堆栈):ERROR com.yqd.exception.GlobalExceptionHandler : 系统异常 - 路径:/test/system-error同时打印NullPointerException的完整堆栈信息,便于排查问题。

六、进阶优化

1. 细化异常类型(针对特定业务)

根据业务场景定义更具体的异常类,如UserNotFoundExceptionOrderExpiredException,使异常处理更精准。

示例:UserNotFoundException.java

package com.yqd.exception;

import lombok.Getter;

@Getter
public class UserNotFoundException extends BusinessException {
    // 继承业务异常,默认状态码404
    public UserNotFoundException(Long userId) {
        super(404, "用户不存在:" + userId);
    }
}

在 Controller 中使用:

@GetMapping("/user/{id}")
public Result<String> getUser(@PathVariable Long id) {
    if (id == 999) {
        throw new UserNotFoundException(id); // 抛出自定义细分异常
    }
    return Result.success("用户信息:" + id);
}

无需修改全局异常处理器(BusinessException的处理器会自动捕获其子类异常)。

2. 集成参数校验框架(validation)

结合spring-boot-starter-validation处理请求参数校验(如字段长度、格式),并在全局异常处理器中捕获校验异常。

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

创建实体类并添加校验注解

package com.yqd.dto;

import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

@Data
public class UserDTO {
    @NotBlank(message = "用户名不能为空") // 非空校验
    @Size(min = 3, max = 20, message = "用户名长度必须为3-20个字符") // 长度校验
    private String username;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, message = "密码长度不能少于6位")
    private String password;
}

在 Controller 中使用@Valid触发校验

@PostMapping("/user")
public Result<UserDTO> addUser(@Valid @RequestBody UserDTO user) {
    return Result.success(user);
}

在全局异常处理器中添加校验异常处理

import org.springframework.validation.BindException;

@ExceptionHandler(BindException.class)
public Result<Void> handleBindException(BindException e, HttpServletRequest request) {
    // 获取第一个校验失败的消息
    String errorMsg = e.getBindingResult().getFieldError().getDefaultMessage();
    log.warn("参数校验异常 - 路径:{},消息:{}", request.getRequestURI(), errorMsg);
    return Result.fail(400, "参数校验失败:" + errorMsg);
}

测试请求:发送 POST 请求到/test/user,传入{"username":"ab","password":"123"},响应:

{
  "code": 400,
  "message": "参数校验失败:用户名长度必须为3-20个字符",
  "data": null,
  "timestamp": "2024-10-24T16:40:00"
}

3. 区分环境返回异常信息

开发环境需要详细异常信息(如堆栈)便于调试,生产环境则返回脱敏后的通用消息。可通过@Profile或配置文件实现。

示例:通过配置文件区分环境application-dev.yml(开发环境):

server:
  port: 8080
custom:
  exception:
    show-detail: true # 开发环境显示详细异常信息

application-prod.yml(生产环境):

server:
  port: 8080
custom:
  exception:
    show-detail: false # 生产环境隐藏详细信息

修改全局异常处理器

import org.springframework.beans.factory.annotation.Value;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @Value("${custom.exception.show-detail:false}")
    private boolean showDetail; // 是否显示详细异常信息

    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e, HttpServletRequest request) {
        log.error("系统异常 - 路径:{}", request.getRequestURI(), e);
        // 根据环境决定返回消息
        String message = showDetail ? e.getMessage() : "服务器内部错误,请联系管理员";
        return Result.fail(500, message);
    }
}

启动时指定环境(如--spring.profiles.active=dev),开发环境会返回异常详情,生产环境返回通用消息。

七、总结

本教程通过以下步骤实现了 Spring Boot 统一异常处理:

  1. 引入 Lombok,使用@Data简化实体类,@Slf4j简化日志记录。
  2. 定义统一响应类Result,保证所有响应格式一致。
  3. 创建自定义业务异常BusinessException,区分业务错误和系统错误。
  4. 实现全局异常处理器GlobalExceptionHandler,通过@RestControllerAdvice@ExceptionHandler集中捕获异常,并使用@Slf4j记录日志。
  5. 细化异常处理逻辑,支持参数校验、类型转换等常见异常,并可根据环境返回不同详细程度的信息。
posted @ 2025-10-24 11:26  碧水云天4  阅读(17)  评论(0)    收藏  举报