当我们想提供可靠的 API 接口,对参数的校验,以保证最终数据入库的正确性,是 必不可少 的活。比如下图就是 我们一个项目里 新增一个菜单校验 参数的函数,写了一大堆的 if else 进行校验,非常的不优雅,比起枯燥的 CRUD 来说,参数校验更是枯燥。
这只是一个创建菜单的校验,只需要判断菜单,菜单 url 以及菜单的父类 id 是否为空,上级菜单是否挂载正确,这样已经消耗掉了 30,40 行代码了,更不要说,管理后台这种参数贼多的接口。估计要写几百行校验代码了。
if(mapper.get("customerId") == null){
return RespBean.error("customerId is null!");
}
if(mapper.get("name") == null){
return RespBean.error("name is null!");
}
if(mapper.get("userName") == null){
return RespBean.error("userName is null!");
}
//查询条件 判断用户名是否重复
Map args = new HashMap<>();
args.put("userName",mapper.get("userName").toString());
args.put("customerId",mapper.get("customerId").toString());
Integer findNum = userService.findUserNameIsExist(args);
if(!Objects.equals(findNum, 0)){ //查询结果不为0
return RespBean.error("用户名重复,请重新输入!");
}
int ret = userService.addUser(mapper);
if(ret >= 0){
return RespBean.ok("UserController add user success");
}else{
return RespBean.error("UserController add User ailed");
}
在开始入门之前,我们先了解下本文可能会涉及到的注解。javax.validation.constraints 包下,定义了一系列的约束( constraint )注解。共 22 个,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HMb3Jaa9-1690357601543)(file:///C:\Users\ADMINI~1\AppData\Local\Temp\ksohtml16688\wps1.jpg)]
大致可以分为以下几类:
@NotBlank :只能用于字符串不为 null ,并且字符串 #trim() 以后 length 要大于 0 。
@NotBlank(message = "文本不能为空")
private String text;
@NotEmpty :集合对象的元素不为 0 ,即集合不为空,也可以用于字符串不为 null 。
@NotEmpty(message = "密码不能为空")
private String password;
@NotNull :不能为 null 。
@NotNull(message = "用户名不能为null")
private String userName;
@Null :必须为 null 。
@Null(message = "精度必须为null")
private Double jd;
@DecimalMax(value) :被注释的元素必须是一个数字,其值必须小于等于指定的最大值。
@DecimalMax(value = "50.5",message = "number1必须小于等于50.5")
private Double number1;
@DecimalMin(value) :被注释的元素必须是一个数字,其值必须大于等于指定的最小值。
@DecimalMin(value = "100.6",message = "number2必须大于等于100.6")
private Double number2;
@Max(value) :该字段的值只能小于或等于该值。不支持小数位判断
@Max(value = 100,message = "number7必须小于等于100")
private Long number7;
@Min(value) :该字段的值只能大于或等于该值。 不支持小数位判断
@Min(value = 60,message = "number6必须大于等于60")
private Integer number6;
@Digits(integer, fraction) :被注释的元素必须是一个数字,其值必须在可接受的范围内。
@Digits(integer = 4,fraction = 3,message = "整数位上限4,小数位上限3")
private Double number3;
@Positive :判断正数。
@Positive(message = "number4必须为正数")
private Double number4;
@PositiveOrZero :判断正数或 0 。
@PositiveOrZero(message = "number5必须为正数或0")
private Double number5;
@Negative :判断负数。
@Negative(message = "number8必须为负数")
private Double number8;
@NegativeOrZero :判断负数或 0 。
@NegativeOrZero(message = "number9必须为负数或0")
private Double number9;
@AssertFalse :被注释的元素必须为 true 。
@AssertTrue(message = "b1只能为true")
private Boolean b1;
@AssertTrue :被注释的元素必须为 false 。
@AssertFalse(message = "b2只能为false")
private Boolean b2;
@Size(max, min) :检查该字段的 size 是否在 min 和 max 之间,可以是字符串、数组、集合、Map 等。
@Size(max = 5,min = 2,message = "字符串长度在2-5之间")
private String str;
@Future :被注释的元素必须是一个将来的日期。
@Future
private Date date1;
@FutureOrPresent :判断日期是否是将来或现在日期。
@FutureOrPresent
private Date date2;
@Past :检查该字段的日期是在过去。
@Past
private Date date3;
@PastOrPresent :判断日期是否是过去或现在日期。
@PastOrPresent
private Date date4;
@Email :被注释的元素必须是电子邮箱地址。
@Email
private String email;
@Pattern(value) :被注释的元素必须符合指定的正则表达式。注解需要传的参数:一般默认就填入正则表达式正则表达式即可,但是java中字符串需要转义
@Pattern(regexp = "^\\d{15}|\\d{18}$", message = "身份证号码在15-18位")
private String cardNum;
org.hibernate.validator.constraints 包下,定义了一系列的约束( constraint )注解。如下:
@Range(min=, max=) :被注释的元素必须在合适的范围内。只判断整数位数值
@Range(min = 10,max = 20,message = "数值范围在10-20之间")
private Double range;
@Length(min=, max=) :被注释的字符串的大小必须在指定的范围内。
@Length(min = 20,max = 25,message = "字符串长度在20-25之间")
private String str1;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ctYx6pQh-1690357601545)(file:///C:\Users\ADMINI~1\AppData\Local\Temp\ksohtml16688\wps2.jpg)]
@Valid 注解,是 Bean Validation 所定义,可以添加在普通方法、构造方法、方法参数、方法返回、成员变量上,表示它们需要进行约束校验。
@Validated 注解,是 Spring Validation 锁定义,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验。同时,@Validated 有 value 属性,支持分组校验。属性如下:
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
/**
* Specify one or more validation groups to apply to the validation step
* kicked off by this annotation.
* JSR-303 defines validation groups as custom annotations which an application declares
* for the sole purpose of using them as type-safe group arguments, as implemented in
* {@link org.springframework.validation.beanvalidation.SpringValidatorAdapter}.
*
Other {@link org.springframework.validation.SmartValidator} implementations may
* support class arguments in other ways as well.
*/
Class>[] value() default {};
}
对于初学的来说,很容易搞混 @Valid(javax.validation 包下) 和 @Validated (org.springframework.validation.annotation 包下)注解。两者大致有以下的区别:
名称 | Spring validation是否实现了声明式检验 | 是否支持嵌套校验 | 是否支持分组校验 |
---|---|---|---|
@Validated | 是 | 否 | 是 |
@Valid | 否 | 是 | 否 |
总的来说,绝大多数场景下,我们使用 @Validated 注解即可。而在有嵌套校验的场景,我们使用 @Valid 注解添加到成员属性上。
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework
spring-aspects
org.projectlombok
lombok
provided
com.github.xiaoymin
knife4j-spring-boot-starter
3.0.3
首先,需要在实体类的相关字段上添加需要校验的注解。
public class User {
private String id;
@NotBlank(message = "密码不能为空")
private String password;
@Min(value = 18,message = "未成年禁止入内")
private Integer age;
}
其次,在controller层的方法的要校验的参数上添加@Valid注解
@PostMapping("/action/register")
public Result registerByForm(@Valid @RequestBody User user){
return userService.register(user);
}
嵌套功能的使用
在检验 Country对象的 provinces字段时,在provinces字段上添加 @Valid 注解后,就可以检验 list 中的 provinces的属性是否符合要求;
否则只会检验 citys的集合大小是否大于1,不会校验集合中的 citys对象,比如 citys对象的 name 是否符合要求。
@GetMapping (value = "/test1")
public RespBean test1(@RequestBody @Valid Country country) {
log.info("country:"+country);
return RespBean.sucess();
}
@Data
public class Country {
@NotNull(message = "国家id不能为null")
private Long countryId;
@Valid
@Size(min = 1,max = 2,message = "省份数量在1-2之间")
private List provinces;
@Size(min = 1,message = "城市大于1")
private List citys;
}
@Data
public class Province {
@NotNull(message = "省份id不能为null")
private Long id;
@NotBlank(message = "省份名称不能为空")
private String name;
}
@Data
public class City {
@NotNull(message = "城市id不能为null")
private Long id;
@NotBlank(message = "城市名称不能为空")
private String name;
}
@Validated 是 @Valid 的一次封装,在@Valid的基础上增加了分组以及验证排序的功能。
分组功能的使用
当一个实体类需要多种验证方式时,比如:添加时需要对姓名进行非空验证,修改时需要对id进行验证,而添加时就不需要对id进行验证。
首先,定义两个分组的接口:
public interface Add{
}
public interface Update{
}
其次,在实体类上使用@Validated的分组功能。
@Data
public class Person {
@NotNull(groups = Update.class, message = "更新时候id不能为空")
private Long id;
@NotEmpty(groups = Add.class, message = "姓名不能为空")
private String name;
}
在controller中,使用分组进行接口验证。
@RestController
@Slf4j
public class VerifyController {
@PostMapping(value = "/validated/add")
public void add(@Validated(value = Add.class) @RequestBody Person person) {
...
}
@PostMapping(value = "/validated/update")
public void update(@Validated(value = Update.class) @RequestBody Person person) {
...
}
}
异常共四种
BindException:处理所有RequestParam注解数据验证异常
MethodArgumentNotValidException:处理所有RequestBody注解参数验证异常
ConstraintViolationException
UnexpectedTypeException
@RestControllerAdvice
@Slf4j
public class GlobalDefaultExceptionHandler {
/**
* 处理所有RequestBody注解参数验证异常
* @param exception
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public RespBean methodArgumentNotValidException(MethodArgumentNotValidException exception){
//封装需要返回的错误信息
List fieldErrors = exception.getBindingResult().getFieldErrors();
log.error("参数绑定异常,ex = {}", fieldErrors.get(0).getDefaultMessage());
return RespBean.error(fieldErrors.get(0).getDefaultMessage());
}
/**
* 处理所有RequestParam注解数据验证异常
* @param e
* @return
*/
@ExceptionHandler(BindException.class)
public RespBean validationBodyException(BindException e){
e.printStackTrace();
//打印校验住的所有的错误信息
StringBuilder sb = new StringBuilder("参数错误:[");
List list = ((BindException) e).getAllErrors();
for (ObjectError item : list) {
sb.append(item.getDefaultMessage()).append(',');
}
sb.deleteCharAt(sb.length()-1);
sb.append(']');
String msg = sb.toString();
return RespBean.error(msg);
}
@ExceptionHandler(UnexpectedTypeException.class)
public RespBean unexpectedTypeException(UnexpectedTypeException exception){
//封装需要返回的错误信息
log.error("参数绑定异常,ex = {}", exception.getMessage());
return RespBean.error(exception.getMessage());
}
@ExceptionHandler(value = ConstraintViolationException.class)
public RespBean ConstraintViolationExceptionHandler(ConstraintViolationException ex) {
Set> constraintViolations = ex.getConstraintViolations();
Iterator> iterator = constraintViolations.iterator();
List msgList = new ArrayList<>();
while (iterator.hasNext()) {
ConstraintViolation> cvl = iterator.next();
msgList.add(cvl.getMessageTemplate());
}
return RespBean.error(msgList.toString());
}
}
补充:分组功能使用多个组检验数据
contoller层使用{}添加多个组别
@PostMapping(value = "/validated/del")
public RespBean dealMore(@Validated(value = {Add.class,Update.class}) @RequestBody Person person) {
return RespBean.sucess();
}
自定义注解
1-添加自定义注解类接口
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {MobileValidator.class})
public @interface MobileCheck {
boolean required() default true;
String message() default "手机号码格式有误!";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
2-实现自定义校验器类
/*
* 手机号校验器
* */
public class MobileValidator implements ConstraintValidator {
private boolean required = false;
private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}");
public static boolean isMobile(String src){
if (StringUtils.isEmpty(src)){
return false;
}
Matcher matcher = mobile_pattern.matcher(src);
return matcher.matches();
}
@Override
public void initialize(MobileCheck isMobile) {
required = isMobile.required();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (required) { //如果必须(不能为空),进行校验
return isMobile(value);
} else { //如果不必须,非空进行校验
if (StringUtils.isEmpty(value)) {
return true;
}
return isMobile(value);
}
}
}
3-使用
@Data
public class Country {
@MobileCheck
private String phone;
}
@PostMapping(value = "/checkPhone")
public RespBean checkPhone(@Valid @RequestBody Country country) {
return RespBean.sucess();
}
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (required) { //如果必须(不能为空),进行校验
return isMobile(value);
} else { //如果不必须,非空进行校验
if (StringUtils.isEmpty(value)) {
return true;
}
return isMobile(value);
}
}
}
3-使用
@Data
public class Country {
@MobileCheck
private String phone;
}
@PostMapping(value = “/checkPhone”)
public RespBean checkPhone(@Valid @RequestBody Country country) {
return RespBean.sucess();
}