千家信息网

Java SpringBoot Validation怎么用

发表于:2025-01-21 作者:千家信息网编辑
千家信息网最后更新 2025年01月21日,这篇文章主要为大家展示了"Java SpringBoot Validation怎么用",内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下"Java SpringB
千家信息网最后更新 2025年01月21日Java SpringBoot Validation怎么用

这篇文章主要为大家展示了"Java SpringBoot Validation怎么用",内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下"Java SpringBoot Validation怎么用"这篇文章吧。

    提到输入参数的基本验证(非空、长度、大小、格式…),在以前我们还是通过手写代码,各种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进行展开。

    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 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 orgs上,即会对list中的每个OrgDto进行验证

    SpringBoot集成自动验证

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

    集成maven依赖

        org.springframework.boot    spring-boot-starter-validation

    验证RequestBody、Form对象参数

    在参数前加@Validated

    验证简单参数

    在controller类上加@Validated

    验证指定分组

    全局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 */@ControllerAdvicepublic 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 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);    }}

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

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

    自定义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}. * 

    * 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[] 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 { 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}. * 

    * 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[] 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/

    以上是"Java SpringBoot Validation怎么用"这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注行业资讯频道!

    0