SpringBoot对表单数据进行复杂校验
HashMap节点删除, 我打算拖一下, 因为红黑树删除那一块搞起来, 又要画图又要举例子, 还是挺费事的. 这篇就水一下, 讲一下自定义校验的方法.
一般情况下, 用户提交的表单数据的校验往往是逻辑相关的, 如果我输入的年龄为20, 但是身份证出生日和这个年龄不符那么这种情况下似乎不能使用已有注解进行校验. 公司很多代码都是写在接口, 或者业务层进行校验. 这好吗? 肯定是不好啊. 况且springboot支持自定义校验注解.
校验一个表单对象
通常我们校验一个表单会加上如下的一些注解, @NotBlank, @Min, @NotNull
等等. 但是这些属性都是单独校验的, 彼此之间没有关系. 虽然这已经解决了大部分的校验问题. 但是单独依靠工具提供的校验注解, 总是有捉襟见肘的时候.
@Data
@Slf4j
public class UserInfoForm {
@NotBlank(message = "叫撒子")
private String username;
@Min(value = 0, message = "年龄值不能小于0")
@Max(value = 500, message = "年龄值过大, 还想再活500年")
private Integer age;
@NotNull(message = "不是男人就是娘炮, 选一个")
private Boolean man;
}
自定义一个注解
我们可以参考@NotBlank去定义一个我们自己的校验注解.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Constraint(validatedBy = UserInfoFormValidator.class)
public @interface UserInfoFormValidate {
String message() default "";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
定义这样一个自定义注解必须要包含以下内容:
- message 这个是校验规则触发对应的消息
- groups 指明在什么条件下触发
- payload 允许定义要通过此验证传递的有效负载 (这个不是很清楚作用是什么)
- Constraint这个注解用于指明实际实现校验逻辑的类, 注意这个类要实现ConstraintValidator接口
Target这里我指明该注解用于类型上.
注意这个注解要加载UserInfoForm
上
@UserInfoFormValidate
public class UserInfoForm {
...............
}
校验逻辑
UserInfoFormValidator
具体的校验逻辑如下:
@Slf4j
public class UserInfoFormValidator implements ConstraintValidator<UserInfoFormValidate, UserInfoForm> {
@Override
public boolean isValid(UserInfoForm userInfoForm, ConstraintValidatorContext constraintValidatorContext) {
if (userInfoForm.getMan() && userInfoForm.getAge() < 20) {
constraintValidatorContext.buildConstraintViolationWithTemplate("年龄小于20岁不是男人.").addConstraintViolation();
return false;
}
return true;
}
@Override
public void initialize(UserInfoFormValidate constraintAnnotation) {
}
}
constraintValidatorContext.buildConstraintViolationWithTemplate
设置isValid为false的情况下的message. 如果校验通过直接返回true.
controller接口
@PostMapping("/test")
public void save(@RequestBody @Valid UserInfoForm userInfoForm) {
log.info("表单数据: {}", userInfoForm);
}
有了这些还不够, 为了更友好的返回给前端提示, 因此我们需要使用@ControllerAdvice
对参数校验异常进行处理. 这里的方法可以参考下面的代码片段, 设计具体实现就不在详述.
@RestControllerAdvice
public class ExceptionCatchConfig {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public BaseResponse methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
return BaseResponse.buildValidFail(e.getBindingResult());
}
}
验证
- 单个属性的校验
比如username输入为空返回消息提示为: - 表单实体的验证
当输入年龄为10, man为true, 则返回提示消息为:
问题
以上的方法还不足以让校验按照我们想要的顺序进行(先按照单个参数进行校验, 之后进行相关逻辑校验)
如果man参数前端没有传过来, 由于先进行表单类的就会导致空指针出现.
这个时候有两种处理方式:
1.所有校验都在自定义校验类中进行处理.
2.指定校验顺序. 其实我推荐使用第一种方法进行校验.
因为这样代码结构比较清晰. 相反第二方式, 如果你忽略了某个属性的校验, 很容易导致NPE.
但是第二种方式还是要提及一下的.
下面我们还是以上面的为例, 探讨如何解决这个空指针问题.
groups
先回头看一下自定义注解中的groups. 另外补充一个注解GroupSequence
这个注解可以用于定义字段的校验顺序
我们先定义几个接口, 用来表示校验的顺序, 接口可以随意定义.
public interface SingleGroup {
interface First{};
interface Second{};
interface Third{};
}
public interface ComplexGroup {
}
下面看一下具体的使用方法. 在需要进行先进行校验的属性上添加@GroupSequence
中顺序在前的属性, 可以参考如下示例.
/**
* @author NikolaZhang
*/
@Data
@Slf4j
@GroupSequence({SingleGroup.First.class, SingleGroup.Second.class, ComplexGroup.class, UserInfoForm.class})
@UserInfoFormValidate(groups = {ComplexGroup.class})
public class UserInfoForm {
@NotBlank(message = "叫撒子", groups = {SingleGroup.Second.class})
private String username;
@Min(value = 0, message = "年龄值不能小于0")
@Max(value = 500, message = "年龄值过大, 还想再活500年")
private Integer age;
@NotNull(message = "不是男人就是娘炮, 选一个", groups = {SingleGroup.First.class})
private Boolean man;
}
重新调用接口结果如下:
问题解决.
总结
这一节介绍了如何使用自定义注解进行表单中相关逻辑字段的校验. 以及如何通过groups控制校验的顺序.