【項目實踐】后端接口統(tǒng)一規(guī)范的同時,如何優(yōu)雅得擴展規(guī)范

以項目驅動學習搏讶,以實踐檢驗真知

前言

我在上一篇博客中寫了如何通過參數校驗 + 統(tǒng)一響應碼 + 統(tǒng)一異常處理來構建一個優(yōu)雅后端接口體系:

【項目實踐】SpringBoot三招組合拳佳鳖,手把手教你打出優(yōu)雅的后端接口
。我們做到了:

  • 通過Validator + 自動拋出異常來完成了方便的參數校驗
  • 通過全局異常處理 + 自定義異常完成了異常操作的規(guī)范
  • 通過數據統(tǒng)一響應完成了響應數據的規(guī)范
  • 多個方面組裝非常優(yōu)雅的完成了后端接口的協(xié)調媒惕,讓開發(fā)人員有更多的經歷注重業(yè)務邏輯代碼系吩,輕松構建后端接口

這樣看上去好像挺完美的,很多地方做到了統(tǒng)一和規(guī)范妒蔚。但穿挨!事物往往是一體兩面的月弛,統(tǒng)一和規(guī)范帶來的好處自然不必多說,那壞處呢科盛?壞處就是不夠靈活帽衙。

數據統(tǒng)一響應

不夠靈活主要體現(xiàn)在哪呢,就是數據統(tǒng)一響應這一塊贞绵。后端響應給前端的數據一共分為三個部分:

code:響應碼厉萝,比如1000代表響應成功,1001代表響應失敗等等

msg:響應信息榨崩,用來說明/描述響應情況

data:響應的具體數據

我們通過響應碼枚舉做到了code和msg的統(tǒng)一谴垫,無論怎樣我們只會響應枚舉規(guī)定好的code和msg。我天真的以為這樣就能滿足所有應用場景了母蛛,直到我碰到了一位網友的提問:

想請問下如果我檢驗的每個參數對應不同的錯誤信息翩剪,即code,message都不同 這樣該如何處理呢溯祸?因為這些錯誤碼是有業(yè)務含義的肢专,比如說手機號校驗的錯誤碼是V00001,身份證號錯誤碼是V00002焦辅。

這一下把我問的有點懵博杖,當時回答道validation參數校驗失敗的話可以手動捕捉參數校驗異常對象,判斷是哪個字段筷登,再根據字段手動返回錯誤代碼剃根。我先來演示一下我所說的這種極為麻煩的做法:

手動捕捉異常對象

因為BindingResult對象里封裝了很多信息,我們可以拿到校驗錯誤的字段名前方,拿到了字段名后再響應對應的錯誤碼和錯誤信息狈醉。在Controller層里對BindingResult進行了處理自然就不會被我們之前寫的全局異常處理給捕獲到,也就不會響應那統(tǒng)一的錯誤碼了惠险,從而達到了每個字段有自己的響應碼和響應信息:

@PostMapping("/addUser")
public ResultVO<String> addUser(@RequestBody @Valid User user, BindingResult bindingResult) {
    for (ObjectError error : bindingResult.getAllErrors()) {
        // 拿到校驗錯誤的參數字段
        String field = bindingResult.getFieldError().getField();
        // 判斷是哪個字段發(fā)生了錯誤苗傅,然后返回數據響應體
        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));
}

我們故意輸錯參數,來看下效果:

image

嗯班巩,是達到效果了渣慕。不過這代碼一放出來簡直就讓人頭疼不已。繁瑣抱慌、維護性差逊桦、復用性差,這才判斷三個字段就這樣子了抑进,要那些特別多字段的還不得起飛咯强经?

這種方式直接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校驗規(guī)則的便利性逮矛,這種方式簡直又臭又長。

那有什么辦法既享受validation的校驗規(guī)則转砖,又能做到為每個字段制定響應碼呢须鼎?不賣關子了,當然是有滴嘛府蔗!

還記得我們前面所說的BindingResult可以拿到校驗錯誤的字段名嗎晋控?既然可以拿到字段名,我們再進一步當然也可以拿到字段Field對象姓赤,能夠拿到Field對象我們也能同時拿到字段的注解嘛赡译。對,咱們就是要用注解來優(yōu)雅的實現(xiàn)上面的功能不铆!

自定義注解

如果validation校驗失敗了蝌焚,我們可以拿到字段對象并能夠獲取字段的注解信息,那么只要我們?yōu)槊總€字段帶上注解誓斥,注解中帶上我們自定義的錯誤碼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);
        }

        // 沒有注解就提取錯誤提示信息進行返回統(tǒng)一錯誤碼
        return new ResultVO<>(ResultCode.VALIDATE_FAILED, defaultMessage);
    }

}

這里做了全局異常處理,那么Controller層那邊就只用專心做業(yè)務邏輯就好了:

@ApiOperation("添加用戶")
@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user) {
    return userService.addUser(user);
}

我們來看下效果:

image

可以看到距芬,只要加了我們自定義的注解涝开,參數校驗失敗了就會返回注解的錯誤碼code和錯誤信息msg。這種做法相比前兩種做法帶來了以下好處:

  • 方便框仔。從之前一大堆手動判斷代碼舀武,到現(xiàn)在一個注解搞定
  • 復用性強。不單單可以對一個對象有效果离斩,對其他受校驗的對象都有效果奕剃,不用再寫多余的代碼
  • 能夠和統(tǒng)一響應碼配合。前兩種方式是要么就對一個對象所有參數用自定義的錯誤碼捐腿,要么就所有參數用統(tǒng)一響應碼。這種方式如果你不想為某個字段設置自定義響應碼柿顶,那么不加注解自然而然就會返回統(tǒng)一響應碼

簡直不要太方便茄袖!這種方式就像在數據統(tǒng)一響應上加了一個擴展功能,既規(guī)范又靈活嘁锯!

當然宪祥,我這里只是提供了一個思路聂薪,我們還可以用自定義注解做很多事情。比如蝗羊,我們可以讓注解直接加在整個類上藏澳,讓某個類都參數用一個錯誤碼;也可以讓注解的值設置為枚舉類,這樣能夠進一步的統(tǒng)一規(guī)范……

繞過數據統(tǒng)一響應

上面演示了如何讓錯誤碼變得靈活耀找,我們繼續(xù)進一步擴展翔悠。

全局統(tǒng)一處理數據響應體會讓所有數據都被ResultVO包裹起來返還給前端,這樣我們前端接到的所有響應都是固定格式的野芒,方便的很蓄愁。但是!如果我們的接口并不是給我們自己前端所用呢狞悲?我們要調用其他第三方接口并給予響應數據撮抓,別人要接受的響應可不一定按照code、msg摇锋、data來哦丹拯!所以,我們還得提供一個擴展性荸恕,就是允許繞過數據統(tǒng)一響應乖酬!

我想大家猜到了,我們依然要用自定義注解來完成這個功能:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD}) // 表明該注解只能放在方法上
public @interface NotResponseBody {
}

只要加了這個注解的方法戚炫,我們就不做數據統(tǒng)一響應處理剑刑,返回類型是啥就是返回的啥

@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;
}

我們接下來再數據統(tǒng)一響應處理類里對這個注解進行判斷:

@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));
    }
    
    ...
}

好双肤,我們來看看效果施掏。沒加注解前,數據是被響應體包裹了的:

image

方法加了注解后數據就直接返回了數據本身:

image

非常好茅糜,在數據統(tǒng)一響應上又加了一層擴展七芭。

總結

經過一波操作后,我們從沒有規(guī)范到有規(guī)范蔑赘,再從有規(guī)范到擴展規(guī)范:

沒有規(guī)范(一團糟) --> 有規(guī)范(缺乏靈活) --> 擴展規(guī)范(Nice)

寫這篇文章的起因就是我前面所說的狸驳,一個網友突然問了我那個問題,我才赫然發(fā)現(xiàn)項目開發(fā)中各種各樣的情況都可能會出現(xiàn)缩赛,沒有任何一個架構可以做到完美耙箍,與其說我們要去追求完美,倒不如說我們應該要去追求酥馍,處理需求變化紛雜的能力辩昆!

最后在這里放上此項目的github地址,clone到本地即可直接運行旨袒,并且我將每一次的優(yōu)化記錄都分別做了代碼提交汁针,你可以清晰的看到項目的改進過程术辐,如果對你有幫助請在github上點個star,我還會繼續(xù)更新更多【項目實踐】哦施无!

博客辉词、Github、微信公眾號都是:RudeCrab猾骡,歡迎關注瑞躺!如果對你有幫助可以收藏、點贊卓练、star隘蝎、在看、分享~~ 你的支持襟企,就是我寫文的最大動力

微信上轉載請聯(lián)系公眾號開啟白名單嘱么,其他地方轉載請標明原地址、原作者顽悼!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末曼振,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蔚龙,更是在濱河造成了極大的恐慌冰评,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件木羹,死亡現(xiàn)場離奇詭異甲雅,居然都是意外死亡,警方通過查閱死者的電腦和手機坑填,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門抛人,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人脐瑰,你說我怎么就攤上這事妖枚。” “怎么了苍在?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵绝页,是天一觀的道長。 經常有香客問我寂恬,道長续誉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任初肉,我火速辦了婚禮屈芜,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己井佑,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布眠寿。 她就那樣靜靜地躺著躬翁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盯拱。 梳的紋絲不亂的頭發(fā)上盒发,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音狡逢,去河邊找鬼宁舰。 笑死,一個胖子當著我的面吹牛奢浑,可吹牛的內容都是我干的蛮艰。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼雀彼,長吁一口氣:“原來是場噩夢啊……” “哼壤蚜!你這毒婦竟也來了?” 一聲冷哼從身側響起徊哑,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤袜刷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后莺丑,有當地人在樹林里發(fā)現(xiàn)了一具尸體著蟹,經...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年梢莽,在試婚紗的時候發(fā)現(xiàn)自己被綠了萧豆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡蟹漓,死狀恐怖炕横,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情葡粒,我是刑警寧澤份殿,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站嗽交,受9級特大地震影響卿嘲,放射性物質發(fā)生泄漏。R本人自食惡果不足惜夫壁,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一拾枣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦梅肤、人聲如沸司蔬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俊啼。三九已至,卻和暖如春左医,著一層夾襖步出監(jiān)牢的瞬間授帕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工浮梢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留跛十,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓秕硝,卻偏偏與公主長得像芥映,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子缝裤,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容