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判断状态,再拿data或msg,逻辑更简单,效率更高。
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接口返回的是User、List<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 中抛出自定义异常
比如我们想在 “新增用户时,如果手机号已存在,就抛异常”,修改UserServiceImpl的addUser方法:
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对象(比如还是返回User或String); - 解决方案:确保所有 Controller 方法的返回值都是
Result类型(比如Result<User>、Result<Product>)。
2. 全局异常处理没生效?
- 原因 1:
GlobalExceptionHandler类没加@RestControllerAdvice注解; - 原因 2:
@ExceptionHandler注解指定的异常类型不对(比如想处理NullPointerException,却写了RuntimeException之外的类型); - 解决方案:检查注解是否正确,异常类型是否匹配。
3. 自定义异常没被捕获?
- 原因 1:
GlobalExceptionHandler中没有添加处理BusinessException的方法; - 原因 2:自定义异常没有继承
RuntimeException(如果继承Exception,需要在方法上加throws,否则捕获不到); - 解决方案:确保
GlobalExceptionHandler中有@ExceptionHandler(BusinessException.class)方法,且BusinessException继承RuntimeException。
七、总结:接口规范升级完成!
今天我们完成了接口的 “规范化升级”,学会了:
- 封装 Result 类:让所有接口返回统一格式的 JSON,前端处理更简单;
- 全局异常处理:用
@RestControllerAdvice捕获所有异常,返回友好提示,保护系统安全; - 自定义业务异常:处理业务逻辑相关的异常(比如手机号已存在),让异常分类更清晰。
现在你的接口已经非常健壮了 —— 返回格式统一、异常提示友好、代码结构清晰,完全符合企业开发标准!
下一篇文章,我会教大家 “接口文档自动生成”(用 Swagger/Knife4j),让前端同学能自动看到接口的参数、返回格式,不用再手动写文档,大大提升协作效率!
👉 关注+私信“Web基础源码”,获取完整工程!有问题评论区见~