springboot之全域性處理統一返回
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 歡