一今艺、痛點(diǎn)韵丑?
在應(yīng)用程序的業(yè)務(wù)邏輯中,經(jīng)常會(huì)碰到參數(shù)校驗(yàn)的情況虚缎,通常我們使用spring-mvc來(lái)接收用戶請(qǐng)求數(shù)據(jù)一般會(huì)封裝成一個(gè)Bean撵彻,需要校驗(yàn)字段值是否空,長(zhǎng)度遥巴,枚舉格式等情況下千康,如果使用SringUtils或者if判斷來(lái)解決,代碼會(huì)閱讀不友好铲掐,維護(hù)成本大拾弃,代碼冗余。 因此有了JSR 303.
Bean Validation為JavaBean提供了相應(yīng)的API來(lái)給我們做參數(shù)的驗(yàn)證摆霉。通過(guò)Bean Validation比如@NotNull @Pattern等方法來(lái)對(duì)我們字段的值做進(jìn)一步的教研豪椿。
Bean Validation 是一個(gè)運(yùn)行時(shí)框架,在驗(yàn)證之后錯(cuò)誤信息會(huì)直接返回携栋。
二搭盾、使用
1. 添加maven依賴
<!--添加依賴-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
注: hibernate-validator 擴(kuò)展了些自定義的validator可供參考。
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.0.Final</version>
</dependency>
2. 常用注解說(shuō)明
- constraints 包下邊即定義好的注解婉支,可直接使用
注解 | 用途 |
---|---|
AssertFalse | 用于boolean字段鸯隅,該字段的值只能為false |
AssertTrue | 用于boolean字段,該字段只能為true |
DecimalMax(value) | 被注釋的元素必須是一個(gè)數(shù)字向挖,只能大于或等于該值 |
DecimalMin(value) | 被注釋的元素必須是一個(gè)數(shù)字蝌以,只能小于或等于該值 |
Digits(integer,fraction) | 檢查是否是一種數(shù)字的(整數(shù),小數(shù))的位數(shù) |
被注釋的元素必須是電子郵箱地址 | |
Future | 檢查該字段的日期是否是屬于將來(lái)的日期 |
FutureOrPresent | 判斷日期是否是將來(lái)或現(xiàn)在日期 |
Max(value) | 該字段的值只能小于或等于該值 |
Min(value) | 該字段的值只能大于或等于該值 |
Negative | 判斷負(fù)數(shù) |
NegativeOrZero | 判斷負(fù)數(shù)或0 |
NotBlank | 只能用于字符串不為null,并且字符串trim()以后length要大于0 |
NotEmpty | 集合對(duì)象的元素不為0何之,即集合不為空跟畅,也可以用于字符串不為null |
NotNull | 不能為null |
Null | 必須為 null |
Past | 檢查該字段的日期是在過(guò)去 |
PastOrPresent | 判斷日期是否是過(guò)去或現(xiàn)在日期 |
Pattern(value) | 被注釋的元素必須符合指定的正則表達(dá)式 |
Positive | 判斷正數(shù) |
PositiveOrZero | 判斷正數(shù)或0 |
Size(max, min) | 檢查該字段的size是否在min和max之間,可以是字符串溶推、數(shù)組徊件、集合、Map等 |
Length(max, min) | 判斷字符串長(zhǎng)度 |
CreditCardNumber | 被注釋的字符串必須通過(guò)Luhn校驗(yàn)算法蒜危,銀行卡虱痕,信用卡等號(hào)碼一般都用Luhn計(jì)算合法性 |
三、自定義注解
1. 自定義Mobile注解
//注解作用范圍
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
//注解保留階段
@Retention(RetentionPolicy.RUNTIME)
@Documented
//指定驗(yàn)證器
@Constraint(
validatedBy = MobileValidator.class
)
public @interface Mobile {
String message() default "手機(jī)號(hào)格式不正確";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2. 驗(yàn)證器 MobileValidator
實(shí)現(xiàn)ConstraintValidator 的isValid方法即可
public class MobileValidator implements ConstraintValidator<Mobile, String> {
@Override
public void initialize(Mobile annotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果手機(jī)號(hào)為空辐赞,默認(rèn)不校驗(yàn)皆疹,即校驗(yàn)通過(guò)
if (!StringUtils.hasText(value)) {
return true;
}
// 校驗(yàn)手機(jī)
return ValidationUtil.isMobile(value);
}
}
3. 編寫(xiě)UserVo
public class UserVo {
@NotEmpty(message = "用戶名不能為空")
@Length(min = 4, max = 6, message = "手機(jī)驗(yàn)證碼長(zhǎng)度為 4-6 位")
@Pattern(regexp = "^[0-9]+$", message = "手機(jī)驗(yàn)證碼必須都是數(shù)字")
private String name;
@Mobile()
private String mobile;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
@Override
public String toString() {
return new org.apache.commons.lang3.builder.ToStringBuilder(this)
.append("name", name)
.append("mobile", mobile)
.toString();
}
}
4. 測(cè)試Controller
@RequestMapping("/validTest")
@ResponseBody
public String validTest(@Valid @RequestBody UserVo userVo) {
String print = userVo.toString();
log.info(print);
return print;
}
注意:如果輸入的參數(shù)不正確,會(huì)拋出MethodArgumentNotValidException 異常占拍,并會(huì)返回400錯(cuò)誤給前端略就, 我們進(jìn)一步美化輸出統(tǒng)一格式的錯(cuò)誤。
如果controller簽名方法 參數(shù)使用
@GetMapping("/label/list")
@ResponseBody
@Validated //加上才能被spring處理
public RdfaResult<List<LabelVo>> label_list_parent( @NotEmpty(message = "缺少參數(shù)") @RequestParam("kpi_level") String kpi_level) throws Exception {
return RdfaResult.success(ResponseErrorEnum.SUCESS.getCode(), ResponseErrorEnum.SUCESS.getVal(), resp);
}
5. 定義全局異常處理器
/**
* 全局異常處理器晃酒,將 Exception 翻譯成 CommonResult + 對(duì)應(yīng)的異常編號(hào)
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 處理 SpringMVC 參數(shù)校驗(yàn)異常 Validator 校驗(yàn)
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public CommonResult MethodArgumentNotValidException(MethodArgumentNotValidException ex) {
logger.warn("[MethodArgumentNotValidException]", ex);
FieldError fieldError = ex.getBindingResult().getFieldError();
//斷言表牢,避免告警
assert fieldError != null;
String format = String.format("請(qǐng)求參數(shù)不正確:%s", fieldError.getDefaultMessage());
CommonResult<String> commonResult =CommonResult.fail(msg);
return commonResult;
}
}
-
spring-web 組件, org.springframework.web.bind即常見(jiàn)的參數(shù)綁定異常
四贝次、自定義枚舉校驗(yàn)器
檢驗(yàn)器實(shí)現(xiàn)方式有好多種崔兴,基本原理就是從自定義枚舉注解上邊獲取原數(shù)據(jù),然后使用當(dāng)前值與枚舉所有值進(jìn)行比較蛔翅,存在則返回true, 否則返回fase.
- 枚舉校驗(yàn)注解
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumCheckValidator.class)
public @interface EnumCheck {
/**
* 是否必填 默認(rèn)是必填的
* @return
*/
boolean required() default true;
/**
* 驗(yàn)證失敗的消息
* @return
*/
String message() default "枚舉的驗(yàn)證失敗";
/**
* 分組的內(nèi)容
* @return
*/
Class<?>[] groups() default {};
/**
* 錯(cuò)誤驗(yàn)證的級(jí)別
* @return
*/
Class<? extends Payload>[] payload() default {};
/**
* 枚舉的Class
* @return
*/
Class<? extends Enum<?>> enumClass();
/**
* 枚舉中的驗(yàn)證方法
* @return
*/
String enumMethod() default "validation";
}
- 枚舉校驗(yàn)器
public class EnumCheckValidator implements ConstraintValidator<EnumCheck, Object> {
private static final Logger logger = LoggerFactory.getLogger(EnumCheckValidator.class);
private EnumCheck enumCheck;
@Override
public void initialize(EnumCheck enumCheck) {
this.enumCheck =enumCheck;
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
// 注解表明為必選項(xiàng) 則不允許為空敲茄,否則可以為空
if (value == null) {
return !this.enumCheck.required();
}
Boolean result = Boolean.FALSE;
Class<?> valueClass = value.getClass();
try {
//通過(guò)反射執(zhí)行枚舉類中validation方法
Method method = this.enumCheck.enumClass().getMethod(this.enumCheck.enumMethod(), valueClass);
result = (Boolean)method.invoke(null, value);
if(result == null){
return false;
}
} catch (Exception e) {
logger.error("custom EnumCheckValidator error", e);
}
return result;
}
}
六、原理
- 在配置spring-mvc 時(shí)有個(gè)核心類 WebMvcConfigurationSupport.java 會(huì)配置 Validator 的一個(gè)實(shí)例山析。
@Bean
public Validator mvcValidator() {
Validator validator = getValidator();
if (validator == null) {
if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) {
Class<?> clazz;
try {
String className = "org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean";
clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader());
}
catch (ClassNotFoundException | LinkageError ex) {
throw new BeanInitializationException("Failed to resolve default validator class", ex);
}
validator = (Validator) BeanUtils.instantiateClass(clazz);
}
else {
validator = new NoOpValidator();
}
}
return validator;
}
這個(gè)實(shí)例的作用是:用于驗(yàn)證標(biāo)注了@ModelAttribute和@RequestBody注解的方法參數(shù)堰燎。實(shí)例化時(shí),首先委托g(shù)etValidator()笋轨,如果返回null秆剪,則檢查類路徑上是否存在 JSR-303實(shí)現(xiàn)(javax.validation.Validator), 存在則實(shí)例化OptionalValidatorFactoryBean爵政,否則返回一個(gè)無(wú)操作的Validator仅讽。
在 實(shí)例化 OptionalValidatorFactoryBean 的時(shí)候,初始化了SpringConstraintValidatorFactory 用來(lái)代理ConstraintValidatorFactory(JSR-303)钾挟。
-
HandlerMethodArgumentResolver 的實(shí)現(xiàn)類 RequestPartMethodArgumentResolver 里邊作注解參數(shù)的解析及驗(yàn)證洁灵。
默認(rèn)檢測(cè) @javax.validation.Valid、Spring's Validated掺出、自定義以 "Valid" 開(kāi)頭的注解
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
binder.validate(validationHints);
break;
}
}
}