SpringBoot 如何统一 API 响应格式

news/2025/2/23 14:35:24

一、为什么要统一

在真实项目开发中,你是否遇到过这些问题?

  • 前端需要为不同接口编写差异化处理逻辑
  • 错误信息格式五花八门,定位问题困难
  • 全局异常处理缺失导致敏感信息泄露

在前后端分离架构中,统一的 API 响应格式是提升协作效率的关键。本文将手把手教你用 SpringBoot 实现标准化响应封装,从此告别接口格式混乱的烦恼!

二、统一响应格式的价值

  1. 标准化:所有接口遵循相同数据结构
  2. 可维护性:集中处理异常和成功响应
  3. 安全性:隐藏技术细节,暴露友好提示
  4. 可扩展性:轻松添加统一字段(如 traceId )

三、结果要求

1、正常情况下,API 的相应结果是统一格式的

2、API 开发人员的 controller 方法的返回值依旧可以是原生的类型,比如 List、或者其他自定义类等,不需要 API 开发人员手动的将结果构造成统一格式,而是由底层框架统一格式化。

3、有些特殊接口需要忽略统一格式化。比如下载接口,或者是和第三方对接,需要满足第三方的自定义报文格式。

4、具有全局异常处理。如果有异常,代码中直接 throw new Exception("消息提示"),API 统一将提示信息返回,不需要给业务方法的正常响应对象再包一层结果信息。

四、设计标准化响应体

基础响应结构示例

{
  code : "200",
  msg : "操作成功",
  data : null,
  timestamp : 1759321000000
  extra : {}
}

字段说明

字段

类型

说明

code

String

业务状态码(非HTTP状态码)

msg

String

提示信息

data

Object

业务数据(可为null)

timestamp

Long

响应时间戳

五、实现步骤

步骤1:创建统一响应类

java">@Getter
@Setter
public class Result<T> implements Serializable {
    private static final String DEF_ERROR_MESSAGE = "系统繁忙,请稍后再试!"
    /**
     * 业务状态码:200-请求处理成功
     */
    private String code;
    /**
     * 提示消息
     */
    private String msg = "操作成功";
    /**
     * 响应数据
     */
    private T data;

    /**
     * 扩展数据
     */
    private Map<String, Object> extra;

    /**
     * 响应时间
     */
    private long timestamp = System.currentTimeMillis();

    private Result() {
    }

    private Result(String code, T data, String msg) {
        this.code = code;
        this.data = data;
        this.msg = msg;
    }

    public static <E> Result<E> result(String code, E data, String msg) {
        return new Result<>(code, data, msg);
    }

    public static <E> Result<E> ok(E data, String msg) {
        if(StrUtil.isBlank(msg)){
            msg = "操作成功";
        }
        return new Result<>(String.valueOf(SUCCESS_CODE), data, msg);
    }

    public static <E> Result<E> ok(E data) {
        return ok(data,null);
    }

    public static Result<Boolean> ok() {
        return ok(true);
    }

    public static <E> Result<E> fail(String code, String msg) {
        if(StrUtil.isBlank(code)){
            code = String.valueOf(FAIL_CODE);
        }
        return new Result<>(code, null, StrUtil.isAllBlank(msg) ? DEF_ERROR_MESSAGE : msg);
    }

    public static <E> Result<E> fail(String msg) {
        return fail(String.valueOf(Status.FAIL_CODE), msg);
    }

    public static <E> Result<E> fail(String msg, Object... args) {
        String message = StrUtil.isAllBlank(msg) ? DEF_ERROR_MESSAGE : msg;
        return fail(String.format(message, args));
    }

    public static <E> Result<E> fail(String code, String msg, Object... args) {
        String message = StrUtil.isAllBlank(msg) ? DEF_ERROR_MESSAGE : msg;
        return fail(code,String.format(message, args));
    }

    public static <E> Result<E> fail(Status status) {
        return fail(String.valueOf(status.getCode()),
                StrUtil.isBlank(status.getTip()) ? DEF_ERROR_MESSAGE : status.getTip());
    }

    public Result<T> put(String key, Object value) {
        if (this.extra == null) {
            this.extra = new HashMap<>();
        }
        this.extra.put(key, value);
        return this;
    }

    public Boolean success() {
        return String.valueOf(SUCCESS_CODE).equals(this.code);
    }

    public Boolean error() {
        return !success();
    }

    @Override
    public String toString() {
        return JSONUtil.toJsonStr(this);
    }
}

步骤2:自定义注解排除特定接口

java">@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreFormatResponse {
}

步骤3:实现响应体增强(ResponseBodyAdvice)

java">@RestControllerAdvice
public class ResponseFormatAdvice implements ResponseBodyAdvice {

    private static List<String> defaultUnencryptedUrls = new ArrayList<>();

    static {
        defaultUnencryptedUrls.add("swagger-resources");
        defaultUnencryptedUrls.add("api-docs");
    }

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        // 是否格式化,false 时不处理
        return !returnType.hasMethodAnnotation(IgnoreFormatResponse.class);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, 
                                  MediaType selectedContentType,
                                  Class selectedConverterType, 
                                  ServerHttpRequest request, 
                                  ServerHttpResponse response) {
        
        body = format(body,returnType,request);
        return body;
    }

    private Object format(Object body, MethodParameter returnType,ServerHttpRequest request){
        String url=((ServletServerHttpRequest) request).getServletRequest().getRequestURL().toString();
        if(isUnFormatUrl(url) ){
            return body;
        }
        // 已经是 Result 类型直接返回
        if(body instanceof Result){
            return body;
        }
        Result<?> result = Result.ok(body);
        if(body instanceof String){
            return JSONUtil.toJsonStr(result);
        }
        return result;
    }

    private boolean isUnFormatUrl(String url){
        if(StrUtil.isBlank(url)){
            return true;
        }
        // 默认内置不格式化
        if(CollectionUtils.isNotEmpty(defaultUnencryptedUrls)){
            for(String s:defaultUnencryptedUrls){
                if(url.contains(s)){
                    return true;
                }
            }
        }
        return false;
    }
}

步骤4:全局异常处理

java">@RestControllerAdvice
public class GlobalExceptionHandler {

    // 处理业务自定义异常,如果没有自定义异常,则可以删除
    @ExceptionHandler(BusinessException.class)
    public ResponseResult<Void> handleBusinessException(BusinessException ex) {
        return ResponseResult.error(ex.getCode(), ex.getMessage());
    }

    // 处理参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseResult<Void> handleValidationException(MethodArgumentNotValidException ex) {
        String errorMsg = ex.getBindingResult().getAllErrors()
                          .stream()
                          .map(DefaultMessageSourceResolvable::getDefaultMessage)
                          .collect(Collectors.joining("; "));
        return ResponseResult.error(400, errorMsg);
    }

    // 兜底异常处理
    @ExceptionHandler(Exception.class)
    public ResponseResult<Void> handleException(Exception ex) {
        log.error("System error: {}", ex.getMessage());
        return ResponseResult.error(500, "系统繁忙,请稍后再试");
    }
}

步骤5:实战应用示例

Controller层

java">@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/{id}")
    public ResponseResult<UserVO> getUser(@PathVariable Long id) {
        UserVO user = userService.getById(id);
        return ResponseResult.success(user);
    }

    @PostMapping
    public ResponseResult<Long> createUser(@Valid @RequestBody UserCreateDTO dto) {
        Long userId = userService.createUser(dto);
        return ResponseResult.success(userId);
    }

    // 排除统一格式处理的接口(如导出接口)
    @IgnoreFormatResponse
    @GetMapping("/export")
    public void exportUser(HttpServletResponse response) {
        // 直接操作response输出流
    }
}

六、前后对比效果

传统方式

java">// 成功
{"id": 1, "name": "张三"}

// 失败
{
  "status": 400,
  "error": "Bad Request",
  "path": "/api/users",
  "stackTrace": "..."
}

统一格式后

java">// 成功
{
  "code": 200,
  "message": "success",
  "data": {"id": 1, "name": "张三"},
  "timestamp": 1659321000000
}

// 参数校验失败
{
  "code": 400,
  "message": "用户名不能为空; 密码长度需6-20位",
  "data": null,
  "timestamp": 1659321000001
}

七、进阶优化方向

  1. 链路追踪增强
    添加 traceId 字段,便于分布式日志追踪
  2. 国际化支持
    根据请求头自动切换多语言消息
  3. 响应压缩
    集成GZIP压缩减少网络传输量
  4. 监控埋点
    统计接口响应时间/成功率

Tips:建议结合 AOP 实现接口耗时监控,完成企业级接口监控闭环,具体如何实现,且看下回分解。


http://www.niftyadmin.cn/n/5863481.html

相关文章

详解Virtualhome环境搭建教程 | 智能体

&#x1f64b;大家好&#xff01;我是毛毛张! &#x1f308;个人首页&#xff1a; 神马都会亿点点的毛毛张 本篇文章毛毛张想分享的是搭建VitrualHome环境的教程&#xff0c;这个内容在国内很少有人在做&#xff0c;或者做的时候分享的教程比较少&#xff0c;毛毛张在此记录…

趣味数学300题1981版-十五个正方形

分析&#xff1a;移动两根变成11个正方形很简单&#xff1a; 移动4根变成15个正方形&#xff0c;分析&#xff1a; 一个田字格包含5个正方形&#xff0c;若要15个正方形需要3个田字格&#xff0c;如果3个田字格完全不重合&#xff0c;需要6*318根火柴。如果合并正方形的边&…

AI大模型学习(二): LangChain(二)

Langchain构建聊天机器人 安装依赖 pip install langchain_community Chat History:它允许聊天机器人"记住"过去的互动,并在回应后续问题时考虑他们 代码 # 创建模型 from langchain_core.messages import HumanMessage from langchain_core.prompts import ChatP…

如何保存爬虫获取商品评论的数据?

保存爬取的评论数据是爬虫项目中的一个重要环节。根据需求&#xff0c;你可以选择将数据保存为本地文件&#xff08;如CSV、JSON、TXT&#xff09;&#xff0c;或者存储到数据库&#xff08;如MySQL、MongoDB等&#xff09;。以下是几种常见的数据保存方式及其示例代码。 1. 保…

ChatGPT平替自由!DeepSeek-R1私有化部署全景攻略

一、DeepSeek-R1本地部署配置要求 &#xff08;一&#xff09;轻量级模型 ▌DeepSeek-R1-1.5B 内存容量&#xff1a;≥8GB 显卡需求&#xff1a;支持CPU推理&#xff08;无需独立GPU&#xff09; 适用场景&#xff1a;本地环境验证测试/Ollama集成调试 &#xff08;二&a…

python: 并发编程 (Concurrent Programming) Simple example

# encoding: utf-8 # 版权所有 2025 ©涂聚文有限公司™ # 许可信息查看&#xff1a;言語成了邀功盡責的功臣&#xff0c;還需要行爲每日來值班嗎 # 描述&#xff1a;并发编程 (Concurrent Programming) pip install mysql.connector # Author : geovindu,Geovin Du 涂…

力扣每日一题【算法学习day.132】

前言 ###我做这类文章一个重要的目的还是记录自己的学习过程&#xff0c;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关键点&#xff0c;力扣上的大佬们的题解质量是非常非常高滴&#xff01;&#xff01;&#xff01; 习题 1.统计相似字符串对的数目 题目链…

Cannot deserialize instance of java.lang.String out of START_ARRAY token

这个错误 Cannot deserialize instance of java.lang.String out of START_ARRAY token 表示 Jackson 正在尝试将一个 JSON 数组反序列化成一个 String 类型的字段&#xff0c;但是 JSON 中传递的是一个数组而不是单一的字符串。 具体来说&#xff0c;这段堆栈信息&#xff1a…