SpringBoot 参数校验

1. validation bean

validation bean 是基于JSR-303标准开发出来的,使用注解方式实现,及其方便,但是这只是一个接口,没有具体实现.Hibernate Validator是一个hibernate独立的包,可以直接引用,他实现了validation bean同时有做了扩展,比较强大.

2. 在Spring中如何使用

2.1 引入依赖

<!--参数校验-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2.2 常用注解

JSR提供的校验注解:         
@Null   被注释的元素必须为 null    
@NotNull    被注释的元素必须不为 null    
@AssertTrue     被注释的元素必须为 true    
@AssertFalse    被注释的元素必须为 false    
@Min(value)     被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@Max(value)     被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@DecimalMin(value)  被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@DecimalMax(value)  被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@Size(max=, min=)   被注释的元素的大小必须在指定的范围内    
@Digits (integer, fraction)     被注释的元素必须是一个数字,其值必须在可接受的范围内    
@Past   被注释的元素必须是一个过去的日期    
@Future     被注释的元素必须是一个将来的日期    
@Pattern(regex=,flag=)  被注释的元素必须符合指定的正则表达式    


Hibernate Validator提供的校验注解:  
@NotBlank(message =)   验证字符串非null,且trim后长度必须大于0    
@Email  被注释的元素必须是电子邮箱地址    
@Length(min=,max=)  被注释的字符串的大小必须在指定的范围内    
@NotEmpty   被注释的字符串的必须非空    
@Range(min=,max=,message=)  被注释的元素必须在合适的范围内

注意:

@NotNull 适用于任何类型被注解的元素必须不能与NULL
@NotEmpty 适用于String Map或者数组不能为Null且长度必须大于0
@NotBlank 只能用于String上面 不能为null,调用trim()后,长度必须大于0

2.3 简单使用

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;

/**
 * @Author: Sivan
 * @Date: 2020/12/24 0:13
 */
@Validated
@RestController
@RequestMapping("/valid")
public class ValidController {

    /**
     * 在Controller直接使用验证注解
     * 需要在类上添加@Validated
     */
    @GetMapping("/email")
    public String validEmail(@NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") String email) {
        return email;
    }
}

当处理GET请求时或只传入少量参数的时候,我们可能不会建一个bean来接收这些参数,就可以像上面这样直接在controller方法的参数中进行校验。

注意:这里一定要在方法所在的controller类上加入@Validated注解,不然没有任何效果。

这时会有这样一个错误:
javax.validation.ConstraintViolationException: validEmail.email: 邮箱不能为空

{
    "timestamp": "2020-12-23T18:33:34.600+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "message": "",
    "path": "/api/valid/email"
}

2.4 实体参数校验

创建一个接收参数的实体类:

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import org.springframework.format.annotation.DateTimeFormat;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Date;

/**
 * @Author: Sivan
 * @Date: 2020/12/23 12:51
 */
@Data
public class UserVo {

    @NotBlank(message = "姓名不能为空")
    private String name;

    @NotNull(message = "年龄不能为空")
    @Range(min = 10, max = 150, message = "年龄不正确")
    private Integer age;

    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;

    @NotNull(message = "出生日期不能为空")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthday;
}

使用:

import cn.sivan.api.model.vo.UserVo;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;

/**
 * @Author: Sivan
 * @Date: 2020/12/24 0:13
 */
@Validated
@RestController
@RequestMapping("/valid")
public class ValidController {

    /**
     * 在实体上使用验证注解
     */
    @GetMapping("/user")
    public UserVo validUser(@Validated UserVo userVo) {
        return userVo;
    }
}

2.5 嵌套使用
在原来的实体上添加一个新的属性,类型为AddressVo

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import org.springframework.format.annotation.DateTimeFormat;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Date;

/**
 * @Author: Sivan
 * @Date: 2020/12/23 12:51
 */
@Data
public class UserVo {

    @NotBlank(message = "姓名不能为空")
    private String name;

    @NotNull(message = "年龄不能为空")
    @Range(min = 10, max = 150, message = "年龄不正确")
    private Integer age;

    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;

    @NotNull(message = "出生日期不能为空")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthday;

    @Valid
    @NotEmpty(message = "请填写收货地址")
    private AddressVo[] address;
}
import cn.sivan.api.core.annotation.Mobile;
import lombok.Data;

import javax.validation.constraints.NotBlank;

/**
 * @Author: Sivan
 * @Date: 2020/12/24 2:55
 */
@Data
public class AddressVo {

    @NotBlank(message = "姓名不能为空")
    private String name;

    @Mobile(message = "联系方式不正确")
    private String phone;

    @NotBlank(message = "地址不能为空")
    private String address;
}

注意:嵌套验证不要忘了添加@Valid

    @Valid
    @NotEmpty(message = "请填写收货地址")
    private AddressVo[] address;

2.6 分组校验

分组校验,就是在不同的情况下,需要验证的情况不同,而你又不想为这点不同去建个新的类接收参数。例如:在新增用户时我们不需要校验id,但是在更新时我们需要校验id不能为空。

/**
 * 分组校验
 * @Author: Sivan
 * @Date: 2020/12/24 2:26
 */
public interface ModifyUser {
}
import cn.sivan.api.model.interfaces.ModifyUser;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import org.springframework.format.annotation.DateTimeFormat;

import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Date;

/**
 * @Author: Sivan
 * @Date: 2020/12/16 12:51
 */
@Data
public class UserVo {

    @NotBlank(message = "userId不能为空", groups = ModifyUser.class)
    private Integer userId;

    @NotBlank(message = "姓名不能为空")
    private String name;

    @NotNull(message = "年龄不能为空")
    @Range(min = 10, max = 150, message = "年龄不正确")
    private Integer age;

    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;

    @NotNull(message = "出生日期不能为空")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthday;

    @Valid
    @NotEmpty(message = "请填写收货地址")
    private AddressVo[] address;
}

使用:

import cn.sivan.api.model.interfaces.ModifyUser;
import cn.sivan.api.model.vo.UserVo;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;

/**
 * @Author: Sivan
 * @Date: 2020/12/24 0:13
 */
@RestController
@RequestMapping("/valid")
public class ValidController {

    /**
     * 嵌套验证
     */
    @PostMapping("/insert")
    public UserVo insert(@Validated @RequestBody UserVo userVo) {
        return userVo;
    }

    /**
     * 分组校验
     */
    @PostMapping("/modify")
    public UserVo validUser2(@Validated({ModifyUser.class}) @RequestBody UserVo userVo) {
        return userVo;
    }
}

3. 自定义校验注解

创建一个验证手机号码的注解。

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import cn.sivan.api.core.annotation.impl.*;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 自定义校验手机号码注解
 * @Author: Sivan
 * @Date: 2020/11/16 1:02
 */
@Documented
@Constraint(validatedBy = {MobileValidatorImpl.class})
@Target({FIELD})
@Retention(RUNTIME)
public @interface Mobile {

    String message() default "手机号码格式错误";

    String regexp() default "^1[345678]\\d{9}$";

    boolean required() default true;

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

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

指定其实现类:
@Constraint(validatedBy = {MobileValidatorImpl.class})

import cn.sivan.api.core.annotation.Mobile;

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

/**
 * 自定义校验手机号码注解-实现类
 * @Author: Sivan
 * @Date: 2020/11/16 1:03
 */
public class MobileValidatorImpl implements ConstraintValidator<Mobile, String> {
    private String regexp;

    @Override
    public void initialize(Mobile constraintAnnotation) {
        regexp = constraintAnnotation.regexp();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }
        if (value.matches(regexp)) {
            return true;
        }
        return false;
    }
}

4. 捕捉异常

当参数校验异常的时候,该统一异常处理类在控制台打印信息的同时把bad request的字符串和HttpStatus.BAD_REQUEST所表示的状态码400返回给调用方(用@ResponseBody注解实现,表示该方法的返回结果直接写入HTTP response body 中)。其中:

@ControllerAdvice:控制器增强,使@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法应用到所有的 @RequestMapping注解的方法。
@ExceptionHandler:异常处理器,此注解的作用是当出现其定义的异常时进行处理的方法,此例中处理ValidationException异常。

如果多个请求参数都校验失败,则遇到第一个校验失败就抛出异常,接下来的异常参数不做校验,配置如下:

import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

/**
 * @Author: Sivan
 * @Date: 2020/11/5 0:00
 */
@Configuration
public class WebConfig {

    @Bean
    public Validator validator() {

        //failFast的意思只要出现校验失败的情况,就立即结束校验,不再进行后续的校验。
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                .failFast(true)
                .buildValidatorFactory();

        return validatorFactory.getValidator();
    }

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor(Validator validator) {
        MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
        methodValidationPostProcessor.setValidator(validator);
        return methodValidationPostProcessor;
    }
}
import cn.sivan.api.core.exception.ApiAuthorizationException;
import cn.sivan.api.core.result.JsonResult;
import cn.sivan.api.core.result.ResultCode;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;

/**
 * 全局异常处理
 * @Author: Sivan
 * @Date: 2020/12/14 22:49
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 捕捉 404 异常
     * #spring.mvc.throw-exception-if-no-handler-found=true
     * #spring.resources.add-mappings=false
     */
    @ExceptionHandler(value = NoHandlerFoundException.class)
    public JsonResult noHandlerFoundException(NoHandlerFoundException e) {
        return JsonResult.error(ResultCode.NOT_FOUNT);
    }

    /**
     * 请求方式不允许
     */
    @ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
    public JsonResult httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        return JsonResult.error(ResultCode.METHOD_NOT_ALLOWED);
    }

    /**
     * 认证失败
     */
    @ExceptionHandler(value = ApiAuthorizationException.class)
    public JsonResult apiAuthorizationException(ApiAuthorizationException e) {
        return JsonResult.error(ResultCode.UNAUTHORIZED);
    }

    /**
     * 缺少请求参数或参数格式错误
     * body为空或Json格式错误
     */
    @ExceptionHandler({HttpMessageNotReadableException.class, MissingServletRequestParameterException.class})
    public JsonResult apiAuthorizationException(HttpMessageNotReadableException e) {
        return JsonResult.error(ResultCode.PARAMETER_FAILED.getCode(), e.getMessage());
    }

    /**
     * 参数校验失败 @Validated UserVo userVo
     */
    @ExceptionHandler(value = BindException.class)
    public JsonResult BindExceptionHandler(BindException e) {
        String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
        return JsonResult.error(ResultCode.PARAMETER_FAILED.getCode(), message);
    }

    /**
     * 参数校验失败 @NotBlank @Email String email
     */
    @ExceptionHandler(value = ConstraintViolationException.class)
    public JsonResult constraintViolationExceptionHandler(ConstraintViolationException e) {
        String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining());
        return JsonResult.error(ResultCode.PARAMETER_FAILED.getCode(), message);
    }

    /**
     * 参数校验失败 RequestBody
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public JsonResult methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
        return JsonResult.error(ResultCode.PARAMETER_FAILED.getCode(), message);
    }

    /**
     * 未知的运行时异常
     */
    @ExceptionHandler(value = RuntimeException.class)
    public JsonResult runtimeException(Exception e) {
        return JsonResult.error("运行时异常:" + e.getMessage());
    }

    /**
     * 捕捉系统异常
     */
    @ExceptionHandler(value = Exception.class)
    public JsonResult exception(Exception e) {
        return JsonResult.error("服务器错误,请联系管理员:" + e.getMessage());
    }
}
{
    "requestId": "20122404363388021008",
    "detail": {
        "path": "/api/valid/insert",
        "body": {
            "name": "宁川",
            "age": 12455,
            "email": "",
            "birthday": "2020-12-24",
            "addressVos": [
                {
                    "name": "宁川",
                    "phone": "",
                    "address": "d"
                }
            ]
        },
        "timestamp": 1608755793882,
        "code": 412,
        "message": "年龄不正确",
        "data": null
    }
}
上一篇:javax validation


下一篇:idea build项目出现错误“Error: java: 程序包javax.validation不存在”