以項目驅動學習,以實踐檢驗真知
前言
我在上一篇博客中寫了如何通過參數校驗 + 統一響應碼 + 統一異常處理來構建一個優雅后端接口體系:
【項目實踐】SpringBoot三招組合拳,手把手教你打出優雅的后端接口。我們做到了:
- 通過Validator + 自動拋出異常來完成了方便的參數校驗
- 通過全局異常處理 + 自定義異常完成了異常操作的規范
- 通過數據統一響應完成了響應數據的規范
- 多個方面組裝非常優雅的完成了后端接口的協調,讓開發人員有更多的經歷注重業務邏輯代碼,輕松構建后端接口
這樣看上去好像挺完美的,很多地方做到了統一和規范。但!事物往往是一體兩面的,統一和規范帶來的好處自然不必多說,那壞處呢?壞處就是不夠靈活。
數據統一響應
不夠靈活主要體現在哪呢,就是數據統一響應這一塊。后端響應給前端的數據一共分為三個部分:
code:響應碼,比如1000代表響應成功,1001代表響應失敗等等
msg:響應信息,用來說明/描述響應情況
data:響應的具體數據
我們通過響應碼枚舉做到了code和msg的統一,無論怎樣我們只會響應枚舉規定好的code和msg。我天真的以為這樣就能滿足所有應用場景了,直到我碰到了一位網友的提問:
想請問下如果我檢驗的每個參數對應不同的錯誤信息,即code,message都不同 這樣該如何處理呢?因為這些錯誤碼是有業務含義的,比如說手機號校驗的錯誤碼是V00001,身份證號錯誤碼是V00002。
這一下把我問的有點懵,當時回答道validation參數校驗失敗的話可以手動捕捉參數校驗異常對象,判斷是哪個字段,再根據字段手動返回錯誤代碼。我先來演示一下我所說的這種極為麻煩的做法:
手動捕捉異常對象
因為BindingResult對象里封裝了很多信息,我們可以拿到校驗錯誤的字段名,拿到了字段名后再響應對應的錯誤碼和錯誤信息。在Controller層里對BindingResult進行了處理自然就不會被我們之前寫的全局異常處理給捕獲到,也就不會響應那統一的錯誤碼了,從而達到了每個字段有自己的響應碼和響應信息:
@PostMapping("/addUser")
public ResultVO<String> 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));
}
我們故意輸錯參數,來看下效果:
嗯,是達到效果了。不過這代碼一放出來簡直就讓人頭疼不已。繁瑣、維護性差、復用性差,這才判斷三個字段就這樣子了,要那些特別多字段的還不得起飛咯?
這種方式直接pass!
那我們不手動捕捉異常,我們直接舍棄validation校驗,手動校驗呢?
手動校驗
我們來試試:
@PostMapping("/addUser")
public ResultVO<String> 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,這樣就能方便的返回響應體啦!
首先我們自定義一個注解:
/**
* @author RC
* @description 自定義參數校驗錯誤碼和錯誤信息注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD}) // 表明該注解只能放在類的字段上
public @interface ExceptionCode {
// 響應碼code
int value() default 100000;
// 響應信息msg
String message() default "參數校驗錯誤";
}
然后我們給參數的字段上加上我們的自定義注解:
@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;
}
然后我們跑到我們的全局異常處理來進行操作,注意看代碼注釋:
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<String> 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層那邊就只用專心做業務邏輯就好了:
@ApiOperation("添加用戶")
@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user) {
return userService.addUser(user);
}
我們來看下效果:
可以看到,只要加了我們自定義的注解,參數校驗失敗了就會返回注解的錯誤碼code和錯誤信息msg。這種做法相比前兩種做法帶來了以下好處:
- 方便。從之前一大堆手動判斷代碼,到現在一個注解搞定
- 復用性強。不單單可以對一個對象有效果,對其他受校驗的對象都有效果,不用再寫多余的代碼
- 能夠和統一響應碼配合。前兩種方式是要么就對一個對象所有參數用自定義的錯誤碼,要么就所有參數用統一響應碼。這種方式如果你不想為某個字段設置自定義響應碼,那么不加注解自然而然就會返回統一響應碼
簡直不要太方便!這種方式就像在數據統一響應上加了一個擴展功能,既規范又靈活!
當然,我這里只是提供了一個思路,我們還可以用自定義注解做很多事情。比如,我們可以讓注解直接加在整個類上,讓某個類都參數用一個錯誤碼等等!
繞過數據統一響應
上面演示了如何讓錯誤碼變得靈活,我們繼續進一步擴展。
全局統一處理數據響應體會讓所有數據都被ResultVO
包裹起來返還給前端,這樣我們前端接到的所有響應都是固定格式的,方便的很。但是!如果我們的接口並不是給我們自己前端所用呢?我們要調用其他第三方接口並給予響應數據,別人要接受的響應可不一定按照code、msg、data來哦!所以,我們還得提供一個擴展性,就是允許繞過數據統一響應!
我想大家猜到了,我們依然要用自定義注解來完成這個功能:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD}) // 表明該注解只能放在方法上
public @interface NotResponseBody {
}
只要加了這個注解的方法,我們就不做數據統一響應處理,返回類型是啥就是返回的啥
@GetMapping("/getUser")
@NotResponseBody
public User getUser() {
User user = new User();
user.setId(1L);
user.setAccount("12345678");
user.setPassword("12345678");
user.setEmail("123@qq.com");
return user;
}
我們接下來再數據統一響應處理類里對這個注解進行判斷:
@RestControllerAdvice(basePackages = {"com.rudecrab.demo.controller"})
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
// 如果接口返回的類型本身就是ResultVO那就沒有必要進行額外的操作,返回false
// 如果方法上加了我們的自定義注解也沒有必要進行額外的操作
return !(returnType.getParameterType().equals(ResultVO.class) || returnType.hasMethodAnnotation(NotResponseBody.class));
}
...
}
好,我們來看看效果。沒加注解前,數據是被響應體包裹了的:
方法加了注解后數據就直接返回了數據本身:
非常好,在數據統一響應上又加了一層擴展。
總結
經過一波操作后,我們從沒有規范到有規范,再從有規范到擴展規范:
沒有規范(一團糟) --> 有規范(缺乏靈活) --> 擴展規范(Nice)
寫這篇文章的起因就是我前面所說的,一個網友突然問了我那個問題,我才赫然發現項目開發中各種各樣的情況都可能會出現,沒有任何一個架構可以做到完美,與其說我們要去追求完美,倒不如說我們應該要去追求,處理需求變化紛雜的能力!
最后在這里放上此項目的github地址,克隆到本地即可直接運行,並且我將每一次的優化記錄都分別做了代碼提交,你可以清晰的看到項目的改進過程,如果對你有幫助請在github上點個star,我還會繼續更新更多【項目實踐】哦!