詳解SpringBoot?統(tǒng)一后端返回格式的方法
首先我們來看看為什么要返回統(tǒng)一的標(biāo)準(zhǔn)格式?
為什么要對(duì)SpringBoot返回統(tǒng)一的標(biāo)準(zhǔn)格式
在默認(rèn)情況下,SpringBoot的返回格式常見的有三種:
第一種:返回 String
@GetMapping("/hello") public String getStr(){ return "hello,javadaily"; }
此時(shí)調(diào)用接口獲取到的返回值是這樣:
hello,javadaily
第二種:返回自定義對(duì)象
@GetMapping("/aniaml") public Aniaml getAniaml(){ Aniaml aniaml = new Aniaml(1,"pig"); return aniaml; }
此時(shí)調(diào)用接口獲取到的返回值是這樣:
{ "id": 1, "name": "pig" }
第三種:接口異常
@GetMapping("/error") public int error(){ int i = 9/0; return i; }
此時(shí)調(diào)用接口獲取到的返回值是這樣:
{ "timestamp": "2021-07-08T08:05:15.423+00:00", "status": 500, "error": "Internal Server Error", "path": "/wrong" }
基于以上種種情況,如果你和前端開發(fā)人員聯(lián)調(diào)接口她們就會(huì)很懵逼,由于我們沒有給他一個(gè)統(tǒng)一的格式,前端人員不知道如何處理返回值。
還有甚者,有的同學(xué)比如小張喜歡對(duì)結(jié)果進(jìn)行封裝,他使用了Result對(duì)象,小王也喜歡對(duì)結(jié)果進(jìn)行包裝,但是他卻使用的是Response對(duì)象,當(dāng)出現(xiàn)這種情況時(shí)我相信前端人員一定會(huì)抓狂的。
所以我們項(xiàng)目中是需要定義一個(gè)統(tǒng)一的標(biāo)準(zhǔn)返回格式的。
定義返回標(biāo)準(zhǔn)格式
一個(gè)標(biāo)準(zhǔn)的返回格式至少包含3部分:
- status 狀態(tài)值:由后端統(tǒng)一定義各種返回結(jié)果的狀態(tài)碼
- message 描述:本次接口調(diào)用的結(jié)果描述
- data 數(shù)據(jù):本次返回的數(shù)據(jù)。
{ "status":"100", "message":"操作成功", "data":"hello,javadaily" }
當(dāng)然也可以按需加入其他擴(kuò)展值,比如我們就在返回對(duì)象中添加了接口調(diào)用時(shí)間
timestamp: 接口調(diào)用時(shí)間
定義返回對(duì)象
@Data public class ResultData<T> { /** 結(jié)果狀態(tài) ,具體狀態(tài)碼參見ResultData.java*/ private int status; private String message; private T data; private long timestamp ; public ResultData (){ this.timestamp = System.currentTimeMillis(); } public static <T> ResultData<T> success(T data) { ResultData<T> resultData = new ResultData<>(); resultData.setStatus(ReturnCode.RC100.getCode()); resultData.setMessage(ReturnCode.RC100.getMessage()); resultData.setData(data); return resultData; } public static <T> ResultData<T> fail(int code, String message) { ResultData<T> resultData = new ResultData<>(); resultData.setStatus(code); resultData.setMessage(message); return resultData; } }
定義狀態(tài)碼
public enum ReturnCode { /**操作成功**/ RC100(100,"操作成功"), /**操作失敗**/ RC999(999,"操作失敗"), /**服務(wù)限流**/ RC200(200,"服務(wù)開啟限流保護(hù),請(qǐng)稍后再試!"), /**服務(wù)降級(jí)**/ RC201(201,"服務(wù)開啟降級(jí)保護(hù),請(qǐng)稍后再試!"), /**熱點(diǎn)參數(shù)限流**/ RC202(202,"熱點(diǎn)參數(shù)限流,請(qǐng)稍后再試!"), /**系統(tǒng)規(guī)則不滿足**/ RC203(203,"系統(tǒng)規(guī)則不滿足要求,請(qǐng)稍后再試!"), /**授權(quán)規(guī)則不通過**/ RC204(204,"授權(quán)規(guī)則不通過,請(qǐng)稍后再試!"), /**access_denied**/ RC403(403,"無訪問權(quán)限,請(qǐng)聯(lián)系管理員授予權(quán)限"), /**access_denied**/ RC401(401,"匿名用戶訪問無權(quán)限資源時(shí)的異常"), /**服務(wù)異常**/ RC500(500,"系統(tǒng)異常,請(qǐng)稍后重試"), INVALID_TOKEN(2001,"訪問令牌不合法"), ACCESS_DENIED(2003,"沒有權(quán)限訪問該資源"), CLIENT_AUTHENTICATION_FAILED(1001,"客戶端認(rèn)證失敗"), USERNAME_OR_PASSWORD_ERROR(1002,"用戶名或密碼錯(cuò)誤"), UNSUPPORTED_GRANT_TYPE(1003, "不支持的認(rèn)證模式"); /**自定義狀態(tài)碼**/ private final int code; /**自定義描述**/ private final String message; ReturnCode(int code, String message){ this.code = code; this.message = message; } public int getCode() { return code; } public String getMessage() { return message; } }
統(tǒng)一返回格式
@GetMapping("/hello") public ResultData<String> getStr(){ return ResultData.success("hello,javadaily"); }
此時(shí)調(diào)用接口獲取到的返回值是這樣:
{ "status": 100, "message": "hello,javadaily", "data": null, "timestamp": 1625736481648, "httpStatus": 0 }
這樣確實(shí)已經(jīng)實(shí)現(xiàn)了我們想要的結(jié)果,我在很多項(xiàng)目中看到的都是這種寫法,在Controller層通過ResultData.success()
對(duì)返回結(jié)果進(jìn)行包裝后返回給前端。
看到這里我們不妨停下來想想,這樣做有什么弊端呢?
最大的弊端就是我們后面每寫一個(gè)接口都需要調(diào)用ResultData.success()
這行代碼對(duì)結(jié)果進(jìn)行包裝,重復(fù)勞動(dòng),浪費(fèi)體力;
而且還很容易被其他老鳥給嘲笑。
所以呢我們需要對(duì)代碼進(jìn)行優(yōu)化,目標(biāo)就是不要每個(gè)接口都手工制定ResultData
返回值。
高級(jí)實(shí)現(xiàn)方式
要優(yōu)化這段代碼很簡(jiǎn)單,我們只需要借助SpringBoot提供的ResponseBodyAdvice
即可。
“ResponseBodyAdvice的作用:攔截Controller方法的返回值,統(tǒng)一處理返回值/響應(yīng)體,一般用來統(tǒng)一返回格式,加解密,簽名等等。
先來看下ResponseBodyAdvice
的源碼:
public interface ResponseBodyAdvice<T> { /** * 是否支持advice功能 * true 支持,false 不支持 */ boolean supports(MethodParameter var1, Class<? extends HttpMessageConverter<?>> var2); /** * 對(duì)返回的數(shù)據(jù)進(jìn)行處理 */ @Nullable T beforeBodyWrite(@Nullable T var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<?>> var4, ServerHttpRequest var5, ServerHttpResponse var6); }
我們只需要編寫一個(gè)具體實(shí)現(xiàn)類即可
/** * @author jam * @date 2021/7/8 10:10 上午 */ @RestControllerAdvice public class ResponseAdvice implements ResponseBodyAdvice<Object> { @Autowired private ObjectMapper objectMapper; @Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { return true; } @SneakyThrows @Override public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { if(o instanceof String){ return objectMapper.writeValueAsString(ResultData.success(o)); } return ResultData.success(o); } }
需要注意兩個(gè)地方:
@RestControllerAdvice
注解
@RestControllerAdvice
是@RestController
注解的增強(qiáng),可以實(shí)現(xiàn)三個(gè)方面的功能:
- 全局異常處理
- 全局?jǐn)?shù)據(jù)綁定
- 全局?jǐn)?shù)據(jù)預(yù)處理
String類型判斷
if(o instanceof String){ return objectMapper.writeValueAsString(ResultData.success(o)); }
這段代碼一定要加,如果Controller直接返回String的話,SpringBoot是直接返回,故我們需要手動(dòng)轉(zhuǎn)換成json。
經(jīng)過上面的處理我們就再也不需要通過ResultData.success()
來進(jìn)行轉(zhuǎn)換了,直接返回原始數(shù)據(jù)格式,SpringBoot自動(dòng)幫我們實(shí)現(xiàn)包裝類的封裝。
@GetMapping("/hello") public String getStr(){ return "hello,javadaily"; }
此時(shí)我們調(diào)用接口返回的數(shù)據(jù)結(jié)果為:
@GetMapping("/hello") public String getStr(){ return "hello,javadaily"; }
是不是感覺很完美,別急,還有個(gè)問題在等著你呢。
接口異常問題
此時(shí)有個(gè)問題,由于我們沒對(duì)Controller的異常進(jìn)行處理,當(dāng)我們調(diào)用的方法一旦出現(xiàn)異常,就會(huì)出現(xiàn)問題,比如下面這個(gè)接口
@GetMapping("/wrong") public int error(){ int i = 9/0; return i; }
返回的結(jié)果為:
這顯然不是我們想要的結(jié)果,接口都報(bào)錯(cuò)了還返回操作成功的響應(yīng)碼,前端看了會(huì)打人的。
別急,接下來我們進(jìn)入第二個(gè)議題,如何優(yōu)雅的處理全局異常。
SpringBoot為什么需要全局異常處理器
不用手寫try...catch,由全局異常處理器統(tǒng)一捕獲
使用全局異常處理器最大的便利就是程序員在寫代碼時(shí)不再需要手寫try...catch
了,前面我們講過,默認(rèn)情況下SpringBoot出現(xiàn)異常時(shí)返回的結(jié)果是這樣:
{ "timestamp": "2021-07-08T08:05:15.423+00:00", "status": 500, "error": "Internal Server Error", "path": "/wrong" }
這種數(shù)據(jù)格式返回給前端,前端是看不懂的,所以這時(shí)候我們一般通過try...catch
來處理異常
@GetMapping("/wrong") public int error(){ int i; try{ i = 9/0; }catch (Exception e){ log.error("error:{}",e); i = 0; } return i; }
我們追求的目標(biāo)肯定是不需要再手動(dòng)寫try...catch
了,而是希望由全局異常處理器處理。
對(duì)于自定義異常,只能通過全局異常處理器來處理
@GetMapping("error1") public void empty(){ throw new RuntimeException("自定義異常"); }
當(dāng)我們引入Validator參數(shù)校驗(yàn)器的時(shí)候,參數(shù)校驗(yàn)不通過會(huì)拋出異常,此時(shí)是無法用try...catch
捕獲的,只能使用全局異常處理器。
“SpringBoot集成參數(shù)校驗(yàn)請(qǐng)參考這篇文章SpringBoot開發(fā)秘籍 - 集成參數(shù)校驗(yàn)及高階技巧
如何實(shí)現(xiàn)全局異常處理器
@Slf4j @RestControllerAdvice public class RestExceptionHandler { /** * 默認(rèn)全局異常處理。 * @param e the e * @return ResultData */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResultData<String> exception(Exception e) { log.error("全局異常信息 ex={}", e.getMessage(), e); return ResultData.fail(ReturnCode.RC500.getCode(),e.getMessage()); } }
有三個(gè)細(xì)節(jié)需要說明一下:
@RestControllerAdvice
,RestController的增強(qiáng)類,可用于實(shí)現(xiàn)全局異常處理器@ExceptionHandler
,統(tǒng)一處理某一類異常,從而減少代碼重復(fù)率和復(fù)雜度,比如要獲取自定義異??梢?code>@ExceptionHandler(BusinessException.class)@ResponseStatus
指定客戶端收到的http狀態(tài)碼
體驗(yàn)效果
這時(shí)候我們調(diào)用如下接口:
@GetMapping("error1") public void empty(){ throw new RuntimeException("自定義異常"); }
返回的結(jié)果如下:
{ "status": 500, "message": "自定義異常", "data": null, "timestamp": 1625795902556 }
基本滿足我們的需求了。
但是當(dāng)我們同時(shí)啟用統(tǒng)一標(biāo)準(zhǔn)格式封裝功能ResponseAdvice
和RestExceptionHandler
全局異常處理器時(shí)又出現(xiàn)了新的問題:
{ "status": 100, "message": "操作成功", "data": { "status": 500, "message": "自定義異常", "data": null, "timestamp": 1625796167986 }, "timestamp": 1625796168008 }
此時(shí)返回的結(jié)果是這樣,統(tǒng)一格式增強(qiáng)功能會(huì)給返回的異常結(jié)果再次封裝,所以接下來我們需要解決這個(gè)問題。
全局異常接入返回的標(biāo)準(zhǔn)格式
要讓全局異常接入標(biāo)準(zhǔn)格式很簡(jiǎn)單,因?yàn)槿之惓L幚砥饕呀?jīng)幫我們封裝好了標(biāo)準(zhǔn)格式,我們只需要直接返回給客戶端即可。
@SneakyThrows @Override public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { if(o instanceof String){ return objectMapper.writeValueAsString(ResultData.success(o)); } if(o instanceof ResultData){ return o; } return ResultData.success(o); }
關(guān)鍵代碼:
if(o instanceof ResultData){ return o; }
如果返回的結(jié)果是ResultData對(duì)象,直接返回即可。
這時(shí)候我們?cè)僬{(diào)用上面的錯(cuò)誤方法,返回的結(jié)果就符合我們的要求了。
{ "status": 500, "message": "自定義異常", "data": null, "timestamp": 1625796580778 }
好了,今天的文章就到這里了,希望通過這篇文章你能掌握如何在你項(xiàng)目中友好實(shí)現(xiàn)統(tǒng)一標(biāo)準(zhǔn)格式到返回并且可以優(yōu)雅的處理全局異常。
github地址:https://github.com/jianzh5/cloud-blog/
到此這篇關(guān)于詳解SpringBoot 統(tǒng)一后端返回格式的方法的文章就介紹到這了,更多相關(guān)SpringBoot 統(tǒng)一后端返回格式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java 中動(dòng)態(tài)代理詳解及實(shí)例
這篇文章主要介紹了java 中動(dòng)態(tài)代理詳解及實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-06-06對(duì)ThreadLocal內(nèi)存泄漏及弱引用的理解
這篇文章主要介紹了對(duì)ThreadLocal內(nèi)存泄漏及弱引用的理解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01Java Volatile關(guān)鍵字實(shí)現(xiàn)原理過程解析
這篇文章主要介紹了Java Volatile關(guān)鍵字實(shí)現(xiàn)原理過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03Java應(yīng)用程序的CPU使用率飆升原因詳細(xì)分析
這篇文章主要介紹了Java應(yīng)用程序的CPU使用率飆升原因詳細(xì)分析,在 Java 中,我們使用 JVM 進(jìn)行線程調(diào)度,所以一般來說,線程的調(diào)度有兩種模式:分時(shí)調(diào)度和搶占式調(diào)度,線程和進(jìn)程在阻塞或者等待時(shí),都不會(huì)使用 CPU 資源,需要的朋友可以參考下2024-01-01手把手帶你分析SpringBoot自動(dòng)裝配完成了Ribbon哪些核心操作
這篇文章主要介紹了詳解Spring Boot自動(dòng)裝配Ribbon哪些核心操作的哪些操作,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-08-08