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方法處理里。

  1. 判斷Object o是否為null,為null構建Result對象進行返回。
  2. 判斷Object o是否是Result子類或其本身,該情況下,可能是介面返回時創建了Result,為了避免再次封裝一次,判斷是Result子類或其本身就返回Object o本身。
  3. 判斷Object o是否是為String,在測試的過程中發現String的特殊情況,在這裡做了一次判斷操作,如果為String就進行JSON.toJSON(Result.ofSuccess(o)).toString()序列號操作。
  4. 其他情況默認返回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註解即可。

  1. 業務使用

介面:

@GetMapping("test")  public String test(){    return "test";  }

返回

{    "succ": true,             // 是否成功    "ts": 1566386951005,      // 時間戳    "data": "test",           // 數據    "code": null,             // 錯誤類型    "msg": null,              // 錯誤描述    "fail": false  }
  1. 忽略封裝註解:@IgnorReponseAdvice

@IgnorReponseAdvice允許範圍為:類 + 方法,標識在類上這個類下的說有方法的返回都將忽略返回封裝。

介面:

@IgnorReponseAdvice // 忽略數據包裝 可添加到類、方法上  @GetMapping("test")  public String test(){    return "test";  }

返回 test

總結

項目里很多重複的code,我們可以通過一定的方式去簡化,以達到一定目的減少開發量。

示例程式碼地址:unified-dispose-springboot

作者GitHub:
Purgeyao 歡迎關注

Tags: