浅谈自定义校验注解ConstraintValidator


Posted in Java/Android onJune 30, 2021
目录
  • 一、前言
  • 二、自定义参数校验器
  • 三、使用自定义注解

一、前言

系统执行业务逻辑之前,会对输入数据进行校验,检测数据是否有效合法的。所以我们可能会写大量的if else等判断逻辑,特别是在不同方法出现相同的数据时,校验的逻辑代码会反复出现,导致代码冗余,阅读性和可维护性极差。

JSR-303是Java为Bean数据合法性校验提供的标准框架,它定义了一整套校验注解,可以标注在成员变量,属性方法等之上。

hibernate-validator就提供了这套标准的实现,我们在用Springboot开发web应用时,会引入spring-boot-starter-web依赖,它默认会引入spring-boot-starter-validation依赖,而spring-boot-starter-validation中就引用了hibernate-validator依赖。

浅谈自定义校验注解ConstraintValidator但是,在比较高版本的spring-boot-starter-web中,默认不再引用spring-boot-starter-validation,自然也就不会默认引入到hibernate-validator依赖,需要我们手动添加依赖。

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

hibernate-validator中有很多非常简单好用的校验注解,例如NotNull,@NotEmpty,@Min,@Max,@Email,@PositiveOrZero等等。这些注解能解决我们大部分的数据校验问题。如下所示:

package com.nobody.dto;

import lombok.Data;

import javax.validation.constraints.*;

@Data
public class UserDTO {

    @NotBlank(message = "姓名不能为空")
    private String name;

    @Min(value = 18, message = "年龄不能小于18")
    private int age;

	@NotEmpty(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
}

二、自定义参数校验器

但是,hibernate-validator中的这些注解不一定能满足我们全部的需求,我们想校验的逻辑比这复杂。所以,我们可以自定义自己的参数校验器。

首先引入依赖是必不可少的。

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

最近不是基金很火吗,一大批的韭菜疯狂地涌入买基金的浪潮中。我就以用户开户为例,首先要校验此用户是不是成年人(即不能小于18岁),以及名字是不是以"新韭菜"开头的,符合条件的才允许开户。

定义一个注解,用于校验用户的姓名是不是以“新韭菜”开头的。

package com.nobody.annotation;

import com.nobody.validator.IsLeekValidator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Constraint(validatedBy = IsLeekValidator.class) // 指定我们自定义的校验类
public @interface IsLeek {

    /**
     * 是否强制校验
     * 
     * @return 是否强制校验的boolean值
     */
    boolean required() default true;

    /**
     * 校验不通过时的报错信息
     * 
     * @return 校验不通过时的报错信息
     */
    String message() default "此用户不是韭零后,无法开户!";

    /**
     * 将validator进行分类,不同的类group中会执行不同的validator操作
     * 
     * @return validator的分类类型
     */
    Class<?>[] groups() default {};

    /**
     * 主要是针对bean,很少使用
     * 
     * @return 负载
     */
    Class<? extends Payload>[] payload() default {};

}

定义校验类,实现ConstraintValidator接口,接口使用了泛型,需要指定两个参数,第一个是自定义注解,第二个是需要校验的数据类型。重写2个方法,initialize方法主要做一些初始化操作,它的参数是我们使用到的注解,可以获取到运行时的注解信息。isValid方法就是要实现的校验逻辑,被注解的对象会传入此方法中。

package com.nobody.validator;

import com.nobody.annotation.IsLeek;
import org.springframework.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class IsLeekValidator implements ConstraintValidator<IsLeek, String> {

    // 是否强制校验
    private boolean required;

    @Override
    public void initialize(IsLeek constraintAnnotation) {
        this.required = constraintAnnotation.required();
    }

    @Override
    public boolean isValid(String name, ConstraintValidatorContext constraintValidatorContext) {
        if (required) {
            // 名字以"新韭菜"开头的则校验通过
            return !StringUtils.isEmpty(name) && name.startsWith("新韭菜");
        }
        return false;
    }
}

三、使用自定义注解

通过以上几个步骤,我们自定义的校验注解就完成了,我们使用测试下效果。

package com.nobody.dto;

import com.nobody.annotation.IsLeek;
import lombok.Data;

import javax.validation.constraints.*;

@Data
public class UserDTO {

    @NotBlank(message = "姓名不能为空")
    @IsLeek // 我们自定义的注解
    private String name;

    @Min(value = 18, message = "年龄不能小于18")
    private int age;

    @NotEmpty(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
}

写个接口,模拟用户开户业务,调用测试。注意,记得加上@Valid注解开启校验,不然不生效。

package com.nobody.controller;

import com.nobody.dto.UserDTO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
@RequestMapping("user")
public class UserController {

    @PostMapping("add")
    public UserDTO add(@RequestBody @Valid UserDTO userDTO) {
        System.out.println(">>> 用户开户成功...");
        return userDTO;
    }

}

如果参数校验不通过,会抛出MethodArgumentNotValidException异常,我们全局处理下然后返回给接口。

package com.nobody.exception;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import lombok.extern.slf4j.Slf4j;

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    // 处理接口参数数据格式错误异常
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    @ResponseBody
    public Object errorHandler(HttpServletRequest request, MethodArgumentNotValidException e) {
        return e.getBindingResult().getAllErrors();
    }
}

我们先测试用户姓名不带"新韭菜"前缀的进行测试,发现校验不通过,证明注解生效了。

POST http://localhost:8080/user/add

Content-Type: application/json

 

{"name": "小绿", "age": 19, "email": "845136542@qq.com"}

[
  {
    "codes": [
      "IsLeek.userDTO.name",
      "IsLeek.name",
      "IsLeek.java.lang.String",
      "IsLeek"
    ],
    "arguments": [
      {
        "codes": [
          "userDTO.name",
          "name"
        ],
        "arguments": null,
        "defaultMessage": "name",
        "code": "name"
      },
      true
    ],
    "defaultMessage": "此用户不是韭零后,无法开户!",
    "objectName": "userDTO",
    "field": "name",
    "rejectedValue": "小绿",
    "bindingFailure": false,
    "code": "IsLeek"
  }

如果多个参数校验失败,报错信息也都能获得。如下所示,姓名和邮箱都校验失败。

POST http://localhost:8080/user/add

Content-Type: application/json

 

{"name": "小绿", "age": 19, "email": "84513654"}

[
  {
    "codes": [
      "Email.userDTO.email",
      "Email.email",
      "Email.java.lang.String",
      "Email"
    ],
    "arguments": [
      {
        "codes": [
          "userDTO.email",
          "email"
        ],
        "arguments": null,
        "defaultMessage": "email",
        "code": "email"
      },
      [],
      {
        "defaultMessage": ".*",
        "codes": [
          ".*"
        ],
        "arguments": null
      }
    ],
    "defaultMessage": "邮箱格式不正确",
    "objectName": "userDTO",
    "field": "email",
    "rejectedValue": "84513654",
    "bindingFailure": false,
    "code": "Email"
  },
  {
    "codes": [
      "IsLeek.userDTO.name",
      "IsLeek.name",
      "IsLeek.java.lang.String",
      "IsLeek"
    ],
    "arguments": [
      {
        "codes": [
          "userDTO.name",
          "name"
        ],
        "arguments": null,
        "defaultMessage": "name",
        "code": "name"
      },
      true
    ],
    "defaultMessage": "此用户不是韭零后,无法开户!",
    "objectName": "userDTO",
    "field": "name",
    "rejectedValue": "小绿",
    "bindingFailure": false,
    "code": "IsLeek"
  }
]

以下是所有参数校验通过的情况:

POST http://localhost:8080/user/add

Content-Type: application/json

 

{"name": "新韭菜小绿", "age": 19, "email": "84513654@qq.com"}

{

  "name": "新韭菜小绿",

  "age": 19,

  "email": "84513654@qq.com"

}

我们可能会将UserDTO对象用在不同的接口中接收参数,比如在新增和修改接口中。在新增接口中,不需要校验userId;在修改接口中需要校验userId。那注解中的groups字段就派上用场了。groups和@Validated配合能控制哪些注解需不需要开启校验。

我们首先定义2个groups分组接口Update和Create,并且继承Default接口。当然也可以不继承Default接口,因为使用注解时不显示指定groups的值,则默认为groups = {Default.class}。所以继承了Default接口,在用@Validated(Create.class)时,也会校验groups = {Default.class}的注解。

package com.nobody.annotation;

import javax.validation.groups.Default;

public interface Create extends Default {
}
package com.nobody.annotation;

import javax.validation.groups.Default;

public interface Update extends Default {
}

在用到注解的地方,填写groups的值。

package com.nobody.dto;

import com.nobody.annotation.Create;
import com.nobody.annotation.IsLeek;
import com.nobody.annotation.Update;
import lombok.Data;

import javax.validation.constraints.*;

@Data
public class UserDTO {

    @NotBlank(message = "用户ID不能为空", groups = Update.class)
    private String userId;

    @NotBlank(message = "姓名不能为空", groups = {Update.class, Create.class})
    @IsLeek
    private String name;

    @Min(value = 18, message = "年龄不能小于18")
    private int age;

    @NotEmpty(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;
}

最后,在需要声明校验的地方,通过@Validated的指定即可。

package com.nobody.controller;

import com.nobody.annotation.Create;
import com.nobody.annotation.Update;
import com.nobody.dto.UserDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("user")
public class UserController {

    @PostMapping("add")
    public Object add(@RequestBody @Validated(Create.class) UserDTO userDTO) {
        System.out.println(">>> 用户开户成功...");
        return userDTO;
    }

    @PostMapping("update")
    public Object update(@RequestBody @Validated(Update.class) UserDTO userDTO) {
        System.out.println(">>> 用户信息修改成功...");
        return userDTO;
    }

}

调用add接口时,即使不传userId也能通过,即不对userId进行校验。

POST http://localhost:8080/user/add

Content-Type: application/json

 

{"name": "新韭菜小绿", "age": 18, "email": "84513654@qq.com"}

调用update接口时,不传userId,会校验不通过。

POST http://localhost:8080/user/update

Content-Type: application/json

 

{"name": "新韭菜小绿", "age": 18, "email": "84513654@qq.com"}

[
  {
    "codes": [
      "NotBlank.userDTO.userId",
      "NotBlank.userId",
      "NotBlank.java.lang.String",
      "NotBlank"
    ],
    "arguments": [
      {
        "codes": [
          "userDTO.userId",
          "userId"
        ],
        "arguments": null,
        "defaultMessage": "userId",
        "code": "userId"
      }
    ],
    "defaultMessage": "用户ID不能为空",
    "objectName": "userDTO",
    "field": "userId",
    "rejectedValue": null,
    "bindingFailure": false,
    "code": "NotBlank"
  }
]

此演示项目已上传到Github,如有需要可自行下载,欢迎 Star 。 https://github.com/LucioChn/spring

以上就是浅谈自定义校验注解ConstraintValidator的详细内容,更多关于自定义校验注解ConstraintValidator的资料请关注三水点靠木其它相关文章!

Java/Android 相关文章推荐
springcloud之Feign超时问题的解决
Jun 24 Java/Android
详解Java分布式事务的 6 种解决方案
Jun 26 Java/Android
JavaWeb 入门篇:创建Web项目,Idea配置tomcat
Jul 16 Java/Android
Sleuth+logback 设置traceid 及自定义信息方式
Jul 26 Java/Android
Java网络编程之UDP实现原理解析
Sep 04 Java/Android
深入浅出讲解Java8函数式编程
Jan 18 Java/Android
引用计数法和root搜索算法以及JVM中判定对象需要回收的方法
Apr 19 Java/Android
Flutter Navigator 实现路由传递参数
Apr 22 Java/Android
SpringBoot接入钉钉自定义机器人预警通知
Jul 15 Java/Android
spring 项目实现限流方法示例
Jul 15 Java/Android
springboot+rabbitmq实现智能家居实例详解
Jul 23 Java/Android
Java中的Kafka为什么性能这么快及4大核心详析
Sep 23 Java/Android
ObjectMapper 如何忽略字段大小写
Java常用函数式接口总结
分析并发编程之LongAdder原理
SpringBoot整合JWT的入门指南
jackson json序列化实现首字母大写,第二个字母需小写
Java数组与堆栈相关知识总结
分析JVM源码之Thread.interrupt系统级别线程打断
Jun 29 #Java/Android
You might like
PHP 使用二进制保存用户状态的实例
2018/01/29 PHP
Javacript实现颜色梯度变化和渐变的效果代码
2013/05/31 Javascript
Jquery Ajax解析XML数据(同步及异步调用)简单实例
2014/02/12 Javascript
jquery取消选择select下拉框示例代码
2014/02/22 Javascript
JS制作手机端自适应缩放显示
2015/06/11 Javascript
JavaScript实现ASC转汉字及汉字转ASC的方法
2016/01/23 Javascript
微信小程序 开发之滑块视图容器(swiper)详解及实例代码
2017/02/22 Javascript
使用mint-ui开发项目的一些心得(分享)
2017/09/07 Javascript
小程序点赞收藏功能的实现代码示例
2018/09/07 Javascript
详解如何在vscode里面调试js和node.js的方法步骤
2018/12/24 Javascript
利用Webpack实现小程序多项目管理的方法
2019/02/25 Javascript
Vue执行方法,方法获取data值,设置data值,方法传值操作
2020/08/05 Javascript
微信小程序中data-key属性之数据传输(经验总结)
2020/08/22 Javascript
[14:36]2014 DOTA2国际邀请赛中国区预选赛5.21 Orenda VS NE
2014/05/22 DOTA
python实现连接mongodb的方法
2015/05/08 Python
Python及Django框架生成二维码的方法分析
2018/01/31 Python
python2.7 json 转换日期的处理的示例
2018/03/07 Python
Pyinstaller将py打包成exe的实例
2018/03/31 Python
Django框架搭建的简易图书信息网站案例
2019/05/25 Python
python opencv 读取图片 返回图片某像素点的b,g,r值的实现方法
2019/07/03 Python
Python中的上下文管理器相关知识详解
2019/09/19 Python
python将音频进行变速的操作方法
2020/04/08 Python
python实现与redis交互操作详解
2020/04/21 Python
python代码实现将列表中重复元素之间的内容全部滤除
2020/05/22 Python
numpy的Fancy Indexing和array比较详解
2020/06/11 Python
Python descriptor(描述符)的实现
2020/11/15 Python
可自定义箭头样式的CSS3气泡提示框
2016/03/16 HTML / CSS
HTML5 canvas基本绘图之填充样式实现
2016/06/27 HTML / CSS
表演方阵解说词
2014/02/08 职场文书
信息员培训方案
2014/06/12 职场文书
2014医学院领导干部四风对照检查材料思想汇报
2014/09/16 职场文书
幼儿园园务工作总结2015
2015/05/18 职场文书
2015年生产部工作总结范文
2015/05/25 职场文书
鲁冰花观后感
2015/06/10 职场文书
2019入党申请书格式和范文
2019/06/25 职场文书
发工资啦!教你用Python实现邮箱自动群发工资条
2021/05/10 Python