浅谈SpringMVC之数据校验

浅谈SpringMVC之数据绑定 已经介绍了如何获取数据,本文就来介绍一下 SpringMVC 中如何对数据进行校验。

认识 Validation

自 Spring3.0 开始,SpringMVC 提供了对 Java校验API ( Java Validation AP , 又称 JSR-303 ) 的支持,JSR303 是一个标准, 一般会使用它的实现,如 hibernate-validator

BTW : 当前已更新至 JSR-380

若使用 springboot ,则 spring-boot-starter-web 已包含,本文部分基于 springboot,存在差异( 6.0+ 后包结构改变)。

1
2
3
4
5
6
<!-- hibernate-validator (自动依赖 validation-api) -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.1.1.Final</version>
</dependency>

校验注解

这里介绍一下 javax.validation.constraints 所定义的常用注解,来自 validation-api

hibernate-validator 下依然存在许多未过时的校验注解,这里不会介绍。

常用校验注解 说明
@AssertFalse 限制必须为false。
@AssertTrue 限制必须为true。
@DecimalMax(value) 限制必须为一个不大于指定值的数字。
@DecimalMin(value) 限制必须为一个不小于指定值的数字。
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction。
@Future 限制必须是一个将来的日期。
@Max(value) 限制必须为一个不大于指定值的数字。
@Min(value) 限制必须为一个不小于指定值的数字。
@NotBlank 验证注解的元素值不为空(不为null且去除首尾空格后长度为0),不同于@NotEmpty@NotBlank只应用于字符串且在比较时会去除字符串的空格。
@NotEmpty 验证注解的元素值不为 null 且不为空(字符串长度不为0、集合大小不为0)。
@NotNull 限制必须不为 null 。
@Null 限制只能为 null 。
@Size(max,min) 限制字符长度必须在 min 到 max 之间。

@Valid 与 @Validated

一般校验注解会作用于 pojo类,而使用 @Valid@Validated 可以通知 Spring 进行校验,校验结果默认以异常抛出或使用 BindingResult 接收。

触发校验注解 提供者 说明
@Valid validation-api 标记用于验证级联的属性,方法参数或方法返回类型,并且递归应用。符合 JSR303。
@Validated spring-context 对 @Valid 的特殊扩展,支持验证组 。支持 JSR303 ,特殊除外。

使用 Validation

简单示例

pojo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package tk.gushizone.web.validation.controller.dto;

import lombok.Data;

import javax.validation.constraints.NotNull;

@Data
public class ValidParam {

@NotNull(message = "id不能为空")
private Integer id;

private String name;
}

*Controller.java

在 Spring 中使用 @Valid@Validated ,校验结果信息会被保存到 BindingResult 中。

1
GET /mvc/validation/valid?name=Foo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@GetMapping("/valid")
public String valid(@Valid ValidParam param, BindingResult bindingResult) {

if (bindingResult.hasErrors()){
return fieldErrors(bindingResult).toString();
}

return "OK";
}

private Map<String, String> fieldErrors(BindingResult bindingResult) {
if (!bindingResult.hasErrors()) {
return Maps.newHashMap();
}
Map<String, String> map = Maps.newHashMapWithExpectedSize(bindingResult.getFieldErrors().size());
for (FieldError error : bindingResult.getFieldErrors()) {
map.put(error.getField(), error.getDefaultMessage());
}
return map;
}

相关提示默认会做国际化兼容,自定义信息需要自己提供。

1
{id=id不能为空!}

BindingResult

除了可以获取验证结果,还可以通过 BindingResult 直接注册错误。

1
GET /mvc/validation/binding-result?name=admin
1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping("/binding-result")
public String bindingResult(ValidatedParam param, BindingResult bindingResult) {

if ("admin".equals(param.getName())) {
bindingResult.rejectValue("name", "nonname", "此名称不允许使用!");
}

if (bindingResult.hasErrors()){
return fieldErrors(bindingResult).toString();
}

return "OK";
}
1
{name=此名称不允许使用!}

校验组

定义校验组

校验组接收 Class类,这里以接口为例,利于扩展。

1
2
3
4
// 编辑校验组
public interface EditValidateGroup {}
// 删除校验组
public interface DeleteValidateGroup {}

使用校验组

pojo.java

pojo 同时用于编辑校验和删除校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package tk.gushizone.web.validation.controller.dto;

import lombok.Data;
import tk.gushizone.web.validation.groups.DeleteValidateGroup;
import tk.gushizone.web.validation.groups.EditValidateGroup;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

@Data
public class ValidatedParam {

@NotNull(message = "id为空", groups = {EditValidateGroup.class, DeleteValidateGroup.class})
private Integer id;

@NotEmpty(message = "name不能为空", groups = EditValidateGroup.class)
private String name;

}

*Controller.java

校验时只使用删除校验组。

1
DELETE /mvc/validation/validated
1
2
3
4
5
6
7
8
9
@DeleteMapping("/validated")
public String validated(@Validated(DeleteValidateGroup.class) ValidatedParam param, BindingResult bindingResult) {

if (bindingResult.hasErrors()){
return fieldErrors(bindingResult).toString();
}

return "OK";
}
1
{id=id不能为空!}

自定义 Validation

我们也可以自定义校验规则来满足复杂需求,只需要定义一个校验注解并实现对应的校验实现逻辑类。这里通过一个简单示例来演示一下。

自定义校验注解

使用 @Constraint 来指定校验逻辑类。

也有其他方式,但这种方式最简单直接。

必要属性 说明
message() 校验提示信息。校验不通过的提示信息。
groups() 校验组。如果有多个校验公用一个 POJO ,可以通过校验组进行区分。
payload() 校验错误级别。类日志的错误级别。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package tk.gushizone.web.validation.constraints;


import tk.gushizone.web.validation.validator.LengthValidator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@Constraint(validatedBy = {LengthValidator.class})
public @interface Length {

String fieldName() default "";

int min() default 0;

int max() default 20;

int length() default 0;

String message() default "lengthCheck不通过";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}

实现校验逻辑类

校验逻辑实现类需要实现 ConstraintValidator 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package tk.gushizone.web.validation.validator;

import tk.gushizone.web.validation.constraints.Length;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class LengthValidator implements ConstraintValidator<Length, Object> {

private String message;

private int min;

private int max;

private int length;

@Override
public void initialize(Length constraintAnnotation) {
int max = constraintAnnotation.max();
int min = constraintAnnotation.min();
int length = constraintAnnotation.length();
if (min > 0) {
this.min = min;
}
if (max > 0) {
this.max = max;
}
}

@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
int strLength = value.toString().length();

return strLength >= min && strLength <= max;
}
}

使用示例

*pojo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package tk.gushizone.web.validation.controller.dto;

import lombok.Data;
import tk.gushizone.web.validation.constraints.Length;

import javax.validation.constraints.NotNull;

@Data
public class ValidatorParam {

private Integer id;

@Length(min = 6)
private String name;
}

*Controller.java

1
GET /mvc/validation/validator?name=foo
1
2
3
4
5
6
7
8
9
@GetMapping("/validator")
public String validator(@Validated ValidatorParam param, BindingResult bindingResult) {

if (bindingResult.hasErrors()){
return fieldErrors(bindingResult).toString();
}

return "OK";
}
1
{name=lengthCheck不通过}

独立校验

如之前所说, Java Validation API 是独立的,可以脱离 springmvc 使用。这里展示一个简单的示例。

ValidationUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package tk.gushizone.web.validation.util;

import com.google.common.collect.Lists;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.List;
import java.util.Set;

public class ValidationUtils {

public static Validator factory(){
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
return factory.getValidator();
}

public static <T> List<String> getFieldErrorMessages(T object, Class<?>... groups) {
List<String> messages = Lists.newArrayList();

Validator validator = factory();
Set<ConstraintViolation<T>> validate = validator.validate(object, groups);
validate.forEach( e -> messages.add(e.getMessage()));
return messages;
}

}

ValidationTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package tk.gushizone.web.validation;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import tk.gushizone.web.validation.controller.dto.ValidatedParam;
import tk.gushizone.web.validation.groups.EditValidateGroup;
import tk.gushizone.web.validation.util.ValidationUtils;

import java.util.List;

@Slf4j
public class ValidationTest {

@Test
public void test() {
List<String> errorMessages = ValidationUtils.getFieldErrorMessages(new ValidatedParam(), EditValidateGroup.class);
log.warn("errorMessages : {}", errorMessages);
}
}
1
errorMessages : [id为空, name不能为空]