眾所周知,使用@RequestParam(required = false)
封裝請求參數的時候,如果客戶端不提交參數,或者是只聲明參數,並不賦值。那么方法的形參值,默認為null(基本數據類型除外)。
一個Controller方法,有2個參數
@GetMapping
public Object update(@RequestParam(value = "number", required = false) Integer number,
@RequestParam(value = "phone", required = false) String phone) {
LOGGER.info("number={}, phone={}", number, phone);
return Message.success(phone);
}
很簡單的一個Controller方法。有兩個參數,都不是必須的。只是這倆參數的數據類型不同。
// 都不聲明參數
http://localhost:8080/test
日志輸出:number=null, phone=null
// 都只聲明參數,但是不賦值
http://localhost:8080/test?number=&phone=
日志輸出出:number=null, phone=
這里可以看出,String
類型的參數。在聲明,不賦值的情況下。默認值為空字符串。
使用spring-validation遇到@RequestParam(required = false)
字符串參數的問題
一個驗證手機號碼的注解
極其簡單,通過正則驗證字符串是否是手機號碼
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.Pattern;
@Retention(RUNTIME)
@Target(value = { ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
@Constraint(validatedBy = {})
@ReportAsSingleViolation
@Pattern(regexp = "^1[3-9]\\d{9}$")
public @interface Phone {
String message() default "手機號碼不正確,只支持大陸手機號碼";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
一般這樣使用
private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);
@GetMapping
public Object update(@RequestParam(value = "number", required = false) Integer number,
@RequestParam(value = "phone", required = false) @Phone String phone) {
LOGGER.info("number={}, phone={}", number, phone);
return Message.success(phone);
}
這是一個修改接口,允許用戶修改自己的手機號碼,但手機號碼並不是必須的,允許以空字符串的形式存儲在數據庫。通俗的說就是,phone
參數,要么是一個合法的手機號碼。要么是空字符串,或者null。
客戶端發起了請求
// 假如用戶什么也不輸入,清空了 phone 輸入框,客戶端js序列化表單后提交。
http://localhost:8080/test?number=&phone=
果然得到了異常:
javax.validation.ConstraintViolationException: update.phone: 手機號碼不正確,只支持大陸手機號碼
很顯然,空字符串 "",並不符合手機號碼的正則校驗。
這種情況就是,在校驗規則,和默認值之間,出現了一點點沖突
解決辦法
求前端大哥改巴改巴
提交之前,遍歷一下請求參數。把空值參數,從請求體中移除。那么后端接收到的形參就是,null。是業務可以接受的數據類型。
修改驗證規則
這個也不算難,自己修改一下驗證的正則,或者重新實現一個自定義的 ConstraintValidator
,允許手機號碼為空字符串。但是,也有一個問題,這個注解就不能用在必填的手機號碼參數上了。例如:注冊業務,因為它的規則是允許空字符串的。
當然,也可以維護多個不同的驗證規則注解。
@Phone 驗證必須是標准手機號碼
@Retention(RUNTIME)
@Target(value = { ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
@Constraint(validatedBy = {})
@ReportAsSingleViolation
@Pattern(regexp = "^1[3-9]\\d{9}$")
public @interface Phone {
String message() default "手機號碼不正確,只支持大陸手機號碼";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@PhoneOrEmpty 可以是空字符串或者標准的手機號碼
@Retention(RUNTIME)
@Target(value = { ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
@Constraint(validatedBy = {})
@ReportAsSingleViolation
@Pattern(regexp = "^(1[3-9]\\d{9})|(.{0})$")
public @interface PhoneOrEmpty {
String message() default "手機號碼不正確,只支持大陸手機號碼";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
好了,這倆可以用在不同的驗證地方。唯一的不同就是驗證的正則不同。這也是讓我覺得不舒服的地方。需要維護兩個正則表達式
一種我認為比較"優雅"的方式
還是一樣,定義不同的注解來處理不同的驗證場景。但是,我並不選擇自立門戶(單獨維護一個正則),而是在@Phone
的基礎上,進行一個加強
。
@Retention(RUNTIME)
@Target(value = { ElementType.FIELD, ElementType.PARAMETER })
@Constraint(validatedBy = {})
@ReportAsSingleViolation
@Phone // 使用已有的@Phone作為校驗規則,參數必須是一個合法的手機號碼
@Length(max = 0, min = 0) // 使用Hiberante提供的字符串長度校驗規則,在這里,表示慘參數字符串的長度必須:最短0,最長0(就是空字符串)
@ConstraintComposition(CompositionType.OR) // 核心的來了,這個注解表示“多個驗證注解之間的邏輯關系”,這里使用“or”,滿足任意即可
public @interface PhoneOrEmpty {
String message() default "手機號碼不正確,只支持大陸手機號碼";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
核心的說明,都在上面的注釋代碼上了。
自定義使用組合Constraint
,在原來@Phone
的驗證規則上,再添加一個 @Length(max = 0, min = 0)
規則。使用@ConstraintComposition
描述這兩個驗證規則的邏輯關系。
ConstraintComposition
只有一個枚舉屬性
@Documented
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface ConstraintComposition {
/**
* The value of this element specifies the boolean operator,
* namely disjunction (OR), negation of the conjunction (ALL_FALSE),
* or, the default, simple conjunction (AND).
*
* @return the {@code CompositionType} value
*/
CompositionType value() default AND;
}
public enum CompositionType {
OR, // 多個驗證規則中,只要有一個通過就算驗證成功
AND, // 多個驗證規則中,必須全部通過才算成功(默認)
ALL_FALSE // 多個驗證規則中,必須全部失敗,才算通過(少見)
}
試試看
Controller
@GetMapping
public Object update (@RequestParam(value = "number", required = false) Integer number,
@RequestParam(value = "phone", required = false) @PhoneOrEmpty String phone) {
LOGGER.info("number={}, phone={}", number, phone);
return Message.success(phone);
}