zoukankan      html  css  js  c++  java
  • 高效使用hibernate-validator校验框架

    一、前言

      高效、合理的使用hibernate-validator校验框架可以提高程序的可读性,以及减少不必要的代码逻辑。接下来会介绍一下常用一些使用方式。

    二、常用注解说明

    限制 说明
    @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格式

    三、定义校验分组

    public class ValidateGroup {
        public interface FirstGroup {
        }
    
        public interface SecondeGroup {
        }
    
        public interface ThirdGroup {
        }
    }

    四、定义校验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区别

    五、validator基本使用

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

      这种使用方式有一个弊端,不能自定义返回异常。spring如果验证失败,则直接抛出异常,一般不可控。

    六、借助BindingResult

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

      如果方法中有BindingResult类型的参数,spring校验完成之后会将校验结果传给这个参数。通过BindingResult控制程序抛出自定义类型的异常或者返回不同结果。

    七、全局拦截校验器

      当然了,需要在借助BindingResult的前提下...

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

      这种方式可以减少controller层校验的代码,校验逻辑统一处理,更高效。

     八、借助ValidatorUtils工具类

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

    LocalValidatorFactoryBean官方示意

      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。

    @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());
        }
    }

      为什么要使用这个工具类呢?

      1、controller方法中不用加入BindingResult参数

      2、controller方法中需要校验的参数也不需要加入@Valid或者@Validated注解

      怎么样是不是又省去了好多代码,开不开心。

      具体使用,在controller方法或者全局拦截校验器中调用 ValidatorUtils.validateResultProcess(需要校验的Bean) 直接获取校验的结果。

      请参考更多功能的ValidatorUtils工具类

    九、自定义校验器

      定义一个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;
        ...
    }

      实现自定义校验逻辑也很简单......

      1、自定义一个带有 @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 {};
    }

      2、自定义校验器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禁用掉默认的校验配置,然后自定义校验配置,比如校验失败后返回的信息

    十、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)

  • 相关阅读:
    jquery--blur()事件,在页面加载时自动获取焦点
    jquery三级联动
    工具集
    兼容各个浏览器:禁止鼠标选择文字事件
    jquery 事件委托(利用冒泡)
    小功能1:多种方法实现网页加载进度条
    JavaSE| 泛型
    SSM整合
    Redis数据库 02事务| 持久化| 主从复制| 集群
    Hadoop| MapperReduce02 框架原理
  • 原文地址:https://www.cnblogs.com/hujunzheng/p/9952563.html
Copyright © 2011-2022 走看看