springboot之全局處理統一返回
- 2019 年 10 月 3 日
- 筆記
springboot之全局處理統一返回
簡介
在REST風格的開發中,避免通常會告知前台返回是否成功以及狀態碼等資訊。這裡我們通常返回的時候做一次util的包裝處理工作,如:Result類似的類,裡面包含succ、code、msg、data等欄位。
介面調用返回類似如下:
{ "succ": false, // 是否成功 "ts": 1566467628851, // 時間戳 "data": null, // 數據 "code": "CLOUD800", // 錯誤類型 "msg": "業務異常", // 錯誤描述 "fail": true } 當然在每個介面里返回要通過Result的工具類將這些資訊給封裝一下,這樣導致業務和技術類的程式碼耦合在一起。
介面調用處理類似如下:
@GetMapping("hello") public Result list(){ return Result.ofSuccess("hello"); } 結果:
{ "succ": ture, // 是否成功 "ts": 1566467628851, // 時間戳 "data": "hello", // 數據 "code": null, // 錯誤類型 "msg": null, // 錯誤描述 "fail": true } 我們將這些操抽出一個公共starter包,各個服務依賴即可,做一層統一攔截處理的工作,進行技術解耦。
配置
unified-dispose-springboot-starter
這個模組里包含異常處理以及全局返回封裝等功能,下面。
完整目錄結構如下:
├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ └── purgetiem │ │ │ └── starter │ │ │ └── dispose │ │ │ ├── GlobalDefaultConfiguration.java │ │ │ ├── GlobalDefaultProperties.java │ │ │ ├── Interceptors.java │ │ │ ├── Result.java │ │ │ ├── advice │ │ │ │ └── CommonResponseDataAdvice.java │ │ │ ├── annotation │ │ │ │ ├── EnableGlobalDispose.java │ │ │ │ └── IgnorReponseAdvice.java │ │ │ └── exception │ │ │ ├── GlobalDefaultExceptionHandler.java │ │ │ ├── category │ │ │ │ └── BusinessException.java │ │ │ └── error │ │ │ ├── CommonErrorCode.java │ │ │ └── details │ │ │ └── BusinessErrorCode.java │ │ └── resources │ │ ├── META-INF │ │ │ └── spring.factories │ │ └── dispose.properties │ └── test │ └── java 統一返回處理
按照一般的模式,我們都需要創建一個可以進行處理包裝的工具類以及一個返回對象。
Result(返回類):
創建Result<T> T為data的數據類型,這個類包含了前端常用的欄位,還有一些常用的靜態初始化Result對象的方法。
/** * 返回統一數據結構 * * @author purgeyao * @since 1.0 */ @Data @ToString @NoArgsConstructor @AllArgsConstructor public class Result<T> implements Serializable { /** * 是否成功 */ private Boolean succ; /** * 伺服器當前時間戳 */ private Long ts = System.currentTimeMillis(); /** * 成功數據 */ private T data; /** * 錯誤碼 */ private String code; /** * 錯誤描述 */ private String msg; public static Result ofSuccess() { Result result = new Result(); result.succ = true; return result; } public static Result ofSuccess(Object data) { Result result = new Result(); result.succ = true; result.setData(data); return result; } public static Result ofFail(String code, String msg) { Result result = new Result(); result.succ = false; result.code = code; result.msg = msg; return result; } public static Result ofFail(String code, String msg, Object data) { Result result = new Result(); result.succ = false; result.code = code; result.msg = msg; result.setData(data); return result; } public static Result ofFail(CommonErrorCode resultEnum) { Result result = new Result(); result.succ = false; result.code = resultEnum.getCode(); result.msg = resultEnum.getMessage(); return result; } /** * 獲取 json */ public String buildResultJson(){ JSONObject jsonObject = new JSONObject(); jsonObject.put("succ", this.succ); jsonObject.put("code", this.code); jsonObject.put("ts", this.ts); jsonObject.put("msg", this.msg); jsonObject.put("data", this.data); return JSON.toJSONString(jsonObject, SerializerFeature.DisableCircularReferenceDetect); } } 這樣已經滿足一般返回處理的需求了,在介面可以這樣使用:
@GetMapping("hello") public Result list(){ return Result.ofSuccess("hello"); } 當然這樣是耦合的使用,每次都需要調用Result里的包裝方法。
ResponseBodyAdvice返回統一攔截處理
ResponseBodyAdvice在 spring 4.1 新加入的一個介面,在消息體被HttpMessageConverter寫入之前允許Controller 中 @ResponseBody修飾的方法或ResponseEntity調整響應中的內容,比如做一些返回處理。
ResponseBodyAdvice介面里一共包含了兩個方法
supports:該組件是否支援給定的控制器方法返回類型和選擇的{@code HttpMessageConverter}類型beforeBodyWrite:在選擇{@code HttpMessageConverter}之後調用,在調用其寫方法之前調用。
那麼我們就可以在這兩個方法做一些手腳。
supports用於判斷是否需要做處理。beforeBodyWrite用於做返回處理。
CommonResponseDataAdvice類實現ResponseBodyAdvice兩個方法。
filter(MethodParameter methodParameter) 私有方法里進行判斷是否要進行攔截統一返回處理。
如:
- 添加自定義註解
@IgnorReponseAdvice忽略攔截。 - 判斷某些類不進行攔截.
- 判斷某些包下所有類不進行攔截。如
swagger的springfox.documentation包下的介面忽略攔截等。
filter方法:
判斷為false就不需要進行攔截處理。
private Boolean filter(MethodParameter methodParameter) { Class<?> declaringClass = methodParameter.getDeclaringClass(); // 檢查過濾包路徑 long count = globalDefaultProperties.getAdviceFilterPackage().stream() .filter(l -> declaringClass.getName().contains(l)).count(); if (count > 0) { return false; } // 檢查<類>過濾列表 if (globalDefaultProperties.getAdviceFilterClass().contains(declaringClass.getName())) { return false; } // 檢查註解是否存在 if (methodParameter.getDeclaringClass().isAnnotationPresent(IgnorReponseAdvice.class)) { return false; } if (methodParameter.getMethod().isAnnotationPresent(IgnorReponseAdvice.class)) { return false; } return true; } CommonResponseDataAdvice類:
最核心的就在beforeBodyWrite方法處理里。
- 判斷
Object o是否為null,為null構建Result對象進行返回。 - 判斷
Object o是否是Result子類或其本身,該情況下,可能是介面返回時創建了Result,為了避免再次封裝一次,判斷是Result子類或其本身就返回Object o本身。 - 判斷
Object o是否是為String,在測試的過程中發現String的特殊情況,在這裡做了一次判斷操作,如果為String就進行JSON.toJSON(Result.ofSuccess(o)).toString()序列號操作。 - 其他情況默認返回
Result.ofSuccess(o)進行包裝處理。
/** * {@link IgnorReponseAdvice} 處理解析 {@link ResponseBodyAdvice} 統一返回包裝器 * * @author purgeyao * @since 1.0 */ @RestControllerAdvice public class CommonResponseDataAdvice implements ResponseBodyAdvice<Object> { private GlobalDefaultProperties globalDefaultProperties; public CommonResponseDataAdvice(GlobalDefaultProperties globalDefaultProperties) { this.globalDefaultProperties = globalDefaultProperties; } @Override @SuppressWarnings("all") public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { return filter(methodParameter); } @Nullable @Override @SuppressWarnings("all") public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { // o is null -> return response if (o == null) { return Result.ofSuccess(); } // o is instanceof ConmmonResponse -> return o if (o instanceof Result) { return (Result<Object>) o; } // string 特殊處理 if (o instanceof String) { return JSON.toJSON(Result.ofSuccess(o)).toString(); } return Result.ofSuccess(o); } private Boolean filter(MethodParameter methodParameter) { ···略 } } 這樣基本完成了核心的處理工作。當然還少了上文提到的@IgnorReponseAdvice註解。
@IgnorReponseAdvice:
比較簡單點,只作為一個標識的作用。
/** * 統一返回包裝標識註解 * * @author purgeyao * @since 1.0 */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface IgnorReponseAdvice { } 加入spring容器
最後將GlobalDefaultExceptionHandler以bean的方式注入spring容器。
@Configuration @EnableConfigurationProperties(GlobalDefaultProperties.class) @PropertySource(value = "classpath:dispose.properties", encoding = "UTF-8") public class GlobalDefaultConfiguration { ···略 @Bean public CommonResponseDataAdvice commonResponseDataAdvice(GlobalDefaultProperties globalDefaultProperties){ return new CommonResponseDataAdvice(globalDefaultProperties); } } 將GlobalDefaultConfiguration在resources/META-INF/spring.factories文件下載入。
org.springframework.boot.autoconfigure.EnableAutoConfiguration= com.purgetime.starter.dispose.GlobalDefaultConfiguration 不過我們這次使用註解方式開啟。其他項目依賴包後,需要添加@EnableGlobalDispose才可以將全局攔截的特性開啟。
將剛剛創建的spring.factories注釋掉,創建EnableGlobalDispose註解。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(GlobalDefaultConfiguration.class) public @interface EnableGlobalDispose { } 使用@Import將GlobalDefaultConfiguration導入即可。
使用
添加依賴
<dependency> <groupId>io.deepblueai</groupId> <artifactId>unified-dispose-deepblueai-starter</artifactId> <version>0.1.0.RELEASE</version> </dependency> 啟動類開啟@EnableGlobalDispose註解即可。
- 業務使用
介面:
@GetMapping("test") public String test(){ return "test"; } 返回
{ "succ": true, // 是否成功 "ts": 1566386951005, // 時間戳 "data": "test", // 數據 "code": null, // 錯誤類型 "msg": null, // 錯誤描述 "fail": false }
- 忽略封裝註解:@IgnorReponseAdvice
@IgnorReponseAdvice允許範圍為:類 + 方法,標識在類上這個類下的說有方法的返回都將忽略返回封裝。
介面:
@IgnorReponseAdvice // 忽略數據包裝 可添加到類、方法上 @GetMapping("test") public String test(){ return "test"; } 返回 test
總結
項目里很多重複的code,我們可以通過一定的方式去簡化,以達到一定目的減少開發量。
示例程式碼地址:unified-dispose-springboot
作者GitHub:
Purgeyao 歡迎關注