统一响应格式

统一响应格式为所有 API 接口返回固定结构的 JSON,避免各接口自定义返回值,便于前端统一处理和监控告警。

响应结构设计

@Data
public class ApiResponse<T> {
 
    private int code;        // 业务状态码,0 = 成功
    private String message;  // 人类可读描述
    private T data;          // 业务数据(失败时为 null)
    private long timestamp;  // 服务器时间戳(毫秒)
 
    private ApiResponse() {
        this.timestamp = System.currentTimeMillis();
    }
 
    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> resp = new ApiResponse<>();
        resp.code = 0;
        resp.message = "success";
        resp.data = data;
        return resp;
    }
 
    public static <T> ApiResponse<T> success() {
        return success(null);
    }
 
    public static <T> ApiResponse<T> fail(int code, String message) {
        ApiResponse<T> resp = new ApiResponse<>();
        resp.code = code;
        resp.message = message;
        return resp;
    }
 
    public static <T> ApiResponse<T> fail(ErrorCode errorCode) {
        return fail(errorCode.getCode(), errorCode.getMessage());
    }
}

典型响应示例:

// 成功
{
  "code": 0,
  "message": "success",
  "data": { "id": 1, "username": "zhangsan" },
  "timestamp": 1714204800000
}
 
// 业务失败
{
  "code": 20001,
  "message": "用户不存在",
  "data": null,
  "timestamp": 1714204800000
}

错误码设计

按模块划分错误码段,避免冲突:

public enum ErrorCode {
 
    // 通用错误 1xxxx
    PARAM_ERROR(10001, "参数错误"),
    PARAM_MISSING(10002, "缺少必要参数"),
    UNAUTHORIZED(10003, "未登录或 Token 已过期"),
    FORBIDDEN(10004, "无权限访问"),
    NOT_FOUND(10005, "资源不存在"),
    TOO_MANY_REQUESTS(10006, "请求过于频繁"),
    SYSTEM_ERROR(19999, "系统内部错误"),
 
    // 用户模块 2xxxx
    USER_NOT_FOUND(20001, "用户不存在"),
    USER_ALREADY_EXISTS(20002, "用户名已被注册"),
    PASSWORD_WRONG(20003, "密码错误"),
    ACCOUNT_DISABLED(20004, "账号已被禁用"),
 
    // 订单模块 3xxxx
    ORDER_NOT_FOUND(30001, "订单不存在"),
    ORDER_STATUS_INVALID(30002, "订单状态不支持此操作"),
    STOCK_INSUFFICIENT(30003, "库存不足");
 
    private final int code;
    private final String message;
 
    ErrorCode(int code, String message) {
        this.code = code;
        this.message = message;
    }
 
    public int getCode() { return code; }
    public String getMessage() { return message; }
}

ResponseBodyAdvice 全局自动包装

通过 ResponseBodyAdvice 拦截所有 Controller 返回值,自动包装为 ApiResponse,无需每个方法手动调用 ApiResponse.success()

@RestControllerAdvice
public class ResponseWrapAdvice implements ResponseBodyAdvice<Object> {
 
    // 标记不需要包装的接口
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface NoWrap {}
 
    @Override
    public boolean supports(MethodParameter returnType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        // 已经是 ApiResponse 的不再包装
        if (ApiResponse.class.isAssignableFrom(returnType.getParameterType())) {
            return false;
        }
        // 标注 @NoWrap 的不包装
        if (returnType.hasMethodAnnotation(NoWrap.class)
                || returnType.getDeclaringClass().isAnnotationPresent(NoWrap.class)) {
            return false;
        }
        // ResponseEntity / StreamingResponseBody(文件下载)不包装
        Class<?> paramType = returnType.getParameterType();
        if (ResponseEntity.class.isAssignableFrom(paramType)
                || StreamingResponseBody.class.isAssignableFrom(paramType)) {
            return false;
        }
        // Actuator 端点不包装
        String className = returnType.getDeclaringClass().getName();
        if (className.startsWith("org.springframework.boot.actuate")) {
            return false;
        }
        return true;
    }
 
    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {
        // String 返回类型走 StringHttpMessageConverter,需特殊处理
        if (body instanceof String) {
            try {
                response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                return new ObjectMapper().writeValueAsString(ApiResponse.success(body));
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        return ApiResponse.success(body);
    }
}

使用后,Controller 直接返回业务对象即可:

@RestController
@RequestMapping("/users")
public class UserController {
 
    @GetMapping("/{id}")
    public UserVO getUser(@PathVariable Long id) {
        return userService.findById(id);   // 自动包装为 ApiResponse<UserVO>
    }
 
    @PostMapping
    public Long createUser(@RequestBody @Validated CreateUserRequest req) {
        return userService.create(req);    // 自动包装为 ApiResponse<Long>
    }
 
    // 文件下载不包装(ResponseEntity 被 supports() 排除)
    @GetMapping("/export")
    public ResponseEntity<Resource> export() { ... }
}

分页响应

@Data
@AllArgsConstructor
public class PageResult<T> {
 
    private List<T> list;      // 当前页数据
    private long total;        // 总记录数
    private int page;          // 当前页码(从 1 开始)
    private int size;          // 每页条数
    private int totalPages;    // 总页数
 
    // Spring Data JPA
    public static <T> PageResult<T> of(Page<T> page) {
        return new PageResult<>(
            page.getContent(),
            page.getTotalElements(),
            page.getNumber() + 1,
            page.getSize(),
            page.getTotalPages()
        );
    }
 
    // MyBatis-Plus
    public static <T> PageResult<T> of(IPage<T> page) {
        return new PageResult<>(
            page.getRecords(),
            page.getTotal(),
            (int) page.getCurrent(),
            (int) page.getSize(),
            (int) page.getPages()
        );
    }
}

分页响应结果:

{
  "code": 0,
  "message": "success",
  "data": {
    "list": [...],
    "total": 100,
    "page": 1,
    "size": 20,
    "totalPages": 5
  },
  "timestamp": 1714204800000
}

与全局异常处理集成

@RestControllerAdvice 同时处理异常(失败响应)与正常包装,两者放在同一个类或分开均可:

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
 
    // 业务异常
    @ExceptionHandler(BusinessException.class)
    public ApiResponse<Void> handleBusiness(BusinessException e) {
        return ApiResponse.fail(e.getErrorCode());
    }
 
    // 参数校验失败(@Validated)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<Void> handleValidation(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors().stream()
            .map(FieldError::getDefaultMessage)
            .findFirst()
            .orElse("参数错误");
        return ApiResponse.fail(ErrorCode.PARAM_ERROR.getCode(), msg);
    }
 
    // 路径参数类型转换失败
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ApiResponse<Void> handleTypeMismatch(MethodArgumentTypeMismatchException e) {
        return ApiResponse.fail(ErrorCode.PARAM_ERROR.getCode(),
            "参数类型错误: " + e.getName());
    }
 
    // 未认证
    @ExceptionHandler(AuthenticationException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ApiResponse<Void> handleUnauthorized(AuthenticationException e) {
        return ApiResponse.fail(ErrorCode.UNAUTHORIZED);
    }
 
    // 权限不足
    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ApiResponse<Void> handleForbidden(AccessDeniedException e) {
        return ApiResponse.fail(ErrorCode.FORBIDDEN);
    }
 
    // 兜底
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiResponse<Void> handleGeneral(Exception e) {
        log.error("未处理异常", e);
        return ApiResponse.fail(ErrorCode.SYSTEM_ERROR);
    }
}

Feign 客户端自动解包

服务间调用时,Feign 返回 ApiResponse<T>,需自定义解码器自动解包,让调用方直接得到 T

public class ApiResponseDecoder implements Decoder {
 
    private final Decoder delegate;
 
    public ApiResponseDecoder(Decoder delegate) {
        this.delegate = delegate;
    }
 
    @Override
    public Object decode(Response response, Type type) throws IOException, FeignException {
        // 构造 ApiResponse<T> 的泛型类型
        ParameterizedType wrappedType = TypeUtils.parameterize(ApiResponse.class, type);
        ApiResponse<?> apiResponse = (ApiResponse<?>) delegate.decode(response, wrappedType);
 
        if (apiResponse == null) return null;
        if (apiResponse.getCode() != 0) {
            throw new FeignBusinessException(apiResponse.getCode(), apiResponse.getMessage());
        }
        return apiResponse.getData();
    }
}
 
// 注册解码器
@Configuration
public class FeignConfig {
    @Bean
    public Decoder feignDecoder(ObjectFactory<HttpMessageConverters> converters) {
        return new ApiResponseDecoder(new SpringDecoder(converters));
    }
}

相关链接