答應(yīng)我,別再if/else校驗請求參數(shù)了可以嗎

大家好询筏,我是二哥呀榕堰。最近正在開發(fā)一個知識庫學(xué)習(xí)網(wǎng)站編程貓,需要對請求參數(shù)進(jìn)行校驗嫌套,比如說非空啊逆屡、長度限制啊等等,可選的解決方案有兩種:

  • 一種是用 Hibernate Validator 來處理
  • 一種是用全局異常來處理

兩種方式踱讨,我們一一來實踐體驗一下魏蔗。

一、Hibernate Validator

Spring Boot 已經(jīng)內(nèi)置了 Hibernate Validator 校驗框架痹筛,這個可以通過 Spring Boot 官網(wǎng)查看和確認(rèn)莺治。

第一步,進(jìn)入 Spring Boot 官網(wǎng)帚稠,點擊 learn 這個面板谣旁,點擊參考文檔。

第二步滋早,在參考文檔頁點擊「依賴的版本」榄审。

第三步,在依賴版本頁就可以查看到所有的依賴了杆麸,包括版本號搁进。

PS:如果發(fā)現(xiàn)沒有起效浪感,可能是依賴版本沖突了,手動把 Hibernate Validator 依賴添加到 pom.xml 文件就可以了饼问。

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.17.Final</version>
</dependency>
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

通過 Hibernate Validator 校驗框架篮撑,我們可以直接在請求參數(shù)的字段上加入注解來完成校驗。

具體該怎么做呢匆瓜?

第一步赢笨,在需要驗證的字段上加上 Hibernate Validator 提供的校驗注解。

比如說我現(xiàn)在有一個用戶名和密碼登錄的請求參數(shù) UsersLoginParam 類:

@Data
@ApiModel(value="用戶登錄", description="用戶表")
public class UsersLoginParam implements Serializable {
    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "登錄名")
    @NotBlank(message="登錄名不能為空")
    private String userLogin;

    @ApiModelProperty(value = "密碼")
    @NotBlank(message="密碼不能為空")
    private String userPass;
}

就可以通過 @NotBlank 注解來對用戶名和密碼進(jìn)行判空校驗驮吱。除了 @NotBlank 注解茧妒,Hibernate Validator 還提供了以下常用注解:

  • @NotNull:被注解的字段不能為 null;
  • @NotEmpty:被注解的字段不能為空左冬;
  • @Min:被注解的字段必須大于等于其value值桐筏;
  • @Max:被注解的字段必須小于等于其value值;
  • @Size:被注解的字段必須在其min和max值之間拇砰;
  • @Pattern:被注解的字段必須符合所定義的正則表達(dá)式梅忌;
  • @Email:被注解的字段必須符合郵箱格式。

第二步除破,在對應(yīng)的請求接口(UsersController.login())中添加 @Validated 注解牧氮,并注入一個 BindingResult 參數(shù)。

@Controller
@Api(tags="用戶")
@RequestMapping("/users")
public class UsersController {
    @Autowired
    private IUsersService usersService;

    @ApiOperation(value = "登錄以后返回token")
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    @ResponseBody
    public ResultObject login(@Validated UsersLoginParam users, BindingResult result) {
        String token = usersService.login(users.getUserLogin(), users.getUserPass());
        if (token == null) {
            return ResultObject.validateFailed("用戶名或密碼錯誤");
        }
        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("token", token);
        tokenMap.put("tokenHead", tokenHead);
        return ResultObject.success(tokenMap);
    }
}

第三步瑰枫,為控制層(UsersController)創(chuàng)建一個切面踱葛,將通知注入到 BindingResult 對象中,然后再判斷是否有校驗錯誤光坝,有錯誤的話返回校驗提示信息尸诽,否則放行。

@Aspect
@Component
@Order(2)
public class BindingResultAspect {
    @Pointcut("execution(public * com.codingmore.controller.*.*(..))")
    public void BindingResult() {
    }

    @Around("BindingResult()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg instanceof BindingResult) {
                BindingResult result = (BindingResult) arg;
                if (result.hasErrors()) {
                    FieldError fieldError = result.getFieldError();
                    if(fieldError!=null){
                        return ResultObject.validateFailed(fieldError.getDefaultMessage());
                    }else{
                        return ResultObject.validateFailed();
                    }
                }
            }
        }
        return joinPoint.proceed();
    }
}

這里涉及到了 SpringBoot AOP 的知識盯另,我在前面的文章里講解過了性含,戳這個鏈接可以直達(dá):SpringBoot AOP 掃盲

第四步,訪問登錄接口鸳惯,用戶名和密碼都不傳入的情況下商蕴,就會返回“用戶名不能為空”的提示信息。

通過 debug 的形式悲敷,體驗一下整個工作流程究恤。

可以看得出,Hibernate Validator 帶來的優(yōu)勢有這些:

  • 驗證邏輯與業(yè)務(wù)邏輯進(jìn)行了分離后德,降低了程序耦合度;
  • 統(tǒng)一且規(guī)范的驗證方式抄腔,無需再次編寫重復(fù)的驗證代碼瓢湃。

不過理张,也帶來一些弊端,比如說:

  • 需要在請求接口的方法中注入 BindingResult 對象
  • 只能校驗一些非常簡單的邏輯绵患,涉及到數(shù)據(jù)查詢就無能為力了雾叭。

二、全局異常處理

使用全局異常處理的優(yōu)點就是比較靈活落蝙,可以處理比較復(fù)雜的邏輯校驗织狐,在校驗失敗的時候直接拋出異常,然后進(jìn)行捕獲處理就可以了筏勒。

第一步移迫,新建一個自定義異常類 ApiException。

public class ApiException extends RuntimeException {
    private IErrorCode errorCode;

    public ApiException(IErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }

    public ApiException(String message) {
        super(message);
    }

    public ApiException(Throwable cause) {
        super(cause);
    }

    public ApiException(String message, Throwable cause) {
        super(message, cause);
    }

    public IErrorCode getErrorCode() {
        return errorCode;
    }
}

第二步管行,新建一個斷言處理類 Asserts厨埋,簡化拋出 ApiException 的步驟。

public class Asserts {
    public static void fail(String message) {
        throw new ApiException(message);
    }

    public static void fail(IErrorCode errorCode) {
        throw new ApiException(errorCode);
    }
}

第三步捐顷,新建一全局異常處理類 GlobalExceptionHandler荡陷,對異常信息進(jìn)行解析,并封裝到統(tǒng)一的返回對象 ResultObject 中迅涮。

@ControllerAdvice
public class GlobalExceptionHandler {
    @ResponseBody
    @ExceptionHandler(value = ApiException.class)
    public ResultObject handle(ApiException e) {
        if (e.getErrorCode() != null) {
            return ResultObject.failed(e.getErrorCode());
        }
        return ResultObject.failed(e.getMessage());
    }
}

全局異常處理類用到了兩個注解废赞,@ControllerAdvice@ExceptionHandler

@ControllerAdvice 是一個特殊的 @Component(可以通過源碼看得到)叮姑,用于標(biāo)識一個類蛹头,這個類中被以下三種注解標(biāo)識的方法:@ExceptionHandler@InitBinder戏溺,@ModelAttribute渣蜗,將作用于所有@Controller 類的接口上。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
}

@ExceptionHandler 注解的作用就是標(biāo)識統(tǒng)一異常處理旷祸,它可以指定要統(tǒng)一處理的異常類型耕拷,比如說我們自定義的 ApiException。

第四步托享,在需要校驗的地方通過 Asserts 類拋出異常 ApiException骚烧。還拿用戶登錄這個接口來說明吧。

@Controller
@Api(tags="用戶")
@RequestMapping("/users")
public class UsersController {
    @ApiOperation(value = "登錄以后返回token")
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    @ResponseBody
    public ResultObject login(@Validated UsersLoginParam users, BindingResult result) {
        String token = usersService.login(users.getUserLogin(), users.getUserPass());
     
        Map<String, String> tokenMap = new HashMap<>();
        tokenMap.put("token", token);
        tokenMap.put("tokenHead", tokenHead);
        return ResultObject.success(tokenMap);
    }
}

該接口需要查詢數(shù)據(jù)庫驗證密碼是否正確闰围,如果密碼不正確就拋出校驗信息“密碼不正確”赃绊。

@Service
public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users> implements IUsersService {
    public String login(String username, String password) {
        String token = null;
        //密碼需要客戶端加密后傳遞
        UserDetails userDetails = loadUserByUsername(username);
        if (!passwordEncoder.matches(password, userDetails.getPassword())) {
            Asserts.fail("密碼不正確");
         }
        // 其他代碼省略
        return token;
    }
}

第五步,通過 ApiPost 來測試一下接口羡榴,故意把密碼輸錯碧查。

也可以通過 debug 的形式,體驗一下整個工作流程。

三忠售、總結(jié)

實際開發(fā)中把兩者結(jié)合在一起用传惠,就可以彌補(bǔ)彼此的短板了,簡單校驗用 Hibernate Validator稻扬,復(fù)雜一點的邏輯校驗卦方,比如說需要數(shù)據(jù)庫查詢用全局異常處理來實現(xiàn)。

源碼地址:https://github.com/itwanger/coding-more
參考鏈接:http://www.macrozheng.com


本篇已收錄至 GitHub 上星標(biāo) 1.6k+ star 的開源專欄《Java 程序員進(jìn)階之路》泰佳,據(jù)說每一個優(yōu)秀的 Java 程序員都喜歡她盼砍,風(fēng)趣幽默、通俗易懂逝她。內(nèi)容包括 Java 基礎(chǔ)浇坐、Java 并發(fā)編程、Java 虛擬機(jī)汽绢、Java 企業(yè)級開發(fā)吗跋、Java 面試等核心知識點。學(xué) Java宁昭,就認(rèn)準(zhǔn) Java 程序員進(jìn)階之路??跌宛。

https://github.com/itwanger/toBeBetterJavaer

star 了這個倉庫就等于你擁有了成為了一名優(yōu)秀 Java 工程師的潛力。也可以戳下面的鏈接跳轉(zhuǎn)到《Java 程序員進(jìn)階之路》的官網(wǎng)網(wǎng)址积仗,開始愉快的學(xué)習(xí)之旅吧疆拘。

https://tobebetterjavaer.com/

image

沒有什么使我停留——除了目的,縱然岸旁有玫瑰、有綠蔭、有寧靜的港灣销睁,我是不系之舟响鹃。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末咒劲,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖旨涝,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異侣背,居然都是意外死亡白华,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進(jìn)店門贩耐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弧腥,“玉大人,你說我怎么就攤上這事潮太」芴拢” “怎么了?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長抛蚤。 經(jīng)常有香客問我台谢,道長寻狂,這世上最難降的妖魔是什么岁经? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮蛇券,結(jié)果婚禮上缀壤,老公的妹妹穿的比我還像新娘。我一直安慰自己纠亚,他們只是感情好塘慕,可當(dāng)我...
    茶點故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蒂胞,像睡著了一般图呢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上骗随,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天蛤织,我揣著相機(jī)與錄音,去河邊找鬼鸿染。 笑死指蚜,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的涨椒。 我是一名探鬼主播摊鸡,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蚕冬!你這毒婦竟也來了免猾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤囤热,失蹤者是張志新(化名)和其女友劉穎猎提,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赢乓,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡忧侧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了牌芋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蚓炬。...
    茶點故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖躺屁,靈堂內(nèi)的尸體忽然破棺而出肯夏,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布驯击,位于F島的核電站烁兰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏徊都。R本人自食惡果不足惜沪斟,卻給世界環(huán)境...
    茶點故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望暇矫。 院中可真熱鬧主之,春花似錦、人聲如沸李根。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽房轿。三九已至粤攒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間囱持,已是汗流浹背夯接。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留洪唐,地道東北人钻蹬。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像凭需,于是被迫代替她去往敵國和親问欠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,652評論 2 354

推薦閱讀更多精彩內(nèi)容