1. 程式人生 > >【專案實踐】後端介面統一規範的同時,如何優雅得擴充套件規範

【專案實踐】後端介面統一規範的同時,如何優雅得擴充套件規範

> 以專案驅動學習,以實踐檢驗真知 # 前言 我在上一篇部落格中寫了如何通過引數校驗 + 統一響應碼 + 統一異常處理來構建一個優雅後端介面體系: [【專案實踐】SpringBoot三招組合拳,手把手教你打出優雅的後端介面](https://www.cnblogs.com/RudeCrab/p/13416442.html)。我們做到了: - 通過Validator + 自動丟擲異常來完成了方便的引數校驗 - 通過全域性異常處理 + 自定義異常完成了異常操作的規範 - 通過資料統一響應完成了響應資料的規範 - 多個方面組裝非常優雅的完成了後端介面的協調,讓開發人員有更多的經歷注重業務邏輯程式碼,輕鬆構建後端介面 這樣看上去好像挺完美的,很多地方做到了統一和規範。但!事物往往是一體兩面的,統一和規範帶來的好處自然不必多說,那壞處呢?壞處就是**不夠靈活**。 # 資料統一響應 不夠靈活主要體現在哪呢,就是資料統一響應這一塊。後端響應給前端的資料一共分為三個部分: code:響應碼,比如1000代表響應成功,1001代表響應失敗等等 msg:響應資訊,用來說明/描述響應情況 data:響應的具體資料 我們通過響應碼列舉做到了code和msg的統一,無論怎樣我們只會響應列舉規定好的code和msg。我天真的以為這樣就能滿足所有應用場景了,直到我碰到了一位網友的提問: > 想請問下如果我檢驗的每個引數對應不同的錯誤資訊,即code,message都不同 這樣該如何處理呢?因為這些錯誤碼是有業務含義的,比如說手機號校驗的錯誤碼是V00001,身份證號錯誤碼是V00002。 這一下把我問的有點懵,當時回答道validation引數校驗失敗的話可以手動捕捉引數校驗異常物件,判斷是哪個欄位,再根據欄位手動返回錯誤程式碼。我先來演示一下我所說的這種極為麻煩的做法: ## 手動捕捉異常物件 因為BindingResult物件裡封裝了很多資訊,我們可以拿到校驗錯誤的欄位名,拿到了欄位名後再響應對應的錯誤碼和錯誤資訊。在Controller層裡對BindingResult進行了處理自然就不會被我們之前寫的全域性異常處理給捕獲到,也就不會響應那統一的錯誤碼了,從而達到了每個欄位有自己的響應碼和響應資訊: ```java @PostMapping("/addUser") public ResultVO addUser(@RequestBody @Valid User user, BindingResult bindingResult) { for (ObjectError error : bindingResult.getAllErrors()) { // 拿到校驗錯誤的引數欄位 String field = bindingResult.getFieldError().getField(); // 判斷是哪個欄位發生了錯誤,然後返回資料響應體 switch (field) { case "account": return new ResultVO<>(100001, "賬號驗證錯誤", error.getDefaultMessage()); case "password": return new ResultVO<>(100002, "密碼驗證錯誤", error.getDefaultMessage()); case "email": return new ResultVO<>(100003, "郵箱驗證錯誤", error.getDefaultMessage()); } } // 沒有錯誤則返回則直接返回正確的資訊 return new ResultVO<>(userService.addUser(user)); } ``` 我們故意輸錯引數,來看下效果: ![](https://rudecrab-image-hosting.oss-cn-shenzhen.aliyuncs.com/blog/20200428231039.png) 嗯,是達到效果了。不過這程式碼一放出來簡直就讓人頭疼不已。繁瑣、維護性差、複用性差,這才判斷三個欄位就這樣子了,要那些特別多欄位的還不得起飛咯? 這種方式直接pass! 那我們不手動捕捉異常,我們直接捨棄validation校驗,手動校驗呢? ## 手動校驗 我們來試試: ```java @PostMapping("/addUser") public ResultVO addUser(@RequestBody User user) { // 引數校驗 if (user.getAccount().length() < 6 || user.getAccount().length() > 11) { return new ResultVO<>(100001, "賬號驗證錯誤", "賬號長度必須是6-11個字元"); } if (user.getPassword().length() < 6 || user.getPassword().length() > 16) { return new ResultVO<>(100002, "密碼驗證錯誤", "密碼長度必須是6-16個字元"); } if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", user.getEmail())) { return new ResultVO<>(100003, "郵箱驗證錯誤", "郵箱格式不正確"); } // 沒有錯誤則返回則直接返回正確的資訊 return new ResultVO<>(userService.addUser(user)); } ``` 我去,這還不如上面那種方式呢。上面那種方式至少還能享受validation校驗規則的便利性,這種方式簡直又臭又長。 那有什麼辦法既享受validation的校驗規則,又能做到為每個欄位制定響應碼呢?不賣關子了,當然是有滴嘛! 還記得我們前面所說的BindingResult可以拿到校驗錯誤的欄位名嗎?既然可以拿到欄位名,我們再進一步當然也可以拿到欄位Field物件,能夠拿到Field物件我們也能同時拿到欄位的註解嘛。對,咱們就是要用註解來優雅的實現上面的功能! ## 自定義註解 如果validation校驗失敗了,我們可以拿到欄位物件並能夠獲取欄位的註解資訊,那麼只要我們為每個欄位帶上註解,註解中帶上我們自定義的錯誤碼code和錯誤資訊msg,這樣就能方便的返回響應體啦! 首先我們自定義一個註解: ```java /** * @author RC * @description 自定義引數校驗錯誤碼和錯誤資訊註解 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) // 表明該註解只能放在類的欄位上 public @interface ExceptionCode { // 響應碼code int value() default 100000; // 響應資訊msg String message() default "引數校驗錯誤"; } ``` 然後我們給引數的欄位上加上我們的自定義註解: ```java @Data public class User { @NotNull(message = "使用者id不能為空") private Long id; @NotNull(message = "使用者賬號不能為空") @Size(min = 6, max = 11, message = "賬號長度必須是6-11個字元") @ExceptionCode(value = 100001, message = "賬號驗證錯誤") private String account; @NotNull(message = "使用者密碼不能為空") @Size(min = 6, max = 11, message = "密碼長度必須是6-16個字元") @ExceptionCode(value = 100002, message = "密碼驗證錯誤") private String password; @NotNull(message = "使用者郵箱不能為空") @Email(message = "郵箱格式不正確") @ExceptionCode(value = 100003, message = "郵箱驗證錯誤") private String email; } ``` 然後我們跑到我們的全域性異常處理來進行操作,注意看程式碼註釋: ```java @RestControllerAdvice public class ExceptionControllerAdvice { @ExceptionHandler(MethodArgumentNotValidException.class) public ResultVO MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) throws NoSuchFieldException { // 從異常物件中拿到錯誤資訊 String defaultMessage = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); // 引數的Class物件,等下好通過欄位名稱獲取Field物件 Class parameterType = e.getParameter().getParameterType(); // 拿到錯誤的欄位名稱 String fieldName = e.getBindingResult().getFieldError().getField(); Field field = parameterType.getDeclaredField(fieldName); // 獲取Field物件上的自定義註解 ExceptionCode annotation = field.getAnnotation(ExceptionCode.class); // 有註解的話就返回註解的響應資訊 if (annotation != null) { return new ResultVO<>(annotation.value(),annotation.message(),defaultMessage); } // 沒有註解就提取錯誤提示資訊進行返回統一錯誤碼 return new ResultVO<>(ResultCode.VALIDATE_FAILED, defaultMessage); } } ``` 這裡做了全域性異常處理,那麼Controller層那邊就只用專心做業務邏輯就好了: ```java @ApiOperation("新增使用者") @PostMapping("/addUser") public String addUser(@RequestBody @Valid User user) { return userService.addUser(user); } ``` 我們來看下效果: ![](https://rudecrab-image-hosting.oss-cn-shenzhen.aliyuncs.com/blog/20200428234819.png) 可以看到,只要加了我們自定義的註解,引數校驗失敗了就會返回註解的錯誤碼code和錯誤資訊msg。這種做法相比前兩種做法帶來了以下好處: * 方便。從之前一大堆手動判斷程式碼,到現在一個註解搞定 * 複用性強。不單單可以對一個物件有效果,對其他受校驗的物件都有效果,不用再寫多餘的程式碼 * 能夠和統一響應碼配合。前兩種方式是要麼就對一個物件所有引數用自定義的錯誤碼,要麼就所有引數用統一響應碼。這種方式如果你不想為某個欄位設定自定義響應碼,那麼不加註解自然而然就會返回統一響應碼 簡直不要太方便!**這種方式就像在資料統一響應上加了一個擴充套件功能,既規範又靈活!** 當然,我這裡只是提供了一個思路,我們還可以用自定義註解做很多事情。比如,我們可以讓註解直接加在整個類上,讓某個類都引數用一個錯誤碼等等! ## 繞過資料統一響應 上面演示瞭如何讓錯誤碼變得靈活,我們繼續進一步擴充套件。 全域性統一處理資料響應體會讓所有資料都被`ResultVO`包裹起來返還給前端,這樣我們前端接到的所有響應都是固定格式的,方便的很。但是!如果我們的介面並不是給我們自己前端所用呢?我們要呼叫其他第三方介面並給予響應資料,別人要接受的響應可不一定按照code、msg、data來哦!所以,我們還得提供一個擴充套件性,就是允許繞過資料統一響應! 我想大家猜到了,我們依然要用自定義註解來完成這個功能: ```java @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) // 表明該註解只能放在方法上 public @interface NotResponseBody { } ``` 只要加了這個註解的方法,我們就不做資料統一響應處理,返回型別是啥就是返回的啥 ```java @GetMapping("/getUser") @NotResponseBody public User getUser() { User user = new User(); user.setId(1L); user.setAccount("12345678"); user.setPassword("12345678"); user.setEmail("[email protected]"); return user; } ``` 我們接下來再資料統一響應處理類裡對這個註解進行判斷: ```java @RestControllerAdvice(basePackages = {"com.rudecrab.demo.controller"}) public class ResponseControllerAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, Class> aClass) { // 如果介面返回的型別本身就是ResultVO那就沒有必要進行額外的操作,返回false // 如果方法上加了我們的自定義註解也沒有必要進行額外的操作 return !(returnType.getParameterType().equals(ResultVO.class) || returnType.hasMethodAnnotation(NotResponseBody.class)); } ... } ``` 好,我們來看看效果。沒加註解前,資料是被響應體包裹了的: ![](https://rudecrab-image-hosting.oss-cn-shenzhen.aliyuncs.com/blog/20200219205620.png) 方法加了註解後資料就直接返回了資料本身: ![](https://rudecrab-image-hosting.oss-cn-shenzhen.aliyuncs.com/blog/20200429002000.png) 非常好,在資料統一響應上又加了一層擴充套件。 # 總結 經過一波操作後,我們從沒有規範到有規範,再從有規範到擴充套件規範: 沒有規範(一團糟) --> 有規範(缺乏靈活) --> 擴充套件規範(Nice) 寫這篇文章的起因就是我前面所說的,一個網友突然問了我那個問題,**我才赫然發現專案開發中各種各樣的情況都可能會出現,沒有任何一個架構可以做到完美,與其說我們要去追求完美,倒不如說我們應該要去追求,處理需求變化紛雜的能力!** 最後在這裡放上此專案的[github地址](https://github.com/RudeCrab/rude-java),克隆到本地即可直接執行,並且我將每一次的優化記錄都分別做了程式碼提交,你可以清晰的看到專案的改進過程,如果對你有幫助請在github上點個star,我還會繼續更新更多【專案實踐】哦! ![](https://img2020.cnblogs.com/blog/1496775/202101/1496775-20210108125800650-1747993