浅谈自定义校验注解ConstraintValidator
作者:陈皮的JavaLib 发布时间:2023-07-06 03:10:53
一、前言
系统执行业务逻辑之前,会对输入数据进行校验,检测数据是否有效合法的。所以我们可能会写大量的if else等判断逻辑,特别是在不同方法出现相同的数据时,校验的逻辑代码会反复出现,导致代码冗余,阅读性和可维护性极差。
JSR-303是Java为Bean数据合法性校验提供的标准框架,它定义了一整套校验注解,可以标注在成员变量,属性方法等之上。
hibernate-validator就提供了这套标准的实现,我们在用Springboot开发web应用时,会引入spring-boot-starter-web依赖,它默认会引入spring-boot-starter-validation依赖,而spring-boot-starter-validation中就引用了hibernate-validator依赖。
但是,在比较高版本的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
来源:https://www.cnblogs.com/luciochn/p/14529281.html


猜你喜欢
- 没有结果时,去.First()时,会报错,所以一定要先.Count()判断一下而用FirstOrDefault(),如果集合中没有数据,则返
- IO操作字节流java.io.InputStream 输入流,主要是用来读取文件内容的。java.io.OutputStream 输出流,主
- 本文实例为大家分享了Android仿IOS UIAlertView对话框的具体代码,供大家参考,具体内容如下显示效果:我在参考链接中看到了作
- 单例模式是最常用到的设计模式之一,熟悉设计模式的朋友对单例模式都不会陌生。一般介绍单例模式的书籍都会提到 饿汉式 和 懒汉式 这两种实现方式
- 效果图如下所示:先给大家说下实现android 跳转到通讯录的实现思路:1.点击跳转到通讯录界面2.获取通讯录姓名和手机号码3.回调显示姓名
- 在application.xml加上以下配置mybatis-plus.configuration.map-underscore-to-cam
- 前言关于mybatis-plus的简介以及基本使用,我在《SpringBoot整合mybatis-plus–入门超详细》一文中已做介绍,此处
- 从服务器下载文件中文名乱码解决方案,具体文字说明不多了,直接贴代码了,具体代码如下:try { &n
- Path接口1、Path表示的是一个目录名序列,其后还可以跟着一个文件名,路径中第一个部件是根部件时就是绝对路径,例如 / 或 C:\ ,而
- 本文实例讲述了C#实现在图像中绘制文字图形的方法。分享给大家供大家参考。具体实现方法如下:using System;using System
- bootstrap.yml和bootstrap.properties优先级直接先说结论 bootstrap.properties 优于boo
- 传输层安全性协议(英语:Transport Layer Security,缩写作 TLS),及其前身安全套接层(Secure So
- 前言:在 Java 语言中,提高程序的执行效率有两种实现方法,一个是使用线程、另一个是使用线程池。而在生产环境下,我们通常会采用后者。为什么
- 本文实例讲述了C#适用于like语句的SQL格式化函数,分享给大家供大家参考。具体实现代码如下:/// <summary> //
- 要爬取一个网站遇到了极验的验证码,这周都在想着怎么破解这个,网上搜了好多知乎上看到有人问了这问题,我按照这思路去大概实现了一下。1.使用ht
- 需求:android存储字符串数据简单的有SharePerfence不过只能存储89kb最多的数据(好像),超过这个数据如果不方便网络存储,
- 1.抽象类与抽象方法:(1)使用关键字abstract修饰的类,称为抽象类.(2)抽象类只是用到一个类所具有的行为,不能单独通过创建对象来使
- 构造函数、析构函数构造函数:1.若没提供任何构造函数,则系统会自动提供一个默认的构造函数,初始化所有成员为默认值(引用类型为空引用null,
- 本文介绍了Android 仿微信自定义数字键盘的实现代码,分享给大家,希望对大家有帮助最终效果:实现这个自定义键盘的思路很简单:要写出一个数
- 一、 测试代码:二、添加参数1、在终端工具中①先编译: javac Test.java②再运行: java Test args1 args2