還在用 if 硬剛參數(shù)校驗(yàn)?這波操作土到掉渣!SpringBoot 高階玩法直接封神
兄弟們,有一個(gè)現(xiàn)象特別有意思:新手寫代碼的時(shí)候,特別喜歡用 if 語(yǔ)句來(lái)做參數(shù)校驗(yàn),一頓操作猛如虎,代碼寫得像瀑布。老手呢,雖然知道這樣不好,但有時(shí)候?yàn)榱粟s進(jìn)度,也不得不繼續(xù)用這種 “土方法”。咱就是說(shuō),難道就沒(méi)有更優(yōu)雅、更高效的辦法嗎?當(dāng)然有啦!今天咱就來(lái)聊聊 Spring Boot 里那些能讓參數(shù)校驗(yàn)直接 “封神” 的高階玩法。
一、傳統(tǒng)參數(shù)校驗(yàn):土味十足的 “體力活”
先說(shuō)說(shuō)大家最熟悉的傳統(tǒng)參數(shù)校驗(yàn)吧。假設(shè)咱們有一個(gè)用戶注冊(cè)的接口,需要校驗(yàn)用戶名、手機(jī)號(hào)、郵箱等參數(shù)。按照傳統(tǒng)做法,那就是在方法里瘋狂寫 if 語(yǔ)句:
public User register(String username, String phone, String email) {
if (username == null || username.trim().length() < 3 || username.trim().length() > 20) {
throw new IllegalArgumentException("用戶名長(zhǎng)度必須在3到20之間");
}
if (phone == null || !phone.matches("^1[3-9]\\d{9}$")) {
throw new IllegalArgumentException("手機(jī)號(hào)格式不正確");
}
if (email == null || !email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$")) {
throw new IllegalArgumentException("郵箱格式不正確");
}
// 接下來(lái)是業(yè)務(wù)邏輯
User user = new User();
user.setUsername(username);
user.setPhone(phone);
user.setEmail(email);
return userService.save(user);
}
這樣的代碼看起來(lái)是不是特別 “樸實(shí)無(wú)華”?但問(wèn)題可不少。首先,代碼冗余度極高,每個(gè)參數(shù)都要寫好幾行校驗(yàn)邏輯,要是參數(shù)多一點(diǎn),整個(gè)方法就變得又長(zhǎng)又臭,跟裹腳布似的。其次,維護(hù)起來(lái)特別麻煩,要是哪天需求變了,比如用戶名長(zhǎng)度限制改了,你得在所有用到這個(gè)校驗(yàn)的地方都改一遍,稍有不慎就會(huì)漏掉,埋下 bug 的隱患。而且,這種校驗(yàn)邏輯和業(yè)務(wù)邏輯混在一起,顯得特別混亂,就像把菜湯和米飯攪在一起吃,看著就鬧心。
再?gòu)拇a的可復(fù)用性來(lái)說(shuō),如果你在多個(gè)地方都需要校驗(yàn)手機(jī)號(hào),難道每次都要把那段正則表達(dá)式和 if 語(yǔ)句復(fù)制粘貼一遍嗎?這也太 low 了吧,完全不符合咱們程序員 “DRY(Don't Repeat Yourself)” 的原則。而且,這種土味校驗(yàn)方法對(duì)于復(fù)雜的業(yè)務(wù)場(chǎng)景根本招架不住,比如需要多個(gè)參數(shù)之間相互校驗(yàn),或者需要結(jié)合數(shù)據(jù)庫(kù)查詢來(lái)做校驗(yàn),這時(shí)候 if 語(yǔ)句就顯得力不從心了,就像讓一個(gè)小學(xué)生去解高考數(shù)學(xué)題,根本搞不定。
二、Spring Validation:參數(shù)校驗(yàn)的 “正規(guī)軍”
好在 Spring 框架為我們提供了一套強(qiáng)大的參數(shù)校驗(yàn)機(jī)制 ——Spring Validation,它就像是參數(shù)校驗(yàn)領(lǐng)域的 “正規(guī)軍”,讓我們告別土味的 if 語(yǔ)句,走向優(yōu)雅開(kāi)發(fā)的道路。
(一)基本用法:注解加持,簡(jiǎn)潔高效
Spring Validation 主要通過(guò)一系列的注解來(lái)實(shí)現(xiàn)參數(shù)校驗(yàn),這些注解可以直接加在方法的參數(shù)上,或者加在實(shí)體類的字段上。咱們先來(lái)看一個(gè)簡(jiǎn)單的例子,還是以用戶注冊(cè)為例,這次我們用實(shí)體類來(lái)接收參數(shù):
public class UserRegisterForm {
@NotBlank(message = "用戶名不能為空")
@Size(min = 3, max = 20, message = "用戶名長(zhǎng)度必須在3到20之間")
private String username;
@NotBlank(message = "手機(jī)號(hào)不能為空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手機(jī)號(hào)格式不正確")
private String phone;
@NotBlank(message = "郵箱不能為空")
@Email(message = "郵箱格式不正確")
private String email;
// 省略 getter 和 setter 方法
}
然后在控制器的方法里,加上 @Valid 注解來(lái)開(kāi)啟參數(shù)校驗(yàn):
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody UserRegisterForm form, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
List<String> errorMessages = bindingResult.getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.toList());
return ResponseEntity.badRequest().body(errorMessages);
}
// 業(yè)務(wù)邏輯
User user = new User();
user.setUsername(form.getUsername());
user.setPhone(form.getPhone());
user.setEmail(form.getEmail());
userService.save(user);
return ResponseEntity.ok().build();
}
你看,這樣一來(lái),代碼是不是清爽多了?原來(lái)需要好幾行 if 語(yǔ)句才能完成的校驗(yàn),現(xiàn)在只需要在實(shí)體類的字段上加上對(duì)應(yīng)的注解就行了,校驗(yàn)邏輯和業(yè)務(wù)邏輯也分開(kāi)了,再也不用像以前那樣 “糾纏不清”。而且這些注解都是 Spring 自帶的,常用的校驗(yàn)場(chǎng)景基本都能覆蓋,比如 @NotNull 用于非空校驗(yàn),@Min 和 @Max 用于數(shù)值范圍校驗(yàn),@Email 專門用于郵箱格式校驗(yàn),簡(jiǎn)直不要太方便。
(二)分組校驗(yàn):不同場(chǎng)景,精準(zhǔn)校驗(yàn)
在實(shí)際開(kāi)發(fā)中,我們經(jīng)常會(huì)遇到這樣的情況:同一個(gè)實(shí)體類在不同的業(yè)務(wù)場(chǎng)景下,需要校驗(yàn)的參數(shù)不一樣。比如用戶注冊(cè)時(shí),需要校驗(yàn)所有的必填字段,而用戶修改個(gè)人信息時(shí),可能有些字段是允許為空的。這時(shí)候,分組校驗(yàn)就派上用場(chǎng)了。
Spring Validation 支持分組校驗(yàn),我們可以通過(guò)定義不同的分組接口,來(lái)指定不同場(chǎng)景下需要校驗(yàn)的字段。首先,定義一個(gè)分組接口:
public interface RegisterGroup {
}
public interface UpdateGroup {
}
然后在實(shí)體類的字段上指定分組:
public class UserForm {
@NotBlank(message = "用戶名不能為空", groups = {RegisterGroup.class})
private String username;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手機(jī)號(hào)格式不正確", groups = {RegisterGroup.class, UpdateGroup.class})
private String phone;
@Email(message = "郵箱格式不正確", groups = {UpdateGroup.class})
private String email;
// 省略 getter 和 setter 方法
}
在控制器方法中,指定需要校驗(yàn)的分組:
@PostMapping("/register")
public ResponseEntity<?> register(@Validated(RegisterGroup.class) @RequestBody UserForm form, BindingResult bindingResult) {
// 注冊(cè)場(chǎng)景的校驗(yàn)和業(yè)務(wù)邏輯
}
@PutMapping("/update")
public ResponseEntity<?> update(@Validated(UpdateGroup.class) @RequestBody UserForm form, BindingResult bindingResult) {
// 更新場(chǎng)景的校驗(yàn)和業(yè)務(wù)邏輯
}
這樣,在注冊(cè)場(chǎng)景下,就會(huì)按照 RegisterGroup 分組來(lái)校驗(yàn),用戶名、手機(jī)號(hào)都會(huì)被校驗(yàn);而在更新場(chǎng)景下,就會(huì)按照 UpdateGroup 分組來(lái)校驗(yàn),手機(jī)號(hào)和郵箱會(huì)被校驗(yàn),用戶名允許為空(因?yàn)樵?UpdateGroup 分組中沒(méi)有對(duì)用戶名進(jìn)行校驗(yàn))。分組校驗(yàn)就像是給不同的業(yè)務(wù)場(chǎng)景定制了專屬的校驗(yàn) “套餐”,精準(zhǔn)又高效,再也不用為了不同場(chǎng)景寫不同的實(shí)體類或者重復(fù)寫校驗(yàn)邏輯了,簡(jiǎn)直是開(kāi)發(fā)中的 “貼心小棉襖”。
(三)方法級(jí)校驗(yàn):復(fù)雜邏輯,輕松搞定
雖然字段級(jí)的注解已經(jīng)能滿足大部分的校驗(yàn)需求,但對(duì)于一些復(fù)雜的校驗(yàn)邏輯,比如需要多個(gè)參數(shù)之間相互校驗(yàn),或者需要結(jié)合業(yè)務(wù)邏輯進(jìn)行校驗(yàn),這時(shí)候就需要用到方法級(jí)的校驗(yàn)了。Spring Validation 允許我們?cè)诜椒ㄉ咸砑有r?yàn)注解,或者自定義方法級(jí)的校驗(yàn)邏輯。
比如,我們有一個(gè)訂單創(chuàng)建的方法,需要校驗(yàn)訂單金額和優(yōu)惠金額的關(guān)系,優(yōu)惠金額不能超過(guò)訂單金額,而且訂單金額和優(yōu)惠金額都不能為負(fù)數(shù)。這時(shí)候,我們可以在方法上添加校驗(yàn)邏輯:
public class Order {
private BigDecimal amount;
private BigDecimal discount;
// 省略 getter 和 setter 方法
@AssertTrue(message = "優(yōu)惠金額不能超過(guò)訂單金額")
public boolean isDiscountValid() {
return discount == null || amount == null || discount.compareTo(amount) <= 0;
}
@AssertTrue(message = "訂單金額和優(yōu)惠金額不能為負(fù)數(shù)")
public boolean isAmountNonNegative() {
return amount == null || amount.compareTo(BigDecimal.ZERO) >= 0 && (discount == null || discount.compareTo(BigDecimal.ZERO) >= 0);
}
}
然后在控制器方法中,對(duì)訂單對(duì)象進(jìn)行校驗(yàn):
@PostMapping("/orders")
public ResponseEntity<?> createOrder(@Valid @RequestBody Order order, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
// 處理校驗(yàn)錯(cuò)誤
}
// 業(yè)務(wù)邏輯
return ResponseEntity.ok().build();
}
這樣,通過(guò)方法級(jí)的校驗(yàn),我們就可以處理復(fù)雜的參數(shù)之間的關(guān)系校驗(yàn),而不用在控制器方法里寫一堆繁瑣的 if 語(yǔ)句了。方法級(jí)校驗(yàn)就像是一把 “萬(wàn)能鑰匙”,能打開(kāi)復(fù)雜校驗(yàn)場(chǎng)景的大門,讓我們的代碼更加簡(jiǎn)潔、優(yōu)雅。
三、自定義校驗(yàn):打造專屬的校驗(yàn) “神器”
雖然 Spring 自帶的校驗(yàn)注解已經(jīng)很強(qiáng)大了,但在實(shí)際開(kāi)發(fā)中,我們難免會(huì)遇到一些特殊的校驗(yàn)需求,比如校驗(yàn)一個(gè)用戶是否存在于數(shù)據(jù)庫(kù)中,或者校驗(yàn)一個(gè)文件是否符合特定的格式。這時(shí)候,我們就需要自定義校驗(yàn)注解和校驗(yàn)器了,打造屬于自己的校驗(yàn) “神器”。
(一)自定義校驗(yàn)注解:隨心所欲,定義規(guī)則
首先,我們需要定義一個(gè)自定義的校驗(yàn)注解。比如,我們要校驗(yàn)一個(gè)手機(jī)號(hào)是否已經(jīng)被注冊(cè),我們可以定義一個(gè) @UniquePhone 注解:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniquePhoneValidator.class)
public @interface UniquePhone {
String message() default "手機(jī)號(hào)已被注冊(cè)";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
這里,@Target 注解指定了這個(gè)注解可以用在字段和參數(shù)上,@Retention 注解指定了注解在運(yùn)行時(shí)有效,@Constraint 注解指定了對(duì)應(yīng)的校驗(yàn)器 UniquePhoneValidator。
(二)自定義校驗(yàn)器:實(shí)現(xiàn)邏輯,精準(zhǔn)校驗(yàn)
接下來(lái),我們需要實(shí)現(xiàn)這個(gè)校驗(yàn)器 UniquePhoneValidator,它需要繼承 ConstraintValidator<UniquePhone, String> 接口,并重寫 initialize 和 isValid 方法:
public class UniquePhoneValidator implements ConstraintValidator<UniquePhone, String> {
@Autowired
private UserService userService;
@Override
public void initialize(UniquePhone constraintAnnotation) {
// 初始化操作,這里可以獲取注解的參數(shù)等
}
@Override
public boolean isValid(String phone, ConstraintValidatorContext context) {
if (phone == null) {
returntrue; // 如果允許為空,可以根據(jù)實(shí)際情況處理
}
User user = userService.findByPhone(phone);
return user == null;
}
}
在 isValid 方法中,我們通過(guò)調(diào)用 UserService 的 findByPhone 方法來(lái)查詢數(shù)據(jù)庫(kù),判斷該手機(jī)號(hào)是否已經(jīng)存在。如果存在,就返回 false,表示校驗(yàn)不通過(guò);如果不存在,就返回 true,表示校驗(yàn)通過(guò)。
(三)使用自定義校驗(yàn)注解
定義好自定義校驗(yàn)注解和校驗(yàn)器之后,我們就可以在實(shí)體類中使用它了:
public class UserRegisterForm {
// 其他字段的校驗(yàn)注解
@UniquePhone(message = "手機(jī)號(hào)已被注冊(cè)")
private String phone;
// 省略 getter 和 setter 方法
}
然后在控制器方法中,依然使用 @Valid 注解來(lái)開(kāi)啟校驗(yàn),就像使用 Spring 自帶的注解一樣方便。這樣,我們就實(shí)現(xiàn)了一個(gè)基于數(shù)據(jù)庫(kù)查詢的自定義校驗(yàn),滿足了特殊的業(yè)務(wù)需求。自定義校驗(yàn)注解和校驗(yàn)器的組合,讓我們可以根據(jù)自己的業(yè)務(wù)需求,靈活地定義各種復(fù)雜的校驗(yàn)規(guī)則,不再受限于 Spring 自帶的注解。這就好比我們可以自己動(dòng)手打造一把適合自己的 “倚天劍”,在參數(shù)校驗(yàn)的江湖里所向披靡。
四、全局異常處理:讓錯(cuò)誤處理更優(yōu)雅
前面我們已經(jīng)學(xué)會(huì)了如何使用 Spring Validation 來(lái)進(jìn)行參數(shù)校驗(yàn),并且通過(guò) BindingResult 來(lái)獲取校驗(yàn)錯(cuò)誤信息。但是,每次都在控制器方法里處理 bindingResult.hasErrors() 這種情況,難免會(huì)顯得代碼冗余,而且不夠優(yōu)雅。這時(shí)候,我們可以使用 Spring Boot 的全局異常處理機(jī)制,來(lái)統(tǒng)一處理參數(shù)校驗(yàn)錯(cuò)誤,讓代碼更加簡(jiǎn)潔、整潔。
首先,我們定義一個(gè)全局異常處理類,使用 @RestControllerAdvice 和 @ExceptionHandler 注解:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
List<String> errorMessages = ex.getBindingResult().getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.toList());
return ResponseEntity.badRequest().body(errorMessages);
}
// 其他異常處理方法
}
這樣,當(dāng)參數(shù)校驗(yàn)失敗時(shí),就會(huì)拋出 MethodArgumentNotValidException 異常,全局異常處理類會(huì)捕獲這個(gè)異常,并將校驗(yàn)錯(cuò)誤信息以統(tǒng)一的格式返回給客戶端。這樣一來(lái),我們的控制器方法就變得更加簡(jiǎn)潔了,不再需要每次都處理 bindingResult,只需要專注于業(yè)務(wù)邏輯即可。全局異常處理就像是一個(gè) “管家”,幫我們統(tǒng)一管理各種異常情況,包括參數(shù)校驗(yàn)錯(cuò)誤,讓我們的代碼更加規(guī)范、優(yōu)雅,維護(hù)起來(lái)也更加方便。
五、總結(jié):告別土味校驗(yàn),擁抱優(yōu)雅開(kāi)發(fā)
說(shuō)了這么多,咱們來(lái)總結(jié)一下。傳統(tǒng)的 if 語(yǔ)句參數(shù)校驗(yàn)方法,雖然簡(jiǎn)單直接,但就像 “土八路” 一樣,存在代碼冗余、維護(hù)困難、可復(fù)用性差等問(wèn)題,在復(fù)雜場(chǎng)景下更是力不從心。而 Spring Boot 提供的參數(shù)校驗(yàn)機(jī)制,就像是 “正規(guī)軍”,通過(guò)各種注解、分組校驗(yàn)、方法級(jí)校驗(yàn)、自定義校驗(yàn)以及全局異常處理等高階玩法,讓參數(shù)校驗(yàn)變得簡(jiǎn)潔、高效、靈活、優(yōu)雅。
使用 Spring Validation,我們可以大大減少重復(fù)的 if 語(yǔ)句,讓代碼更加簡(jiǎn)潔明了,校驗(yàn)邏輯和業(yè)務(wù)邏輯分離,提高代碼的可維護(hù)性和可復(fù)用性。自定義校驗(yàn)功能更是讓我們能夠應(yīng)對(duì)各種復(fù)雜的業(yè)務(wù)需求,打造專屬的校驗(yàn)規(guī)則。全局異常處理則讓錯(cuò)誤處理更加統(tǒng)一、規(guī)范,提升整個(gè)系統(tǒng)的健壯性。
所以,咱程序員可不能再用那些土味的 if 語(yǔ)句硬剛參數(shù)校驗(yàn)了,趕緊擁抱 Spring Boot 的這些高階玩法吧,讓咱們的代碼也能 “封神”,變得優(yōu)雅又強(qiáng)大。