springMvc中数据校验

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 参数为时间类型且小于或等于当前时间
@Email 参数为邮箱,可指定匹配的正则表达式
@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
在controller处理方法的参数上添加校验注解标识,如注解Validated
在controller处理方法的参数后添加获取校验结果参数,如BindingResult
在controller处理方法中处理校验结果,如bindingResult.getAllErrors方法

四、自定义数据校验注解

除了使用如@Min、@NotNull等已经存在的注解,还可以自定义校验注解。步骤如下:

  1. 自定义注解的校验类,需要实现javax.validation.ConstraintValidator接口;
  2. 自定义校验注解,需要使用@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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章