在Spring Boot/Cloud项目中,统一全局返回格式和全局异常处理是项目开发的标配,它能极大减少重复代码、降低前后端对接成本、提升问题排查效率。
今天就给大家带来一套可以直接复制粘贴、稍作修改就能上线的优雅实现方案。
一、先搞懂核心理论
1. 核心注解说明
@RestControllerAdvice:这是Spring提供的增强注解,是@ControllerAdvice+@ResponseBody的组合,专门用于处理Rest风格接口的全局增强逻辑(返回值包装、异常捕获),默认作用于所有@RestController标注的控制器。ResponseBodyAdvice:接口,用于对Controller返回的结果进行统一包装处理,无需在每个接口手动构建返回对象。@ExceptionHandler:用于标注异常处理方法,指定该方法处理某一种或多种异常类型,配合@RestControllerAdvice实现全局异常捕获。
2. 整体架构设计
我们将实现3个核心组件,形成完整的全局处理链路:
- 统一返回结果封装类(基础载体,前后端数据传输的标准格式)
- 全局返回值增强器(自动包装所有接口返回结果)
- 全局异常处理器(捕获所有未手动处理的异常,统一返回异常信息)
- 配套辅助类(状态码枚举、自定义业务异常,提升扩展性)
二、代码实现(直接复制可用)
步骤1:统一返回结果封装类(Result.java)
这是前后端数据交互的标准格式,所有接口(正常返回/异常返回)都将通过该类返回,消除格式不一致的问题。
java
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 全局统一返回结果封装类
* 说明:所有接口返回结果必须通过此类包装,前后端严格按照该格式进行数据交互
*/
@Data
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
// 响应状态码(200成功、500系统异常、自定义业务异常码等)
private Integer code;
// 响应消息(成功/失败提示信息)
private String msg;
// 响应数据(正常返回时携带的业务数据,异常时可置为null)
private T data;
// 响应时间戳(方便排查问题,记录接口返回时间)
private LocalDateTime timestamp;
// 私有化构造方法,禁止外部直接创建,统一通过静态方法构建
private Result() {
this.timestamp = LocalDateTime.now();
}
// ==================== 静态工具方法:构建返回结果 ====================
/**
* 构建成功返回结果(携带业务数据)
*/
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(ResultCode.SUCCESS.getCode());
result.setMsg(ResultCode.SUCCESS.getMsg());
result.setData(data);
return result;
}
/**
* 构建成功返回结果(不携带业务数据,适用于新增/修改/删除等操作)
*/
public static <T> Result<T> success() {
return success(null);
}
/**
* 构建失败返回结果(自定义状态码和消息)
*/
public static <T> Result<T> fail(Integer code, String msg) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMsg(msg);
result.setData(null);
return result;
}
/**
* 构建失败返回结果(基于状态码枚举)
*/
public static <T> Result<T> fail(ResultCode resultCode) {
return fail(resultCode.getCode(), resultCode.getMsg());
}
// ==================== 【需要修改/扩展】:如果项目有固定的成功提示语,可在这里自定义 ====================
}
步骤2:状态码枚举类(ResultCode.java)
统一管理项目中的所有状态码,避免硬编码,提升可维护性,后续新增状态码直接在枚举中添加即可。
java
/**
* 全局统一状态码枚举
* 说明:
* 1. 遵循HTTP状态码规范,2xx表示成功,4xx表示客户端异常,5xx表示服务端异常
* 2. 业务异常码可在基础状态码上进行扩展(如:40001表示参数校验失败,50001表示业务逻辑异常)
*/
public enum ResultCode {
// ==================== 基础状态码 ====================
SUCCESS(200, "操作成功"),
SYSTEM_ERROR(500, "系统内部异常,请稍后重试"),
PARAM_ERROR(400, "请求参数格式不正确"),
NOT_FOUND(404, "请求资源不存在"),
METHOD_NOT_ALLOWED(405, "请求方式不支持"),
// ==================== 【需要修改/扩展】:业务状态码(根据项目需求添加) ====================
BUSINESS_ERROR(50001, "业务逻辑异常"),
USER_NOT_EXIST(40001, "用户不存在"),
TOKEN_EXPIRED(40101, "登录令牌已过期");
// 状态码
private final Integer code;
// 状态描述
private final String msg;
ResultCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
// getter方法
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
步骤3:自定义业务异常类(BusinessException.java)
项目开发中,我们经常需要手动抛出业务异常(如:用户不存在、订单已关闭),该类专门用于封装业务异常信息,与系统异常区分开,方便精准处理和排查。
java
/**
* 自定义业务异常类
* 说明:用于抛出业务逻辑相关的异常,由全局异常处理器专门捕获处理
*/
public class BusinessException extends RuntimeException {
// 业务异常码
private Integer code;
// ==================== 构造方法 ====================
public BusinessException(ResultCode resultCode) {
super(resultCode.getMsg());
this.code = resultCode.getCode();
}
public BusinessException(Integer code, String msg) {
super(msg);
this.code = code;
}
public BusinessException(String msg) {
super(msg);
this.code = ResultCode.BUSINESS_ERROR.getCode();
}
// ==================== getter方法 ====================
public Integer getCode() {
return code;
}
}
步骤4:全局返回值增强器(ControllerResponseAdvice.java)
实现ResponseBodyAdvice接口,自动包装所有@RestController接口的返回结果,无需在每个接口手动调用Result.success(),极大减少重复代码。
java
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.annotation.Resource;
/**
* 全局返回值增强器
* 说明:自动将所有RestController接口的返回结果包装为统一的Result格式
*/
@RestControllerAdvice(
// 【需要修改】:指定当前项目的Controller包路径(必填,避免作用于第三方包的接口)
basePackages = "com.example.demo.controller"
)
public class ControllerResponseAdvice implements ResponseBodyAdvice<Object> {
// 注入Jackson对象转换器,用于处理String类型返回值的特殊情况
@Resource
private ObjectMapper objectMapper;
/**
* 判断当前返回结果是否需要进行包装处理(返回true表示需要包装)
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 核心逻辑:排除已经是Result类型的返回结果,避免重复包装
return !returnType.getMethod().getReturnType().isAssignableFrom(Result.class);
}
/**
* 对返回结果进行包装处理(核心方法)
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
// 1. 处理String类型返回值(特殊情况:String类型会被StringHttpMessageConverter处理,直接返回Result会报错)
if (body instanceof String) {
try {
return objectMapper.writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
throw new RuntimeException("String类型返回值包装失败", e);
}
}
// 2. 处理null值(避免返回null,统一包装为成功状态的空数据)
if (body == null) {
return Result.success();
}
// 3. 正常包装:将返回结果封装为Result.success(body)
return Result.success(body);
}
}
步骤5:全局异常处理器(GlobalExceptionAdvice.java)
捕获项目中所有未手动处理的异常,统一封装为Result格式返回,避免前端收到晦涩的异常堆栈信息,同时方便后端排查问题。
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
/**
* 全局异常处理器
* 说明:捕获所有RestController接口抛出的未处理异常,统一返回格式化的异常信息
* 异常处理顺序:子类异常在前,父类异常在后(Spring会优先匹配最具体的异常类型)
*/
@Slf4j
@RestControllerAdvice(
// 【需要修改】:与ControllerResponseAdvice保持一致,指定项目的Controller包路径
basePackages = "com.example.demo.controller"
)
public class GlobalExceptionAdvice {
/**
* 捕获自定义业务异常(优先级最高,因为是我们手动抛出的,最具体)
*/
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e) {
// 打印业务异常日志(级别为warn,方便区分系统异常)
log.warn("业务异常:{}", e.getMessage(), e);
return Result.fail(e.getCode(), e.getMessage());
}
/**
* 捕获请求参数校验异常(如:@NotBlank、@NotNull等注解校验失败)
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.warn("参数校验异常:{}", e.getMessage(), e);
// 提取第一个参数校验失败的提示信息(返回给前端,提升用户体验)
String errorMsg = e.getBindingResult().getFieldErrors().get(0).getDefaultMessage();
return Result.fail(ResultCode.PARAM_ERROR.getCode(), errorMsg);
}
/**
* 捕获404异常(资源不存在)
*/
@ExceptionHandler(NoHandlerFoundException.class)
public Result<?> handleNoHandlerFoundException(NoHandlerFoundException e) {
log.error("404异常:{}", e.getMessage(), e);
return Result.fail(ResultCode.NOT_FOUND);
}
/**
* 捕获所有未处理的系统异常(兜底处理,父类Exception)
*/
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
// 系统异常打印error级别日志,方便排查问题(包含完整堆栈信息)
log.error("系统内部异常", e);
// 注意:返回给前端的是友好提示,不返回具体异常信息(避免泄露系统细节)
return Result.fail(ResultCode.SYSTEM_ERROR);
}
// ==================== 【需要扩展】:根据项目需求,可添加更多异常处理方法(如:NullPointerException、IOException等) ====================
}
三、关键修改点标注
以上代码复制到项目后,只需要修改以下3处,即可快速上线:
@RestControllerAdvice的basePackages属性 :将com.example.demo.controller修改为你项目中实际的Controller包路径(如:com.xxx.project.user.controller),确保只作用于当前项目的控制器,避免影响第三方依赖。ResultCode枚举类:根据项目业务需求,新增/修改业务状态码(如:订单相关、支付相关的异常码),删除无用的状态码。- (可选)异常处理扩展 :如果项目中有特殊异常需要单独处理(如:Redis连接异常、数据库连接异常),在
GlobalExceptionAdvice中新增对应的@ExceptionHandler方法即可。
四、实战场景演示
我们创建一个测试Controller,验证全局返回值增强和全局异常处理的效果,大家可以直接复制该Controller进行测试。
java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotBlank;
/**
* 测试Controller:验证全局返回值增强和全局异常处理
*/
@RestController
@RequestMapping("/test")
public class TestController {
/**
* 场景1:正常返回(携带业务数据)
* 预期结果:自动包装为 Result{code=200, msg='操作成功', data='Hello World', timestamp=xxx}
*/
@GetMapping("/normal")
public String normal() {
return "Hello World";
}
/**
* 场景2:正常返回(不携带业务数据)
* 预期结果:自动包装为 Result{code=200, msg='操作成功', data=null, timestamp=xxx}
*/
@GetMapping("/empty")
public void empty() {
// 无返回值
}
/**
* 场景3:抛出自定义业务异常
* 预期结果:捕获异常,返回 Result{code=40001, msg='用户不存在', data=null, timestamp=xxx}
*/
@GetMapping("/business/{userId}")
public void businessException(@PathVariable String userId) {
if ("10086".equals(userId)) {
throw new BusinessException(ResultCode.USER_NOT_EXIST);
}
}
/**
* 场景4:抛出系统异常(空指针异常,未手动处理)
* 预期结果:捕获兜底异常,返回 Result{code=500, msg='系统内部异常,请稍后重试', data=null, timestamp=xxx}
*/
@GetMapping("/system")
public void systemException() {
String str = null;
str.length(); // 手动制造空指针异常
}
/**
* 场景5:参数校验异常(@NotBlank注解校验失败)
* 预期结果:捕获参数异常,返回 Result{code=400, msg='姓名不能为空', data=null, timestamp=xxx}
*/
@GetMapping("/param")
public void paramException(@NotBlank(message = "姓名不能为空") String name) {
}
}
五、使用效果与注意事项
1. 预期效果
无论接口正常返回还是抛出异常,前端收到的都是格式统一的JSON数据,示例如下:
java
// 正常返回
{
"code": 200,
"msg": "操作成功",
"data": "Hello World",
"timestamp": "2026-01-15T15:30:00"
}
// 业务异常返回
{
"code": 40001,
"msg": "用户不存在",
"data": null,
"timestamp": "2026-01-15T15:31:00"
}
2. 注意事项
- 依赖说明:该方案使用了
lombok(@Data注解),如果项目未引入lombok,需要手动添加getter/setter方法,或在pom.xml中引入lombok依赖:
xml
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
- 404异常捕获:需要在application.yml中添加配置,开启404异常抛出:
yaml
spring:
mvc:
throw-exception-if-no-handler-found: true
web:
resources:
add-mappings: false
- 避免重复包装:
ControllerResponseAdvice的supports方法已经排除了Result类型,因此如果有接口需要手动返回特殊格式,直接返回Result对象即可,不会被重复包装。
六、总结
- 这套框架是Spring项目的基础标配,大家完全可以掌握并落地到实际项目中,它能帮你规避很多后续的对接和维护问题。
- 进阶扩展:可以在
Result类中添加traceId(链路追踪ID),配合SkyWalking、Zipkin等链路追踪工具,提升分布式项目的问题排查效率。 - 安全优化:对于系统异常,返回给前端的是友好提示,而后端日志要打印完整堆栈信息,同时避免日志泄露敏感信息(如:用户密码、数据库连接信息)。