前言
??在日常的開發(fā)中,參數(shù)校驗(yàn)是非常重要的一個(gè)環(huán)節(jié),嚴(yán)格參數(shù)校驗(yàn)會(huì)減少很多出bug的概率,增加接口的安全性俭缓。在此之前寫過一篇SpringBoot統(tǒng)一參數(shù)校驗(yàn)主要介紹了一些簡單的校驗(yàn)方法克伊。而這篇?jiǎng)t是介紹一些進(jìn)階的校驗(yàn)方式。比如說:在某個(gè)接口編寫的過程中肯定會(huì)遇到华坦,當(dāng)xxType值為A愿吹,paramA值必傳。xxType值為B惜姐,paramB值必須傳犁跪。對(duì)于這樣的,通常的做法就是在controller加上各種if判斷歹袁。顯然這樣的代碼是不夠優(yōu)雅的坷衍,而分組校驗(yàn)及自定義參數(shù)校驗(yàn),就是來解決這個(gè)問題的条舔。
PathVariable參數(shù)校驗(yàn)
??Restful的接口枫耳,在現(xiàn)在來講應(yīng)該是比較常見的了,常用的地址欄的參數(shù)孟抗,我們都是這樣校驗(yàn)的迁杨。
/**
* 獲取電話號(hào)碼信息
*/
@GetMapping("/phoneInfo/{phone}")
public ResultVo phoneInfo(@PathVariable("phone") String phone){
// 驗(yàn)證電話號(hào)碼是否有效
String pattern = "^[1][3,4,5,7,8][0-9]{9}$";
boolean isValid = Pattern.matches(pattern, phone);
if(isValid){
// 執(zhí)行相應(yīng)邏輯
return ResultVoUtil.success(phone);
} else {
// 返回錯(cuò)誤信息
return ResultVoUtil.error("手機(jī)號(hào)碼無效");
}
}
很顯然上面的代碼不夠優(yōu)雅,所以我們可以在參數(shù)后面凄硼,添加對(duì)應(yīng)的正則表達(dá)式phone:正則表達(dá)式
來進(jìn)行驗(yàn)證铅协。這樣就省去了在controller編寫校驗(yàn)代碼了。
/**
* 獲取電話號(hào)碼信息
*/
@GetMapping("/phoneInfo/{phone:^[1][3,4,5,7,8][0-9]{9}$}")
public ResultVo phoneInfo(@PathVariable("phone") String phone){
return ResultVoUtil.success(phone);
}
雖然這樣處理后代碼更精簡了帆喇。但是如果傳入的手機(jī)號(hào)碼,不符合規(guī)則會(huì)直接返回404亿胸。而不是提示手機(jī)號(hào)碼錯(cuò)誤坯钦。錯(cuò)誤信息如下:
自定義校驗(yàn)注解
??我們以校驗(yàn)手機(jī)號(hào)碼為例,雖然validation
提供了@Pattern
這個(gè)注解來使用正則表達(dá)式進(jìn)行校驗(yàn)侈玄。如果被使用在多處婉刀,一旦正則表達(dá)式發(fā)生更改,則需要一個(gè)一個(gè)的進(jìn)行修改序仙。很顯然為了避免做這樣的無用功突颊,自定義校驗(yàn)注解
就是你的好幫手。
@Data
public class PhoneForm {
/**
* 電話號(hào)碼
*/
@Pattern(regexp = "^[1][3,4,5,7,8][0-9]{9}$" , message = "電話號(hào)碼有誤")
private String phone;
}
??要實(shí)現(xiàn)一個(gè)自定義校驗(yàn)注解潘悼,主要是有兩步律秃。一是注解本身,二是校驗(yàn)邏輯實(shí)現(xiàn)類治唤。
PhoneVerify 校驗(yàn)注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手機(jī)號(hào)碼格式有誤";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
PhoneValidator 校驗(yàn)實(shí)現(xiàn)類
public class PhoneValidator implements ConstraintValidator<Phone, Object> {
@Override
public boolean isValid(Object telephone, ConstraintValidatorContext constraintValidatorContext) {
String pattern = "^1[3|4|5|7|8]\\d{9}$";
return Pattern.matches(pattern, telephone.toString());
}
}
CustomForm 表單數(shù)據(jù)
@Data
public class CustomForm {
/**
* 電話號(hào)碼
*/
@Phone
private String phone;
}
測(cè)試接口
@PostMapping("/customTest")
public ResultVo customTest(@RequestBody @Validated CustomForm form){
return ResultVoUtil.success(form.getPhone());
}
注解的含義
@Target({ElementType.FIELD})
??注解是指定當(dāng)前自定義注解可以使用在哪些地方棒动,這里僅僅讓他可以使用屬性上。但還可以使用在更多的地方宾添,比如說方法上船惨、構(gòu)造器上等等柜裸。
- TYPE - 類,接口(包括注解類型)或枚舉
- FIELD - 字段(包括枚舉常量)
- METHOD - 方法
- PARAMETER - 參數(shù)
- CONSTRUCTOR - 構(gòu)造函數(shù)
- LOCAL_VARIABLE - 局部變量
- ANNOTATION_TYPE -注解類型
- PACKAGE - 包
- TYPE_PARAMETER - 類型參數(shù)
- TYPE_USE - 使用類型
@Retention(RetentionPolicy.RUNTIME)
??指定當(dāng)前注解保留到運(yùn)行時(shí)粱锐。保留策略有下面三種:
- SOURCE - 注解只保留在源文件疙挺,當(dāng)Java文件編譯成class文件的時(shí)候,注解被遺棄怜浅。
- CLASS - 注解被保留到class文件铐然,但jvm加載class文件時(shí)候被遺棄,這是默認(rèn)的生命周期海雪。
- RUNTIME - 注解不僅被保存到class文件中锦爵,jvm加載class文件之后,仍然存在奥裸。
@Constraint(validatedBy = PhoneValidator.class)
??指定了當(dāng)前注解使用哪個(gè)校驗(yàn)類來進(jìn)行校驗(yàn)险掀。
分組校驗(yàn)
UserForm
@Data
public class UserForm {
/**
* id
*/
@Null(message = "新增時(shí)id必須為空", groups = {Insert.class})
@NotNull(message = "更新時(shí)id不能為空", groups = {Update.class})
private String id;
/**
* 類型
*/
@NotEmpty(message = "姓名不能為空" , groups = {Insert.class})
private String name;
/**
* 年齡
*/
@NotEmpty(message = "年齡不能為空" , groups = {Insert.class})
private String age;
}
Insert分組
public interface Insert {
}
Update分組
public interface Update {
}
測(cè)試接口
/**
* 添加用戶
*/
@PostMapping("/addUser")
public ResultVo addUser(@RequestBody @Validated({Insert.class}) UserForm form){
// 選擇對(duì)應(yīng)的分組進(jìn)行校驗(yàn)
return ResultVoUtil.success(form);
}
/**
* 更新用戶
*/
@PostMapping("/updateUser")
public ResultVo updateUser(@RequestBody @Validated({Update.class}) UserForm form){
// 選擇對(duì)應(yīng)的分組進(jìn)行校驗(yàn)
return ResultVoUtil.success(form);
}
測(cè)試結(jié)果
添加測(cè)試
更新測(cè)試
順序校驗(yàn)@GroupSequence
??在@GroupSequence
內(nèi)可以指定,分組校驗(yàn)的順序湾宙。比如說@GroupSequence({Insert.class, Update.class, UserForm.class})
先執(zhí)行Insert
校驗(yàn)樟氢,然后執(zhí)行Update
校驗(yàn)。如果Insert
分組侠鳄,校驗(yàn)失敗了埠啃,則不會(huì)進(jìn)行Update
分組的校驗(yàn)。
@Data
@GroupSequence({Insert.class, Update.class, UserForm.class})
public class UserForm {
/**
* id
*/
@Null(message = "新增時(shí)id必須為空", groups = {Insert.class})
@NotNull(message = "更新時(shí)id不能為空", groups = {Update.class})
private String id;
/**
* 類型
*/
@NotEmpty(message = "姓名不能為空" , groups = {Insert.class})
private String name;
/**
* 年齡
*/
@NotEmpty(message = "年齡不能為空" , groups = {Insert.class})
private String age;
}
測(cè)試接口
/**
* 編輯用戶
*/
@PostMapping("/editUser")
public ResultVo editUser(@RequestBody @Validated UserForm form){
return ResultVoUtil.success(form);
}
測(cè)試結(jié)果
??哈哈哈伟恶,測(cè)試結(jié)果其實(shí)是個(gè)死循環(huán)碴开,不管你咋輸入都會(huì)報(bào)錯(cuò),小伙伴可以嘗試一下哦博秫。上面的例子只是個(gè)演示潦牛,在實(shí)際中還是別這樣做了,需要根據(jù)具體邏輯進(jìn)行校驗(yàn)挡育。
自定義分組校驗(yàn)
??對(duì)于之前提到了當(dāng)xxType值為A巴碗,paramA值必傳。xxType值為B即寒,paramB值必須傳這樣的場(chǎng)景橡淆。單獨(dú)使用分組校驗(yàn)和分組序列是無法實(shí)現(xiàn)的。需要使用@GroupSequenceProvider
才行母赵。
自定義分組表單
@Data
@GroupSequenceProvider(value = CustomSequenceProvider.class)
public class CustomGroupForm {
/**
* 類型
*/
@Pattern(regexp = "[A|B]" , message = "類型不必須為 A|B")
private String type;
/**
* 參數(shù)A
*/
@NotEmpty(message = "參數(shù)A不能為空" , groups = {WhenTypeIsA.class})
private String paramA;
/**
* 參數(shù)B
*/
@NotEmpty(message = "參數(shù)B不能為空", groups = {WhenTypeIsB.class})
private String paramB;
/**
* 分組A
*/
public interface WhenTypeIsA {
}
/**
* 分組B
*/
public interface WhenTypeIsB {
}
}
CustomSequenceProvider
public class CustomSequenceProvider implements DefaultGroupSequenceProvider<CustomGroupForm> {
@Override
public List<Class<?>> getValidationGroups(CustomGroupForm form) {
List<Class<?>> defaultGroupSequence = new ArrayList<>();
defaultGroupSequence.add(CustomGroupForm.class);
if (form != null && "A".equals(form.getType())) {
defaultGroupSequence.add(CustomGroupForm.WhenTypeIsA.class);
}
if (form != null && "B".equals(form.getType())) {
defaultGroupSequence.add(CustomGroupForm.WhenTypeIsB.class);
}
return defaultGroupSequence;
}
}
測(cè)試接口
/**
* 自定義分組
*/
@PostMapping("/customGroup")
public ResultVo customGroup(@RequestBody @Validated CustomGroupForm form){
return ResultVoUtil.success(form);
}
測(cè)試結(jié)果
Type類型為A
Type類型為B
小結(jié)一下
??GroupSequence
注解是一個(gè)標(biāo)準(zhǔn)的Bean認(rèn)證注解逸爵。正如之前,它能夠讓你靜態(tài)的重新定義一個(gè)類的凹嘲,默認(rèn)校驗(yàn)組順序痊银。然而GroupSequenceProvider
它能夠讓你動(dòng)態(tài)的定義一個(gè)校驗(yàn)組的順序。
注意的一個(gè)點(diǎn)
SpringBoot 2.3.x 移除了validation
依賴需要手動(dòng)引入依賴施绎。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
總結(jié)
??個(gè)人的一些小經(jīng)驗(yàn)溯革,參數(shù)的非空判斷贞绳,這個(gè)應(yīng)該是校驗(yàn)的第一步了,除了非空校驗(yàn)致稀,我們還需要做到下面這幾點(diǎn):
- 普通參數(shù) - 需要限定字段的長度冈闭。如果會(huì)將數(shù)據(jù)存入數(shù)據(jù)庫,長度以數(shù)據(jù)庫為準(zhǔn)抖单,反之根據(jù)業(yè)務(wù)確定萎攒。
- 類型參數(shù) - 最好使用正則對(duì)可能出現(xiàn)的類型做到嚴(yán)格校驗(yàn)。比如
type
的值是【0|1|2】這樣的矛绘。 - 列表(list)參數(shù) - 不僅需要對(duì)list內(nèi)的參數(shù)是否合格進(jìn)行校驗(yàn)耍休,還需要對(duì)list的size進(jìn)行限制。比如說 100货矮。
- 日期羊精,郵件,金額囚玫,URL這類參數(shù)都需要使用對(duì)于的正則進(jìn)行校驗(yàn)喧锦。
- 參數(shù)真實(shí)性 - 這個(gè)主要針對(duì)于 各種
Id
比如說userId
、merchantId
抓督,對(duì)于這樣的參數(shù)燃少,都需要進(jìn)行真實(shí)性校驗(yàn),判斷系統(tǒng)內(nèi)是有含有铃在,并且對(duì)應(yīng)的狀態(tài)是否正常阵具。
??參數(shù)校驗(yàn)越嚴(yán)格越好,嚴(yán)格的校驗(yàn)規(guī)則不僅能減少接口出錯(cuò)的概率定铜,同時(shí)還能避免出現(xiàn)臟數(shù)據(jù)阳液,從而來保證系統(tǒng)的安全性和穩(wěn)定性。
錯(cuò)誤的提醒信息需要友好一點(diǎn)哦宿稀,防止等下被前端大哥吐槽哦趁舀。
上期回顧
結(jié)尾
??如果覺得對(duì)你有幫助赖捌,可以多多評(píng)論祝沸,多多點(diǎn)贊哦,也可以到我的主頁看看越庇,說不定有你喜歡的文章罩锐,也可以隨手點(diǎn)個(gè)關(guān)注哦,謝謝卤唉。
??我是不一樣的科技宅涩惑,每天進(jìn)步一點(diǎn)點(diǎn),體驗(yàn)不一樣的生活桑驱。我們下期見竭恬!