一 前言

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

鉴于通用性和普遍性,Spring框架提供了validator组件,通过一些校验器,可以对一些数据进行统一的完整性和有效性等校验,即简单又好用。

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.*; /**
* @Description 校验是否韭菜的注解
* @Author Mr.nobody
* @Date 2021/3/11
* @Version 1.0
*/
@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; /**
* @Description 自定义校验器
* @Author Mr.nobody
* @Date 2021/3/11
* @Version 1.0
*/
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.*; /**
* @Description
* @Author Mr.nobody
* @Date 2021/3/11
* @Version 1.0
*/
@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; /**
* @Description
* @Author Mr.nobody
* @Date 2021/3/11
* @Version 1.0
*/
@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; /**
* @Description 统一异常处理
* @Author Mr.nobody
* @Date 2020/10/23
* @Version 1.0
*/
@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;

/**
* @Description
* @Author Mr.nobody
* @Date 2021/3/13
* @Version 1.0
*/
public interface Create extends Default {
}
package com.nobody.annotation;

import javax.validation.groups.Default;

/**
* @Description
* @Author Mr.nobody
* @Date 2021/3/13
* @Version 1.0
*/
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.*; /**
* @Description
* @Author Mr.nobody
* @Date 2021/3/11
* @Version 1.0
*/
@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; /**
* @Description
* @Author Mr.nobody
* @Date 2021/3/11
* @Version 1.0
*/
@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"}
{
"userId": null,
"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的更多相关文章

  1. hibernate validator参数校验&自定义校验注解

    参数校验:简单的就逐个手动写代码校验,推荐用Valid,使用hibernate-validator提供的,如果参数不能通过校验,报400错误,请求格式不正确: 步骤1:在参数对象的属性上添加校验注解如 ...

  2. SpringBoot 使用 JSR303 自定义校验注解

    JSR303 是 Java EE 6 中的一项子规范,叫做 Bean Validation,官方参考实现是hibernate Validator,有了它,我们可以在实体类的字段上标注不同的注解实现对数 ...

  3. jsr-303 参数校验—自定义校验注解

    1.为什么要自定义? 通过上篇学习,了解到很多常用注解了,但是呢,总是有那么些需求....   2.案例分析(手机号格式) 2.1.需要验证的实体 Bean public class LoginVo ...

  4. hibernate validator自定义校验注解以及基于服务(服务组)的校验

    hibernate validator是Bean Validation 1.1 (JSR 349) Reference Implementation,其广泛的应用在mvc的参数校验中,尤其是使用服务端 ...

  5. 使用spring validation完成数据后端校验-自定义校验的注解-判断是否为空

    引入依赖 我们使用maven构建springboot应用来进行demo演示. <dependencies> <dependency> <groupId>org.sp ...

  6. 更加灵活的参数校验,Spring-boot自定义参数校验注解

    上文我们讨论了如何使用@Min.@Max等注解进行参数校验,主要是针对基本数据类型和级联对象进行参数校验的演示,但是在实际中我们往往需要更为复杂的校验规则,比如注册用户的密码和确认密码进行校验,这个时 ...

  7. hibernate自定义校验Valid

    步骤: 1.定义注解: import javax.validation.Constraint; import javax.validation.Payload; import java.lang.an ...

  8. Springboot学习06-Spring AOP封装接口自定义校验

    Springboot学习06-Spring AOP封装接口自定义校验 关键字 BindingResult.Spring AOP.自定义注解.自定义异常处理.ConstraintValidator 前言 ...

  9. 【参数校验】 自定义校验器 (实现ConstraintValidator)

    日常工作中写接口时,往往需要校验前端传来的枚举状态码,例如"1","2"等等, 这里使用java 303规范的参数校验框架封装一个自定义参数校验器: /** * ...

随机推荐

  1. Docker的OverlayFS存储驱动

    OverlayFS存储驱动 OverlayFS是一个现代的Union Filesystem,类似于AUFS,但速度更快,实现更简单.Docker为OverlayFS提供了两个存储驱动程序:overla ...

  2. 3.使用nginx-ingress

    作者 微信:tangy8080 电子邮箱:914661180@qq.com 更新时间:2019-06-25 13:54:15 星期二 欢迎您订阅和分享我的订阅号,订阅号内会不定期分享一些我自己学习过程 ...

  3. springboot demo(一)快速开始

    快速入门 maven构建项目 1.访问http://start.spring.io/ 2.选择构建工具Maven Project.Spring Boot版本2.26以及一些工程基本信息,点击" ...

  4. 将从摄像头即时读入的人像放入背景视频中_with_OpenCV_in_Python

    import cv2 import numpy as np import time cap = cv2.VideoCapture(0) background_capture = cv2.VideoCa ...

  5. 如何使用 js 检测控制台被用户打开了

    如何使用 js 检测控制台被用户打开了 js solutions 监听 F12 事件 监听键盘快捷键组合 Ctrl + Shift + I Option + Command + I Object.to ...

  6. how to share UI components

    how to share UI components The shared component cloud · Bit https://bit.dev/ A better way to build w ...

  7. git whoami

    git whoami $ git config --list $ git config --global --list # quit $ q $ git config user.name xgqfrm ...

  8. APP 金刚区图标设计 & UI

    APP 金刚区图标设计 & UI https://www.zcool.com.cn/article/ZNzk4Njg0.html

  9. Flutter使用WebSockets

    文档 注意是WebSockets而不是socket.io install dependencies: web_socket_channel: demo import 'dart:convert'; i ...

  10. 华盛顿金融等多家媒体报道VAST超高价值!

    近日,华盛顿金融时报联合洛杉矶商业报等多家媒体就即将推出的VAST进行了专题报道. 华盛顿金融时报专栏记者福吉瑞斯问到,之前有报道称NGK官方将全力支持算力市场,那么现在官方有什么计划可以透露一下吗? ...