別錯(cuò)過(guò)!Spring Boot 實(shí)現(xiàn)統(tǒng)一響應(yīng)結(jié)果的五種方案
環(huán)境:SpringBoot3.4.2
1. 簡(jiǎn)介
在 Spring Boot 項(xiàng)目開發(fā)中,隨著接口數(shù)量增多,不同接口的響應(yīng)結(jié)果格式各異,這給前端開發(fā)和接口維護(hù)帶來(lái)諸多不便。前端需要針對(duì)不同格式做適配,增加開發(fā)成本;后端維護(hù)時(shí),也易因格式混亂導(dǎo)致錯(cuò)誤。
統(tǒng)一響應(yīng)結(jié)果能解決這些問(wèn)題。它規(guī)定所有接口返回相同格式的數(shù)據(jù),如包含狀態(tài)碼、消息、數(shù)據(jù)等字段。例如,成功時(shí)返回狀態(tài)碼 200、消息“操作成功”和數(shù)據(jù);失敗時(shí)返回對(duì)應(yīng)錯(cuò)誤碼和消息。這樣前端只需按統(tǒng)一格式解析,降低開發(fā)復(fù)雜度;后端維護(hù)也更便捷,只需關(guān)注業(yè)務(wù)邏輯。統(tǒng)一響應(yīng)結(jié)果提升了代碼可讀性、可維護(hù)性,增強(qiáng)了前后端協(xié)作效率。
在本篇文章里,我們將針對(duì) Spring Boot 中實(shí)現(xiàn)統(tǒng)一響應(yīng)結(jié)果,詳細(xì)闡述 6 種切實(shí)可行的方案。
2.實(shí)戰(zhàn)案例
2.1 自定義響應(yīng)實(shí)體類
定義一個(gè)通用的響應(yīng)類,包含狀態(tài)碼、消息和數(shù)據(jù)。然后在Controller中返回這個(gè)類的實(shí)例。
自定義狀態(tài)碼
public enum ResultCode {
SUCCESS(200, "操作成功"), FAILURE(400, "業(yè)務(wù)異常"),
UNAUTHORIZED(401, "未授權(quán)"), FORBIDDEN(403, "禁止訪問(wèn)"),
NOT_FOUND(404, "資源不存在"), INTERNAL_ERROR(500, "系統(tǒng)錯(cuò)誤") ;
private final int code ;
private final String msg ;
// getters
}
自定義結(jié)果對(duì)象
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;
}
// 成功響應(yīng)(帶數(shù)據(jù))
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), data);
}
// 失敗響應(yīng)
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")) ;
}
// 錯(cuò)誤處理
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception e) {
return ApiResponse.fail(ResultCode.NOT_FOUND);
}
}
2.2 自定義ResponseBodyAdvice
通過(guò)實(shí)現(xiàn) ResponseBodyAdvice 接口,可以對(duì)Controller返回的結(jié)果進(jìn)行統(tǒng)一封裝。這種方式不需要修改每個(gè)Controller方法。
自定義注解(標(biāo)注那些不需要處理的接口)
@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類型特殊轉(zhuǎn)換
if (body instanceof String) {
try {
return this.objectMapper.writeValueAsString(ApiResponse.success(body)) ;
} catch (JsonProcessingException e) {
System.err.printf("JSON序列化錯(cuò)誤: %s%n", e.getMessage()) ;
return body ;
}
}
// 已封裝過(guò)的響應(yīng)直接返回
if (body instanceof ApiResponse) {
return body ;
}
// 空響應(yīng)
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技術(shù)
我們也可以使用AOP來(lái)攔截Controller方法的返回值,然后進(jìn)行統(tǒng)一封裝。但是AOP技術(shù)的局限性非常大,我們首先要統(tǒng)一Controller接口的響應(yīng)類型比如:使用ResponseEntity 作為統(tǒng)一方法返回類型。
@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) {
// 對(duì)ResponseEntity的body進(jìn)行統(tǒng)一處理
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接口都已經(jīng)統(tǒng)一了ResponseEntity,那么我還搞個(gè)AOP做什么?
2.4 使用Filter
此種方式可行,但是也不推薦,復(fù)雜且易出錯(cuò),破壞流式響應(yīng)。
@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")) ;
}
圖片
通過(guò)Filter方式可能對(duì)那些 遺留系統(tǒng)改造 有用吧。
2.5 自定義HttpMessageConverter
通過(guò)自定義HttpMessageConverter方法提供了對(duì)響應(yīng)處理過(guò)程的精細(xì)控制,特別適合需要完全定制響應(yīng)格式的場(chǎng)景。
@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否則統(tǒng)一都會(huì)通過(guò)text/html響應(yīng)這會(huì)出現(xiàn)亂碼問(wèn)題
outputMessage.getHeaders().add("Content-Type", "application/json;charset=utf-8");
// 創(chuàng)建統(tǒng)一響應(yīng)體
ApiResponse<Object> response = ApiResponse.success(body);
try {
// 序列化響應(yīng)體
String json = objectMapper.writeValueAsString(response);
// 寫入響應(yīng)
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 "查詢完成" ;
}
圖片
圖片
總結(jié)
該方案核心優(yōu)勢(shì)在于細(xì)粒度控制與高性能:
- 直接操作輸出流,性能最優(yōu)
- 精準(zhǔn)控制封裝邏輯,通過(guò)supports()方法實(shí)現(xiàn)條件過(guò)濾
- 完全掌控響應(yīng)結(jié)構(gòu),支持特殊類型(如void/String)
- 與Spring MVC原生機(jī)制無(wú)縫集成,不破壞現(xiàn)有流程
- 避免全局包裝的過(guò)度處理,保持框架靈活性
適用于需要極致性能優(yōu)化和深度定制響應(yīng)格式的高要求場(chǎng)景。