【Java Bean Validation API】Spring3 集成 Bean 参数校验框架

news/2024/9/21 19:03:15/

Spring3 集成 Bean 参数校验框架 Java Bean Validation API

1. 依赖

Spring 版本:3.0.5

Java 版本:jdk21

检验框架依赖(也可能不需要,在前面 spring 的启动依赖里就有):

<!-- 自定义验证注解 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>

目前我还没有找到 spring/java 低版本的一个很方便的方式去进行参数校验,之前的 javax 无法实现,在高版本中被更名为 jakarta,我们使用的就是其中的 jakarta

2. 基本使用

2.1 常见注解

AnnotationDescription
@NotNull参数不能为 null
@NotBlank参数不能为 null 或 trim 后为空字符串
@NotEmpty字符串、数组、集合是否为 null 也不为空字符串、空数组、空集合
@Size若不为 null,指定字符串、数组、集合的长度范围
@Max若不为 null,参数最大值
@Min若不为 null,参数最小值
@DecimalMax若不为 null,采取精度较高的最小值限制
@DecimalMax若不为 null,采取精度较高的最大值限制
@Pattern若不为 null,字符串正则表达式匹配
@Email若不为 null,字符串是否符合邮件格式

按需去查就行,更多注解在:

java">package jakarta.validation.constraints;

自觉规范地据场景去使用,注解可以标注在任何地方,但是不是每个地方都有用,轻则失效,重则报错

精力有限,这些我们也没法一一去探寻“乱搞的现象”

2.2 自定义校验注解

如果框架自带的不足以满足我们的要求,那么我们可以选择自定义注解

例如,这些注解都无法针对 Map 这种非单列的类型

或者,我们需要一个注解,其可以检测一个 Number 类型的或者其数组集合的对象,若不为 null,元素在一个特定的数值范围内

我们就要自己去写一个会被 Jakarta 框架识别的注解:

java">/*** Created With Intellij IDEA* User: 马拉圈* Date: 2024-08-07* Time: 17:19* Description: 此注解用于判断数值是否在规定氛围内* min 代表最小值,max 代表最大值,被注解的变量数值必须在闭区间 [min, max]* 支持该变量是 Number 类型的变量,以及其数组、集合;* 对于数组和集合,必须每个元素都满足该规则,否则就不通过*/
@Documented
@Constraint(validatedBy = {IntRangeValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IntRange {String message() default "数值不在有效范围内"; // 默认消息int min();int max();Class<?>[] groups() default {}; // 分组校验Class<? extends Payload>[] payload() default {}; // 负载信息
}

在这里插入图片描述

黄色为必须部分,红色为自定义部分,其中 IntRangeValidator.class 是自定义的处理类:

java">public class IntRangeValidator implements ConstraintValidator<IntRange, Object> {private int min;private int max;@Overridepublic void initialize(IntRange intRange) {this.min = intRange.min();this.max = intRange.max();}private int compare(Number number1, Number number2) {return Double.compare(number1.doubleValue(), number2.doubleValue());}private boolean isValid(Object value) {if(Objects.isNull(value)) {return Boolean.TRUE;} else if (value instanceof Number number) {return compare(number, min) >= 0 && compare(number, max) <= 0;} else if (value instanceof Collection<?> collection) {return collection.stream().allMatch(this::isValid);} else if (value.getClass().isArray()) {int length = Array.getLength(value);for (int i = 0; i < length; i++) {if(!isValid(Array.get(value, i))) {return Boolean.FALSE;}}return Boolean.TRUE;} else {return Boolean.FALSE;}}@Overridepublic boolean isValid(Object value, ConstraintValidatorContext context) {return isValid(value);}
}

代码就不解释了,主要是得实现 ConstraintValidator 接口,其中第一个泛型是自定义注解类,第二个泛型是预期注解标注在什么类型的对象上,isValid 返回 false,就拦截

在这里插入图片描述

2.3 自定义异常处理

如果拦截,会统一抛出异常:MethodArgumentNotValidException.class 或者 ConstraintViolationException.class

  • MethodArgumentNotValidException 由一整个对象被检测出问题时抛出
  • ConstraintViolationException 由单一属性或单一参数被检测出问题时抛出
  • 可能有其他,但是如果是违背我们的注解那一定是上面这两个,其他可能是使用不当的问题

我觉得都处理就行,不要纠结抛哪个异常,都处理就行:

java">public static SystemJsonResponse getGlobalServiceExceptionResult(GlobalServiceException e, HttpServletRequest request) {String requestURI = request.getRequestURI();String message = e.getMessage();GlobalServiceStatusCode statusCode = e.getStatusCode();log.error("请求地址'{}', {}: {}", requestURI, statusCode, message);return SystemJsonResponse.CUSTOMIZE_MSG_ERROR(statusCode, message);
}
/*** 自定义验证异常*/
@ExceptionHandler(ConstraintViolationException.class)
public SystemJsonResponse constraintViolationException(ConstraintViolationException e, HttpServletRequest request) {log.error("数据校验出现问题,异常类型:{}", e.getMessage());String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).filter(Objects::nonNull).collect(Collectors.joining("\n"));return getGlobalServiceExceptionResult(new GlobalServiceException(message, GlobalServiceStatusCode.PARAM_FAILED_VALIDATE),request);
}@ExceptionHandler(MethodArgumentNotValidException.class)
public SystemJsonResponse ValidationHandler(MethodArgumentNotValidException e, HttpServletRequest request) {log.error("数据校验出现问题,异常类型:{}", e.getMessage());String message = e.getBindingResult().getFieldErrors().stream().map(FieldError::getDefaultMessage).filter(Objects::nonNull).collect(Collectors.joining("\n"));return getGlobalServiceExceptionResult(new GlobalServiceException(message, GlobalServiceStatusCode.PARAM_FAILED_VALIDATE),request);
}

2.4 如何应用

2.4.1 触发条件

最重要的条件是:

  1. 必须是 Bean 对象的实例方法
  2. 检测的对象是方法的形参
2.4.1 形参是普通类型

我们使用 @NotNull 等检测参数的注解,或者自定义的校验注解,标注在形参之前,注解的校验可以进行叠加

并且,我们需要在 Bean 的类之前标注 @Validated,声明这个 Bean 的方法参数受代理

这个 bean 在调用这个方法的时候,输入参数就会被监控

2.4.2 形参是自定义类型

我们使用 @NotNull 等检测参数的注解,或者自定义的校验注解,标注在形参之前,注解的校验可以进行叠加

如果这个类的定义设置了属性校验,我们要对其内部每个属性都校验,那就标注 @Valid,表示循环递归校验

其中,@Valid 的触发不依赖于类上的 @Validated,其他注解则依赖

  1. 什么是循环递归校验

    • 如果 @Valid 标注的是集合或数组,则依次对每个元素校验,递归就是校验元素内部的属性
  2. 什么是类的定义设置了属性校验

    • 例如这个对象:

    • java">@Data
      public class EmailLoginDTO {@NotBlank(message = "code 不能为空")private String code;@NotBlank(message = "邮箱不能为空")@Email(message = "邮箱格式不合法")private String email;
      }
      

值得注意的是,@Valid 只会在非 null 的时候触发

若这个对象,的属性又有自定义对象,则继续标注 @Valid 循环递归校验即可,

java">@Data
public class LoginDTO {@Validprivate EmailLoginDTO emailLoginDTO;@Validprivate WxLoginDTO wxLoginDTO;
}
2.4.3 特殊需求

如果你需要对一个方法的返回值进行校验,如果直接标注注解在返回值类型前,是无效的;

  1. 对于普通类型,我们手动校验没啥大问题
  2. 对于自定义类型,且类的定义设置了属性校验,我们可不想再写一遍啊~

我们其实可以通过封装以下这个方法进行校验:

java">import cn.hutool.extra.spring.SpringUtil;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validator;import java.util.Set;public class ValidatorUtils {private static final Validator validator = SpringUtil.getBean(Validator.class);public static <T> void validate(T object, Class<?>... groups) {Set<ConstraintViolation<T>> validate = validator.validate(object, groups);if (!validate.isEmpty()) {String message = String.format("请求对象:'%s'", object.toString());throw new ConstraintViolationException(message, validate);}}
}

我们只需要将方法的返回值传进去就行(groups 可以为空数组),便可完成对自定义类的校验

2.4.4 主要应用场景

主要应用场景就是用于 Controller 的目标方法,因为 Controller 也是 Bean 嘛,接受请求的时候,会调用这个 Bean 对应的目标方法,例如一下示例:

对于无状态的参数进行校验,与业务控制层解耦,避免了重复校验的冗余现象,也不会犯是在控制层还是在业务层进行校验的选择困难症

java">@RestController
@RequiredArgsConstructor
@Validated
public class XXXController {@PostMapping("/set/{value}")@Operation(summary = "设置值")public SystemJsonResponse setValue(@Valid @RequestBody XXXDTO xxxDTO,@NotBlank @RequestHeader("token") String token@IntRange (min = 1, max = 7) @PathVariable("value") Integer value) {// ......}}

更多使用场景,只要合理推理就应该没问题,举一反三一下就行,更多细节需要就去查去探索,这里就不一一罗列了


http://www.ppmy.cn/news/1528508.html

相关文章

STM32G431RBT6(蓝桥杯)串口(发送)

一、基础配置 (1) PA9和PA10就是串口对应在单片机上的端口 注意&#xff1a;一定要先选择PA9的TX和PA10的RX&#xff0c;再去打开异步的模式 (2) 二、查看单片机的端口连接至电脑的哪里 &#xff08;1&#xff09;此电脑->右击属性 &#xff08;2&#xff09;找到端…

好课程:uni-app实战音频小说app小程序

知识储备&#xff1a;具有HtmlCssJavaScript基础&#xff0c;有一定的Vue.js基础 学习这套课程&#xff0c;可以带来多方面的好处&#xff1a; 全面掌握技术栈&#xff1a;通过实战课程&#xff0c;你将学习到如何使用uni-app结合Vue.js进行跨平台应用开发&#xff0c;包括前…

微信getUserProfile不弹出授权框

当我们在微信小程序开发工具中想要使用getUserProfile来获取个人信息的时候&#xff0c;会发现不弹出授权框&#xff0c;这是什么原因呢&#xff1f; 早在2022年的小程序官方公告中就已经明确给出了小程序用户头像昵称获取规则调整公告 因此如果还想继续使用getUserProfile的弹…

【Elasticsearch系列十一】聚合 DSL API

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

企业内网安全

企业内网安全 1.安全域2.终端安全3.网络安全网络入侵检测系统异常访问检测系统隐蔽信道检测系统 4.服务器安全基础安全配置入侵防护检测 5.重点应用安全活动目录邮件系统VPN堡垒机 6.蜜罐体系建设蜜域名蜜网站蜜端口蜜服务蜜库蜜表蜜文件全民皆兵 1.安全域 企业出于不同安全防…

Python 从入门到实战23(属性property)

我们的目标是&#xff1a;通过这一套资料学习下来&#xff0c;通过熟练掌握python基础&#xff0c;然后结合经典实例、实践相结合&#xff0c;使我们完全掌握python&#xff0c;并做到独立完成项目开发的能力。 上篇文章我们讨论了类的定义、使用方法的相关知识。今天我们将学…

SQL - 基础语法

SQL作为一种操作命令集, 以其丰富的功能受到业内人士的广泛欢迎, 成为提升数据库操作效率的保障。SQL Server数据库的应用&#xff0c;能够有效提升数据请求与返回的速度&#xff0c;有效应对复杂任务的处理&#xff0c;是提升工作效率的关键。 由于SQL Servers数据库管理系统…

neo4j(spring) 使用示例

文章目录 前言一、neo4j是什么二、开始编码1. yml 配置2. crud 测试3. node relation 与java中对象的关系4. 编码测试 总结 前言 图数据库先驱者 neo4j&#xff1a;neo4j官网地址 可以选择桌面版安装等多种方式,我这里采用的是docker安装 直接执行docker安装命令: docker run…