javascript
SpringBoot - 统一格式封装及高阶全局异常处理
文章目錄
- Pre
- 演進過程
- 版本V1
- 版本2
- Step1 約定統一返回格式
- Step2 開發統一返回對象
- Step3 約定接口狀態碼
- Step4 驗證
- Step5 完善全局異常處理 @RestControllerAdvice + @ExceptionHandler
- 全局異常處理器的必要行
- 版本3 (ResponseBodyAdvice)
- Step1 自定義ResponseBodyAdvice接口實現類
- Step2 全局異常整合到返回的標準格式
- 源碼
Pre
Spring Boot2.x-11 使用@ControllerAdvice和@ExceptionHandler實現自定義全局異常
演進過程
我們搞個boot工程 ,來看下為什么以及如何來實現統一格式封裝及高階全局異常處理
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>jakarta.validation</groupId><artifactId>jakarta.validation-api</artifactId></dependency></dependencies>版本V1
@RestController // 返回JSON @RequestMapping("/v1") public class ArtisanV1Controller {/*** 返回字符串** @return*/@GetMapping("/getString")public String getStr() {return "OOOOOOOK";}/*** 返回自定義對象** @return*/@GetMapping("/getArtisan")public Artisan getArt() {Artisan artisan = new Artisan();artisan.setJob("ArtisanJob");artisan.setAge(18);return artisan;}/*** 接口異常** @return*/@GetMapping("/getMockError")public int getMockError() {int i = 1 / 0;return i;}}分別測試下
這混亂的格式, 前端同學怎么想
版本2
Step1 約定統一返回格式
一個合格的標準的返回格式至少包含3部分:
-
status 狀態值
由后端統一定義各種返回結果的狀態碼
-
message 描述
本次接口調用的結果描述
-
data 數據
本次接口返回的數據
如果需要可以加入其他節點,比如在返回對象中添加了接口調用時間 (timestamp: 接口調用時間)
Step2 開發統一返回對象
package com.artisan.resp;import lombok.Data;/*** @author 小工匠* @version 1.0* @description: 公共結果 * @mark: show me the code , change the world*/ @Data public class ResponseData<T> {/*** 結果狀態 ,具體狀態碼參見ResponseCode*/private int status;/*** 響應消息**/private String message;/*** 響應數據**/private T data;/*** 接口請求時間**/private long timestamp;/*** 初始化,增加接口請求事件*/public ResponseData() {this.timestamp = System.currentTimeMillis();}/*** 成功** @param <T>* @return*/public static <T> ResponseData<T> success() {ResponseData<T> resultData = new ResponseData<>();resultData.setStatus(ResponseCode.RC100.getCode());resultData.setMessage(ResponseCode.RC100.getMessage());return resultData;}/*** 成功** @param message* @param <T>* @return*/public static <T> ResponseData<T> success(String message) {ResponseData<T> resultData = new ResponseData<>();resultData.setStatus(ResponseCode.RC100.getCode());resultData.setMessage(message);return resultData;}/*** 成功** @param data* @param <T>* @return*/public static <T> ResponseData<T> success(T data) {ResponseData<T> resultData = new ResponseData<>();resultData.setStatus(ResponseCode.RC100.getCode());resultData.setMessage(ResponseCode.RC100.getMessage());resultData.setData(data);return resultData;}/*** 失敗** @param message* @param <T>* @return*/public static <T> ResponseData<T> fail(String message) {ResponseData<T> resultData = new ResponseData<>();resultData.setStatus(ResponseCode.RC999.getCode());resultData.setMessage(message);return resultData;}/*** 失敗** @param code* @param message* @param <T>* @return*/public static <T> ResponseData<T> fail(int code, String message) {ResponseData<T> resultData = new ResponseData<>();resultData.setStatus(code);resultData.setMessage(message);return resultData;}/*** 失敗** @param <T>* @return*/public static <T> ResponseData<T> fail() {ResponseData<T> resultData = new ResponseData<>();resultData.setStatus(ResponseCode.RC999.getCode());resultData.setMessage(ResponseCode.RC999.getMessage());return resultData;}}Step3 約定接口狀態碼
package com.artisan.resp;import lombok.Getter;/*** @author 小工匠* @version 1.0* @description: 狀態碼集合* @mark: show me the code , change the world*/ public enum ResponseCode {/*** 操作成功**/RC100(100, "操作成功"),/*** 操作失敗**/RC999(999, "操作失敗"),/*** access_denied**/RC403(403, "無訪問權限,請聯系管理員授予權限"),/*** access_denied**/RC401(401, "匿名用戶訪問無權限資源時的異常"),/*** 服務異常**/RC500(500, "系統異常,請稍后重試"),ILLEGAL_ARGUMENT(3001, "非法參數"),INVALID_TOKEN(2001, "訪問令牌不合法"),ACCESS_DENIED(2003, "沒有權限訪問該資源"),CLIENT_AUTHENTICATION_FAILED(1001, "客戶端認證失敗"),USERNAME_OR_PASSWORD_NOTMATCH(1002, "用戶名或密碼錯誤"); /*** 自定義狀態碼**/@Getterprivate final int code;/*** 自定義描述**/@Getterprivate final String message;ResponseCode(int code, String message) {this.code = code;this.message = message;}}Step4 驗證
package com.artisan.controller;import com.artisan.entity.Artisan; import com.artisan.resp.ResponseData; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;/*** @author 小工匠* @version 1.0* @description: 版本2* @mark: show me the code , change the world*/@RestController @RequestMapping("/v2") public class ArtisanV2Controller {@GetMapping("/getString")public ResponseData<String> getStr() {return ResponseData.success("OOOOOOK");}@GetMapping("/getArtisan")public ResponseData<Artisan> getArt() {Artisan artisan = new Artisan();artisan.setJob("CodeMonkey");artisan.setAge(18);return ResponseData.success(artisan);}@GetMapping("/getMockError")public ResponseData<Integer> getMockError() {int i = 1 / 0;return ResponseData.success(i);}}好像部分實現了統一格式返回,確實也是有很多項目在Controller層通過ResponseData.success()對返回結果進行包裝后返回給前端。
但是這個拋異常的這么玩還是不行呀? ------------------------> 全局異常處理
Step5 完善全局異常處理 @RestControllerAdvice + @ExceptionHandler
package com.artisan.resp;import com.artisan.exception.BaseException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindException; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice;import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import javax.validation.ValidationException; import java.util.stream.Collectors;/*** @author 小工匠* @version 1.0* @description: 全局異常處理* @mark: show me the code , change the world*/ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler {/*** 默認全局異常處理。** @param e e* @return ResponseData*/@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public ResponseData<String> exception(Exception e) {log.error("兜底異常信息 ex={}", e.getMessage());return ResponseData.fail(ResponseCode.RC500.getCode(), e.getMessage());}/*** Assert異常*/@ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class})@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public ResponseData<String> exception(IllegalArgumentException e) {return ResponseData.fail(ResponseCode.ILLEGAL_ARGUMENT.getCode(), e.getMessage());}/*** 抓取自定義異常 BaseException*/@ExceptionHandler(BaseException.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public ResponseData<String> exception(BaseException e) {return ResponseData.fail(e.getErrorCode(), e.getMessage());}}-
@ExceptionHandler,統一處理某一類異常, 減少代碼重復率和復雜度,比如要捕獲自定義異??梢?#64;ExceptionHandler(BusinessException.class)
-
@ResponseStatus指定客戶端收到的http狀態碼
重新驗證下
全局異常處理器的必要行
。
版本3 (ResponseBodyAdvice)
V2版本有缺陷么?
我們不難發現每寫一個接口都需要調用ResponseData.success()對結果進行包裝 ,程序猿懶啊, 能不寫嗎
Step1 自定義ResponseBodyAdvice接口實現類
ResponseBodyAdvice的作用一般是用于攔截Controller方法的返回值,統一處理返回值/響應體, 加解密,簽名等
package com.artisan.resp.v3;import com.artisan.resp.ResponseData; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;/*** @author 小工匠* @version 1.0* @description: 攔截Controller方法的返回值,統一處理返回值/響應體* @mark: show me the code , change the world*/@RestControllerAdvice public class CustomResponseAdvice implements ResponseBodyAdvice<Object> {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {return true;}@SneakyThrows@Overridepublic Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {// 處理String類型 if (o instanceof String) {return objectMapper.writeValueAsString(ResponseData.success(o));}// 若是統一返回類型,則不用再此封裝 // if (o instanceof ResponseData) { // return o; // }return ResponseData.success(o);} }-
@RestControllerAdvice,RestController的增強類,可用于實現全局異常處理器
-
有一個地方對String做了特殊處理,因為如果Controller直接返回String ,SpringBoot是直接返回,所以我們需要手動轉換成json
接入@RestControllerAdvice后, Controller就正常寫就可以了,不用統一格式去包裝了,如下
@RestController @RequestMapping("/v3") public class ArtisanV3Controller {@GetMapping("/getString")public String getStr() {return "OOOOOOK";}@GetMapping("/getArtisan")public Artisan getArt() {Artisan artisan = new Artisan();artisan.setJob("CodeMonkey");artisan.setAge(18);return artisan;}@GetMapping("/getMockError")public int getMockError() {int i = 1 / 0;return i;}}測試下
看到問題了吧,當我們同時啟用統一標準格式封裝功能ResponseAdvice和RestExceptionHandler全局異常處理器時,統一格式增強功能會給返回的異常結果再次封裝,所以跟前端的接口響應又迷糊了
Step2 全局異常整合到返回的標準格式
因為全局異常處理器已經幫我們封裝好了標準格式,我們只需要直接返回給客戶端即可。
// 若是統一返回類型,則不用再此封裝if (o instanceof ResponseData) {return o;}如果返回的結果是ResponseData對象,直接返回即可。
重新測試 ,
源碼
https://github.com/yangshangwei/boot2
總結
以上是生活随笔為你收集整理的SpringBoot - 统一格式封装及高阶全局异常处理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Cloud Alibaba
- 下一篇: SpringBoot - 优雅的实现【参