Java SpringBoot Validation用法案例详解

作者:罗小爬EX 时间:2023-10-21 15:06:49 

提到输入参数的基本验证(非空、长度、大小、格式…),在以前我们还是通过手写代码,各种if、else、StringUtils.isEmpty、CollectionUtils.isEmpty…,真感觉快要疯了,太繁琐,Low爆了…,其实在Java生态提供了一套标准JSR-380(aka. Bean Validation 2.0,part of Jakarta EE and JavaSE),它已成为对象验证事实上的标准,这套标准可以通过注解的形式(如@NotNull, @Size…)来对bean的属性进行验证。而Hibernate Validator对这套标准进行了实现,SpringBoot Validation无缝集成了Hibernate Validator、自定义验证器、自动验证的功能。下文将对SpringBoot集成Validation进行展开。

注: 完整示例代码可参见GitHub:https://github.com/marqueeluo/spring-boot-validation-demo

constraints分类

JSR-380的支持的constrants注解汇总如下表:

分类注解适用对象null是否验证通过说明
非空@NotNull所有对象No不是null
非空@NotEmptyCharSequence, Collection, Map, ArrayNo不是null、不是""、size>0
非空@NotBlankCharSequenceNo不是null、trim后长度大于0
非空@Null所有对象Yes是null
长度@Size(min=0, max=Integer.MAX_VALUE)CharSequence, Collection, Map, ArrayYes字符串长度、集合size
大小@PositiveBigDecimal, BigInteger, byte, short, int, long, float, doubleYes数字>0
大小@PositiveOrZeroBigDecimal, BigInteger, byte, short, int, long, float, doubleYes数字>=0
大小@NegativeBigDecimal, BigInteger, byte, short, int, long, float, doubleYes数字<0
大小@NegativeOrZeroBigDecimal, BigInteger, byte, short, int, long, float, doubleYes数字<=0
大小@Min(value=0L)BigDecimal, BigInteger, byte, short, int, longYes数字>=min.value
大小@Max(value=0L)BigDecimal, BigInteger, byte, short, int, longYes数字<=max.value
大小@Range(min=0L, max=Long.MAX_VALUE)BigDecimal, BigInteger, byte, short, int, longYesrange.min<=数字<=range.max
大小@DecimalMin(value="")BigDecimal, BigInteger, CharSequence, byte, short, int, longYes数字>=decimalMin.value
大小@DecimalMax(value="")BigDecimal, BigInteger, CharSequence, byte, short, int, longYes数字<=decimalMax.value
日期@Past
  • java.util.Date

  • java.util.Calendar

  • java.time.Instant

  • java.time.LocalDate

  • java.time.LocalDateTime

  • java.time.LocalTime

  • java.time.MonthDay

  • java.time.OffsetDateTime

  • java.time.OffsetTime

  • java.time.Year

  • java.time.YearMonth

  • java.time.ZonedDateTime

  • java.time.chrono.HijrahDate

  • java.time.chrono.JapaneseDate

  • java.time.chrono.MinguoDate

  • java.time.chrono.ThaiBuddhistDate

Yes时间在当前时间之前
日期@PastOrPresent同上Yes时间在当前时间之前 或者等于此时
日期@Future同上Yes时间在当前时间之后
日期@FutureOrPresent同上Yes时间在当前时间之后 或者等于此时
格式@Pattern(regexp="", flags={})CharSequenceYes匹配正则表达式
格式@Email
           @Email(regexp=".*", flags={})
CharSequenceYes匹配邮箱格式
格式@Digts(integer=0, fraction=0)BigDecimal, BigInteger, CharSequence, byte, short, int, longYes必须是数字类型,且满足整数位数<=digits.integer, 浮点位数<=digits.fraction
布尔@AssertTruebooleanYes必须是true
布尔@AssertFalsebooleanYes必须是false

注: 后续还需补充Hibernate Validator中实现的constraints注解,如表中@Range。

对象集成constraints示例


/**
* 用户 - DTO
*
* @author luohq
* @date 2021-09-04 13:45
*/
public class UserDto {

@NotNull(groups = Update.class)
   @Positive
   private Long id;

@NotBlank
   @Size(max = 32)
   private String name;

@NotNull
   @Range(min = 1, max = 2)
   private Integer sex;

@NotBlank
   @Pattern(regexp = "^\\d{8,11}$")
   private String phone;

@NotNull
   @Email
   private String mail;

@NotNull
   @Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$")
   private String birthDateStr;

@NotNull
   @PastOrPresent
   private LocalDate birthLocalDate;

@NotNull
   @Past
   @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
   private LocalDateTime registerLocalDatetime;

@Valid
   @NotEmpty
   private List<OrgDto> orgs;

//省略getter、setter、toString方法
}

/**
* 组织 - DTO
*
* @author luohq
* @date 2021-09-04 14:10
*/
public class OrgDto {
   @NotNull
   @Positive
   private Long orgId;

@NotBlank
   @Size(min = 1, max = 32)
   private String orgName;

//省略getter、setter、toString方法
}

注:

  • 可通过constraints注解的groups指定分组
       即指定constraints仅在指定group生效,默认均为Default分组,
       后续可通过@Validated({MyGroupInterface.class})形式进行分组的指定

  • 可通过@Valid注解进行级联验证(Cascaded Validation,即嵌套对象验证)
       如上示例中@Valid添加在 List<OrgDto> orgs上,即会对list中的每个OrgDto进行验证

SpringBoot集成自动验证

参考:
https://www.baeldung.com/javax-validation-method-constraints#validation

集成maven依赖


<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

验证RequestBody、Form对象参数

在参数前加@Validated

Java SpringBoot Validation用法案例详解

验证简单参数

在controller类上加@Validated

Java SpringBoot Validation用法案例详解

验证指定分组

Java SpringBoot Validation用法案例详解

全局controller验证异常处理

通过@ControllerAdvice、@ExceptionHandler来对SpringBoot Validation验证框架抛出的异常进行统一处理,
并将错误信息拼接后统一返回,具体处理代码如下:


import com.luo.demo.validation.domain.result.CommonResult;
import com.luo.demo.validation.enums.RespCodeEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* controller增强 - 通用异常处理
*
* @author luohq
* @date 2021-09-04 13:43
*/
@ControllerAdvice
public class ControllerAdviceHandler {

private static final Logger log = LoggerFactory.getLogger(ControllerAdviceHandler.class);

/**
    * 是否在响应结果中展示验证错误提示信息
    */
   @Value("${spring.validation.msg.enable:true}")
   private Boolean enableValidationMsg;

/**
    * 符号常量
    */
   private final String DOT = ".";
   private final String SEPARATOR_COMMA = ", ";
   private final String SEPARATOR_COLON = ": ";

/**
    * 验证异常处理 - 在@RequestBody上添加@Validated处触发
    *
    * @param request
    * @param ex
    * @return
    */
   @ExceptionHandler({MethodArgumentNotValidException.class})
   @ResponseStatus(HttpStatus.OK)
   @ResponseBody
   public CommonResult handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException ex) {
       log.warn("{} - MethodArgumentNotValidException!", request.getServletPath());
       CommonResult commonResult = CommonResult.respWith(RespCodeEnum.PARAM_INVALID.getCode(), this.convertFiledErrors(ex.getBindingResult().getFieldErrors()));
       log.warn("{} - resp with param invalid: {}", request.getServletPath(), commonResult);
       return commonResult;
   }

/**
    * 验证异常处理 - form参数(对象参数,没有加@RequestBody)触发
    *
    * @param request
    * @param ex
    * @return
    */
   @ExceptionHandler({BindException.class})
   @ResponseStatus(HttpStatus.OK)
   @ResponseBody
   public CommonResult handleBindException(HttpServletRequest request, BindException ex) {
       log.warn("{} - BindException!", request.getServletPath());
       CommonResult commonResult = CommonResult.respWith(RespCodeEnum.PARAM_INVALID.getCode(), this.convertFiledErrors(ex.getFieldErrors()));
       log.warn("{} - resp with param invalid: {}", request.getServletPath(), commonResult);
       return commonResult;
   }

/**
    * 验证异常处理 - @Validated加在controller类上,
    * 且在参数列表中直接指定constraints时触发
    *
    * @param request
    * @param ex
    * @return
    */
   @ExceptionHandler({ConstraintViolationException.class})
   @ResponseStatus(HttpStatus.OK)
   @ResponseBody
   public CommonResult handleConstraintViolationException(HttpServletRequest request, ConstraintViolationException ex) {
       log.warn("{} - ConstraintViolationException - {}", request.getServletPath(), ex.getMessage());
       CommonResult commonResult = CommonResult.respWith(RespCodeEnum.PARAM_INVALID.getCode(), this.convertConstraintViolations(ex));
       log.warn("{} - resp with param invalid: {}", request.getServletPath(), commonResult);
       return commonResult;
   }

/**
    * 全局默认异常处理
    *
    * @param request
    * @param ex
    * @return
    */
   @ExceptionHandler({Throwable.class})
   @ResponseStatus(HttpStatus.OK)
   @ResponseBody
   public CommonResult handleException(HttpServletRequest request, Throwable ex) {
       log.warn("{} - Exception!", request.getServletPath(), ex);
       CommonResult commonResult = CommonResult.failed();
       log.warn("{} - resp failed: {}", request.getServletPath(), commonResult);
       return commonResult;
   }

/**
    * 转换FieldError列表为错误提示信息
    *
    * @param fieldErrors
    * @return
    */
   private String convertFiledErrors(List<FieldError> fieldErrors) {
       return Optional.ofNullable(fieldErrors)
               .filter(fieldErrorsInner -> this.enableValidationMsg)
               .map(fieldErrorsInner -> fieldErrorsInner.stream()
                       .flatMap(fieldError -> Stream.of(fieldError.getField(), SEPARATOR_COLON, fieldError.getDefaultMessage(), SEPARATOR_COMMA))
                       .collect(Collectors.joining()))
               .map(msg -> msg.substring(0, msg.length() - SEPARATOR_COMMA.length()))
               .orElse(null);
   }

/**
    * 转换ConstraintViolationException异常为错误提示信息
    *
    * @param constraintViolationException
    * @return
    */
   private String convertConstraintViolations(ConstraintViolationException constraintViolationException) {
       return Optional.ofNullable(constraintViolationException.getConstraintViolations())
               .filter(constraintViolations -> this.enableValidationMsg)
               .map(constraintViolations -> constraintViolations.stream()
                       .flatMap(constraintViolation -> {
                           String path = constraintViolation.getPropertyPath().toString();
                           path = path.substring(path.lastIndexOf(DOT) + 1);
                           String errMsg = constraintViolation.getMessage();
                           return Stream.of(path, SEPARATOR_COLON, errMsg, SEPARATOR_COMMA);
                       }).collect(Collectors.joining())
               ).map(msg -> msg.substring(0, msg.length() - SEPARATOR_COMMA.length()))
               .orElse(null);

}
}

参数验证未通过返回结果示例:

Java SpringBoot Validation用法案例详解

注: 其中CommonResult为统一返回结果,可根据自己业务进行调整

Java SpringBoot Validation用法案例详解

自定义constraints

自定义field constraint注解主要分为以下几步:
(1)定义constraint annotation注解及其属性
(2)通过注解的元注解@Constraint(validatedBy = {})关联的具体的验证器实现
(3)实现验证器逻辑

@DateFormat

具体字符串日期格式constraint @DateFormat定义示例如下:


import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;

/**
* The annotated {@code CharSequence} must match date format.
* The default date format is "yyyy-MM-dd".
* Can override with property "format".
* see {@link java.time.format.DateTimeFormatter}.
* <p>
* Accepts {@code CharSequence}. {@code null} elements are considered valid.
*
* @author luo
* @date 2021-09-05
*/
@Documented
@Constraint(validatedBy = DateFormatValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD, ANNOTATION_TYPE,})
@Retention(RetentionPolicy.RUNTIME)
public @interface DateFormat {
   String message() default "日期格式不正确";

String format() default "yyyy-MM-dd";

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

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

import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.time.format.DateTimeFormatter;

/**
* Date Format validator
*
* @author luohq
* @date 2021-09-05
*/
public class DateFormatValidator implements ConstraintValidator<DateFormat, String> {

private String format;

@Override
   public void initialize(DateFormat dateFormat) {
       this.format = dateFormat.format();
   }

@Override
   public boolean isValid(String dateStr, ConstraintValidatorContext cxt) {
       if (!StringUtils.hasText(dateStr)) {
           return true;
       }
       try {
           DateTimeFormatter.ofPattern(this.format).parse(dateStr);
           return true;
       } catch (Throwable ex) {
           return false;
       }
   }
}

@PhoneNo

在查看hbernate-validator中URL、Email约束实现时,发现可以通过元注解的形式去复用constraint实现(如@Pattern),故参考如上方式实现@PhoneNo约束


import javax.validation.Constraint;
import javax.validation.OverridesAttribute;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.Pattern;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* The annotated {@code CharSequence} must match phone no format.
* The regular expression follows the Java regular expression conventions
* see {@link java.util.regex.Pattern}.
* <p>
* Accepts {@code CharSequence}. {@code null} elements are considered valid.
*
* @author luo
* @date 2021-09-05
*/
@Documented
@Constraint(validatedBy = {})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Repeatable(PhoneNo.List.class)
@ReportAsSingleViolation
@Pattern(regexp = "")
public @interface PhoneNo {
   String message() default "电话号码格式不正确";

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

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

/**
    * @return an additional regular expression the annotated PhoneNo must match. The default is "^\\d{8,11}$"
    */
   @OverridesAttribute(constraint = Pattern.class, name = "regexp") String regexp() default "^\\d{8,11}$";

/**
    * @return used in combination with {@link #regexp()} in order to specify a regular expression option
    */
   @OverridesAttribute(constraint = Pattern.class, name = "flags") Pattern.Flag[] flags() default {};

/**
    * Defines several {@code @URL} annotations on the same element.
    */
   @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
   @Retention(RUNTIME)
   @Documented
   @interface List {
       PhoneNo[] value();
   }
}

注: 同理可以实现@IdNo约束

使用自定义constraint注解

可将之前的对象集成示例中代码调整为使用自定义验证注解如下:


/**
* 用户 - DTO
*
* @author luohq
* @date 2021-09-04 13:45
*/
public class UserDto {
   ...
   @NotBlank
   //@Pattern(regexp = "^\\d{8,11}$")
   @PhoneNo
   private String phone;

@NotBlank
   @IdNo
   private String idNo;

@NotNull
   //@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$")
   @DateFormat
   //@DateTimeFormat
   private String birthDateStr;

...
}

同时自定义constraints还支持跨多参数、验证对象里的多个field、验证返回对象等用法,待后续再详细探索。

问题

通过在对象属性、方法参数上标注注解的形式,需要侵入代码,之前有的架构师不喜欢这种风格。
在一方开发时,我们有全部源码且在公司内部,这种方式还是可以的,且集成比较方便,
但是依赖三方Api jar包(参数对象定义在jar包中),我们无法直接去修改参数对象,依旧使用这种侵入代码的注解方式就不适用了,
针对三方包、或者替代注解这种形式,之前公司内部有实现过基于xml配置的形式进行验证,
这种方式不侵入参数对象,且集成也还算方便,
但是用起来还是没有直接在代码里写注解来的顺手(代码有补全、有提示、程序员友好),
所以一方开发时,首选推荐SpringBoot Validation这套体系,无法直接编辑参数对象时再考虑其他方式。

参考:

【自定义validator - field、class level】https://www.baeldung.com/spring-mvc-custom-validator

【Spring boot集成validation、全局异常处理】https://www.baeldung.com/spring-boot-bean-validation

【JSR380、非Spring框架集成validation】https://www.baeldung.com/javax-validation

【方法约束 - Single param、Cross param、Return value自定义constraints、编程调用验证】https://www.baeldung.com/javax-validation-method-constraints

Spring Validation最佳实践及其实现原理,参数校验没那么简单!

https://reflectoring.io/bean-validation-with-spring-boot/

来源:https://blog.csdn.net/luo15242208310/article/details/120074866

标签:Java,SpringBoot,Validation
0
投稿

猜你喜欢

  • java中的HashMap多层嵌套

    2023-11-27 07:34:52
  • 基于SpringBoot Mock单元测试详解

    2021-09-25 02:49:41
  • 解决Weblogic部署war找不到spring配置文件的问题

    2022-12-29 07:03:08
  • 深入XPath的详解以及Java示例代码分析

    2021-11-01 13:42:33
  • arthas排查jvm中CPU占用过高问题解决

    2022-07-15 20:31:49
  • 让C# Excel导入导出 支持不同版本Office

    2023-01-11 05:30:53
  • java实现超市商品库存管理平台

    2022-05-25 10:09:20
  • Springboot整合微信支付(订单过期取消及商户主动查单)

    2023-05-15 23:40:50
  • Ireport的安装与使用教程

    2021-08-08 00:16:43
  • android UI绘制加减号按钮

    2023-09-11 02:55:28
  • Android图像切换器imageSwitcher的实例应用

    2023-10-06 00:30:56
  • Android SwipeRefreshLayout超详细讲解

    2023-08-22 13:43:28
  • C# 异步多线程入门基础

    2022-01-19 05:23:05
  • spring cloud gateway如何获取请求的真实地址

    2023-11-28 20:20:12
  • Java TreeSet类的简单理解和使用

    2023-01-02 18:32:27
  • SpringBoot使用Atomikos技术整合多数据源的实现

    2021-11-08 23:46:23
  • 超简单C#获取带汉字的字符串真实长度(单个英文长度为1,单个中文长度为2)

    2021-11-13 20:37:55
  • C# zxing二维码写入的实例代码

    2021-09-01 12:23:26
  • flutter实现头部tabTop滚动栏

    2022-03-21 14:00:06
  • C# web.config之<customErrors>节点说明案例详解

    2023-07-06 20:20:15
  • asp之家 软件编程 m.aspxhome.com