通常,当我们需要验证用户输入时,Spring MVC提供标准的预定义验证器。我们会引入spring-boot-starter-validation
依赖来实现数据校验功能。
但是,当我们需要验证特定类型的输入时,我们就需要创建自己的自定义校验逻辑。这里我们取一个相对简单的校验手机号码的功能来实现。
为了校验手机号码,我们需要引入谷歌的 libphonenumber
依赖Spring的和spring-boot-starter-validation
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
</dependency>
创建注解
我们创建一个新的@interface
来创建注解:
/**
* 手机号校验
*/
@Documented
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(List.class)
@Constraint(validatedBy = {MobileValidator.class})
public @interface Mobile {
/**
* 错误消息
*
* @return 错误消息
*/
String message() default "{com.demo.validation.constraints.Mobile.message}";
/**
* 分组
*
* @return 分组
*/
Class<?>[] groups() default {};
/**
* payload
*
* @return payload
*/
Class<? extends Payload>[] payload() default {};
/**
* 默认地域,用于识别手机号
*
* @return 默认地域
*/
String defaultRegion() default "CN";
/**
* 允许的地域列表
*
* @return 允许的地域列表
*/
String[] regions() default {"CN"};
/**
* 列表
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
public @interface List {
/**
* value
*
* @return value
*/
Mobile[] value();
}
}
使用@Constraint
注解,我们定义了实际用来处理验证字段的类。message()
是显示在用户交互界面中的错误消息。最后,附加代码主要是符合Spring标准的样板代码。
这里的message()
如果你不需要国际化功能,你也可以直接写死,比如“手机号不合法”之类的,但此处我们还想实现国际化功能,还是需要设置成多语言信息的形式。
创建验证类
现在让我们创建一个验证器类来执行我们的验证规则:
/**
* 手机号校验
*/
public class MobileValidator implements ConstraintValidator<Mobile, String> {
private String defaultRegion;
private String[] regions;
@Override
public void initialize(Mobile constraintAnnotation) {
defaultRegion = constraintAnnotation.defaultRegion();
regions = constraintAnnotation.regions();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
PhoneNumberUtil phoneUtils = PhoneNumberUtil.getInstance();
try {
PhoneNumber phoneNumber = phoneUtils.parse(value, defaultRegion);
String regionCode = phoneUtils.getRegionCodeForCountryCode(phoneNumber.getCountryCode());
boolean parseRet = Arrays.stream(regions).anyMatch(item -> item.equalsIgnoreCase(regionCode));
if (!parseRet) {
return false;
}
return phoneUtils.isValidNumber(phoneNumber);
} catch (NumberParseException e) {
return false;
}
}
}
验证类主要实现了ConstraintValidator
接口,还必须实现isValid
方法;我们正是在这个方法中定义了验证规则。我们这里简单的借助libphonenumber
依赖来帮我们实现手机号的校验。
而在initialize
中我们主要做了注解参数的处理,这里defaultRegion
主要设置默认的号码地域,比如这里是中国大陆CN
,另外一个regions
主要用于设置校验哪些地区的手机号。
创建多语言文件
为了实现国际化功能,我们还需要添加多语言文件,比如我们建在 resources/com/demo/validator/ValidationMessages
目录下,我们新建对应的多语言文件:
ValidationMessages.properties
com.demo.validation.constraints.Mobile.message=必须为格式规范的手机号
ValidationMessages_en.properties
com.demo.validation.constraints.Mobile.message=must be a well-formed phone number
ValidationMessages_zh_CN.properties
com.demo.validation.constraints.Mobile.message=必须为格式规范的手机号
ValidationMessages_zh_TW.properties
com.demo.validation.constraints.Mobile.message=必須是形式完整的電話號碼
引入多语言文件
/**
* 参数校验自动配置
*/
@Configuration
@ConditionalOnClass(ExecutableValidator.class)
@AutoConfigureBefore(ValidationAutoConfiguration.class)
public class ValidationConfiguration {
/**
* 校验factory
*
* @param messageSource 错误信息
* @return LocalValidatorFactoryBean
*/
@Bean
@ConditionalOnMissingBean
public LocalValidatorFactoryBean localValidatorFactoryBean(@Qualifier("customValidationMessageSource") MessageSource messageSource) {
LocalValidatorFactoryBean localValidator = new LocalValidatorFactoryBean();
localValidator.setValidationMessageSource(messageSource);
return localValidator;
}
/**
* 参数校验的错误信息源
*
* @return MessageSource
*/
@Bean("customValidationMessageSource")
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.addBasenames("classpath:org.hibernate.validator.ValidationMessages",
"classpath:com/demo/validator/ValidationMessages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
}
这里主要有几点需要关注:
-
我们使用
@AutoConfigureBefore(ValidationAutoConfiguration.class)
要求这个配置类在ValidationAutoConfiguration
之前装配,避免LocalValidatorFactoryBean
已经存在造成冲突。 -
通过创建自定义的
MessageSource
,添加了hibernate
的多语言文件,同时把我们自己的多语言文件也添加进去了。 -
将
LocalValidatorFactoryBean
的ValidationMessageSource设置为我们自定义的MessageSource
。
使用
如果要对数据进行校验,我们只需要在字段上添加注解,然后使用@Valid
或者@Validated
对数据校验即可:
@Mobile
private String phone;
@Controller
public class ValidatedPhoneController {
@PostMapping("/addValidatePhone")
public String submitForm(@Valid ValidatedPhone validatedPhone) {
}
}