早期的网站,用户输入一个邮箱地址,需要将邮箱地址发送到服务端,服务端进行校验,校验成功后再给前端一个响应。自从有了 JavaScript 后,校验工作可以放在前端去执行。那么为什么还需要服务端校验呢?因为前端传来的数据不可信,前端很容易获取到后端的接口,如果有人直接调用接口,就可能会出现非法数据,因此服务端也要数据校验。
总的来说:
校验参数基本上是一个体力活,而且冗余代码繁多,也影响代码的可读性,我们需要一个比较优雅的方式来解决这个问题。Hibernate Validator 框架刚好解决了这个问题,可以很优雅地实现参数的校验,让业务代码和校验逻辑分开,不再编写重复的校验逻辑。
hibernate-validator 的优势:
hibernate-validator 的 maven 坐标:
<dependency>
<groupId>org.hibernategroupId>
<artifactId>hibernate-validatorartifactId>
<version>6.0.18.Finalversion>
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还需满足提供的条件 |
简单了解 hibernate-validator 数据校验框架之后,下面便开始着手在 SpringBoot 项目中体验一下它的使用了。
工程的目录结构如下图所示:
第一步,创建 maven 工程 hibernate-validator_demo 并配置 pom.xml 文件
<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.0modelVersion>
<groupId>com.hzzgroupId>
<artifactId>hibernate-validator_demoartifactId>
<version>1.0-SNAPSHOTversion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.2.RELEASEversion>
<relativePath/>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
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不能为空]
上述打印信息验证信息表示只有一个错误,验证了快速失败模式已经开启。