SpringBoot到底該不該用統(tǒng)一包裝類詳解
在SpringBoot項(xiàng)目中,你一定見(jiàn)過(guò)這樣的代碼:
@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
return Result.success(userService.getById(id));
}
或者這樣的:
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
return userService.getById(id);
}
支持統(tǒng)一包裝的人認(rèn)為這樣做規(guī)范、統(tǒng)一,前端對(duì)接方便;反對(duì)的人則認(rèn)為這是多此一舉,增加了代碼復(fù)雜度,而且HTTP本身就有狀態(tài)碼體系,沒(méi)必要重新發(fā)明輪子。
今天從實(shí)際使用場(chǎng)景出發(fā),把這個(gè)問(wèn)題梳理清楚,希望能給你一些參考。
先說(shuō)清楚什么是統(tǒng)一包裝類
所謂統(tǒng)一包裝類,就是將業(yè)務(wù)數(shù)據(jù)再包一層,常見(jiàn)形態(tài)如下:
// 形態(tài)一:code + msg + data
public class Result<T> {
private Integer code;
private String msg;
private T data;
}
// 形態(tài)二:帶上時(shí)間戳、traceId等
public class Response<T> {
private Integer code;
private String msg;
private T data;
private Long timestamp;
private String traceId;
}
這樣包裝的理由主要有三個(gè):
第一,前端解析方便。所有接口返回結(jié)構(gòu)一致,前端只需要寫(xiě)一套解析邏輯,不需要每個(gè)接口單獨(dú)處理。
第二,可以攜帶業(yè)務(wù)錯(cuò)誤碼。比如"用戶不存在"對(duì)應(yīng)10001,"余額不足"對(duì)應(yīng)10002,"參數(shù)校驗(yàn)失敗"對(duì)應(yīng)10003,前端可以根據(jù)不同的錯(cuò)誤碼做不同的處理,比如10002直接跳轉(zhuǎn)到充值頁(yè)面。
第三,方便統(tǒng)一做異常轉(zhuǎn)換。通過(guò) @ControllerAdvice 可以把所有異常統(tǒng)一轉(zhuǎn)換成 Result 格式,避免異常信息直接暴露給前端。
上面主要介紹了包裝類的一些特點(diǎn),下面我們?cè)倏纯窗b及不包裝的三種常見(jiàn)方案的具體實(shí)現(xiàn)方式和優(yōu)缺點(diǎn)。
三種常見(jiàn)方案
方案一:手動(dòng)包裝
每個(gè)接口自己動(dòng)手包一層:
@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
User user = userService.getById(id);
return Result.success(user);
}
@PostMapping("/user")
public Result<Long> createUser(@RequestBody UserCreateDTO dto) {
Long userId = userService.create(dto);
return Result.success(userId);
}
這種方式的好處是明確、可控。你在代碼里一眼就能看出這個(gè)接口返回的是什么,想搞特殊也很方便,直接返回 ResponseEntity 就行。
但寫(xiě)多了你會(huì)覺(jué)得很繁瑣,滿屏都是 Result.success()、Result.error(),改起來(lái)也很麻煩。而且這種方式有個(gè)更大的問(wèn)題:某些場(chǎng)景根本無(wú)法包裝。
最典型的就是文件下載。文件下載需要設(shè)置 Content-Disposition 響應(yīng)頭,指定文件名,還需要設(shè)置正確的 Content-Type,如果用 Result 包一下,這些都沒(méi)法處理了:
@GetMapping("/download")
public ResponseEntity<ByteArrayResource> download() {
byte[] data = fileService.getReport();
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=report.xlsx")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(new ByteArrayResource(data));
}
ResponseEntity 需要直接返回,無(wú)法塞進(jìn) Result 里。類似的場(chǎng)景還有 SSE 推送、文件流、圖片直接輸出等。
所以如果采用手動(dòng)包裝的方案,你還需要在文檔里明確約定哪些接口需要特殊處理,增加維護(hù)成本。
方案二:不包裝,直接返回
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
return userService.getById(id);
}
@PostMapping("/user")
public Long createUser(@RequestBody UserCreateDTO dto) {
return userService.create(dto);
}
這種方式代碼簡(jiǎn)潔,沒(méi)有冗余,通用性好(比如一些負(fù)載、網(wǎng)關(guān)設(shè)備默認(rèn)識(shí)別的是標(biāo)準(zhǔn)HTTP狀態(tài)碼),Swagger 生成的文檔也很清晰,前端一眼就能看出這個(gè)接口返回什么結(jié)構(gòu)。
異常處理怎么解決呢?通過(guò) @ControllerAdvice + @ExceptionHandler:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(BusinessException e) {
return ResponseEntity
.status(e.getHttpStatus()) // 404、400等HTTP狀態(tài)碼
.body(new ErrorResponse(e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnknown(Exception e) {
log.error("系統(tǒng)異常", e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("系統(tǒng)繁忙,請(qǐng)稍后重試"));
}
}
這種方式直接使用 HTTP 標(biāo)準(zhǔn)狀態(tài)碼:200 表示成功,404 表示資源不存在,400 表示參數(shù)錯(cuò)誤,401 表示未登錄,403 表示無(wú)權(quán)限,500 表示服務(wù)器錯(cuò)誤。不需要再定義一套業(yè)務(wù)錯(cuò)誤碼,前端也更容易理解。
不過(guò)有些團(tuán)隊(duì)習(xí)慣了自己定義錯(cuò)誤碼體系,比如 10001、10002 這種,覺(jué)得 HTTP 狀態(tài)碼不夠細(xì)分。其實(shí)大可不必,HTTP 狀態(tài)碼已經(jīng)足夠覆蓋大部分場(chǎng)景了,真需要細(xì)分可以通過(guò)錯(cuò)誤消息來(lái)區(qū)分。
接受這種方式的前提是團(tuán)隊(duì)統(tǒng)一認(rèn)知,不再搞自定義錯(cuò)誤碼那套。如果團(tuán)隊(duì)已經(jīng)有一套成熟的錯(cuò)誤碼體系,遷移成本會(huì)比較高。
方案三:ResponseBodyAdvice自動(dòng)包裝
這是一種折中方案,Controller 代碼保持簡(jiǎn)潔,由框架自動(dòng)包裝:
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return !returnType.hasMethodAnnotation(NoWrap.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
return Result.success(body);
}
}
Controller 里就可以直接寫(xiě):
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
return userService.getById(id); // 被自動(dòng)包裝成 Result<User>
}
這個(gè)方案看起來(lái)很完美,Controller 代碼簡(jiǎn)潔,返回格式又統(tǒng)一。但實(shí)際用起來(lái)有幾個(gè)坑需要特別注意。
第一個(gè)坑是 String 類型的特殊處理。Spring 的消息轉(zhuǎn)換器鏈在處理 String 時(shí),會(huì)優(yōu)先使用 StringHttpMessageConverter,如果我們返回 Result<String>,會(huì)導(dǎo)致類型轉(zhuǎn)換異常。所以需要單獨(dú)判斷:
if (body instanceof String) {
return objectMapper.writeValueAsString(Result.success(body));
}
第二個(gè)坑是調(diào)試不直觀。前端收到數(shù)據(jù)格式不對(duì),你第一反應(yīng)是看 Controller 代碼,但代碼明明寫(xiě)得很正常啊,最后排查半天才發(fā)現(xiàn)是 ResponseAdvice 里的邏輯出了問(wèn)題。這種隱式的包裝,對(duì)不熟悉代碼的人來(lái)說(shuō)就是個(gè)黑盒,調(diào)試成本較高。
第三個(gè)坑是特殊返回類型需要排除。ResponseEntity、SseEmitter、StreamingResponseBody 這些類型不能被包裝,否則就廢了。你需要在 supports() 方法里把這些類型排除掉,或者定義一個(gè) @NoWrap 注解,需要例外的接口自己標(biāo)注。
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 排除 ResponseEntity
if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) {
return false;
}
// 排除標(biāo)注了 @NoWrap 的方法
return !returnType.hasMethodAnnotation(NoWrap.class);
}
按場(chǎng)景選擇
三種方案沒(méi)有絕對(duì)優(yōu)劣,關(guān)鍵是根據(jù)場(chǎng)景選擇。
| 場(chǎng)景 | 建議方案 | 理由 |
|---|---|---|
| 內(nèi)部前后端對(duì)接接口 | 統(tǒng)一包裝 | 前端解析省心,只需一套邏輯 |
| 對(duì)外開(kāi)放 RESTful API | 直接返回 | HTTP 狀態(tài)碼語(yǔ)義更清晰,符合REST規(guī)范 |
| 文件下載/流式接口 | 直接返回 | 無(wú)法包裝,必須直接控制響應(yīng) |
| 第三方回調(diào)接口 | 按對(duì)方要求 | 對(duì)方規(guī)定什么格式就返回什么格式 |
具體項(xiàng)目中可能會(huì)出現(xiàn)多種方案混用的情況,可以通過(guò)三種方式區(qū)分是否返回包裝對(duì)象:
方式一:按接口路徑
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
String path = getPath();
// 只有 /api 開(kāi)頭的內(nèi)部接口才包裝
return path != null && path.startsWith("/api/");
}
約定 /api 開(kāi)頭的是內(nèi)部接口,自動(dòng)包裝;其他路徑保持原樣。這種方式簡(jiǎn)單粗暴,不需要改動(dòng)任何業(yè)務(wù)代碼,但需要團(tuán)隊(duì)遵守路徑規(guī)范。
方式二:按注解
@GetMapping("/download")
@NoWrap
public ResponseEntity<ByteArrayResource> download() {
// ...
}
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
// 默認(rèn)包裝
}
定義一個(gè) @NoWrap 注解,需要例外的接口標(biāo)注一下。這種方式靈活,但缺點(diǎn)是容易漏,每次新增特殊接口都得記得加注解。
方式三:按返回類型
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
Class<?> type = returnType.getParameterType();
// ResponseEntity、SseEmitter 等類型不包裝
if (ResponseEntity.class.isAssignableFrom(type) ||
SseEmitter.class.isAssignableFrom(type) ||
StreamingResponseBody.class.isAssignableFrom(type)) {
return false;
}
return true;
}
這種方式最省心,不用記路徑規(guī)范也不用在每個(gè)接口上加注解,框架自動(dòng)識(shí)別特殊類型。但前提是你的代碼風(fēng)格要統(tǒng)一,不要一會(huì)兒用 ResponseEntity,一會(huì)兒用 Result。
總結(jié)
無(wú)論選哪種方案,關(guān)鍵是規(guī)則清晰并執(zhí)行到位。前端對(duì)接的成本,很大程度上取決于能不能把規(guī)則說(shuō)清楚并堅(jiān)持執(zhí)行下去。
到此這篇關(guān)于SpringBoot到底該不該用統(tǒng)一包裝類的文章就介紹到這了,更多相關(guān)SpringBoot統(tǒng)一包裝類內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot工具類ReflectionUtils使用教程
這篇文章主要介紹了Springboot內(nèi)置的工具類之ReflectionUtils的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-12-12
IDEA 2021.1 操作SVN 最新超詳細(xì)教程(圖文)
本教程將通過(guò)idea從svn服務(wù)器中的任意一個(gè)分支檢出代碼(本文采用branches),然后再idea中創(chuàng)建新的分支、提交代碼、拉取代碼、合并分支等操作進(jìn)行一一記錄,暫不包含代碼合并,對(duì)idea2021.1操作svn相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)下吧2021-05-05
Java動(dòng)態(tài)初始化數(shù)組,元素默認(rèn)值規(guī)則詳解
動(dòng)態(tài)初始化數(shù)組涉及先定義數(shù)組長(zhǎng)度,后填充具體數(shù)據(jù),適用于數(shù)據(jù)量已知但具體值未定的情況,這種初始化方式允許程序運(yùn)行過(guò)程中賦值,并會(huì)根據(jù)數(shù)據(jù)類型設(shè)定默認(rèn)值,如整型為0,字符串為null,動(dòng)態(tài)初始化與靜態(tài)初始化格式不能混用2024-10-10
EntityWrapper如何在and條件中嵌套o(hù)r語(yǔ)句
這篇文章主要介紹了EntityWrapper如何在and條件中嵌套o(hù)r語(yǔ)句,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
jpa實(shí)現(xiàn)多對(duì)多的屬性時(shí)查詢的兩種方法
這篇文章主要介紹了jpa實(shí)現(xiàn)多對(duì)多的屬性時(shí)查詢的兩種方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
基于application和bootstrap的加載順序及區(qū)別說(shuō)明
這篇文章主要介紹了application和bootstrap的加載順序及區(qū)別說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
SpringBoot啟動(dòng)時(shí)執(zhí)行某些操作的8種方式
在真實(shí)項(xiàng)目開(kāi)發(fā)過(guò)程中,我們經(jīng)常會(huì)需要在程序啟動(dòng)時(shí)執(zhí)行一些特定的業(yè)務(wù)操作,比如系統(tǒng)預(yù)熱、系統(tǒng)初始化等,小編為大家介紹 8 種實(shí)現(xiàn)方式,需要的朋友可以參考下2025-10-10

