SpringBoot如何优雅的进行参数校验
写在前面
上一篇文章中我们学会了如何优雅的接收前端参数,传送门
接收到参数后,接下来要做的就是校验参数的合法性。这一步的重要性就不用多说了。
即使前端已经对数据进行了校验,我们后端还是要再对接收到的数据进行一遍彻底的校验。
这样可以避免张三等人利用Http工具,绕过浏览器非法请求数据。
废话不多说,看完这篇文章,你将从繁琐的校验逻辑中解脱出来
一、传统参数校验
虽然往事不堪回首,但还是得回忆一下我们传统参数校验的痛点。
下面是我们传统校验用户名和邮箱是否合法的代码
if (username == null || username.isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
if (isValidEmail(email)) {
throw new IllegalArgumentException("邮箱格式不正确");
}
public boolean isValidEmail(String email) {
String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
Pattern pattern = Pattern.compile(emailRegex);
Matcher matcher = pattern.matcher(email);
return matcher.matches();
}
这样的代码不仅冗长,而且难以维护,尤其是在多个地方重复使用时,容易出错。
面对上面的痛点,我们就得解放双手,利用框架来完成校验。
它只需要通过简单的注解来定义校验规则,让框架来帮助我们处理校验逻辑,让我们代码变得更加的优雅。
二、几个名词
问题①:JSR是什么?
JSR(Java Specification Requests) 是一套 JavaBean
参数校验的标准,它定义了很多常用的校验注解。
我们可以直接将这些注解加在我们 JavaBean
的属性上面,这样就可以在需要校验的时候进行校验了,非常方便!
问题②:Bean Validation是什么?
Bean Validation
是一个抽象的框架,它定义了验证规则,而不会涉及具体的业务逻辑
问题③:Hibernate Validator是什么?
是Bean Validation
的实现,目前最新版的 Hibernate Validator 6.x
是 Bean Validation 2.0(JSR 380)
的参考实现
三、所需依赖
Spring boot 2.3以前版本,
Springboot
的spring-boot-starter-web
默认内置了Hibernate-Validator
这些版本直接引入
spring-boot-starter-web
即可,后面的版本需要单独引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
在后面的测试中会用到lombok
、SpringBoot
的web
、test
等基础依赖,这里就不一一给出
四、注解及作用
注解 | 作用类型 | 作用 |
---|---|---|
@NotBlank(message='') |
字符串 | 被注释字符串非null,且长度必须大于0 |
@NotEmpty |
字符串 | 被注释的字符串必须非空 |
@NotNull |
任意 | 被注释的元素不能为null |
@Null |
任意 | 被注释的元素必须为null |
@Email |
字符串 | 被注释的元素必须是电子邮箱地址 |
@AssertTrue |
布尔值 | 被注释的元素必须为true |
@AssertFalse |
布尔值 | 被注释的元素必须为false |
@Max(value = , message = "") |
数字 | 被注释的元素必须是一个数字,且小于或等于最大值 |
@Min(value = , message = "") |
数字 | 被注释的元素必须是一个数字,且大于或等于最小值 |
@DecimalMax(value = "", message = "") |
数字 | 被注释的元素必须是一个数字,且小于或等于最大值 |
@DecimalMin(value = "", message = "") |
数字 | 被注释的元素必须是一个数字,且大于或等于最小值 |
@Pattern(regex=,flag=) |
字符串 | 被注释的元素是否符合正则表达式规则 |
@Size(max=, min=) |
数字 | 被注释的元素的大小必须在指定范围内,min 表示最小,max表示最大 |
@Digits (integer, fraction) |
数字 | 被注释的元素必须是一个数字,且在可接收范围内 |
@Positive |
数字 | 被注释的元素必须是正数 |
@PositiveOrZero |
数字 | 被注释的元素必须是0或正数 |
@Negative |
数字 | 被注释的元素必须是负数 |
@NegativeOrZero |
数字 | 被注释的元素必须是0或者负数 |
@Past |
日期 | 被注释的元素必须是一个过去的日期 |
@PastOrPresent |
日期 | 被注释的元素必须是一个过去或当前日期 |
@Future |
日期 | 被注释的元素必须是一个过去的日期 |
@FutureOrPresent |
日期 | 被注释的元素必须是一个将来或当期的日期 |
看到这些注解后,大家可能会对【@NotNul
、@NotEmpty
、@NotBlank
】这三个注解有点不理解,这里稍作解释
@NotNull
:任何对象的value不能为null。@NotEmpty
:集合对象的元素不为0,即集合不为空,也可以用于字符串不为null。@NotBlank
:只能用于字符串不为null,并且字符串trim()以后length要大于0。
五、快速入门
5.1 新增加一个一个User
实体类
@Data
public class User {
//姓名
@NotBlank(message = "用户名不能为空") //注解确保姓名不为空
private String name;
//性别
@NotBlank(message = "性别不能为空") //注解确保性别不为空
private String sex;
//年龄
@NotNull(message = "年龄不能为空") //注解确保年龄不为空
@Max(value = 120,message = "年龄不能大于120") //注解确保年龄必须小于等于120
@Min(value = 18,message = "年龄不能小于18") //注解确保年龄必须大于等于18
private Integer age;
//邮箱
@Email(message = "邮箱格式不正确") //注解确保邮箱格式正确
@NotBlank(message = "邮箱不能为空")
private String email;
}
上述代码说明:
@NotBlank
: 此注解确保字符串不为空并且不能为空字符串,且去掉前后空格后的长度必须大于 0。它常用于字符串字段验证。message
属性用于指定提示信息;@NotNull
: 此注解确保整数类型不能为null
;@Min
和@Max
: 这两个注解用于验证数字值是否在指定的范围内。例如,在上面的示例中,我们想要确保age
的值在 18 到 120 之间;@Email
: 此注解用于验证字符串值是否是有效的电子邮件地址格式。
5.2 Controller
层参数校验
下图是controller
层校验流程
@RestController
public class ValidatorController {
//测试参数校验
@RequestMapping("/testValidator")
public ResponseEntity<String> testValidator(@Valid @RequestBody User user, BindingResult bindingResult){
// 是否存在校验错误
if (bindingResult.hasErrors()) {
// 获取校验不通过字段的提示信息
String errorMsg = bindingResult.getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(errorMsg);
}
return ResponseEntity.ok("参数校验成功");
}
}
解释一下上面代码:
@Validated
: 告诉 Spring 需要对User
对象执行校验; 这个一定不要忘记加上BindingResult
: 该类包含校验不通过时的异常信息,校验不通过时,我们通过这个对象来获取注解中message="xxx"中的内容
注意:当注解校验不通过时,直接将异常信息返回给前端其实并不友好,我们可以将异常包装一下再丢给前端
5.3 测试校验结果
这里我们使用postman工具测试一下参数校验是否成功
① 入参正确情况
{
"name":"小凡",
"sex":"男",
"age":18,
"email":"xiezhr@qq.com"
}
②入参不正确的情况
{
"name":null,
"sex":"",
"age":17,
"email":"xiezhrqq.com"
}
通过上面的入门小案例,你学会了么?
上面的返回结果看起来可能不是那么优雅,那么怎么封装统一返回结果呢,
传送门在此优雅的封装返回结果
六、单个参数校验
上面快速入门中我们说了实体参数校验,这小节,我们来看看单个参数的校验
6.1 controller层校验代码
@RequestMapping("/testSingleParmaValidator")
public ResponseEntity<String> testSingleParmaValidator(@NotBlank(message = "姓名不能为空") String name,
@Min(value = 18,message = "年龄不能小于18")
@Max(value = 120,message = "年龄不能大于120") Integer age
){
// 参数校验
return ResponseEntity.ok("参数校验成功");
}
6.2 全局异常捕获
当参数校验不通过会发生如下异常信息
这里我们不能像上面一样通过BindingResult
来获取异常信息,需要添加全局异常捕获校验失败异常,具体代码如下
@RestControllerAdvice
public class GlobalExceptionHandler {
//处理ValidationException异常
@ExceptionHandler(ValidationException.class)
//返回状态码为400
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<String> handleValidationExceptions(ValidationException ex) {
String message = "";
//判断异常类型
if(ex instanceof ConstraintViolationException){
ConstraintViolationException exs = (ConstraintViolationException) ex;
//获取验证不通过的信息
Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
//遍历验证不通过的信息
for (ConstraintViolation<?> item : violations) {
//将验证不通过的信息拼接到message中
message+=item.getMessage()+",";
}
}
//返回错误信息
return ResponseEntity.badRequest().body(message);
}
}
6.3 测试校验结果
①入参正确情况
http://localhost:8080/testSingleParmaValidator?name=小凡&age=18
②入参不正确情况
http://localhost:8080/testSingleParmaValidator?name=&age=17
八、参数校验分组
在实际开发中,我们会遇到这样的情况:同一个实体类可能会在多个接口中使用,但每次的校验场景又不一样。
例如:新增用户和修改用户接口,参数都是
User
实体,在新增用户的时候ID
字段 可以为空,但name
字段 不能为空在修改用户的是由
ID
字段不能为空,这种时候就可以使用参数分组来实现。
8.1 定义验证分组接口
定义两个分组接口
CreateUserGroup
(用户创建组),UpdateUserGroup
(用户更新组),分别继承
javax.validation.groups.Default
,标识不同的业务场景
public interface CreateUserGroup extends Default {
}
public interface UpdateUserGroup extends Default {
}
注:继承Default并不是必须的。只是说,如果继承了Default
,那么@Validated(value = Create.class)
的校验范畴就
为【Create】和【Default】;如果没继承Default,那么@Validated(value = Create.class)
的校验范畴只
为【Create】,而@Validated(value = {Create.class, Default.class})
的校验范畴才为【Create】和【Default】
8.2 分组校验的使用
① 在实体中添加groups
属性
@Data
public class User {
//用户ID
@NotNull(message = "用户ID不能为空",groups = UpdateUserGroup.class) //用户更新接口必须传递用户ID
private Integer id;
//姓名
@NotBlank(message = "用户名不能为空",groups = CreateUserGroup.class) //用户创建接口必须传递用户名
private String name;
//性别
@NotBlank(message = "性别不能为空") //注解确保性别不为空
private String sex;
//年龄
@NotNull(message = "年龄不能为空") //注解确保年龄不为空
@Max(value = 120,message = "年龄不能大于120") //注解确保年龄必须小于等于120
@Min(value = 18,message = "年龄不能小于18") //注解确保年龄必须大于等于18
private Integer age;
//邮箱
@Email(message = "邮箱格式不正确") //注解确保邮箱格式正确
@NotBlank(message = "邮箱不能为空")
private String email;
}
②在接口中使用分组
使用 @Validated
注解,并指定要执行的验证组。
//添加用户
@PostMapping("/addUser")
public ResponseEntity<User> addUser(@Validated(value= CreateUserGroup.class) @RequestBody User user){
return ResponseEntity.ok(user);
}
//更新用户
@PutMapping("/updateUser")
public ResponseEntity<User> updateUserUser(@Validated(value= UpdateUserGroup.class) @RequestBody User user){
return ResponseEntity.ok(user);
}
我们指定create接口指定CreateUserGroup分组,update接口指定UpdateUserGroup
8.3 测试一下接口
接口入参
{
"name":"小凡",
"sex":"男",
"age":18,
"email":"xiezhr@qq.com"
}
①addUser
接口添加用户,不需要id,验证通过
②updateUser
接口修改用户,需要传入id,校验不通过
九、嵌套对象校验
9.1 构造一个员工信息表
@Data
public class Emp {
@NotBlank(message = "员工编号不能为空")
private String empNo;
@NotBlank(message = "员工姓名不能为空")
private String empName;
@NotBlank(message = "员工职位不能为空")
private String job;
@Valid //这里必须使用@Valid注解
private Dept dept;
}
@Data
public class Dept {
@NotBlank(message = "部门编号不能为空")
private String deptNo;
@NotBlank(message = "部门名称不能为空")
private String deptName;
}
在这个示例中, Dept
类包含三个字段需要校验: deptNo
和``deptName字段,通过在
Dept类中的每个字段上添加相应的校验注解,然后在
Emp类中的
dept字段上添加
@Valid` 注解,可以实现对嵌套对象中多个字段进行参数校验。
9.2 嵌套对象的使用
@PostMapping("/emp")
public ResponseEntity<String> createOrder(@Valid @RequestBody Emp emp) {
return ResponseEntity.ok("参数校验成功");
}
9.3 测试一下
① 正确入参情况
{
"empNo":"10001",
"empName":"小凡",
"job":"程序员",
"dept":{
"deptNo":"20001",
"deptName":"研发部111"
}
}
② 不正确入参情况
{
"empNo":"10001",
"empName":"",
"job":"程序员",
"dept":{
"deptNo":"20001",
"deptName":""
}
}
十、自定义参数校验
SpringBoot 提供的注解校验功能可以满足大多数的验证需求,但如果在系统中需要实现一些特殊的校验功能时,
我们可以根据规则自定义校验
下面我们来手把手教你自定义一个字符串校验,校验字符串必须为大写或小写
10.1 自定义注解类
我们要自定义验证功能,需要首先自定义注解,以便我们在实体类中使用它,代码如下
①定义一个枚举类 CaseMode
:
public enum CaseMode {
UPPER,
LOWER;
}
②创建一个自定义的校验注解 @CheckCase
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
public @interface CheckCase {
String message() default "字符串必须是大写或小写";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
CaseMode value();
}
10.2 自定义验证业务逻辑类
public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
private CaseMode caseMode;
@Override
public void initialize(CheckCase constraintAnnotation) {
// 获取约束注解的值
this.caseMode = constraintAnnotation.value();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果值为空,则返回true
if (value == null) {
return true;
}
// 根据caseMode的值,判断value是否需要转换大小写
if (caseMode == CaseMode.UPPER) {
return value.equals(value.toUpperCase());
} else {
return value.equals(value.toLowerCase());
}
}
}
10.3 自定义校验注解使用
①在Car
实体类上添加注解
@Data
public class Car {
//车牌号
@CheckCase(value = CaseMode.UPPER,message = "车牌号必须为大写")
private String brand;
//颜色
@CheckCase(value = CaseMode.LOWER,message = "颜色必须为小写")
private String color;
}
②在controller
中校验参数
@GetMapping ("/car")
public ResponseEntity<String> validatorCar(@Valid @RequestBody Car car) {
return ResponseEntity.ok("参数校验成功");
}
10.4 测试一下
①入参正确情况
{
"brand":"云A.888888",
"color":"red"
}
②入参错误情况
{
"brand":"云a.888888",
"color":"RED"
}
以上就是本期的全部内容,希望对你有所帮助,我们下期再见 (●'◡'●)
SpringBoot如何优雅的进行参数校验的更多相关文章
- SpringBoot Validation优雅的全局参数校验
前言 我们都知道在平时写controller时候,都需要对请求参数进行后端校验,一般我们可能会这样写 public String add(UserVO userVO) { if(userVO.getA ...
- SpringBoot实现通用的接口参数校验
本文介绍基于Spring Boot和JDK8编写一个AOP,结合自定义注解实现通用的接口参数校验. 缘由 目前参数校验常用的方法是在实体类上添加注解,但对于不同的方法,所应用的校验规则也是不一样的,例 ...
- 如何优雅的做参数校验-JSR330
前言: 本文不是讲@Validate.@Valid是如何使用的.区别是什么,想看这些内容的请换篇文章. 背景: 当前端传过来的参数是进行对称性加密.base64加密等处理后过的参数时,在control ...
- springboot @valid与@validated的参数校验使用总结
好久没在这平台写博客了,最近整理了这东西,先给出总结 // @Valid只能用在controller,@Validated可以用在其他被spring管理的类上 // @Valid可以加在成员变量上(本 ...
- SpringBoot 参数校验的方法
Introduction 有参数传递的地方都少不了参数校验.在web开发中,前端的参数校验是为了用户体验,后端的参数校验是为了安全.试想一下,如果在controller层中没有经过任何校验的参数通过s ...
- spring-boot 使用hibernate validation对参数进行优雅的校验
springboot天生支持使用hibernate validation对参数的优雅校验,如果不使用它,只能对参数挨个进行如下方式的手工校验,不仅难看,使用起来还很不方便: if(StringUtil ...
- SpringBoot接口 - 如何优雅的对参数进行校验?
在以SpringBoot开发Restful接口时, 对于接口的查询参数后台也是要进行校验的,同时还需要给出校验的返回信息放到上文我们统一封装的结构中.那么如何优雅的进行参数的统一校验呢? @pdai ...
- 一起来学SpringBoot(十七)优雅的参数校验
参数校验在开发中经常需要写一些字段校验的代码,比如字段非空,字段长度限制,邮箱格式验证等等,写这些与业务逻辑关系不大的代码个人感觉有两个麻烦: 验证代码繁琐,重复劳动方法内代码显得冗长每次要看哪些参数 ...
- SpringBoot Validation参数校验 详解自定义注解规则和分组校验
前言 Hibernate Validator 是 Bean Validation 的参考实现 .Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的 ...
- 【springboot】@Valid参数校验
转自: https://blog.csdn.net/cp026la/article/details/86495659 扯淡: 刚开始写代码的时候对参数的校验要么不做.要么写很多类似 if( xx == ...
随机推荐
- JS Leetcode 198. 打家劫舍 题解分析,再次感受动态规划的魅力
壹 ❀ 引 本题来自LeetCode198. 打家劫舍,难度中等,也很有意思,是一道教小偷如何偷窃最大金额的题,题目描述如下: 你是一个专业的小偷,计划偷窃沿街的房屋.每间房内都藏有一定的现金,影响你 ...
- AIR32F103(十二) 搭载 AIR32F103CBT6 的Bluepill核心板
目录 AIR32F103(一) 合宙AIR32F103CBT6开发板上手报告 AIR32F103(二) Linux环境和LibOpenCM3项目模板 AIR32F103(三) Linux环境基于标准外 ...
- win32 - 将文件的访问权限给特定的用户
需要首先获取特定用户的SID. 这是一些步骤, 验证输入参数. 为可能足够大的SID和域名创建缓冲区. 在循环中,调用LookupAccountName以检索提供的帐户名的SID.如果SID的缓冲区或 ...
- 海康摄像SDK开发笔记(一):海康威视网络摄像头SDK介绍与模块功能
前言 视频监控.人脸识别等应用中经常使用到摄像头,当前占据主流视频监控摄像头就是海康和大华两家,都可通过自家的sdk或者是onvif方式使用和控制摄像头. 本文章讲解海康的sdk方式. 海康 ...
- jdk17新特性梳理
jdk17新特性梳理 目录 jdk17新特性梳理 jdk8升级至jdk17新特性梳理 升级jdk17的理由 新特性梳理 可以在接口中定义私有方法,主要为了jdk8的default方法 局部变量可以使用 ...
- 【LeetCode贪心#01】分饼干,贪心算法入门(入了但是还没完全入)
分饼干 力扣题目链接(opens new window) 假设你是一位很棒的家长,想要给你的孩子们一些小饼干.但是,每个孩子最多只能给一块饼干. 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子 ...
- toml格式配置文件介绍
toml官方wik toml官方文档 此次文档是以v1.0.0为例,进行说明的.如果使用到的版本不同,直接去官方文档中找对应的版本即可. 谈到配置文件,大家都能说出来好几种,比如常见的ini.xml. ...
- 【Azure Redis 缓存】Azure Reids是否可以开启慢日志(slowlog)和执行config指令
问题描述 使用Azure Redis,是否可以开启慢日志来查看最近时间中执行比较耗时的指令呢? 同时,如何执行Redis的Config只能来修改配置呢? 根本原因 一:Azure Reids通过Red ...
- PlatformIO+esp32+添加自己的库(.c.h文件)
什么都放main.c的话,很有可能堆积成屎山,所以我想给分开写,每个功能有自己的.c..h文件. 在lib下新建文件夹,例如led,再在里面分别建led.c.led.h; 写好内容后再main ...
- Java 泛型举例
1 package com.bytezero.genericity; 2 3 import org.junit.Test; 4 5 import java.util.*; 6 7 /** 8 * @a ...