记录一个上手即用的Spring全局返回值&异常处理框架

在Spring Boot/Cloud项目中,统一全局返回格式和全局异常处理是项目开发的标配,它能极大减少重复代码、降低前后端对接成本、提升问题排查效率。

今天就给大家带来一套可以直接复制粘贴、稍作修改就能上线的优雅实现方案。

一、先搞懂核心理论

1. 核心注解说明

  • @RestControllerAdvice:这是Spring提供的增强注解,是@ControllerAdvice + @ResponseBody的组合,专门用于处理Rest风格接口的全局增强逻辑(返回值包装、异常捕获),默认作用于所有@RestController标注的控制器。
  • ResponseBodyAdvice:接口,用于对Controller返回的结果进行统一包装处理,无需在每个接口手动构建返回对象。
  • @ExceptionHandler:用于标注异常处理方法,指定该方法处理某一种或多种异常类型,配合@RestControllerAdvice实现全局异常捕获。

2. 整体架构设计

我们将实现3个核心组件,形成完整的全局处理链路:

  1. 统一返回结果封装类(基础载体,前后端数据传输的标准格式)
  2. 全局返回值增强器(自动包装所有接口返回结果)
  3. 全局异常处理器(捕获所有未手动处理的异常,统一返回异常信息)
  4. 配套辅助类(状态码枚举、自定义业务异常,提升扩展性)

二、代码实现(直接复制可用)

步骤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处,即可快速上线:

  1. @RestControllerAdvice basePackages属性 :将com.example.demo.controller修改为你项目中实际的Controller包路径(如:com.xxx.project.user.controller),确保只作用于当前项目的控制器,避免影响第三方依赖。
  2. ResultCode枚举类:根据项目业务需求,新增/修改业务状态码(如:订单相关、支付相关的异常码),删除无用的状态码。
  3. (可选)异常处理扩展 :如果项目中有特殊异常需要单独处理(如: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. 注意事项

  1. 依赖说明:该方案使用了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>
  1. 404异常捕获:需要在application.yml中添加配置,开启404异常抛出:
yaml 复制代码
spring:
  mvc:
    throw-exception-if-no-handler-found: true
  web:
    resources:
      add-mappings: false
  1. 避免重复包装:ControllerResponseAdvicesupports方法已经排除了Result类型,因此如果有接口需要手动返回特殊格式,直接返回Result对象即可,不会被重复包装。

六、总结

  1. 这套框架是Spring项目的基础标配,大家完全可以掌握并落地到实际项目中,它能帮你规避很多后续的对接和维护问题。
  2. 进阶扩展:可以在Result类中添加traceId(链路追踪ID),配合SkyWalking、Zipkin等链路追踪工具,提升分布式项目的问题排查效率。
  3. 安全优化:对于系统异常,返回给前端的是友好提示,而后端日志要打印完整堆栈信息,同时避免日志泄露敏感信息(如:用户密码、数据库连接信息)。
相关推荐
教游泳的程序员2 小时前
【面试问题精选】java开发工程师
python·面试·职场和发展
悟空码字2 小时前
SpringBoot整合MyBatis-Flex保姆级教程,看完就能上手!
java·spring boot·后端
爬山算法2 小时前
Hibernate(43)Hibernate中的级联删除如何实现?
java·python·hibernate
J_liaty2 小时前
Java工程师的JVM入门教程:从零理解Java虚拟机
java·开发语言·jvm
qq_2500568682 小时前
SpringBoot 引入 smart-doc 接口文档插件
java·spring boot·后端
Stream_Silver2 小时前
【安装与配置Anaconda步骤,包含卸载重装】
python·conda
ai_top_trends2 小时前
AI 生成 PPT 工具横评:效率、质量、稳定性一次说清
人工智能·python·powerpoint
珠穆峰2 小时前
linux清理缓存命令“echo 3 > /proc/sys/vm/drop_caches”
java·linux·缓存
天天睡大觉2 小时前
Python学习9
开发语言·python·学习