springMvc中数据校验
一、简介
JSR(java specification requests)是java规范提案,其中JSR-303提供了java中bean数据检测的标准。这里介绍在spring web开发中,对参数请求进行校验。
二、数据校验
JSR(java specification requests)是java规范提案,其中JSR-303提供了java中bean数据检测的标准。使用注解对数据进行校验。
2.1 JSR-303常用校验注解
2.1.1 JSR-303常用校验注解
注解 | 功能 |
---|---|
@DecimalMax(value) | 参数为数字且小于或等于(可指定是否包含)指定的最大值 |
@DecimalMin(value) | 参数为数字且大于或等于(可指定是否包含)指定的最小值 |
@Digits(integer,fraction) | 参数为数字,可指定整数和小数部分位数 |
@Max(value) | 参数为数字且小于或等于(可指定是否包含)指定的最大值 |
@Min(value) | 参数为数字且大于或等于(可指定是否包含)指定的最小值 |
@Positive | 参数为正数 |
@PositiveOrZero | 参数为正数或等于0 |
@Negative | 参数为负数 |
@NegativeOrZero | 参数为负数或等于0 |
@Future | 参数为时间类型且大于当前时间 |
@FutureOrPresent | 参数为时间类型且大于或等于当前时间 |
@Past | 参数为时间类型且小于当前时间 |
@PastOrPresent | 参数为时间类型且小于或等于当前时间 |
参数为邮箱,可指定匹配的正则表达式 | |
@NotBlank | 参数为字符串,不能为空且不能全是空白符 |
@NotEmpty | 参数不能为空,可用于字符串、集合、hash、数组 |
@NotNull | 参数不能为null |
@Null | 参数必须为null |
@Pattern | 参数必须匹配指定正则 |
@Size(min, max) | 参数内的元素个数必须在指定范围内,可用于字符串、集合、hash、数组 |
@AssertFalse | 参数必须为false |
@AssertTrue | 参数必须为true |
2.1.2 Hibernate Validator扩展校验注解
Hibernate Validator是对JSR-303标准的一个实现,可以独立于hiberate框架而单独使用。在原标准基础上,扩展了新的校验注解,如下:
注解 | 功能 |
---|---|
@Range(min, max) | 参数必须在指定范围内,可用于数字或字符串表示的数值 |
@Length | 参数为字符串且在在指定范围内 |
@URL | 参数为url |
2.2 校验注解源码解析
这里以@Min为例查看源码:
package javax.validation.constraints;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.Min.List;
//被注解的参数必须大于或等于指定的最小值
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface Min {
//参数出错的提示消息
String message() default "{javax.validation.constraints.Min.message}";
//分组,可以基于不同的类实例进行不同的参数校验
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
//指定的最小值,被校验参数应该大于或等于
long value();
/**
* Defines several {@link Min} annotations on the same element.
*
* @see Min
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
Min[] value();
}
}
2.4 开启参数校验
开启参数校验的方式有两种,@Valid和@Validated,放在需要校验参数的前面。
2.4.1 @Valid
@Valid是JSR-303规范的校验机制,不支持分组校验,不支持在类上校验,但标识范围更广,支持嵌套校验。源码如下:
package javax.validation;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
//标识需要校验的方法、属性、构造函数、方法参数、使用类型的语句,可用于嵌套校验
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface Valid {
}
2.4.1 @Validated
@Validated是spring实现校验机制,是JSR-303的变体,支持分组校验,但不支持成员属性校验,不支持嵌套校验,源码如下:
package org.springframework.validation.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//支持类、方法、方法参数校验
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
//分组校验,根据传入类实例不同而实现不同校验策略
Class<?>[] value() default {};
}
2.5 校验结果获取
被校验参数后面添加Errors或BindingResult对象,可以获取校验结果。
2.5.1 Errors
Errors是参数校验结果获取的原始接口,通过内部方法可获取各类验证结果。源码如下:
package org.springframework.validation;
import java.util.List;
import org.springframework.beans.PropertyAccessor;
import org.springframework.lang.Nullable;
//存储验证对象的校验信息
public interface Errors {
//返回校验对象名称
String getObjectName();
//修改内置路径,默认为""
void setNestedPath(String nestedPath);
//获取内置路径
String getNestedPath();
//压入子路径
void pushNestedPath(String subPath);
//弹出子路精
void popNestedPath() throws IllegalStateException;
//添加错误
void reject(String errorCode);
//添加错误
void reject(String errorCode, String defaultMessage);
//添加错误
void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
//添加错误
void rejectValue(@Nullable String field, String errorCode);
//添加错误
void rejectValue(@Nullable String field, String errorCode, String defaultMessage);
//添加错误
void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);
//添加指定Errors的所有错误
void addAllErrors(Errors errors);
//参数是否有错
boolean hasErrors();
//错误个数
int getErrorCount();
//返回所有错误
List<ObjectError> getAllErrors();
//是否有全局错误
boolean hasGlobalErrors();
//全局错误个数
int getGlobalErrorCount();
//返回所有全局错误
List<ObjectError> getGlobalErrors();
//返回第一个全局错误
@Nullable
ObjectError getGlobalError();
//是否有字段错误
boolean hasFieldErrors();
//返回字段错误个数
int getFieldErrorCount();
//返回所有字段错误
List<FieldError> getFieldErrors();
//返回第一个字段错误
@Nullable
FieldError getFieldError();
//指定字段是否有错误
boolean hasFieldErrors(String field);
//指定字段错误个数
int getFieldErrorCount(String field);
//返回指定字段所有错误
List<FieldError> getFieldErrors(String field);
//返回指定字段第一个错误
@Nullable
FieldError getFieldError(String field);
//获取指定字段的当前值
@Nullable
Object getFieldValue(String field);
//获取指定字段类型
@Nullable
Class<?> getFieldType(String field);
}
2.5.2 BindingResult
BindingResult继承自Errors,源码如下:
package org.springframework.validation;
import java.beans.PropertyEditor;
import java.util.Map;
import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.lang.Nullable;
public interface BindingResult extends Errors {
//获取目标对象
@Nullable
Object getTarget();
//返回目标对象和验证结果组成的map
Map<String, Object> getModel();
//获取指定字段的原始值
@Nullable
Object getRawFieldValue(String field);
/**
* Find a custom property editor for the given type and property.
* @param field the path of the property (name or nested path), or
* {@code null} if looking for an editor for all properties of the given type
* @param valueType the type of the property (can be {@code null} if a property
* is given but should be specified in any case for consistency checking)
* @return the registered editor, or {@code null} if none
*/
@Nullable
PropertyEditor findEditor(@Nullable String field, @Nullable Class<?> valueType);
/**
* Return the underlying PropertyEditorRegistry.
* @return the PropertyEditorRegistry, or {@code null} if none
* available for this BindingResult
*/
@Nullable
PropertyEditorRegistry getPropertyEditorRegistry();
/**
* Resolve the given error code into message codes.
* <p>Calls the configured {@link MessageCodesResolver} with appropriate parameters.
* @param errorCode the error code to resolve into message codes
* @return the resolved message codes
*/
String[] resolveMessageCodes(String errorCode);
/**
* Resolve the given error code into message codes for the given field.
* <p>Calls the configured {@link MessageCodesResolver} with appropriate parameters.
* @param errorCode the error code to resolve into message codes
* @param field the field to resolve message codes for
* @return the resolved message codes
*/
String[] resolveMessageCodes(String errorCode, String field);
//添加错误
void addError(ObjectError error);
//添加字段值
default void recordFieldValue(String field, Class<?> type, @Nullable Object value) {
}
/**
* Mark the specified disallowed field as suppressed.
* <p>The data binder invokes this for each field value that was
* detected to target a disallowed field.
* @see DataBinder#setAllowedFields
*/
default void recordSuppressedField(String field) {
}
/**
* Return the list of fields that were suppressed during the bind process.
* <p>Can be used to determine whether any field values were targeting
* disallowed fields.
* @see DataBinder#setAllowedFields
*/
default String[] getSuppressedFields() {
return new String[0];
}
}
三、数据校验使用流程
数据校验的流程如下:
四、自定义数据校验注解
除了使用如@Min、@NotNull等已经存在的注解,还可以自定义校验注解。步骤如下:
- 自定义注解的校验类,需要实现javax.validation.ConstraintValidator接口;
- 自定义校验注解,需要使用@Constraint标记是校验注解,同时指定上一步的校验类;
通过上述步骤后,便生成了自定义的校验注解,使用同常规校验注解类似,后文有示例。
五、使用示例
这里使用spring boot构建项目测试。
4.1 添加maven依赖
添加JSR-303的推荐实现hibernate-validator包的maven依赖, 如下:
<!-- spring-boot-starter-web中包含了hibernate-validator包-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.5.Final</version>
</dependency>
注意代码中的注释,当使用spring-boot开发时,spring-boot-starter-web包中已经添加了hibernate-validator依赖,不需要再单独添加。
4.2 常规校验示例
4.2.1 校验对象定义
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.math.BigDecimal;
@Data
public class Apple {
//同一属性多注解校验
@Pattern(regexp = "\\d{4}", message = "appleId must be 4 number")
@NotNull(message = "appleId should not Null")
private String appleId;
@Length(min = 2, max = 5, message = "address should be in [2, 5]")
private String address;
@DecimalMin(message = "price should gt 0.00", value = "0.00")
private BigDecimal price;
@Max(message = "number should less 10", value = 10)
private int number;
}
4.2.2 请求方法
import com.dragon.study.simplespringstudy.bean.Apple;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.stream.Collectors;
@RestController
@RequestMapping("check")
public class NormalCheckController {
//常规校验
@RequestMapping("normalCheck")
public Object normalCheck(
@Validated Apple apple,
BindingResult br) {
if (br.hasErrors()) {
return br.getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
}
return "success";
}
}
4.2.3 测试
请求:
### 常规校验
GET http://localhost:9090/check/normalCheck?price=-0.11&number=12&address=a&appleId=01
Accept: application/json
响应:
[
"price should gt 0.00",
"number should less 10",
"address should be in [2, 5]",
"appleId must be 4 number"
]
4.3 嵌套校验示例
4.3.1 校验对象定义
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@Data
public class Banana {
@NotNull(message = "bananaId should not null")
private String bananaId;
//嵌套校验
@Valid
@NotNull(message = "farmer should not null")
private Farmer farmer;
//内部类
@Data
static class Farmer {
@NotNull(message = "farmerId should not be null")
private String farmerId;
@Length(min = 2, max = 5, message = "address should be in [2, 5]")
private String address;
}
}
4.3.2 请求方法
import com.dragon.study.simplespringstudy.bean.Banana;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.stream.Collectors;
@RestController
@RequestMapping("check")
public class NestCheckController {
//嵌套校验
@RequestMapping("nestCheck")
public Object nestCheck(
@RequestBody @Validated Banana banana,
BindingResult br) {
if (br.hasErrors()) {
return br.getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
}
return "success";
}
}
4.3.3 测试
请求:
### 嵌套校验
POST http://localhost:9090/check/nestCheck
Accept: application/json
Content-Type: application/json
{
"farmer": {
"address": 1
}
}
响应:
[
"address should be in [2, 5]",
"bananaId should not null",
"farmerId should not be null"
]
4.4 分组校验示例
4.4.1 校验对象定义
import lombok.Data;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
@Data
public class Pearl {
@NotNull(message = "pearlId should not Null")
private String pearlId;
//groups指定分组,不同组使用不同校验规则
@Min(value = 1, groups = {Red.class}, message = "red should ge 1")
@Min(value = 2, groups = {Green.class}, message = "green should ge 2")
private int weight;
//用于分组
public interface Red {
}
//用于分组
public interface Green {
}
}
4.4.2 请求方法
import com.dragon.study.simplespringstudy.bean.Pearl;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.groups.Default;
import java.util.stream.Collectors;
@RestController
@RequestMapping("check")
public class GroupCheckController {
//分组校验
@RequestMapping("groupCheck")
public Object groupCheck(
//指定分组后,只有指定分组的校验才生效,使用Default.class,则未使用分组的走默认校验
@Validated({Pearl.Green.class, Default.class}) Pearl pearl,
BindingResult br) {
if (br.hasErrors()) {
return br.getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
}
return "success";
}
}
特别注意上面的Default.class,表示默认校验规则也走。
4.4.3 测试
请求:
### 分组校验
GET http://localhost:9090/check/groupCheck?weight=1
Accept: application/json
响应:
[
"pearlId should not Null",
"green should ge 2"
]
4.5 多对象校验示例
4.5.1 校验对象示例
import javax.validation.constraints.NotNull;
public class Peach {
@NotNull(message = "peachId should not be null")
private String peachId;
}
import javax.validation.constraints.NotNull;
public class Grape {
@NotNull(message = "grapeId should not be null")
private String grapeId;
}
4.5.2 请求方法
import com.dragon.study.simplespringstudy.bean.Grape;
import com.dragon.study.simplespringstudy.bean.Peach;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("check")
public class MultiCheckController {
//多对象校验
@RequestMapping("multiCheck")
public Object multiCheck(
@Validated Peach peach,
BindingResult pbr,
@Validated Grape grape,
BindingResult gbr) {
List<String> errorList = new ArrayList<>();
if (pbr.hasErrors()) {
errorList.addAll(pbr.getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList()));
}
if (gbr.hasErrors()) {
errorList.addAll(gbr.getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList()));
}
if (CollectionUtils.isNotEmpty(errorList)) {
return errorList;
}
return "success";
}
}
4.5.3 测试
请求:
### 多对象校验
GET http://localhost:9090/check/multiCheck
Accept: application/json
响应:
[
"peachId should not be null",
"grapeId should not be null"
]
4.6 直接参数校验示例
spring中直接参数校验是直接抛异常,在类上添加注解@Validated,然后在请求方法参数上添加校验规则。示例如下:
4.6.1 请求方法
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotNull;
@RestController
@RequestMapping("check")
@Validated
public class DirectCheckController {
//直接校验
@RequestMapping("directCheck")
public Object directCheck(
@NotNull(message = "id should not be null") String id,
@NotNull(message = "name should not be null") String name
) {
return "success";
}
}
4.6.2 测试
请求:
### 直接校验
GET http://localhost:9090/check/directCheck
Accept: application/json
响应:
{
"timestamp": "2020-06-27T06:29:38.054+0000",
"status": 500,
"error": "Internal Server Error",
"message": "directCheck.id: id should not be null, directCheck.name: name should not be null",
"path": "/check/directCheck"
}
4.7 自定义校验示例
4.7.1 自定义注解校验器
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
//自定义注解校验类
public class AuthValidator implements ConstraintValidator<Auth, String> {
//初始化,可获取注解参数
@Override
public void initialize(Auth constraintAnnotation) {
}
//自定义校验规则,True:通过 False:不通过
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
return !StringUtils.isEmpty(s) && s.contains("123");
}
}
4.7.2 自定义校验注解
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;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) //定义注解修饰对象
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {AuthValidator.class}) //@Constraint表示是验证注解,同时指定验证类
public @interface Auth {
String message() default "no auth"; //错误提示信息
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
4.7.3 校验对象定义
import lombok.Data;
@Data
public class Person {
@Auth(message = "no right")
private String name;
}
4.7.4 请求方法
import com.dragon.study.simplespringstudy.bean.Person;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.stream.Collectors;
@RestController
@RequestMapping("check")
public class CustomCheckController {
//子定义注解校验
@RequestMapping("customCheck")
public Object customCheck(
@Validated Person person,
BindingResult br) {
if (br.hasErrors()) {
return br.getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
}
return "success";
}
}
4.7.5 测试
请求:
### 自定义校验
GET http://localhost:9090/check/customCheck?name=app
Accept: application/json
响应:
[
"no right"
]
再次请求:
### 自定义校验
GET http://localhost:9090/check/customCheck?name=app123
Accept: application/json
再次响应:
success