SpringBoot 项目中整合数据校验框架 hibernate-validator
1. hibernate-validator 介绍
早期的网站,用户输入一个邮箱地址,需要将邮箱地址发送到服务端,服务端进行校验,校验成功后再给前端一个响应。自从有了 JavaScript 后,校验工作可以放在前端去执行。那么为什么还需要服务端校验呢?因为前端传来的数据不可信,前端很容易获取到后端的接口,如果有人直接调用接口,就可能会出现非法数据,因此服务端也要数据校验。
总的来说:
前端校验,提高用户体验
后端校验,保证数据安全可靠
校验参数基本上是一个体力活,而且冗余代码繁多,也影响代码的可读性,我们需要一个比较优雅的方式来解决这个问题。Hibernate Validator 框架刚好解决了这个问题,可以很优雅地实现参数的校验,让业务代码和校验逻辑分开,不再编写重复的校验逻辑。
hibernate-validator 的优势:
验证逻辑与业务逻辑之间进行了分离,降低了程序耦合度
统一且规范的验证方式,无需再次编写重复的验证代码
hibernate-validator 的 maven 坐标:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.18.Final</version> </dependency>
hibernate-validator 常用注解及对应描述如下表所示:
注解 | 描述 |
@AssertTrue | 用于boolean字段,该字段只能为true |
@AssertFalse | 用于boolean字段,该字段只能为false |
@CreditCardNumber | 对信用卡号进行一个大致的验证 |
@DecimalMax | 只能小于或等于该值 |
@DecimalMin | 只能大于或等于该值 |
检查是否是一个有效的email地址 | |
@Future | 检查该字段的日期是否是属于将来的日期 |
@Length(min=,max=) | 检查所属的字段的长度是否在min和max之间,只能用于字符串 |
@Max | 该字段的值只能小于或等于该值 |
@Min | 该字段的值只能大于或等于该值 |
@NotNull | 该字段的值不能为null |
@NotBlank | 不能为空,检查时会将空格忽略 |
@NotEmpty | 不能为空,这里的空是指空字符串 |
@Pattern(regex=) | 被注释的元素必须符合指定的正则表达式 |
@URL(protocol=,host,port) | 检查是否是一个有效的URL,如果提供了protocol,host等,则该URL还需满足提供的条件 |
2. hibernate-validator 使用
简单了解 hibernate-validator 数据校验框架之后,下面便开始着手在 SpringBoot 项目中体验一下它的使用了。
工程的目录结构如下图所示:
第一步,创建 maven 工程 hibernate-validator_demo 并配置 pom.xml 文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.hzz</groupId> <artifactId>hibernate-validator_demo</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.2.RELEASE</version> <relativePath/> </parent> <dependencies> <!-- spring-boot-starter-web中已经依赖了hibernate-validator,无需单独导入依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> </project>
第二步,创建实体类 User
package com.hzz.entity; import lombok.Data; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.*; @Data public class User { @NotNull(message = "用户id不能为空") private Integer id; @NotEmpty(message = "用户名不能为空") @Length(max = 50, message = "用户名长度不能超过50") private String username; @Max(value = 80,message = "年龄最大为80") @Min(value = 18,message = "年龄最小为18") private int age; @Pattern(regexp = "[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", message = "邮箱格式不正确") private String email; }
第三步,创建 UserController
package com.hzz.entity; import lombok.Data; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.*; @Data public class User { @NotNull(message = "用户id不能为空") private Integer id; @NotEmpty(message = "用户名不能为空") @Length(max = 50, message = "用户名长度不能超过50") private String username; @Max(value = 80,message = "年龄最大为80") @Min(value = 18,message = "年龄最小为18") private int age; @Pattern(regexp = "[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", message = "邮箱格式不正确") private String email; }
第四步,创建 application.yml
server: port: 9100
第五步,创建启动类 HibernateValidatorApp,并启动项目
package com.hzz; import com.hzz.config.EnableFormValidator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EnableFormValidator public class HibernateValidatorApp { public static void main(String[] args) { SpringApplication.run(HibernateValidatorApp.class, args); } }
在浏览器中访问地址 http://localhost:9100/user/save
观察后台日志信息,打印出现如下信息
2022-07-05 14:48:03.343 WARN 37816 --- [nio-9100-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 3 errors Field error in object 'user' on field 'id': rejected value [null]; codes [NotNull.user.id,NotNull.id,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.id,id]; arguments []; default message [id]]; default message [用户id不能为空] Field error in object 'user' on field 'username': rejected value [null]; codes [NotEmpty.user.username,NotEmpty.username,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.username,username]; arguments []; default message [username]]; default message [用户名不能为空] Field error in object 'user' on field 'age': rejected value [0]; codes [Min.user.age,Min.age,Min.int,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age],18]; default message [年龄最小为18]]
为了能够在前端页面给用户展示友好的效果,做出进一步改变。
第六步,创建全局异常处理类,在前端给出友好的异常提示信息
package com.hzz.config; import org.springframework.stereotype.Controller; import org.springframework.validation.BindException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; /** * 全局异常处理 */ @ControllerAdvice(annotations = {RestController.class, Controller.class}) @ResponseBody public class ExceptionConfiguration { //异常处理的方法 @ExceptionHandler({ConstraintViolationException.class, BindException.class}) public String exceptionHandler(Exception ex, HttpServletRequest request) { ex.printStackTrace(); String msg = ""; if (ex instanceof ConstraintViolationException) { ConstraintViolationException constraintViolationException = (ConstraintViolationException) ex; ConstraintViolation<?> next = constraintViolationException.getConstraintViolations().iterator().next(); msg = next.getMessage(); } else if (ex instanceof BindException) { BindException bindException = (BindException) ex; msg = bindException.getBindingResult().getFieldError().getDefaultMessage(); } return msg; } }
重启项目并访问地址 http://localhost:9100/user/save,前端效果如下所示
控制台的打印信息如下:
org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 3 errors Field error in object 'user' on field 'username': rejected value [null]; codes [NotEmpty.user.username,NotEmpty.username,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.username,username]; arguments []; default message [username]]; default message [用户名不能为空] Field error in object 'user' on field 'id': rejected value [null]; codes [NotNull.user.id,NotNull.id,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.id,id]; arguments []; default message [id]]; default message [用户id不能为空] Field error in object 'user' on field 'age': rejected value [0]; codes [Min.user.age,Min.age,Min.int,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age],18]; default message [年龄最小为18]
通过控制台的输出可以看到,校验框架将我们的多个属性都进行了数据校验(默认行为),这并不好。我们希望只要有一个属性校验失败就直接返回提示信息,后面的属性不再进行校验了,相当于 “短路” 一样,因此作出进一步改进。
第七步,创建 ValidatorConfiguration 类,指定校验时使用快速失败返回模式,只要出现一个数据校验不通过,后面跳过校验,快速返回异常信息
package com.hzz.config; import org.hibernate.validator.HibernateValidator; import org.springframework.context.annotation.Bean; import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; 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; } }
值得注意的是,上面创建的类并不是配置类(没有@Configuration注解修饰),所以到目前为止快速失败返回模式并不会生效。而且,现在的需求是我可以根据需要选择是否开启快速失败模式,而不是一个全局配置类为全局都开启这样的模式。因此,为了达到这样的效果,需要创建一个注解用于控制此模式的开启。
第八步,创建注解 @EnableFormValidator 用于控制快速失败返回模式的开启
package com.hzz.config; import org.springframework.context.annotation.Import; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 在启动类上添加该注解来启动表单验证功能---快速失败返回模式 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(ValidatorConfiguration.class) public @interface EnableFormValidator { }
第九步,在启动类上加入 @EnableFormValidator 注解,开启快速失败返回模式
package com.hzz; import com.hzz.config.EnableFormValidator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EnableFormValidator public class HibernateValidatorApp { public static void main(String[] args) { SpringApplication.run(HibernateValidatorApp.class, args); } }
第十步,重新启动项目,访问地址 http://localhost:9100/user/save,观察控制台打印信息
org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors Field error in object 'user' on field 'id': rejected value [null]; codes [NotNull.user.id,NotNull.id,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.id,id]; arguments []; default message [id]]; default message [用户id不能为空]
上述打印信息验证信息表示只有一个错误,验证了快速失败模式已经开启。