统一响应格式
统一响应格式为所有 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));
}
}