SpringBootValidataion
Spring Boot Bean Validator
springboot 简单验证
Maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
命名规则(Since Spring Boot 1.4):Spring Boot 大多数情况采用
starter(启动器,包含一些自动装配的Spring 组件),官方的命名规则:spring-boot-starter-{name},业界或者民间:{name}-spring-boot-starter
实体类 User
public class User { @Max(value = 10000) private long id; @NotNull private String name; // 卡号 -- GUPAO-123456789 private String cardNumber; // get&set }控制器
@RestController public class UserController { @PostMapping("/user/save") public User save(@Valid @RequestBody User user) { return user; } }postman 测试
id<10000 && name==null,400

id > 10000 && name != null,400

id < 1000 && name != null,200

常用验证技术
Spring Assert API
// Spring API 的方式
Assert.hasText(user.getName(), "name can not be null");
Java assert 断言
// JVM 断言
assert user.getId() <= 10000;
SpringMVC Interceptor
public class UserControllerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 校验逻辑,检验通过返回true,否则返回false
return true;
}
}
Filter
使用 doFilter 进行参数校验
以上方式的缺点,耦合了业务逻辑,虽然可以通过HandlerInterceptor 或者Filter做拦截,但是也是非常恶心的;
还可以通过 AOP 的方式,也可以提升代码的可读性。
以上方法都有一个问题,不是统一的标准。
自定义 Bean Validation
需求说明
通过员工的卡号来校验,需要通过工号的前缀和后缀来判断
- 前缀必须是 "GUPAO-"
- 后缀必须是数字
需要通过 Bean Validator 检验
实现步骤
复制成熟 Bean Validation Annotation的模式,自定义
@ValidCardNumber注解
@Target(FIELD) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {}) public @interface ValidCardNumber { }参考和理解
@Constraint是一个注解,里面有一个 validateBy 方法,接收一个 ConstraintValidator 实现类

实现
ConstraintValidator接口
ConstraintValidator 接口泛型有两个参数,第一个是注解类型,第二个是要校验的参数类型
public class ValidCardNumberConstraintValidator implements ConstraintValidator<ValidCardNumber, String> { @Override public void initialize(ValidCardNumber constraintAnnotation) { } @Override public boolean isValid(String value, ConstraintValidatorContext context) { // 前半部分和后半部分 String[] parts = StringUtils.split(value, "-"); // 为什么不用 String#split 方法,原因在于该方法使用了正则表达式 // 其次是 NPE 保护不够 // 如果在依赖中,没有 StringUtils.delimitedListToStringArray API 的话呢,可以使用 // Apache commons-lang StringUtils // JDK 里面 StringTokenizer(不足类似于枚举 Enumeration API) if (ArrayUtils.getLength(parts) != 2) { return false; } String prefix = parts[0]; String suffix = parts[1]; boolean isValidPrefix = Objects.equals(prefix, "GUPAO"); boolean isValidNumber = StringUtils.isNumeric(suffix); return isValidPrefix && isValidNumber; } }将实现
ConstraintValidator接口 定义到@Constraint#validatedBy@Target({FIELD}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {ValidCardNumberConstraintValidator.class}) public @interface ValidCardNumber { }使用 postman 测试

提示 constraing does not contain a message partner,所以需要添加
给
@ValidCardNumber添加message参数可以直接添加在注解中给出默认值,也可以在使用时指定
@Target({FIELD}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {ValidCardNumberConstraintValidator.class}) public @interface ValidCardNumber { String message() default "{卡号必须以 \"GUPAO\" 开头,以数字结尾}"; }然后再次测试,提示 constraing does not contain a groupts partner,所以需要添加

给
@ValidCardNumber添加groups、payload参数都添加后,如下所示
@Target({FIELD}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {ValidCardNumberConstraintValidator.class}) public @interface ValidCardNumber { String message() default "{卡号必须以 \"GUPAO\" 开头,以数字结尾}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; }使用 postman 再次测试
cardNumber=GUPAO,输入无效,校验不通过

cardNumber=GUPAO-1234,校验通过

发现自定义校验规则生效了
国际化配置
在
@NotNull的 message() 中有这样一句描述 ”javax.validation.constraints.NotNull.message“
全局搜索后发现,在
hibernate-validator\6.0.17.Finaljar包下面有个ValidationMessages_zh_CN.properties配置文件,所以需要同样配置
配置步骤:
修改
@ValidCardNumber中 message() 描述内容@Target({FIELD}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {ValidCardNumberConstraintValidator.class}) public @interface ValidCardNumber { String message() default "{com.gupao.bean.validation.invalid.card.number.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; }在
src/main/resources目录下,新增ValidationMessages_zh_CN.properties和ValidationMessages.properties配置文件
文件内容:
ValidationMessages
com.gupao.bean.validation.invalid.card.number.message=the card number must start with "GUPAO-" and end with a series of numbers.ValidationMessages
# 卡号必须以 "GUPAO-" 开头,以数字结尾 com.gupao.bean.validation.invalid.card.number.message=\u5361\u53F7\u5FC5\u987B\u4EE5 "GUPAO-" \u5F00\u5934\uFF0C\u4EE5\u6570\u5B57\u7ED3\u5C3E
使用 postman 测试


源码分析
org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree#validateSingleConstraint

问答部分
JSON校验如何办?
答:尝试变成 Bean 的方式
实际中 很多参数都要校验 那时候怎么写 这样写会增加很多类
答:确实会增加部分工作量,大多数场景,不需要自定义,除非很特殊情况。
Bean Validation 的主要缺点,单元测试不方便
如果前端固定表单的话,这种校验方式很好。但是灵活性不够,如果表单是动态的话,如何校验呢?
答: 表单字段与 Form 对象绑定即可,再走 Bean Validation 逻辑
<form action="" method="POST" command="form"> <input value="${form.name}" /> ... <input value="${form.age}" /> </form>一个接一个验证,责任链模式(Pipeline):
field 1-> field 2 -> field 3 -> compute -> result
如何自定义,返回格式?如何最佳实现?
答:可以通过REST来实现,比如 XML 或者 JSON 的格式(视图)
面试的看法
答:具备一定的水平
不该问的不要问,因为面试官的水平可能还不及于你!