參數(shù)校驗(yàn)的六大神功!
新手司機(jī)翻車實(shí)錄
"哥,注冊(cè)接口又被刷爆了!
"某一個(gè)周末下午,我接到電話,打開日志一看,NullPointerException
堆棧里有38個(gè)不同位置的校驗(yàn)邏輯。
原來(lái)新人小王在Controller里寫滿了這樣的代碼:
// 典型錯(cuò)誤示范(轉(zhuǎn)載自某小廠祖?zhèn)鞔a)
public String register(UserDTO user) {
if (user.getName() == null) {
return"名字不能為空";
}
if (user.getAge() == null) {
return"年齡不能為空";
}
if (user.getAge() < 18) {
return"年齡不能小于18歲";
}
if (!user.getPhone().matches("^1[3-9]\\d{9}$")) {
return"手機(jī)號(hào)不合法";
}
// ...后續(xù)還有20個(gè)if...
}
這才是代碼界的"九轉(zhuǎn)大腸"——每個(gè)入口都讓人窒息。
作為一位有很多開發(fā)經(jīng)驗(yàn)的老司機(jī),今天,老夫帶你修煉參數(shù)校驗(yàn)的6大神功。
圖片
希望對(duì)你會(huì)有所幫助。
第一重:JSR規(guī)范基礎(chǔ)功
1.1 HibernateValidator瞬煉大法
可以使用Hibernate中Validator框架做參數(shù)校驗(yàn),具體代碼如下:
public class UserDTO {
@NotBlank(message = "名稱要填,皮這一下很開心?")
private String name;
@NotNull
@Min(value = 18, message = "未成年禁止入內(nèi)")
@Max(60)
private Integer age;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "這手機(jī)號(hào)是哪國(guó)來(lái)的?")
private String phone;
}
// Controller層啟用校驗(yàn)(新手必知第一步)
@PostMapping("/register")
public Result register(@Valid @RequestBody UserDTO user) {
// 業(yè)務(wù)代碼...
}
技術(shù)要點(diǎn):
- 引入
spring-boot-starter-validation
依賴(調(diào)料包記得加) @Valid
注解要放在入?yún)?cè)(別貼在DTO類上)- 錯(cuò)誤信息會(huì)進(jìn)
BindingResult
(打掃戰(zhàn)場(chǎng)需要手動(dòng)處理)
第二重:全局異常擒龍手
2.1 統(tǒng)一異常攔截器
我們需要對(duì)異常進(jìn)行統(tǒng)一攔截。
這樣在出現(xiàn)參數(shù)校驗(yàn)異常,比如空指針時(shí),不會(huì)把服務(wù)的內(nèi)部錯(cuò)誤信息直接輸出給用戶。
通過(guò)@RestControllerAdvice和@ExceptionHandler注解實(shí)現(xiàn)統(tǒng)一異常攔截器的功能。
具體代碼如下:
@RestControllerAdvice
publicclass GlobalExceptionHandler {
// 專治各種不服校驗(yàn)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidException(MethodArgumentNotValidException e) {
BindingResult result = e.getBindingResult();
return Result.fail(result.getFieldError().getDefaultMessage());
}
}
// 返回格式規(guī)范(示例)
publicclass Result<T> {
private Integer code;
private String msg;
private T data;
publicstatic <T> Result<T> fail(String message) {
returnnew Result<>(500, message, null);
}
}
反爬蟲機(jī)制:
- 禁止直接暴露字段名給前端(攻擊者會(huì)利用字段名信息)
- 錯(cuò)誤信息字典化管理(后面會(huì)教國(guó)際化這招)
第三重:自定義校驗(yàn)屠龍技
3.1 手機(jī)/郵箱二元校驗(yàn)
有時(shí)候,Hibernate Validator框架或者其他校驗(yàn)框架定義的校驗(yàn)不滿足需求,我們需要自定義校驗(yàn)規(guī)則。
則可以自定義注解,實(shí)現(xiàn)ConstraintValidator接口,來(lái)實(shí)現(xiàn)具體的自定義的校驗(yàn)邏輯。
自定義注解@Contact在字段上使用。
具體代碼如下:
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = ContactValidator.class)
public @interface Contact {
String message() default "聯(lián)系方式格式錯(cuò)誤";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 校驗(yàn)邏輯實(shí)現(xiàn)(不要相信前端的下拉框?。?publicclass ContactValidator implements ConstraintValidator<Contact, String> {
privatestaticfinal Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
privatestaticfinal Pattern EMAIL_PATTERN = Pattern.compile("^\\w+@\\w+\\.\\w+$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return PHONE_PATTERN.matcher(value).matches()
|| EMAIL_PATTERN.matcher(value).matches();
}
}
六邊形戰(zhàn)士培養(yǎng)計(jì)劃:
- 可通過(guò)
context.buildConstraintViolationWithTemplate()
動(dòng)態(tài)修改錯(cuò)誤信息 - 支持DI注入Spring管理的Bean(比如從數(shù)據(jù)庫(kù)加載正則)
第四重:分組校驗(yàn)北冥功
4.1 增刪改查不同校驗(yàn)規(guī)則
對(duì)于增刪改查中,對(duì)于實(shí)體對(duì)象中的同一個(gè)參數(shù),在不同的應(yīng)用場(chǎng)景中需要做不同分組校驗(yàn)。
具體代碼如下:
// 定義校驗(yàn)組別(劃分陣營(yíng))
publicinterface CreateGroup {}
publicinterface UpdateGroup {}
// DTO根據(jù)場(chǎng)景應(yīng)用分組
publicclass ProductDTO {
@Null(groups = UpdateGroup.class)
@NotNull(groups = CreateGroup.class)
private Long id;
@NotBlank(groups = {CreateGroup.class, UpdateGroup.class})
private String name;
}
// 控制層按需激活校驗(yàn)組
@PostMapping("/create")
public Result create(@Validated(CreateGroup.class) @RequestBody ProductDTO dto) {
// 創(chuàng)建邏輯
}
多副本作戰(zhàn)手冊(cè):
- Default組始終生效(除非使用
groups
顯式配置) - 妙用
@ConvertGroup
進(jìn)行分組轉(zhuǎn)換
第五重:跨界校驗(yàn)凌波微步
5.1 跨字段關(guān)系校驗(yàn)
如果存在跨字段關(guān)系校驗(yàn)的情況,即組合條件校驗(yàn),比如:用戶密碼和確認(rèn)密碼,可以將自定義注解作用在類上。
具體代碼如下:
@Target(TYPE)
@Retention(RUNTIME)
@Constraint(validatedBy = PasswordValidator.class)
public @interface PasswordValid {
String message() default "兩次密碼不一致";
// ...
}
publicclass PasswordValidator implements ConstraintValidator<PasswordValid, UserDTO> {
@Override
public boolean isValid(UserDTO user, ConstraintValidatorContext context) {
return user.getPassword().equals(user.getConfirmPassword());
}
}
// 應(yīng)用到類級(jí)別
@PasswordValid
publicclass UserDTO {
private String password;
private String confirmPassword;
}
風(fēng)控新法:
- 適用于訂單金額與優(yōu)惠券匹配等業(yè)務(wù)規(guī)則
- DDD值對(duì)象的天然場(chǎng)景
第六重:規(guī)則引擎之天機(jī)策
天機(jī)殿的自動(dòng)化戰(zhàn)場(chǎng)
新來(lái)的產(chǎn)品小妹指著參數(shù)校驗(yàn)文檔:"每次改個(gè)手機(jī)號(hào)正則都要等發(fā)版?
"我默默掏出了祖?zhèn)鞯囊?guī)則引擎。
這種政商聯(lián)動(dòng)的需求,是時(shí)候施展大型工程的必殺技了!
6.1 規(guī)則引擎的三層境界
第一境:硬編碼校驗(yàn)(青銅段位的if-else)第二境:配置化校驗(yàn)(黃金段位的數(shù)據(jù)庫(kù)規(guī)則表)第三境:熱力場(chǎng)作戰(zhàn)(王者段位的動(dòng)態(tài)規(guī)則引擎)
6.2 Drools天機(jī)大陣部署實(shí)錄
戰(zhàn)場(chǎng)場(chǎng)景:信貸額度動(dòng)態(tài)校驗(yàn)(每小時(shí)調(diào)整風(fēng)控模型) 。
天機(jī)規(guī)則文件如下:
// 天機(jī)規(guī)則文件(credit_rule.drl)
rule "白領(lǐng)貸基礎(chǔ)校驗(yàn)"
when
$req : LoanRequest(
occupation == "白領(lǐng)",
salary > 10000,
age >= 25 && age <= 45
)
then
$req.setRiskScore(-10); //加分項(xiàng)
end
rule "高危行業(yè)攔截"
when
$req : LoanRequest(
industry in ("賭博業(yè)", "傳銷"),
location.contains("緬甸")
)
then
throw new ValidationException("閣下莫非是緬北戰(zhàn)神?");
end
布陣心法:
圖片
陣法要訣:
- 規(guī)則文件按業(yè)務(wù)線拆分(金融/電商/社交各立山頭)
- 使用kie-maven-plugin自動(dòng)編譯規(guī)則文件
- KieScanner監(jiān)聽規(guī)則變更(天機(jī)更新不重啟服務(wù))
6.3 SpringBoot接引天機(jī)大陣
法咒集成:
@Configuration
publicclass DroolsConfig {
@Bean
public KieContainer kieContainer() {
KieServices ks = KieServices.Factory.get();
KieFileSystem kfs = ks.newKieFileSystem();
// 加載天機(jī)卷軸(規(guī)則文件)
Resource resource = new ClassPathResource("rules/credit_rule.drl");
kfs.write(ks.getResources().newInputStreamResource(resource.getInputStream())
.setTargetPath("credit_rule.drl"));
KieBuilder kieBuilder = ks.newKieBuilder(kfs).buildAll();
return ks.newKieContainer(kieBuilder.getKieModule().getReleaseId());
}
}
// Controller層調(diào)用天尊之力
@PostMapping("/apply")
public Result applyLoan(@RequestBody LoanRequest request) {
kieSession.insert(request);
kieSession.fireAllRules(); // 執(zhí)行天機(jī)推演
return riskService.process(request);
}
天機(jī)沙箱防御:
- 限制規(guī)則中eval()的使用次數(shù)(防CPU過(guò)載)
- 為每個(gè)請(qǐng)求創(chuàng)建獨(dú)立KieSession(防線程污染)
- 設(shè)置規(guī)則執(zhí)行超時(shí)熔斷(天機(jī)殿也有算不動(dòng)的時(shí)候)
6.4 天機(jī)策反制訣竅
某次上線后,規(guī)則引擎的神操作:
rule "特殊時(shí)段放水"
when
$req : LoanRequest(hour > 2 && hour < 5)
then
$req.setCreditLimit(50000); //給值夜班的兄弟開后門
end
反制方案:
- 規(guī)則提交走審批流(太上長(zhǎng)老團(tuán)聯(lián)署制)
- 生產(chǎn)環(huán)境禁用update/modify關(guān)鍵字(防自動(dòng)奪舍)
- 規(guī)則版本回滾機(jī)制(祭出玄天寶鏡倒轉(zhuǎn)時(shí)空)
祖師爺級(jí)參數(shù)校驗(yàn)綱領(lǐng)
段位 | 招式名稱 | 修煉難度 | 適用場(chǎng)景 | 破壞力 |
青銅 | if-else硬編碼 | ★☆☆ | 小型工具類 | ??? |
白銀 | JSR注解大法 | ★★☆ | 常規(guī)CRUD | ?? |
黃金 | 全局異常攔截 | ★★★ | RESTful API | ? |
鉑金 | 定制校驗(yàn)規(guī)則 | ★★★☆ | 復(fù)雜業(yè)務(wù)規(guī)則 | ? |
鉆石 | 組合條件校驗(yàn) | ★★★★ | 跨字段業(yè)務(wù)約束 | ? |
王者 | 規(guī)則引擎整合 | ★★★★★ | 動(dòng)態(tài)風(fēng)控場(chǎng)景 | ? |
避坑法門
- 不過(guò)三:Controller層校驗(yàn)不要超過(guò)三層(應(yīng)該轉(zhuǎn)給Service)
- 見好就收:業(yè)務(wù)規(guī)則校驗(yàn)與基礎(chǔ)格式校驗(yàn)分離
- 防君子更防小人:服務(wù)端校驗(yàn)必須存在(前端校驗(yàn)是防君子用的)
- 語(yǔ)義明確:錯(cuò)誤提示避免暴露敏感信息(比如"用戶不存在"改為"賬號(hào)或密碼錯(cuò)誤")
最后提醒各位大俠:好的參數(shù)校驗(yàn)就像空氣——你平時(shí)感受不到它的存在,但一旦失去它,整個(gè)系統(tǒng)瞬間崩塌?。ùafields正提刀趕來(lái))