一、为什么要统一
在真实项目开发中,你是否遇到过这些问题?
- 前端需要为不同接口编写差异化处理逻辑
- 错误信息格式五花八门,定位问题困难
- 全局异常处理缺失导致敏感信息泄露
在前后端分离架构中,统一的 API 响应格式是提升协作效率的关键。本文将手把手教你用 SpringBoot 实现标准化响应封装,从此告别接口格式混乱的烦恼!
二、统一响应格式的价值
- 标准化:所有接口遵循相同数据结构
- 可维护性:集中处理异常和成功响应
- 安全性:隐藏技术细节,暴露友好提示
- 可扩展性:轻松添加统一字段(如 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
}
七、进阶优化方向
- 链路追踪增强
添加 traceId 字段,便于分布式日志追踪 - 国际化支持
根据请求头自动切换多语言消息 - 响应压缩
集成GZIP压缩减少网络传输量 - 监控埋点
统计接口响应时间/成功率
Tips:建议结合 AOP 实现接口耗时监控,完成企业级接口监控闭环,具体如何实现,且看下回分解。