Spring Boot 入门:全局异常处理 + 统一返回格式

106 阅读11分钟

Spring Boot 入门:全局异常处理 + 统一返回格式

大家好呀!之前我们写的接口虽然能正常操作数据库,但存在两个小问题:一是返回格式不统一(比如成功时返回数据对象,失败时返回字符串),二是异常提示不友好(比如查询不到数据返回 null,报错时直接抛一堆代码堆栈)。

今天这篇就教大家用 统一返回格式 规范所有接口的响应结构,再用 全局异常处理 捕获所有异常并返回友好提示,让你的接口瞬间达到企业级规范!

全程分 4 步走,代码可直接复制使用,零基础也能轻松上手~

一、先搞懂:为什么需要统一返回格式和全局异常处理?

在讲实现之前,先跟大家说清楚这两个功能的重要性,避免大家觉得 “多此一举”:

1. 统一返回格式的必要性

之前我们的接口返回是 “混乱” 的:

  • 查询用户成功:返回{"id":1,"name":"小明",...}(User 对象);
  • 新增用户失败:返回"新增失败!"(字符串);
  • 查询不到用户:返回null(空值)。

这种情况下,前端开发同学需要写大量判断逻辑(比如判断返回的是对象还是字符串),很容易出 bug。而统一返回格式会让所有接口都返回相同结构的 JSON,比如:

// 成功时
{
 "code": 200,    // 状态码:200=成功,404=数据不存在,500=服务器错误
 "msg": "success",// 提示信息
 data": {"id":1,"name":"小明",...} // 实际数据(成功时返回,失败时为null)

}

// 失败时(比如查询不到数据)
{
 "code": 404,
 "msg": "数据不存在",
 "data": null
}

这样前端只需根据code判断状态,再拿datamsg,逻辑更简单,效率更高。

2. 全局异常处理的必要性

如果接口出现异常(比如传参错误、数据库报错),默认会返回一堆代码堆栈(比如500 Internal Server Error),既不友好,还可能泄露敏感信息。

全局异常处理能捕获所有异常,统一返回:

{
 "code": 500,
 "msg": "服务器内部错误,请联系管理员",
 "data": null
}

既保护了系统安全,又给用户友好的提示。

二、准备工作:基于之前的项目继续开发

我们直接在之前的first-springboot-project项目上开发,不需要新建项目。确保之前的数据库连接正常,接口能正常运行(比如/api/user/1能返回用户数据)。

三、第一步:实现统一返回格式(核心:封装 Result 类)

要让所有接口返回统一格式,关键是封装一个Result类,所有接口都返回这个类的对象,Spring Boot 会自动把它转成 JSON。

1. 创建 Result 类

(1)创建 common 包

右键点击com.example.firstspringbootproject包 → 「New → Package」,输入包名common(用来放公共类,比如 Result、异常类)。

(2)编写 Result 类

common包下创建Result类,粘贴代码:

package com.example.firstspringbootproject.common;

import lombok.Data;

// @Data:Lombok注解,自动生成getter、setter、toString方法(不用手动写)
@Data
public class Result<T> {

    // 状态码:200=成功,404=数据不存在,500=服务器错误,可自定义更多
    private Integer code;

    // 提示信息:成功时为"success",失败时为具体原因
    private String msg;

    // 实际数据:成功时返回具体数据(比如User、List<Product>),失败时为null
    private T data;

    // 1. 成功时的构造方法(无数据,比如删除成功)
    public Result() {
        this.code = 200;
        this.msg = "success";
    }

    // 2. 成功时的构造方法(有数据,比如查询用户、查询商品列表)
    public Result(T data) {
        this.code = 200;
        this.msg = "success";
        this.data = data;
    }

    // 3. 失败时的构造方法(自定义状态码和提示信息)
    public Result(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
        this.data = null;
    }

    // 4. 静态方法:快速创建“成功无数据”的Result对象(方便调用)
    public static Result success() {
        return new Result();
    }

    // 5. 静态方法:快速创建“成功有数据”的Result对象(方便调用)
    public static <T> Result<T> success(T data) {
        return new Result<>(data);
    }

    // 6. 静态方法:快速创建“失败”的Result对象(方便调用)
    public static Result error(Integer code, String msg) {
        return new Result(code, msg);
    }

}

2. 引入 Lombok 依赖(可选但推荐)

上面的@Data注解来自 Lombok,能帮我们省去手动写 getter、setter 的麻烦。如果没引入过 Lombok,需要在pom.xml中添加依赖:

        <!-- Lombok依赖:简化实体类代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
            <optional>true</optional>
        </dependency>

添加后点击 “Load Maven Changes” 刷新依赖。

如果不想用 Lombok,也可以手动给Result类写 getter、setter 方法(右键 → Generate → Getter and Setter → 勾选所有字段)。

3. 修改 Controller:所有接口返回 Result 对象

之前的DbController接口返回的是UserList<Product>String,现在要改成返回Result对象。打开DbController,修改代码:

package com.example.firstspringbootproject.controller;

import com.example.firstspringbootproject.common.Result;
import com.example.firstspringbootproject.entity.Product;
import com.example.firstspringbootproject.entity.User;
import com.example.firstspringbootproject.service.ProductService;
import com.example.firstspringbootproject.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api")
public class DbController {

   @Autowired
   private UserService userService;

   @Autowired
   private ProductService productService;

   // 1. 根据用户ID查询(返回Result<User>)
   @GetMapping("/user/{userId}")
   public Result<User> getUser(@PathVariable Integer userId) {
       User user = userService.getUserById(userId);

       // 如果查询不到用户,返回404;否则返回200+数据
       if (user == null) {
           return Result.error(404, "用户不存在");
       }

       return Result.success(user);
   }

   // 2. 新增用户(返回Result<Void>,无数据)
   @PostMapping("/user")
   public Result<Void> addUser(@RequestBody User user) {
       boolean success = userService.addUser(user);
       if (success) {
           return Result.success(); // 新增成功,返回200+success
       }
       return Result.error(500, "新增用户失败"); // 失败返回500
   }

   // 3. 查询所有用户(返回Result<List<User>>)

   @GetMapping("/user/all")
   public Result<List<User>> getAllUsers() {
       List<User> users = userService.getAllUsers();
       return Result.success(users); // 直接返回200+用户列表
   }

   // 4. 根据商品ID查询(返回Result<Product>)
   @GetMapping("/product/{productId}")
   public Result<Product> getProduct(@PathVariable Integer productId) {
       Product product = productService.getProductById(productId);
       if (product == null) {
           return Result.error(404, "商品不存在");
       }
       return Result.success(product);
   }

   // 5. 模糊搜索商品(返回Result<List<Product>>)
   @GetMapping("/product/search")
   public Result<List<Product>> searchProduct(@RequestParam(required = false, defaultValue = "") String name) {
       List<Product> products = productService.searchProductByName(name);
       return Result.success(products);
   }

}

4. 测试统一返回格式(用 ApiFox)

启动项目,用 ApiFox 测试接口,看看返回格式是否统一:

  • 测试查询不存在的用户:访问http://localhost:8080/api/user/999,返回:
{
    "code": 404,
    "msg": "用户不存在",
    "data": null
}
  • 测试查询存在的用户:访问http://localhost:8080/api/user/1,返回:
{
    "code": 200,
    "msg": "success",
    "data": {
        "id": 1,
        "name": "小明",
        "age": 20,
        "phone": "13800138000"
    }
}
  • 测试新增用户:新增成功返回:
{
    "code": 200,
    "msg": "success",
    "data": null
}

所有接口都返回统一结构,完美!

四、第二步:实现全局异常处理(核心:@RestControllerAdvice)

虽然我们在 Controller 中处理了 “查询不到数据” 的情况,但还有很多异常没处理(比如传参错误、数据库报错、空指针异常)。这时候就需要全局异常处理,用@RestControllerAdvice注解捕获所有异常。

1. 创建全局异常处理器类

common包下创建GlobalExceptionHandler类,粘贴代码:

package com.example.firstspringbootproject.common;

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

// @RestControllerAdvice:全局异常处理注解,作用于所有@RestController类
// @Slf4j:Lombok注解,自动生成日志对象(用来打印异常信息,方便调试)
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

   // 1. 捕获所有RuntimeException(运行时异常,比如空指针、数据库操作异常)
   @ExceptionHandler(RuntimeException.class)
   public Result<Void> handleRuntimeException(RuntimeException e) {

       // 打印异常信息到控制台(方便开发时排查问题)
       log.error("运行时异常:", e);

       // 返回友好提示(不暴露具体异常信息给前端)
       return Result.error(500, "服务器内部错误,请联系管理员");
   }

   // 2. 捕获JSON格式错误异常(比如前端传的JSON格式不对)
   @ExceptionHandler(HttpMessageNotReadableException.class)
   public Result<Void> handleJsonParseException(HttpMessageNotReadableException e) {

       log.error("JSON格式错误:", e);
       return Result.error(400, "请求参数格式错误,请检查JSON格式");
   }

   // 3. 捕获自定义异常(后面会讲,先预留)
   @ExceptionHandler(BusinessException.class)
   public Result<Void> handleBusinessException(BusinessException e) {

       // 自定义异常的状态码和提示信息,直接返回
       return Result.error(e.getCode(), e.getMsg());
   }

   // 4. 捕获所有其他异常(兜底,防止有遗漏的异常)
   @ExceptionHandler(Exception.class)
   public Result<Void> handleException(Exception e) {

       log.error("未知异常:", e);
       return Result.error(500, "服务器内部错误,请联系管理员");

   }
}

2. 关键注解说明

  • @RestControllerAdvice:全局异常处理的核心注解,会扫描所有@RestController类,当这些类抛出异常时,会被这里的方法捕获;
  • @ExceptionHandler(XXXException.class):指定当前方法处理哪种异常(比如@ExceptionHandler(RuntimeException.class)处理所有运行时异常);
  • @Slf4j:生成日志对象log,用log.error()打印异常堆栈,方便开发时排查问题(生产环境中,这些日志会存在日志文件里,不会暴露给前端)。

3. 自定义业务异常(可选但推荐)

在实际开发中,我们会遇到很多 “业务异常”(比如 “余额不足”“手机号已存在”),这些异常不是系统错误,而是业务逻辑不允许的情况。我们可以自定义一个BusinessException,专门处理这类异常。

(1)创建 BusinessException 类

common包下创建BusinessException类:

package com.example.firstspringbootproject.common;

import lombok.Data;
import lombok.EqualsAndHashCode;

// @Data:生成getter、setter
// @EqualsAndHashCode(callSuper = true):让自定义异常继承RuntimeException的equals和hashCode方法
@Data
@EqualsAndHashCode(callSuper = true)
public class BusinessException extends RuntimeException {

    // 自定义异常的状态码(比如400=参数错误,403=权限不足)
    private Integer code;

    // 自定义异常的提示信息
    private String msg;

    // 构造方法:传入状态码和提示信息
    public BusinessException(Integer code, String msg) {
        super(msg); // 调用父类RuntimeException的构造方法,传入msg
        this.code = code;
        this.msg = msg;
    }

}
(2)在 Service 中抛出自定义异常

比如我们想在 “新增用户时,如果手机号已存在,就抛异常”,修改UserServiceImpladdUser方法:

package com.example.firstspringbootproject.service.impl;

import com.example.firstspringbootproject.common.BusinessException;
import com.example.firstspringbootproject.entity.User;
import com.example.firstspringbootproject.mapper.UserMapper;
import com.example.firstspringbootproject.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

// @Service:标记这是Service层实现类,Spring自动管理
@Service
public class UserServiceImpl implements UserService {

    // 注入UserMapper(Spring自动创建实例)
    @Autowired
    private UserMapper userMapper;

    @Override
    public User getUserById(Integer id) {
        return userMapper.selectById(id); // 调用Mapper方法
    }

    @Override
    public boolean addUser(User user) {

       // 1. 先查询手机号是否已存在(需要在UserMapper中新增方法)
       List<User> existingUsers = userMapper.selectByPhone(user.getPhone());

       if (!existingUsers.isEmpty()) {
           // 2. 如果手机号已存在,抛出自定义异常
           throw new BusinessException(400, "手机号已被注册,请更换手机号");
       }

       // 3. 手机号不存在,新增用户
       return userMapper.insert(user) > 0;

    }

    @Override
    public List<User> getAllUsers() {
        return userMapper.selectAll();
    }

}
(3)在 UserMapper 中新增 selectByPhone 方法

要实现 “根据手机号查询用户”,需要在UserMapper接口和UserMapper.xml中添加代码:

  • UserMapper 接口
    // 4.根据手机号查询用户
    List<User> selectByPhone(String phone);
  • UserMapper.xml
    <!-- 根据手机号查询用户 -->
    <select id="selectByPhone" parameterType="String" resultMap="UserResultMap">
        SELECT id, name, age, phone FROM user WHERE phone = #{phone}
    </select>

五、第三步:测试全局异常处理(用 ApiFox)

启动项目,用 ApiFox 测试各种异常场景,看看是否能被正确捕获并返回友好提示:

1. 测试自定义业务异常(手机号已存在)

  • 用 ApiFox 发送新增用户请求,传入已存在的手机号(比如13800138000):
{
   "name": "小李",
   "age": 22,
   "phone": "13700137000"
}
  • 发送后,返回:
{
    "code": 400,
    "msg": "手机号已被注册,请更换手机号",
    "data": null
}

自定义异常被正确捕获,提示友好!

2. 测试运行时异常(比如空指针)

  • 我们可以故意在代码中写一个空指针(比如在getUser方法中加String test = null; test.length();),然后访问/api/user/1
  • 返回:
{
    "code": 500,
    "msg": "服务器内部错误,请联系管理员",
    "data": null
}

同时,控制台会打印异常堆栈(方便我们排查问题),但前端看不到具体异常信息,保护了系统安全!

六、常见问题:避坑指南

1. 统一返回格式没生效?

  • 原因 1:Controller 方法返回的不是Result对象(比如还是返回UserString);
  • 解决方案:确保所有 Controller 方法的返回值都是Result类型(比如Result<User>Result<Product>)。

2. 全局异常处理没生效?

  • 原因 1GlobalExceptionHandler类没加@RestControllerAdvice注解;
  • 原因 2@ExceptionHandler注解指定的异常类型不对(比如想处理NullPointerException,却写了RuntimeException之外的类型);
  • 解决方案:检查注解是否正确,异常类型是否匹配。

3. 自定义异常没被捕获?

  • 原因 1GlobalExceptionHandler中没有添加处理BusinessException的方法;
  • 原因 2:自定义异常没有继承RuntimeException(如果继承Exception,需要在方法上加throws,否则捕获不到);
  • 解决方案:确保GlobalExceptionHandler中有@ExceptionHandler(BusinessException.class)方法,且BusinessException继承RuntimeException

七、总结:接口规范升级完成!

今天我们完成了接口的 “规范化升级”,学会了:

  1. 封装 Result 类:让所有接口返回统一格式的 JSON,前端处理更简单;
  2. 全局异常处理:用@RestControllerAdvice捕获所有异常,返回友好提示,保护系统安全;
  3. 自定义业务异常:处理业务逻辑相关的异常(比如手机号已存在),让异常分类更清晰。

现在你的接口已经非常健壮了 —— 返回格式统一、异常提示友好、代码结构清晰,完全符合企业开发标准!

下一篇文章,我会教大家 “接口文档自动生成”(用 Swagger/Knife4j),让前端同学能自动看到接口的参数、返回格式,不用再手动写文档,大大提升协作效率!

👉 关注+私信“Web基础源码”,获取完整工程!有问题评论区见~

扫码_搜索联合传播样式-标准色版.png