Spring Boot与Spring Mvc集成Hibernate Validator参数校验

753 阅读7分钟

Spring Boot与Spring Mvc集成Hibernate Validator参数校验

Hibernate Validator概述

Hibernate Validator框架可以很优雅的方式实现参数的校验,让业务代码和校验逻辑分开,不再编写重复的校验逻辑。

hibernate-validator的maven坐标

<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.2.4.Final</version>
</dependency>

Hibernate Validator常用注解

hibernate-validator提供的校验方式为在类的属性上加入相应的注解来达到校验的目的。所有的注解都位于javax.validation.constraints和org.hibernate.validator.constraints包中。

注解说明
@Null只能为null
@NotNull必须不为null
@Digits(integer,fraction)限制必须为一个小数,且整数部分的位数不能超过integer, 小数部分的位数不能超过fraction
@Size(max,min)限制字符长度必须在min到max之间
@SafeHtml字符串是安全的html
@AssertTrue用于boolean字段,该字段只能为true
@AssertFalse用于boolean字段,该字段只能为false
@CreditCardNumber对信用卡号进行一个大致的验证
@DecimalMax只能小于或等于该值
@DecimalMin只能大于或等于该值
@Email检查是否是一个有效的email地址
@Future检查该字段的日期是否是属于将来的日期
@Past限制必须是一个过去的日期
@Length(min=,max=)检查所属的字段的长度是否在min和max之间,只能用于字符串
@Max该字段的值只能小于或等于该值
@Min该字段的值只能大于或等于该值
@NotNull不能为null
@NotBlank不能为空,检查时会将空格忽略
@NotEmpty不能为空,这里的空是指空字符串
@Pattern(regex=)被注释的元素必须符合指定的正则表达式
@URL(protocol=,host,port)检查是否是一个有效的URL,如果提供了protocol,host等,则该URL还需满足提供的条件

使用Validator参数校验

Spring Boot集成Hibernate Validator

添加依赖

 <dependencies>
        <!--spring-boot-starter-web中已经依赖了hibernate-validator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- spring-boot-starter-web会通过maven的依赖传递特性传递了hibernate-validator的maven坐标
		<dependency>
              <groupId>org.hibernate</groupId>
              <artifactId>hibernate-validator</artifactId>
              <version>5.2.4.Final</version>
        </dependency>
		-->
    </dependencies>

简单数据类型校验

在类上使用@Validated注解标注

name参数使用@NotBlank标注,表示不能为空,提示信息为{required}占位符里的内容

email参数使用@Email注解标注,表示必须为一个合法的邮箱值(可以为空),提示信息为{invalid}占位符里的内容

name参数使用@Size(max,min)标注,表示限制字符长度必须在min到max之间,提示信息为{required}占位符里的内容
@RestController
@Validated //开启校验功能
public class TestController {

    @GetMapping("test")
    public String test(@NotBlank(message = "不能为空") String username, @Size(min = 6,max = 15) String password, @Email(message = "{user.email}") String email) {
        return "success";
    }
}

定义占位符的内容

message属性用于添加国际化支持,创建名为ValidationMessages.properties的文件(默认名称,不区分大小写,放在src/main/resources路径下)

ValidationMessages.properties文件中每条信息的key值对应于注解中message属性占位符的值。

在resources目录下新建ValidationMessages.properties文件

user.email=\u683c\u5f0f\u4e0d\u5408\u6cd5

内容为中文转Unicode后的值,可以使用http://tool.chinaz.com/tools/unicode.aspx网站转换

测试简单参数校验

访问http://localhost:8888/test?username=&password=123&email=123

参数校验不通过时,会抛出javax.validation.ConstraintViolationException

javax.validation.ConstraintViolationException: test.username: 不能为空, test.password: 个数必须在615之间, test.email: 格式不合法
	at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116) ~[spring-context-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.12.RELEASE.jar:5.2.12.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) ~[spring-aop-5.2.12.RELEASE.jar:5.2.12.RELEASE]

对象属性校验

新建User类

public class User {

    @NotBlank(message = "不能为空")
    private String username;

    @Size(min = 6,max = 15)
    private String password;

    @Email(message = "{user.email}")
    private String email;
    
}

使用实体对象传参的方式参数校验需要在相应的参数前加上@Valid注解

@RestController
@Validated
public class TestController {

    @GetMapping("test")
    public String test((@Valid User user) {
        return "success";
    }
}

参数校验不通过时,会抛出org.springframework.validation.BindException异常

Field error in object 'user' on field 'password': rejected value [123]; codes [Size.user.password,Size.password,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.password,password]; arguments []; default message [password],15,6]; default message [个数必须在615之间]
Field error in object 'user' on field 'username': rejected value []; codes [NotBlank.user.username,NotBlank.username,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.username,username]; arguments []; default message [username]]; default message [不能为空]
Field error in object 'user' on field 'email': rejected value [123]; codes [Email.user.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@10f84cd,.*]; default message [格式不合法]]

异常处理

使用全局异常捕获来处理这种异常

@RestControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {

    /**
     * 方法参数校验异常拦截处理
     *
     * @param e
     * @return
     */
    @ExceptionHandler(value = ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleConstraintViolationException(ConstraintViolationException e) {
        StringBuilder message = new StringBuilder();
        Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
        for (ConstraintViolation<?> violation : violations) {
            Path path = violation.getPropertyPath();
            String[] split = path.toString().split("\\.");
            message.append(split[1]).append(violation.getMessage()).append(",");
        }
        message = new StringBuilder(message.substring(0, message.length() - 1));
        return message.toString();
    }

    /**
     * 实体对象传参校验异常拦截处理
     *
     * @param e
     * @return
     */
    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String validExceptionHandler(BindException e) {
        StringBuilder message = new StringBuilder();
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        for (FieldError error : fieldErrors) {
            message.append(error.getField()).append(error.getDefaultMessage()).append(",");
        }
        message = new StringBuilder(message.substring(0, message.length() - 1));
        return message.toString();

    }
}

在这里插入图片描述

快速失败返回模式

校验框架会对多个属性都进行数据校验(默认行为),如果希望只要有一个属性校验失败就直接返回提示信息,后面的属性不再进行校验,这是就需要指定校验时使用快速失败返回模式

定义校验时使用快速失败返回模式

创建ValidatorConfiguration类,指定校验时使用快速失败返回模式

public class ValidatorConfiguration {
    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = 
            Validation.byProvider(HibernateValidator.class)
                .configure()
                //快速失败返回模式
                .addProperty("hibernate.validator.fail_fast", "true")
                .buildValidatorFactory();
        return validatorFactory.getValidator();
    }

    /**
     * 开启快速返回
     * 如果参数校验有异常,直接抛异常,不会进入到controller,使用全局异常拦截进行拦截
     */
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
        //设置validator模式为快速失败返回
        postProcessor.setValidator(validator());
        return postProcessor;
    }
}

定义控制快速失败返回模式注解

创建注解EnableFormValidator用于控制快速失败返回模式的开启

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ValidatorConfiguration.class)
public @interface EnableFormValidator {

}

开启快速失败返回模式

在启动类上加入EnableFormValidator注解,开启快速失败返回模式

@SpringBootApplication
@EnableFormValidator
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App .class,args);
    }
}

测试快速失败返回模式

当输入的数据有多个都不符合校验规则,但是只有一个校验失败异常信息,说明已经开启了快速失败返回模式。

Spring Mvc集成Hibernate Validator

添加依赖

		<dependency>
              <groupId>org.hibernate</groupId>
              <artifactId>hibernate-validator</artifactId>
              <version>5.2.4.Final</version>
        </dependency>

配置spring-mvc.xml


	<!-- Bean级别的校验,方法中的参数Bean必须添加@Valid注解,后面紧跟着BindingResult result参数-->
	<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
		<property name="providerClass" value="org.hibernate.validator.HibernateValidator" />
	</bean>
	<!--    方法级别的校验,校验方法所在类必须添加@Validated注解-->
	<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
		<!-- 可以引用自己的 validator 配置,在本文中(下面)可以找到 validator 的参考配置,如果不指定则系统使用默认的 -->
		<property name="validator" ref="validator" />
	</bean>

使用校验注解

    /**
     * 详细地址
     */
    @Size(max = 200,message = "详细地址不可超过200个字符")
    private String address;
    /**
     * 事由
     */
    @Size(max = 350,message = "事由不可超过350个字符")
    private String reason;
    /**
     * 处理结果
     */
    @Size(max = 350,message = "处理结果不可超过350个字符")
    private String results;
}
@GetMapping("/getDamageRegistrationList")
public HashMap<String,Object> getDamageRegistrationList(@NotBlank(message = "车牌号不能为空") String carNumber){
    
    }

配置全局异常处理

/**
 * 全局参数校验异常处理
 */
@RestControllerAdvice
@Log4j
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalViolationException {

    /**
     * 方法参数校验异常拦截处理
     *
     * @param e
     * @return
     */
    @ExceptionHandler(value = ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.OK)
    public ModelAndView handleConstraintViolationException(ConstraintViolationException e) {
        String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(","));

        ModelAndView model = new ModelAndView();
        Map<String, Object> attributes = new HashMap<String, Object>();
        FastJsonJsonView view = new FastJsonJsonView();

        log.error("=======GlobalViolationException start=======");
        log.error("方法参数校验异常拦截处理:" + message);
        attributes.put("global", true);
        attributes.put("code", 0);
        attributes.put("dialog", false);
        attributes.put("msg", message);
        view.setAttributesMap(attributes);
        model.setView(view);
        log.error("=======GlobalViolationException end=======");
        return model;
    }

    /**
     * 实体对象传参校验异常拦截处理
     *
     * @param e
     * @return
     */
    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.OK)
    public ModelAndView validExceptionHandler(BindException e) {
        String message = e.getBindingResult().getFieldErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(","));

        ModelAndView model = new ModelAndView();
        Map<String, Object> attributes = new HashMap<String, Object>();
        FastJsonJsonView view = new FastJsonJsonView();

        log.error("=======GlobalViolationException start=======");
        log.error("对象参校验异常拦截处理:" + message);
        attributes.put("global", true);
        attributes.put("code", 0);
        attributes.put("dialog", false);
        attributes.put("msg", message);
        view.setAttributesMap(attributes);
        model.setView(view);
        log.error("=======GlobalViolationException end=======");
        return model;
    }
}

执行测试

code: 0
dialog: false
global: true
msg: "详细地址不可超过200个字符,处理结果不可超过350个字符"