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