別錯過!Spring Boot 實現統一響應結果的五種方案
環境:SpringBoot3.4.2
1. 簡介
在 Spring Boot 項目開發中,隨著接口數量增多,不同接口的響應結果格式各異,這給前端開發和接口維護帶來諸多不便。前端需要針對不同格式做適配,增加開發成本;后端維護時,也易因格式混亂導致錯誤。
統一響應結果能解決這些問題。它規定所有接口返回相同格式的數據,如包含狀態碼、消息、數據等字段。例如,成功時返回狀態碼 200、消息“操作成功”和數據;失敗時返回對應錯誤碼和消息。這樣前端只需按統一格式解析,降低開發復雜度;后端維護也更便捷,只需關注業務邏輯。統一響應結果提升了代碼可讀性、可維護性,增強了前后端協作效率。
在本篇文章里,我們將針對 Spring Boot 中實現統一響應結果,詳細闡述 6 種切實可行的方案。
2.實戰案例
2.1 自定義響應實體類
定義一個通用的響應類,包含狀態碼、消息和數據。然后在Controller中返回這個類的實例。
自定義狀態碼
public enum ResultCode {
SUCCESS(200, "操作成功"), FAILURE(400, "業務異常"),
UNAUTHORIZED(401, "未授權"), FORBIDDEN(403, "禁止訪問"),
NOT_FOUND(404, "資源不存在"), INTERNAL_ERROR(500, "系統錯誤") ;
private final int code ;
private final String msg ;
// getters
}
自定義結果對象
public class ApiResponse<T> {
private int code ;
private String message ;
private T data;
public ApiResponse(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
// 成功響應(帶數據)
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), data);
}
// 失敗響應
public static <T> ApiResponse<T> fail(ResultCode resultCode) {
return new ApiResponse<>(resultCode.getCode(), resultCode.getMsg(), null);
}
// ...
// getters, setters
}
Controller接口
@RestController
@RequestMapping("/way1")
public class Way1Controller {
@GetMapping("/{id}")
public ApiResponse<User> getUser(@PathVariable Long id) {
return ApiResponse.success(new User(id, "Pack")) ;
}
// 錯誤處理
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception e) {
return ApiResponse.fail(ResultCode.NOT_FOUND);
}
}
2.2 自定義ResponseBodyAdvice
通過實現 ResponseBodyAdvice 接口,可以對Controller返回的結果進行統一封裝。這種方式不需要修改每個Controller方法。
自定義注解(標注那些不需要處理的接口)
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface NoWrap {
}
自定義ResponseBodyAdvice
@RestControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {
private final ObjectMapper objectMapper ;
public GlobalResponseAdvice(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return !returnType.hasMethodAnnotation(NoWrap.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
// 處理String類型特殊轉換
if (body instanceof String) {
try {
return this.objectMapper.writeValueAsString(ApiResponse.success(body)) ;
} catch (JsonProcessingException e) {
System.err.printf("JSON序列化錯誤: %s%n", e.getMessage()) ;
return body ;
}
}
// 已封裝過的響應直接返回
if (body instanceof ApiResponse) {
return body ;
}
// 空響應
if (body == null && returnType.getParameterType().equals(void.class)) {
return ApiResponse.success();
}
return ApiResponse.success(body) ;
}
}
Controller接口
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return new User(id, "Pack") ;
}
@GetMapping("/query")
public String query() {
return "查詢完成" ;
}
圖片
圖片
2.3 使用AOP技術
我們也可以使用AOP來攔截Controller方法的返回值,然后進行統一封裝。但是AOP技術的局限性非常大,我們首先要統一Controller接口的響應類型比如:使用ResponseEntity 作為統一方法返回類型。
@Aspect
@Component
public class ResponseAspect {
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void controllerPointcut() {
}
@Around("controllerPointcut()")
public Object handleResponse(ProceedingJoinPoint pjp) throws Throwable {
Object result = pjp.proceed();
if (result instanceof ResponseEntity<?> ret) {
// 對ResponseEntity的body進行統一處理
Object body = ApiResponse.success(ret.getBody()) ;
return ResponseEntity.ok(body) ;
}
return result;
}
}
Controller接口
@GetMapping("/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
return ResponseEntity.ok(new User(id, "Pack")) ;
}
此種方式最不推薦的了,Controller接口都已經統一了ResponseEntity,那么我還搞個AOP做什么?
2.4 使用Filter
此種方式可行,但是也不推薦,復雜且易出錯,破壞流式響應。
@WebFilter("/way4/*")
public class ResponseContentFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) resp;
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
try {
chain.doFilter(req, responseWrapper);
} finally {
// ...
}
String content = new String(responseWrapper.getContentAsByteArray(), "UTF-8") ;
ObjectMapper mapper = new ObjectMapper();
byte[] ret = mapper.writeValueAsBytes(ApiResponse.success(content)) ;
response.getOutputStream().write(ret) ;
}
}
Controller接口
@GetMapping("/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
return ResponseEntity.ok(new User(id, "Pack")) ;
}
圖片
通過Filter方式可能對那些 遺留系統改造 有用吧。
2.5 自定義HttpMessageConverter
通過自定義HttpMessageConverter方法提供了對響應處理過程的精細控制,特別適合需要完全定制響應格式的場景。
@Component
public class ResponseMessageConverter extends AbstractHttpMessageConverter<Object> {
private final ObjectMapper objectMapper;
public ResponseMessageConverter(ObjectMapper objectMapper) {
super(MediaType.APPLICATION_JSON, MediaType.TEXT_HTML) ;
this.objectMapper = objectMapper;
}
@Override
protected boolean supports(Class<?> clazz) {
// 支持所有類型,除了我們自己的ApiResponse和Void
return !ApiResponse.class.isAssignableFrom(clazz) && !Void.TYPE.equals(clazz);
}
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
// 只處理輸出,不處理輸入
return null;
}
@Override
protected void writeInternal(Object body, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
// 由于我們上面配置的能夠支持text/html,所以必須在這里配置Content-Type否則統一都會通過text/html響應這會出現亂碼問題
outputMessage.getHeaders().add("Content-Type", "application/json;charset=utf-8");
// 創建統一響應體
ApiResponse<Object> response = ApiResponse.success(body);
try {
// 序列化響應體
String json = objectMapper.writeValueAsString(response);
// 寫入響應
outputMessage.getBody().write(json.getBytes(StandardCharsets.UTF_8));
} catch (JsonProcessingException e) {
throw new HttpMessageNotWritableException("Error writing response", e);
}
}
}
Controller接口
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return new User(id, "Pack") ;
}
@GetMapping("/query")
public String query() {
return "查詢完成" ;
}
圖片
圖片
總結
該方案核心優勢在于細粒度控制與高性能:
- 直接操作輸出流,性能最優
- 精準控制封裝邏輯,通過supports()方法實現條件過濾
- 完全掌控響應結構,支持特殊類型(如void/String)
- 與Spring MVC原生機制無縫集成,不破壞現有流程
- 避免全局包裝的過度處理,保持框架靈活性
適用于需要極致性能優化和深度定制響應格式的高要求場景。