通常,当我们需要验证用户输入时,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) {
    }   
}