高效使用hibernate-validator校驗框架

1、前言

  高效、合理的使用hibernate-validator校驗框架能夠提升程序的可讀性,以及減小沒必要要的代碼邏輯。接下來會介紹一下經常使用一些使用方式。java

2、經常使用註解說明

限制 說明
@Null 限制只能爲null
@NotNull 限制必須不爲null
@AssertFalse 限制必須爲false
@AssertTrue 限制必須爲true
@DecimalMax(value) 限制必須爲一個不大於指定值的數字
@DecimalMin(value) 限制必須爲一個不小於指定值的數字
@Digits(integer,fraction) 限制必須爲一個小數,且整數部分的位數不能超過integer,小數部分的位數不能超過fraction
@Future 限制必須是一個未來的日期
@Max(value) 限制必須爲一個不大於指定值的數字
@Min(value) 限制必須爲一個不小於指定值的數字
@Past 限制必須是一個過去的日期
@Pattern(value) 限制必須符合指定的正則表達式
@Size(max,min) 限制字符長度必須在min到max之間
@Past 驗證註解的元素值(日期類型)比當前時間早
@NotEmpty 驗證註解的元素值不爲null且不爲空(字符串長度不爲0、集合大小不爲0)
@NotBlank 驗證註解的元素值不爲空(不爲null、去除首位空格後長度爲0),不一樣於@NotEmpty,@NotBlank只應用於字符串且在比較時會去除字符串的空格
@Email 驗證註解的元素值是Email,也能夠經過正則表達式和flag指定自定義的email格式

3、定義校驗分組

public class ValidateGroup {
    public interface FirstGroup {
    }

    public interface SecondeGroup {
    }

    public interface ThirdGroup {
    }
}

4、定義校驗Bean

@Validated
@GroupSequence({ValidateGroup.FirstGroup.class, BaseMessageRequestBean.class})
public class BaseMessageRequestBean {

    //渠道類型
    @NotNull(message = "channelType爲NULL", groups = ValidateGroup.FirstGroup.class)
    private String channelType;

    //消息(模板消息或者普通消息)
    @NotNull(message = "data爲NUll", groups = ValidateGroup.FirstGroup.class)
@Valid
private Object data; //業務類型 @NotNull(message = "bizType爲NULL", groups = ValidateGroup.FirstGroup.class) private String bizType; //消息推送對象 @NotBlank(message = "toUser爲BLANK", groups = ValidateGroup.FirstGroup.class) private String toUser; private long createTime = Instant.now().getEpochSecond(); ...... }

  請自行參考:@Validated和@Valid區別git

5、validator基本使用

@RestController
public class TestValidatorController {
    @RequestMapping("/test/validator")
    public void test(@Validated BaseMessageRequestBean bean){
... } }

  這種使用方式有一個弊端,不能自定義返回異常。spring若是驗證失敗,則直接拋出異常,通常不可控。github

6、藉助BindingResult

@RestController
public class TestValidatorController {
    @RequestMapping("/test/validator")
    public void test(@Validated BaseMessageRequestBean bean, BindingResult result){
        result.getAllErrors();
        ...
    }
}

  若是方法中有BindingResult類型的參數,spring校驗完成以後會將校驗結果傳給這個參數。經過BindingResult控制程序拋出自定義類型的異常或者返回不一樣結果。正則表達式

7、全局攔截校驗器

  固然了,須要在藉助BindingResult的前提下...算法

@Aspect
@Component
public class ControllerValidatorAspect {
    @Around("execution(* com.*.controller..*.*(..)) && args(..,result)")
    public Object doAround(ProceedingJoinPoint pjp, result result) {
        result.getFieldErrors();
        ...
    }
}

  這種方式能夠減小controller層校驗的代碼,校驗邏輯統一處理,更高效。spring

 8、藉助ValidatorUtils工具類

@Bean
public Validator validator() {
    return new LocalValidatorFactoryBean();
}

LocalValidatorFactoryBean官方示意springboot

  LocalValidatorFactoryBean是Spring應用程序上下文中javax.validation(JSR-303)設置的中心類:它引導javax.validation.ValidationFactory並經過Spring Validator接口以及JSR-303 Validator接口和ValidatorFactory公開它。界面自己。經過Spring或JSR-303 Validator接口與該bean的實例進行通訊時,您將與底層ValidatorFactory的默認Validator進行通訊。這很是方便,由於您沒必要在工廠執行另外一個調用,假設您幾乎老是會使用默認的Validator。這也能夠直接注入Validator類型的任何目標依賴項!從Spring 5.0開始,這個類須要Bean Validation 1.1+,特別支持Hibernate Validator 5.x(參見setValidationMessageSource(org.springframework.context.MessageSource))。這個類也與Bean Validation 2.0和Hibernate Validator 6.0運行時兼容,有一個特別說明:若是你想調用BV 2.0的getClockProvider()方法,經過#unwrap(ValidatorFactory.class)獲取本機ValidatorFactory,在那裏調用返回的本機引用上的getClockProvider()方法。Spring的MVC配置命名空間也使用此類,若是存在javax.validation API但未配置顯式Validator。app

@Component
public class ValidatorUtils implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ValidatorUtils.validator = (Validator) applicationContext.getBean("validator");
    }

    private static Validator validator;

    public static Optional<String> validateResultProcess(Object obj)  {
        Set<ConstraintViolation<Object>> results = validator.validate(obj);
        if (CollectionUtils.isEmpty(results)) {
            return Optional.empty();
        }
        StringBuilder sb = new StringBuilder();

        for (Iterator<ConstraintViolation<Object>> iterator = results.iterator(); iterator.hasNext(); ) {
            sb.append(iterator.next().getMessage());
            if (iterator.hasNext()) {
                sb.append(" ,");
            }
        }
        return Optional.of(sb.toString());
    }
}

  爲何要使用這個工具類呢?框架

  一、controller方法中不用加入BindingResult參數ide

  二、controller方法中須要校驗的參數也不須要加入@Valid或者@Validated註解

  怎麼樣是否是又省去了好多代碼,開不開心。

  具體使用,在controller方法或者全局攔截校驗器中調用 ValidatorUtils.validateResultProcess(須要校驗的Bean) 直接獲取校驗的結果。

  請參考更多功能的ValidatorUtils工具類

9、自定義校驗器

  定義一個MessageRequestBean,繼承BaseMessageRequestBean,signature字段須要咱們自定義校驗邏輯。

@Validated
@GroupSequence({ValidateGroup.FirstGroup.class, ValidateGroup.SecondeGroup.class, MessageRequestBean.class})
@LogicValidate(groups = ValidateGroup.SecondeGroup.class)
public class MessageRequestBean extends BaseMessageRequestBean {

    //簽名信息(除該字段外的其餘字段按照字典序排序,將值順序拼接在一塊兒,進行md5+Base64簽名算法)
    @NotBlank(message = "signature爲BLANK", groups = ValidateGroup.FirstGroup.class)
    private String signature;
    ...
}

  實現自定義校驗邏輯也很簡單......

  一、自定義一個帶有 @Constraint註解的註解@LogicValidate,validatedBy 屬性指向該註解對應的自定義校驗器

@Target({TYPE})
@Retention(RUNTIME)
//指定驗證器  
@Constraint(validatedBy = LogicValidator.class)
@Documented
public @interface LogicValidate {
    String message() default "校驗異常";
    //分組
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

  二、自定義校驗器LogicValidator,泛型要關聯上自定義的註解和須要校驗bean的類型

public class LogicValidator implements ConstraintValidator<LogicValidate, MessageRequestBean> {

    @Override
    public void initialize(LogicValidate logicValidate) {
    }

    @Override
    public boolean isValid(MessageRequestBean messageRequestBean, ConstraintValidatorContext context) {
        String toSignature = StringUtils.join( messageRequestBean.getBizType()
                , messageRequestBean.getChannelType()
                , messageRequestBean.getData()
                , messageRequestBean.getToUser());
        String signature = new Base64().encodeAsString(DigestUtils.md5(toSignature));
        if (!messageRequestBean.getSignature().equals(signature)) {
            context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate("signature校驗失敗") .addConstraintViolation(); return false;
        }
        return true;
    }
}

  能夠經過ConstraintValidatorContext禁用掉默認的校驗配置,而後自定義校驗配置,好比校驗失敗後返回的信息

10、springboot國際化信息配置

@Configuration
@ConditionalOnMissingBean(value = MessageSource.class, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "spring.messages")
public class MessageSourceAutoConfiguration {

    private static final Resource[] NO_RESOURCES = {};

    /**
     * Comma-separated list of basenames, each following the ResourceBundle convention.
     * Essentially a fully-qualified classpath location. If it doesn't contain a package
     * qualifier (such as "org.mypackage"), it will be resolved from the classpath root.
     */
    private String basename = "messages";

    /**
     * Message bundles encoding.
     */
    private Charset encoding = Charset.forName("UTF-8");

    /**
     * Loaded resource bundle files cache expiration, in seconds. When set to -1, bundles
     * are cached forever.
     */
    private int cacheSeconds = -1;

    /**
     * Set whether to fall back to the system Locale if no files for a specific Locale
     * have been found. if this is turned off, the only fallback will be the default file
     * (e.g. "messages.properties" for basename "messages").
     */
    private boolean fallbackToSystemLocale = true;

    /**
     * Set whether to always apply the MessageFormat rules, parsing even messages without
     * arguments.
     */
    private boolean alwaysUseMessageFormat = false;

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        if (StringUtils.hasText(this.basename)) {
            messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(
                    StringUtils.trimAllWhitespace(this.basename)));
        }
        if (this.encoding != null) {
            messageSource.setDefaultEncoding(this.encoding.name());
        }
        messageSource.setFallbackToSystemLocale(this.fallbackToSystemLocale);
        messageSource.setCacheSeconds(this.cacheSeconds);
        messageSource.setAlwaysUseMessageFormat(this.alwaysUseMessageFormat);
        return messageSource;
    }

    public String getBasename() {
        return this.basename;
    }

    public void setBasename(String basename) {
        this.basename = basename;
    }

    public Charset getEncoding() {
        return this.encoding;
    }

    public void setEncoding(Charset encoding) {
        this.encoding = encoding;
    }

    public int getCacheSeconds() {
        return this.cacheSeconds;
    }

    public void setCacheSeconds(int cacheSeconds) {
        this.cacheSeconds = cacheSeconds;
    }

    public boolean isFallbackToSystemLocale() {
        return this.fallbackToSystemLocale;
    }

    public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) {
        this.fallbackToSystemLocale = fallbackToSystemLocale;
    }

    public boolean isAlwaysUseMessageFormat() {
        return this.alwaysUseMessageFormat;
    }

    public void setAlwaysUseMessageFormat(boolean alwaysUseMessageFormat) {
        this.alwaysUseMessageFormat = alwaysUseMessageFormat;
    }

    protected static class ResourceBundleCondition extends SpringBootCondition {

        private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<String, ConditionOutcome>();

        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context,
                AnnotatedTypeMetadata metadata) {
            String basename = context.getEnvironment()
                    .getProperty("spring.messages.basename", "messages");
            ConditionOutcome outcome = cache.get(basename);
            if (outcome == null) {
                outcome = getMatchOutcomeForBasename(context, basename);
                cache.put(basename, outcome);
            }
            return outcome;
        }

        private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context,
                String basename) {
            ConditionMessage.Builder message = ConditionMessage
                    .forCondition("ResourceBundle");
            for (String name : StringUtils.commaDelimitedListToStringArray(
                    StringUtils.trimAllWhitespace(basename))) {
                for (Resource resource : getResources(context.getClassLoader(), name)) {
                    if (resource.exists()) {
                        return ConditionOutcome
                                .match(message.found("bundle").items(resource));
                    }
                }
            }
            return ConditionOutcome.noMatch(
                    message.didNotFind("bundle with basename " + basename).atAll());
        }

        private Resource[] getResources(ClassLoader classLoader, String name) {
            try {
                return new PathMatchingResourcePatternResolver(classLoader)
                        .getResources("classpath*:" + name + ".properties");
            }
            catch (Exception ex) {
                return NO_RESOURCES;
            }
        }

    }

}

  從上面的MessageSource自動配置能夠看出,能夠經過spring.message.basename指定要配置國際化文件位置,默認值是「message」。spring boot默認就支持國際化的,默認會去resouces目錄下尋找message.properties文件。

  這裏就不進行過多關於國際化相關信息的介紹了,確定少不了區域解析器。springboot國際化相關知識請參考:Spring Boot國際化(i18n)

相關文章
相關標籤/搜索