Spring中的数据校验

在这里插入图片描述

文章目录

  • 引言
  • 摘要
  • 正文
    • 基于 `ValidationUtils`的简单校验
    • 基于自定义 `Validator`的校验
    • Spring内置校验 `LocalValidatorFactoryBean`
    • `HibernateValidator`校验
      • 使用HibernateValidator自定义校验规则
  • 总结

引言

我们在日常的软件开发过程中,尤其是WEB开发过程中,数据校验功能是一个很常见的需求,最近在工作中偶然看到一些人还在使用简单的 if-elseif-else逻辑实现数据校验功能。实际上,在Spring框架下,或者说在JavaEE规范下,对数据校验工作已经有了最佳实践,对于简单校验工作可以通过Spring自带的数据校验工具类进行,对于复杂数据校验,更多的使用Hibernate-Validator框架进行实现,在这篇文章中,我们将对数据校验功能的方式做个总结。😶‍🌫️

摘要

本文主要介绍在Spring框架中如何进行数据校验工作,文章介绍了Spring自带的数据校验和基于Hibernate-Validator框架的数据校验,这些功能主要涉及 ValidationUtilsValidatorLocalValidatorFactoryBeanValidatorFactory等几个类。除此之外,还会介绍在SpringMvc框架下对网络请求数据的校验将如何实现,自定义校验规则将如何实现等内容

关键字:Spring,SpringMVC,Hibernate,Validator,校验,网络请求,注解,框架

正文

Spring框架提供了数据校验相关的一系列功能,数据校验功能围绕 org.springframework.validation.Errorsorg.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中还提供了其他几种结果类,包括 BeanPropertyBindingResultDirectFieldBindingResult,两者皆可用于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提供的 LocalValidatorFactoryBeanHibernateValidator是更常用的校验框架,它提供了更加丰富的功能,以及更简单的使用方式,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中规定的校验注解之外,还支持自定义注解的校验,下面以一个校验受雇人是否在合法用工年龄的功能简单介绍自定义规则校验

  1. 自定义注解
@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 {};
}
  1. 继承 ConstraintValidator接口
@Component
public class ChildConstraint implements ConstraintValidator<WorkingAge, Integer> {
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        //年龄允许在18-60之间
        return value >= 18 && value <= 60;
    }
}
  1. 标注自定义注解
public class PersonEntity {
    @NotEmpty(message = "【person.name】不能为空")
    private String name;
    @Min(value = 1,message = "【person.age】不能小于1")
    @WorkingAge
    private int age;
    private Gender gender;
}
  1. 请求校验
@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框架中进行数据校验的方法,从简单到复杂依次介绍了

  1. ValidationUtils
  2. Validator
  3. LocalValidatorFactoryBean
  4. Hibernate-Validator

四种校验方式,其中前三种都是在 spring-context中提供的类,第四种使用的是开发过程中非常常用的 hibernate.validator依赖包,这个包提供了丰富的数据校验功能和扩展功能,基本满足各种各样的数据校验业务需求。


📩 联系方式
邮箱: qijilaoli@foxmail.com
掘金: 我的掘金
CSDN: 我的CSDN

❗版权声明
本文为原创文章,版权归作者所有。未经许可,禁止转载。更多内容请访问我的博客首页

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/295398.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vr眼镜和AR眼镜的区别有哪些?哪些产品可以支持VR应用?

vr眼镜怎么连接手机 要将VR眼镜连接到手机上&#xff0c;您可以按照以下步骤进行&#xff1a; 1. 确保您的手机支持VR应用程序&#xff1a;首先&#xff0c;确保您的手机具备运行VR应用程序的硬件和软件条件。一些VR应用程序可能对设备有特定的要求&#xff0c;如处理器性能、操…

Dart调用JS对10000条定位数据滤波

使用Dart调用JS&#xff0c;还是为了练习跨语言调用&#xff1b; 一、编写对应的JS代码 平时在开发时不推荐将算法放在JS里&#xff0c;我这里是简单的做一下数据过滤&#xff1b; 首先生成一些随机定位数据&#xff0c;在实际开发中可以使用真实数据&#xff1b; // 随机定…

【DevOps-06】Jenkins实现CI/CD操作

一、简要说明 基于Jenkins拉取GitLab的SpringBoot代码进行构建发布到测试环境实现持续集成 基于Jenkins拉取GitLab指定发行版本的SpringBoot代码进行构建发布到生产环境实现CD实现持续部署 二、准备Springboot工程 1、IDEA新建工程 2、填写项目工程信息 3、选择Springboot版本…

从查询到高质量回答:发挥 RAG 和 Rerankers 的潜力

每日推荐一篇专注于解决实际问题的外文&#xff0c;精准翻译并深入解读其要点&#xff0c;助力读者培养实际问题解决和代码动手的能力。 欢迎关注公众号 原文标题&#xff1a;From Queries to Quality Answers: Harnessing the Potentials of RAG and Rerankers 原文地址&…

微信小程序如何搜索iBeacon设备

1.首先在utils文件夹下创建bluetooth.js和ibeacon.js 2.在 bluetooth.js文件中写入 module.exports {initBluetooth: function () {// 初始化蓝牙模块wx.openBluetoothAdapter({success: function (res) {console.log(蓝牙模块初始化成功);},fail: function (res) {console.l…

citeSpace保姆级安装使用教程

citeSpace保姆级安装使用教程 文章目录 citeSpace保姆级安装使用教程CiteSpace功能与参数区安装使用知网数据导出citespace数据导入结果 设置操作隐藏节点 CiteSpace功能与参数区 安装 citeSpace安装教程 citespace下载 网址&#xff1a;https://citespace.podia.com/ 安装之…

Nginx 的 gzip 压缩

目录 1. 为什么要开启gzip 压缩 2.对网站配置文件进行修改 1. 为什么要开启gzip 压缩 nginx使用gzip压缩主要是为了降低网站的带宽消耗和提升访问速度。通过对页面进行压缩&#xff0c;可以减少传输的数据量&#xff0c;从而减少网络传输的时间和带宽消耗。 当浏览器接收到压…

视频剪辑指南:如何将多个视频快速批量合并的方法

在日常生活和工作中&#xff0c;经常要将多个视频片段合并为一个完整的视频。但是手动剪辑每个视频不仅费时&#xff0c;而且效率低下。那么如何解决这个问题呢&#xff0c;可以采用一些快速批量合并视频的方法。现在一起来看看云炫AI智剪如何批量合并视频的具体步骤吧。 合并…

天津大数据培训机构 大数据时代已到来!

大数据时代已经来临&#xff0c;越来越多的人开始关注大数据&#xff0c;并且准备转行大数据。但是&#xff0c;对于一个外行人或者小白来说&#xff0c;大数据是什么&#xff1f;大数据需要学什么&#xff1f;什么样的大数据培训机构是靠谱的&#xff1f;这几个简单的问题就足…

数据库 补充 树,红黑树,b树,b+树

01.树 02.二叉树和二叉平衡树 03.平衡二叉树的恢复 将导致不平衡的结点称作被破坏者&#xff0c;破坏了结点的平衡的结点成为破坏者&#xff0c;经过调整可以让该树平衡的结点称为调整结点。 LL型&#xff1a; 以被破坏者的左孩子结点作为调整结点&#xff0c;对其进行右旋…

【书生·浦语大模型实战营01】《书生·浦语大模型全链路开源体系》

《书生浦语大模型全链路开源体系》 1. 大模型成为热门关键词 LLM发展时间轴 2. 书生浦语全链条开源开放体系 微调&#xff1a;XTuner 支持全参数微调支持LoRA等低成本微调 评测&#xff1a;OpenCompass 全方位评测&#xff0c;性能可复现80套评测集&#xff0c; 40万道题目…

网工内推 | 保险业网工,有绩效奖金,CISP认证优先,最高16K

01 华贵人寿保险股份有限公司 招聘岗位&#xff1a;系统管理岗&#xff08;主机管理方向&#xff09; 职责描述&#xff1a; 1.负责数据中心私有云平台的规划建设以及后期的运行维护&#xff1b; 2.负责公司操作系统的规划、部署与日常维护&#xff1b; 3.负责操作系统运维相关…

idea设置注释在鼠标当前位置,使其不从顶格位置添加注释

idea设置注释在鼠标当前位置&#xff0c;使其不从顶格位置添加注释 默认情况下&#xff0c;注释都是从改行的顶格开始&#xff0c;看起来不太美观而且不易清除分级 设置让其从代码处开始&#xff0c;步骤&#xff1a;File–>Sttings–>Editor–>Code Style &#xff…

在 IDEA 中创建JavaWeb 项目的方式(超详细步骤教程和遇到的问题)

目录 0-1项目图片操作步骤链接0-2项目结构预览1.新建Project2.定义项目名称3.创建完成后项目结构4.创建config文件夹4.1 作用 5.在WEB-INF下创建lib文件夹5.1然后搞几个常用的jar包放入5.1.1jar包全选中后右键选择放入类库5.1.2jar包全选中后右键选择放入类库 6.创建src下文件夹…

【Java并发】深入浅出 synchronized关键词原理-下

上一篇文章&#xff0c;简要介绍了syn的基本用法和monter对象的结构&#xff0c;本篇主要深入理解&#xff0c;偏向锁、轻量级锁、重量级锁的本质。 对象内存布局 Hotspot虚拟机中&#xff0c;对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据 (Instance Da…

Baumer工业相机堡盟工业相机如何联合NEOAPI SDK和OpenCV实现相机图像转换为Mat图像格式(C++)

Baumer工业相机堡盟工业相机如何通过NEOAPI SDK实现相机掉线自动重连&#xff08;C&#xff09; Baumer工业相机Baumer工业相机的图像转换为OpenCV的Mat图像的技术背景在NEOAPI SDK里实现相机图像转换为Mat图像格式联合OpenCV实现相机图像转换为Mat图像格式测试演示图 工业相机…

页面自适应postcss-pxtorem配置

一、项目背景 uniappvue3tsvitepiniavant 二、安装postcss-pxtorem npm i postcss-pxtorem三、vite.config.ts配置 import pxtorem from "postcss-pxtorem";export default defineConfig({plugins: [uni(),Components({resolvers: [VantResolver()]})],css: {pos…

【Java集合篇】HashMap 在 get 和 put 时经过哪些步骤

HashMap在get和put时经过哪些步骤? ✔️ 典型解析✔️get方法✔️put方法✔️ 拓展知识仓✔️ HashMap如何定位key✔️ HashMap定位tablelndex的骚操作作✔️HashMap的key为null时&#xff0c;没有hashCode是如何存储的?✔️ HashMap的value可以为null吗? 有什么优缺点讷? …

ZkSync第一Dex空投交互全教程,Holdstation ZK热点不容错过

2023 年 12 月 8 日&#xff0c;在以太坊基金会的 176 次会议上&#xff0c;开发人员一致同意&#xff0c;如果事情进展顺利&#xff0c;将在 2024 年初定 Goerli 分叉日期&#xff0c;目标是能在 2024 年 1 月激活 Goerli Dencun 测试网&#xff0c;预计能够在 2024 年 3 月~ …

SOFA Framework源代码及插件Win11编译开发环境配置

这篇文章主要记录详细的SOFA Framework软件的源代码编译环境配置过程&#xff0c;开发环境基于Win系统&#xff0c;编译完成后&#xff0c;可以在插件或框架的源代码上进行开发集成。本文纯手写输入&#xff0c;言简意赅&#xff0c;以大方向和思路为准&#xff0c;具体需要注意…