在日常项目开发中,我们都知道参数验证是必不可少的一环,但是有时候为了偷懒,把参数校验交给前端开发人员去处理,这样很容易影响系统稳定性和安全性,毕竟现在有很多手段可以绕过前端,直接后端请求接口。
本文就来介绍一下在 SpringBoot
应用中怎么进行参数校验。
一、使用参数校验注解
在 SpringBoot
项目中可以引用 spring-boot-starter-validation
实现数据验证。spring-boot-starter-validation
不仅支持 JSR-303(Bean Validation 1.0)
规范,还提供了对 JSR-380(Bean Validation 2.0)
规范的全面支持。可以利用 Bean Validation 2.0
的新特性,更灵活地定义验证规则,包括对集合、嵌套对象的验证等。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
通常在实体类上的字段上使用标准的 Bean Validation
注解,以下是一些常用的参数校验注解以及相关例子。
注解名称 | 功能 |
@Null | 检查该字段为空 |
@NotNull | 不能为null |
@NotBlank | 不能为空,常用于检查空字符串 |
@NotEmpty | 不能为空,多用于检测list是否size是0 |
@Max | 该字段的值只能小于或等于该值 |
@Min | 该字段的值只能大于或等于该值 |
@Past | 检查该字段的日期是在过去 |
@Future | 检查该字段的日期是否是属于将来的日期 |
检查是否是一个有效的email地址 | |
@Pattern(regex=,flag=) | 被注释的元素必须符合指定的正则表达式 |
@Range(min=,max=,message=) | 被注释的元素必须在合适的范围内 |
@Size(min=, max=) | 检查该字段的size是否在min和max之间,可以是字符串、数组、集合、Map等 |
@Length(min=,max=) | 检查所属的字段的长度是否在min和max之间,只能用于字符串 |
@AssertTrue | 用于boolean字段,该字段只能为true |
@AssertFalse | 该字段的值只能为false |
1.1、基本用法
1.@NotNull:校验元素值不能为空,如果为空,则校验失败。
@NotNull(message = "名字不能为空")
private String userName;
2.@NotBlank:校验字符串值不能为null和空字符串,必须包含至少一个非空字符即执行trim
(之后不为’‘)。如果元素为null
或者’',则验证失败。
@NotBlank(message = "昵称不能为null和空字符串")
private String nickName;
3.@NotEmpty:校验集合或者数组或者字符串是否非空,通常用于集合和数组字段,需要集合和数组元素个数大于0
。也可以作用于字符串,此时校验字符串不能为null
或空串(可以是一个空格)。
@NotEmpty(message = "postIds不能为空")
private Long[] postIds;
4.@Max:校验数字元素最大值。
@Max(value=100,message = "年龄最大100")
private String age;
5.@Min:校验数字元素最小值。
@Min(value=18,message = "年龄最小100")
private String age;
6.@Past:校验日期或时间元素是否在当前时间之前。即是否是过去时间。作用于Date
相关类型的字段。
@Past(message = "")
private Date createTime;
7.@Future:校验日期或时间元素是否在当前时间之前。即是否是过去时间。作用于Date
相关类型的字段。
@Future(message = "")
private Date createTime;
8.@Email:校验字符串元素是否为有效的电子邮件地址。
@Email(message = "")
private String email;
9.@Pattern:根据正则表达式校验字符串元素的格式。
@Pattern(regexp = "[a-zA-Z0-9]+")
private String userName;
10.@Size:校验集合元素个数或字符串的长度在指定范围内。
@Size(min = 3, max = 10, message = "长度在3到10之间")
private String username;
11.@Length:校验字符串元素的长度。作用于字符串。
@Length(min = 3, max = 10, message = "长度在3到10之间")
private String username;
以上只是部分注解和他们的功能,需要详细的了解需要查看源码。
1.2、用法示例
定义入参请求参数
package com.duan.pojo.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
/**
* @author db
* @version 1.0
* @description SysUserVO
* @since 2024/6/17
*/
@Data
public class SysUserVO {
@ApiModelProperty("部门ID")
private Long deptId;
@NotBlank(message = "名字不能为空")
@ApiModelProperty("用户名")
private String userName;
@NotBlank(message = "昵称不能为null和空字符串")
@ApiModelProperty("昵称")
private String nickName;
@ApiModelProperty("密码")
private String password;
@ApiModelProperty("用户性别(0男,1女")
private Integer gender;
@ApiModelProperty("手机号码")
private String phone;
@Email(message = "请填写正确的邮箱地址")
@ApiModelProperty("邮箱")
private String email;
@ApiModelProperty("头像地址")
private String avatarName;
@ApiModelProperty("用户类型(0管理员,1普通用户")
private Integer userType;
@ApiModelProperty("状态:1启用、0禁用")
private Integer status;
@ApiModelProperty("备注")
private String remark;
}
定义mapper
package com.duan.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.duan.pojo.SysUser;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<SysUser> {
}
定义接口
package com.duan.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.duan.pojo.SysUser;
public interface UserService extends IService<SysUser> {
void AddUser(SysUserVO sysUserVO);
}
定义接口实现
package com.duan.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.duan.mapper.UserMapper;
import com.duan.pojo.SysUser;
import com.duan.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author db
* @version 1.0
* @description UserServiceImpl
* @since 2024/4/15
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public void AddUser(SysUserVO sysUserVO) {
SysUser sysUser = new SysUser();
BeanUtils.copyProperties(sysUserVO,sysUser);
userMapper.insert(sysUser);
}
}
定义controller
package com.duan.controller;
import com.duan.pojo.ResponseResult;
import com.duan.pojo.Result;
import com.duan.pojo.SysUser;
import com.duan.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
* @author db
* @version 1.0
* @description UserController
* @since 2024/4/15
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/addUser")
public ResponseResult addUser(@RequestBody @Validated SysUserVO sysUserVO){
userService.AddUser(sysUserVO);
return ResponseResult.okResult();
}
}
1.3、示例测试
调用增加用户接口
注意:我们需要捕获一下 MethodArgumentNotValidException
,才能如上图显示的那样。
1.4、嵌套对象的验证
在SysUserVO
中增加一个address
的校验,即需要对嵌套对象进行校验。
package com.duan.pojo.vo;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* @author db
* @version 1.0
* @description AddressVO
* @since 2024/6/17
*/
@Data
public class AddressVO {
@NotBlank(message = "省份不能为空")
private String province;
@NotBlank(message = "城市不能为空")
private String city;
}
AddressVO
对象如下所示:
package com.duan.pojo.vo;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* @author db
* @version 1.0
* @description AddressVO
* @since 2024/6/17
*/
@Data
public class AddressVO {
@NotBlank(message = "省份不能为空")
private String province;
@NotBlank(message = "城市不能为空")
private String city;
}
测试
说明:为了能够进行嵌套对象验证,必须手动在
SysUserVO
实体的addressVo
字段上明确指出这个字段里面的实体需要验证,由于@Vaildated
不能作用在成员属性上,而且@Valid
能加在成员属性上,同时配合controller
中在方法参数上@Validated
或@Valid
来进行嵌套验证。
这里必须要说明一下@Validated
和@Valid
的区别
- 来源
- @Validated:Spring 框架特有的注解,是标准 JSR-303 的一个变种,提供了一个分组功能
- @Valid:标准 JSR-303 规范的标记型注解。
- 注解位置
- @Validated:作用在类上、方法上、方法参数上,不能作用于成员属性上。
- @Valid:方法、构造函数、方法参数、成员属性上。
- 分组
- @Validated:支持分组验证。
- @Valid:支持标准的 Bean 验证功能,不支持分组验证。
- 嵌套验证
- @Validated:不支持嵌套验证。
- @Valid:支持嵌套验证。
二、分组验证
同一个应用中,会出现不同的场景,比如:用户创建、用户更新、用户删除,针对不同的场景,有些字段在一个场景中需要验证,但是在另一个场景中该字段就不需要验证,我们可以选择新建不同的实体类去解决这类问题,比如:用户创建 UserCreateVO
、用户更新 UserUpdate
等,但是这样的做法会造成类的膨胀、代码的冗余。其实我们可以使用分组校验有选择的执行特定组的参数校验。定义分组校验需要注意两点:
- 定义分组必须使用接口。
- 要校验的字段必须加上分组,分组只对指定分组生效,不加分组不校验。
2.1、创建分组
创建两个分组接口,标识不同的业务场景CreateGroup用于创建时指定的分组
package com.duan.validatedGroup;
/**
* @author db
* @version 1.0
* @description CreateUserGroup
* @since 2024/6/24
*/
public interface CreateUserGroup {
}
UpdateGroup
用于更新时指定的分组
package com.duan.validatedGroup;
/**
* @author db
* @version 1.0
* @description UpdateUserGroup
* @since 2024/6/24
*/
public interface UpdateUserGroup {
}
2.2、使用分组校验
分组校验是通过在验证注解上指定 groups
属性来实现的。这个属性允许你为验证规则分配一个或多个验证组。假设用户创建时不传递用户ID
,其余的参数必传,用户更新接口必须传递用户ID
,可以不传递用户名,其他参数必须传递。
package com.duan.pojo.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import com.duan.validatedGroup.CreateUserGroup;
import com.duan.validatedGroup.UpdateUserGroup;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* @author db
* @version 1.0
* @description SysUserVO
* @since 2024/6/17
*/
@Data
public class SysUserVO {
@NotBlank(message = "请选择用户",groups = UpdateUserGroup.class)
private Long id;
@ApiModelProperty("部门ID")
private Long deptId;
@NotBlank(message = "名字不能为空",groups = CreateUserGroup.class)
@ApiModelProperty("用户名")
private String userName;
@NotBlank(message = "昵称不能为null和空字符串")
@ApiModelProperty("昵称")
private String nickName;
@ApiModelProperty("密码")
private String password;
@ApiModelProperty("用户性别(0男,1女")
private Integer gender;
@ApiModelProperty("手机号码")
private String phone;
@Email(message = "请填写正确的邮箱地址")
@ApiModelProperty("邮箱")
private String email;
@ApiModelProperty("头像地址")
private String avatarName;
@ApiModelProperty("用户类型(0管理员,1普通用户")
private Integer userType;
@ApiModelProperty("状态:1启用、0禁用")
private Integer status;
@ApiModelProperty("备注")
private String remark;
@NotNull(message = "请输入地址信息")
@Valid
private AddressVO addressVO ;
}
2.3、在Controller中使用分组
使用@Validated
注解,并指定要执行的验证。
package com.duan.controller;
import com.duan.pojo.ResponseResult;
import com.duan.pojo.Result;
import com.duan.pojo.SysUser;
import com.duan.pojo.vo.SysUserVO;
import com.duan.service.UserService;
import com.duan.validatedGroup.CreateUserGroup;
import com.duan.validatedGroup.UpdateUserGroup;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
* @author db
* @version 1.0
* @description UserController
* @since 2024/4/15
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/addUser")
public ResponseResult addUser(@RequestBody @Validated(value = CreateUserGroup.class) SysUserVO sysUserVO){
userService.addUser(sysUserVO);
return ResponseResult.okResult();
}
@PostMapping("/updateUser")
public ResponseResult updateUser(@RequestBody @Validated(value = UpdateUserGroup.class) SysUserVO sysUserVO){
userService.updateUser(sysUserVO);
return ResponseResult.okResult();
}
}
2.4、测试
- 创建用户接口
username 不传值,即不满足 username 不能为空的条件,应该校验不通过。通过测试发现,会提示username不能为空。
- 更新用户update接口
id写成0,username
还是为空,只是报了id
不能小于1
三、自定义验证注解
在项目开发中,我们也经常使用自定义注解去完成字段校验。自定义校验注解步骤如下:
- 编写一个自定义校验注解
- 编写一个自定义的校验器
- 关联自定义的校验器和自定义的校验注解
假如:user
实体中的password
字段,格式是大于八位且包含数字大写字母小写字母,这个自定义校验怎么实现呢?
1、首先定义一个注解PasswordValid
package com.duan.anno;
import com.duan.config.PasswordValidValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Constraint(validatedBy = { PasswordValidValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface PasswordValid {
String message() default "{密码格式不对}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
2、定义一个校验器PasswordValidValidator
自定义校验器需要实现 ConstraintValidator<A extends Annotation, T>
这个接口,第一个泛型是校验注解,第二个是参数类型。
package com.duan.config;
import com.duan.anno.PasswordValid;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* @author db
* @version 1.0
* @description PasswordValidValidator
* @since 2024/6/25
*/
public class PasswordValidValidator implements ConstraintValidator<PasswordValid,String> {
@Override
public void initialize(PasswordValid constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return false;
}
boolean hasUppercase = value.chars().anyMatch(Character::isUpperCase);
boolean hasLowercase = value.chars().anyMatch(Character::isLowerCase);
boolean hasDigit = value.chars().anyMatch(Character::isDigit);
return value.length() >= 8 && hasUppercase && hasLowercase && hasDigit;
}
}
3、关联自定义的校验器和自定义的校验注解
当你使用 @Constraint(validatedBy = EnumValidator.class)
注解时,Java Bean Validation
的实现框架会自动发现并注册相应的验证器。
@Constraint(validatedBy = { PasswordValidValidator.class})
在SysUserVO
中使用
@PasswordValid(groups = CreateUserGroup.class)
@ApiModelProperty("密码")
private String password;
模拟输入password
为纯数字时,校验不通过。
代码地址:https://gitee.com/duan138/practice-code/tree/master/paramValidated
四、总结
本文我们了解和实践在 SpringBoot
项目中,怎么去使用基本的校验注解、嵌套校验、分组校验,同时又学习了怎么使用自定义校验注解,在项目中合理地使用相关校验注解,可以简化代码、提高代码可读性和可维护性。
改变你能改变的,接受你不能改变的,关注公众号:程序员康康,一起成长,共同进步。