文章目录
- 引言
- 摘要
- 正文
- 基于 `ValidationUtils`的简单校验
- 基于自定义 `Validator`的校验
- Spring内置校验 `LocalValidatorFactoryBean`
- `HibernateValidator`校验
- 使用HibernateValidator自定义校验规则
- 总结
引言
我们在日常的软件开发过程中,尤其是WEB开发过程中,数据校验功能是一个很常见的需求,最近在工作中偶然看到一些人还在使用简单的 if-elseif-else
逻辑实现数据校验功能。实际上,在Spring框架下,或者说在JavaEE规范下,对数据校验工作已经有了最佳实践,对于简单校验工作可以通过Spring自带的数据校验工具类进行,对于复杂数据校验,更多的使用Hibernate-Validator框架进行实现,在这篇文章中,我们将对数据校验功能的方式做个总结。😶🌫️
摘要
本文主要介绍在Spring框架中如何进行数据校验工作,文章介绍了Spring自带的数据校验和基于Hibernate-Validator框架的数据校验,这些功能主要涉及 ValidationUtils
、Validator
、LocalValidatorFactoryBean
、ValidatorFactory
等几个类。除此之外,还会介绍在SpringMvc框架下对网络请求数据的校验将如何实现,自定义校验规则将如何实现等内容
关键字:Spring,SpringMVC,Hibernate,Validator,校验,网络请求,注解,框架
正文
Spring框架提供了数据校验相关的一系列功能,数据校验功能围绕 org.springframework.validation.Errors
和 org.springframework.validation.Validator
展开,其中 Errors
接口用于保存被校验对象的数据绑定和验证错误信息,Validator
接口用于执行数据校验工作。而Spring框架提供了多种校验方式,下面我们一一介绍。
注:Spring数据校验相关功能的API都在 org.springframework.validation
这个包下,本文对于该包下的类只以类名显示
基于 ValidationUtils
的简单校验
ValidationUtils
类是Spring提供的一个数据校验工具类,主要提供空值校验和空值或空格校验,也可以通过传入 Validator
实现功能更加丰富的数据校验工作。
下列代码用于校验一个Map
中的 name
属性的值是否为空或只有空格
@Component
public class ValidatorComponent implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
this.testMapValidate();
}
private void testMapValidate() {
Map<String,Object> map=new HashMap<>(8);
map.put("name"," ");
map.put("gender", Gender.MALE);
map.put("employ","wu");
map.put("age",30);
MapBindingResult mapError = new MapBindingResult(map,"zhouyu");
ValidationUtils.rejectIfEmpty(mapError,"age","age is not null","age不能为空");
ValidationUtils.rejectIfEmptyOrWhitespace(mapError,"name","name is not empty","name不能为空");
for (ObjectError error : mapError.getAllErrors()) {
String msg = error.getDefaultMessage();
System.out.println("默认消息:"+msg);
}
}
}
执行这段程序的结果为
默认消息:name不能为空
除了设置默认提示消息之外,还可以通过SpringMessage
模块设置自定义的提示消息,关于 SpringMessage
的内容不属于本文的讨论范围,可以参考这篇文章,添加本地消息后的代码
//配置本地消息
@Configuration
public class Config{
@Bean
public MessageSource validatorMessageSource(){
StaticMessageSource source = new StaticMessageSource();
source.addMessage("age is not null", Locale.getDefault(),"年龄不能为空");
source.addMessage("name is not empty",Locale.getDefault(),"姓名不能为空");
source.addMessage("person is minor",Locale.getDefault(),"禁止雇佣未成年人");
source.addMessage("this is female toilet",Locale.getDefault(),"男士止步");
return source;
}
}
@Component
public class ValidatorComponent implements ApplicationRunner {
@Resource(name = "validatorMessageSource")
private MessageSource messageSource;
@Override
public void run(ApplicationArguments args) throws Exception {
PersonEntity person = new PersonEntity();
}
private void testMapValidate() {
Map<String,Object> map=new HashMap<>(8);
map.put("name","zhouyu");
map.put("gender", Gender.MALE);
map.put("employ","wu");
map.put("age",30);
MapBindingResult mapError = new MapBindingResult(map,"zhouyu");
ValidationUtils.rejectIfEmpty(mapError,"age","age is not null","age不能为空");
ValidationUtils.rejectIfEmptyOrWhitespace(mapError,"name","name is not empty","name不能为空");
for (ObjectError error : mapError.getAllErrors()) {
String msg = error.getDefaultMessage();
System.out.println("默认消息:"+msg);
//读取本地消息
String code = error.getCode();
Object[] argArr = error.getArguments();
String message = this.messageSource.getMessage(code, argArr, Locale.getDefault());
System.out.println("本地消息:"+message);
}
}
}
打印结果
默认消息:age不能为空
本地消息:年龄不能为空
除了对应Map类型校验结果的 MapBindingResult
类,Spring中还提供了其他几种结果类,包括 BeanPropertyBindingResult
、DirectFieldBindingResult
,两者皆可用于POJO对象的校验。BeanPropertyBindingResult
的测试代码如下
private void testBeanValidate() {
PersonEntity person = PersonEntity.bornFemale("daqiao");
BeanPropertyBindingResult beanError = new BeanPropertyBindingResult(person, "person");
beanError.rejectValue("name","name is not empty");
for (ObjectError error : beanError.getAllErrors()) {
String msg = this.messageSource.getMessage(error.getCode(),error.getArguments(),Locale.getDefault());
System.out.println(msg);
}
}
执行结果如下
姓名不能为空
基于自定义 Validator
的校验
以上API只能实现简单的非空和空格校验,如果需要实现更加丰富的校验功能,可以通过实现 Validator
接口自定义校验逻辑,一个简单的校验功能如下
@Bean
public Validator validator(){
return new Validator() {
@Override
public boolean supports(Class<?> clazz) {
return PersonEntity.class.equals(clazz);
}
//实际的校验逻辑代码
@Override
public void validate(Object target, Errors errors) {
if (target instanceof PersonEntity){
PersonEntity person = (PersonEntity) target;
if(person.getAge()<18){
errors.reject("person is minor");
}
if (Gender.MALE.equals(person.getGender())){
errors.rejectValue("gender","this is female toilet");
}
}
}
};
}
测试自定义校验功能
@Component
public class ValidatorComponent implements ApplicationRunner {
//自定义validator
@Resource
private Validator validator;
//自定义消息
@Resource(name = "validatorMessageSource")
private MessageSource messageSource;
@Override
public void run(ApplicationArguments args) throws Exception {
this.customValidate();
}
private void customValidate() {
PersonEntity person = PersonEntity.bornMale("tb");
//确认validator是否支持这个对象的校验
if(this.validator.supports(person.getClass())){
BeanPropertyBindingResult beanBindResult = new BeanPropertyBindingResult(person, "tb");
//执行校验
this.validator.validate(person, beanBindResult);
for (ObjectError error : beanBindResult.getAllErrors()) {
String msg = this.messageSource.getMessage(error.getCode(), error.getArguments(), Locale.getDefault());
System.out.println(msg);
}
}
}
}
这段代码的执行结果为
禁止雇佣未成年人
男士止步
除了直接调用 Validator
类的方法,还可以通过 ValidatiionUtils
工具类,传入 Validator
实现校验,这样上例中的 customValidate()
方法可以通过如下方式优化
private void customValidate() {
PersonEntity person = PersonEntity.bornMale("tb");
BeanPropertyBindingResult beanBindResult = new BeanPropertyBindingResult(person, "tb");
ValidationUtils.invokeValidator(validator,person,beanBindResult);
//打印校验结果
beanBindResult.getAllErrors().forEach(o-> System.out.println(this.messageSource.getMessage(o.getCode(),o.getArguments(),Locale.getDefault())));
}
Spring内置校验 LocalValidatorFactoryBean
如果觉得自定义 Validator
接口比较麻烦,可以尝试使用Spring提供的 LocalValidatorFactoryBean
,这个类实现了 JSR-303(javax.validation)系列注解的支持。为方便测试,我们先修改 PersonEntity
类,添加JSR-303校验注解
public class PersonEntity {
@NotEmpty(message = "【person.name】不能为空")
private String name;
@Min(value = 1,message = "【person.age】不能小于1")
private int age;
private Gender gender;
public static PersonEntity bornMale(String name){
PersonEntity res = new PersonEntity();
res.setGender(Gender.MALE);
res.setAge(1);
res.setName(name);
return res;
}
public static PersonEntity bornFemale(String name){
PersonEntity res = new PersonEntity();
res.setGender(Gender.FEMALE);
res.setAge(1);
res.setName(name);
return res;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("PersonEntity{");
sb.append("name='").append(name).append('\'');
sb.append(", age=").append(age);
sb.append(", gender=").append(gender);
sb.append('}');
return sb.toString();
}
}
配置 Validator
@Bean
public Validator localValidator(){
return new LocalValidatorFactoryBean();
}
编辑测试类
@Component
public class ValidatorComponent implements ApplicationRunner {
//依赖注入LocalValidatorFactoryBean
@Resource
private Validator validator;
@Override
public void run(ApplicationArguments args) throws Exception {
this.localBeanValidator();
}
private void localBeanValidator() {
PersonEntity person = new PersonEntity();
person.setAge(1);
person.setGender(Gender.FEMALE);
BeanPropertyBindingResult personResult = new BeanPropertyBindingResult(person, "person");
this.validator.validate(person,personResult);
personResult.getAllErrors().forEach(o-> System.out.println(o.getDefaultMessage()));
}
}
输出结果
【person.name】不能为空
HibernateValidator
校验
相比于Spring提供的 LocalValidatorFactoryBean
,HibernateValidator
是更常用的校验框架,它提供了更加丰富的功能,以及更简单的使用方式,HibernateValidator
同样也支持 JSR-303
,同时它也是SpringBoot(spring-boot-starter-validation)中默认提供的校验框架
HibernateValidator可以直接使用,Person类还是上例中的代码
@Override
public void run(ApplicationArguments args) throws Exception {
this.hibernateValidate();
}
private static void hibernateValidate() {
PersonEntity person = PersonEntity.bornFemale("");
person.setAge(-1);
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
javax.validation.Validator valid = factory.getValidator();
Set<ConstraintViolation<PersonEntity>> res = valid.validate(person);
res.forEach(o-> System.out.println(o.getMessage()));
}
校验结果如下
【person.name】不能为空
【person.age】不能小于1
除了主动使用 Hibernate-Validator进行校验,更加常用的一种方式是集成到SpringMVC中,对Web请求发来的参数进行校验,只需要引入响应的依赖包即可实现数据校验功能
<!--spring版本和hibernate-validator版本要一一对应,在本例中使用这两个版本-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.15.RELEASE</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.5.Final</version>
</dependency>
使用 @Validated
注解校验
@Controller
@RequestMapping("/test")
public class TestDemoController {
@PostMapping("/person")
@ResponseBody
public PersonEntity testPerson(@Validated @RequestBody PersonEntity person){
return person;
}
}
配置好项目后,启动tomcat服务器,发送如下请求测试
###
POST http://localhost:8080/mybatis_spring/test/person
Content-Type: application/json
{
"age":12,"gender":"MALE"
}
发送请求后发现,服务其报错
05-Jan-2024 14:06:17.511 警告 [http-nio-8080-exec-3] org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.logException Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public top.sunyog.demo.mybatisspring.entity.PersonEntity top.sunyog.demo.mybatisspring.controller.TestDemoController.testPerson(top.sunyog.demo.mybatisspring.entity.PersonEntity): [Field error in object 'personEntity' on field 'name': rejected value [null]; codes [NotEmpty.personEntity.name,NotEmpty.name,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [personEntity.name,name]; arguments []; default message [name]]; default message [【person.name】不能为空]] ]
这时说明Hibernate-Validator已经开始参与校验工作了,为了更好的服务体验,可以捕获这个异常,然后返回给服务端正常的提示语,下面列出示例代码
@ControllerAdvice
public class ErrorHandlerController {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseBody
public String validError(MethodArgumentNotValidException e){
BindingResult result = e.getBindingResult();
StringBuffer buffer = new StringBuffer("校验错误:");
result.getAllErrors().forEach(o->buffer.append(o.getDefaultMessage()).append("; "));
return buffer.toString();
}
}
这时请求返回值变成了
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 83
Date: Fri, 05 Jan 2024 06:46:32 GMT
Keep-Alive: timeout=60
Connection: keep-alive
数据校验错误:【person.name】不能为空;
使用HibernateValidator自定义校验规则
在HibernateValidator中,除了可以使用JSR-303中规定的校验注解之外,还支持自定义注解的校验,下面以一个校验受雇人是否在合法用工年龄的功能简单介绍自定义规则校验
- 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.TYPE,ElementType.FIELD})
@Documented
//标注具体的校验类
@Constraint(validatedBy = ChildConstraint.class)
public @interface WorkingAge {
String message() default "年龄超过许可范围";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default {};
}
- 继承
ConstraintValidator
接口
@Component
public class ChildConstraint implements ConstraintValidator<WorkingAge, Integer> {
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
//年龄允许在18-60之间
return value >= 18 && value <= 60;
}
}
- 标注自定义注解
public class PersonEntity {
@NotEmpty(message = "【person.name】不能为空")
private String name;
@Min(value = 1,message = "【person.age】不能小于1")
@WorkingAge
private int age;
private Gender gender;
}
- 请求校验
@PostMapping("/valid")
public PersonEntity validPerson(@RequestBody @Validated PersonEntity person){
return person;
}
POST http://localhost:18080/test/valid
Content-Type: application/json
{
"name": "sunshangxiang",
"age": 15,
"gender": "FEMALE"
}
校验结果如下
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 47
Date: Fri, 05 Jan 2024 07:42:06 GMT
Keep-Alive: timeout=60
Connection: keep-alive
数据校验错误:年龄超过许可范围;
总结
在这篇文章中我们介绍了Spring框架中进行数据校验的方法,从简单到复杂依次介绍了
- ValidationUtils
- Validator
- LocalValidatorFactoryBean
- Hibernate-Validator
四种校验方式,其中前三种都是在 spring-context
中提供的类,第四种使用的是开发过程中非常常用的 hibernate.validator
依赖包,这个包提供了丰富的数据校验功能和扩展功能,基本满足各种各样的数据校验业务需求。
📩 联系方式
邮箱: qijilaoli@foxmail.com
掘金: 我的掘金
CSDN: 我的CSDN❗版权声明
本文为原创文章,版权归作者所有。未经许可,禁止转载。更多内容请访问我的博客首页