Bean Validation ( JSR 303 规范)

Bean Validation ( JSR 303 规范)

  • Hibernate ValidatorJakarta Bean Validation(即 JSR 303)的参考实现。
  • 在开发中,从表现层到持久层,数据校验都是一项逻辑差不多,但容易出错的任务

教程网址:https://www.cnblogs.com/summerday152/p/13984576.html

  • 前端架构往往会采取一些检查参数的手段,比如校验并提示信息,那么,既然前端已经存在校验手段,后端的校验是否还有必要,是否多余了呢 ?

    1. 并不是,正常情况下,参数确实会经过前端校验后传向后端,但如果后端不做校验,一但通过特殊手段越过前端的检测,系统就会出现漏洞。

    2. 不使用 Validator 的参数处理逻辑,if / else 就能搞定,写法干脆,但 if / else 太多,过于臃肿,更何况这只是区区一个接口的两个参数而已,要是需要更多参数校验,甚至更多方法都需要的校验,这代码量可想而知。于是,这种做法显然是不可取的,我们可以利用 Validator 框架更加优雅的处理参数。

    3. Jakarta Bean Validation2.0 定义了一个元数据模型,为实体类和方法提供了数据验证的 API ,默认将注解作为源,可以通过 XML 扩展源。

SpringBoot 自动配置 ValidationAutoConfiguration

  • Hibernate ValidatorJakarta Bean Validation的参考实现。
  • 在 SpringBoot 中,只要类路径上存在 JSR-303 的实现,如 Hibernate Validator,就会自动开启 Bean Validation 验证功能,这李我们只要引入 spring-boot-starter-validation 的依赖,就能完成所需

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

简单实现步骤:

  1. 引入上面依赖
  2. 创建 Bean, Bean 的属性和前端的请求参数一致,在 Bean 的属性加入注解,注解提供了数据验证的规则
  3. 在 Controller 中,形参是 Bean 对象,使用 @Validated 说明这个 Bean 要进行属性验证
  4. 使用 Spring Boot 异常处理机制,获取验证的结果

SpringBoot Validtor 常用注解

@AssertFalseBoolean,boolean验证注解的元素值是false
@AssertTrueBoolean,boolean验证注解的元素值是true
@NotNull任意类型验证注解的元素值不是null
@Null任意类型验证注解的元素值是null
@Min(value=值)BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型验证注解的元素值大于等于@Min指定的value值
@Max(value=值)和@Min要求一样验证注解的元素值小于等于@Max指定的value值
@DecimalMin(value=值)和@Min要求一样验证注解的元素值大于等于@ DecimalMin指定的value值
@DecimalMax(value=值)和@Min要求一样验证注解的元素值小于等于@ DecimalMax指定的value值
@Digits(integer=整数位数, fraction=小数位数)和@Min要求一样验证注解的元素值的整数位数和小数位数上限
@Size(min=下限, max=上限)字符串、Collection、Map、数组等验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小
@Pastjava.util.Date,java.util.Calendar;Joda Time类库的日期类型验证注解的元素值(日期类型)比当前时间早
@Future与@Past要求一样验证注解的元素值(日期类型)比当前时间晚
@NotBlankCharSequence子类型验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格
@Length(min=下限, max=上限)CharSequence子类型验证注解的元素值长度在min和max区间内
@NotEmptyCharSequence子类型、Collection、Map、数组验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@Range(min=最小值, max=最大值)BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型验证注解的元素值在最小值和最大值之间
@Email(regexp=正则表达式,flag=标志的模式)CharSequence子类型(如String)验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式
@Pattern(regexp=正则表达式,flag=标志的模式)String,任何CharSequence的子类型验证注解的元素值与指定的正则表达式匹配
@Valid任何非原子类型指定递归验证关联的对象;如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证

下面是网站详细步骤

目的其实是为了引入如下依赖:

    <!-- Unified EL 获取动态表达式-->
	<dependency>
      <groupId>org.glassfish</groupId>
      <artifactId>jakarta.el</artifactId>
      <version>3.0.3</version>
      <scope>compile</scope>
    </dependency>

    <dependency>
      <groupId>org.hibernate.validator</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>6.1.5.Final</version>
      <scope>compile</scope>
    </dependency>

SpringBoot 对 BeanValidation 的支持的自动装配定义在:

org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration	类中,
    提供了默认的						LocalValidatorFactoryBean
    和支持方法级别的拦截器				 MethodValidationPostProcessor
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	@ConditionalOnMissingBean(Validator.class)
	public static LocalValidatorFactoryBean defaultValidator() {
        //ValidatorFactory
		LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
		MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
		factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
		return factoryBean;
	}

    // 支持Aop,MethodValidationInterceptor方法级别的拦截器
	@Bean
	@ConditionalOnMissingBean
	public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
			@Lazy Validator validator) {
		MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
		boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
		processor.setProxyTargetClass(proxyTargetClass);
        // factory.getValidator(); 通过factoryBean获取了Validator实例,并设置
		processor.setValidator(validator);
		return processor;
	}

}


Validator+BindingResult优雅处理

为实体类定义约束注解

/**
 * 实体类字段加上javax.validation.constraints定义的注解
 * @author Summerday
 */

@Data
@ToString
public class Person {
    private Integer id;
    
    @NotNull
    @Size(min = 6,max = 12)
    private String name;

    @NotNull
    @Min(20)
    private Integer age;
}

使用@Valid或@Validated注解

  • @Valid和@Validated在Controller层做方法参数校验时功能相近,具体区别可以往后面看
    @RestController
    public class ValidateController {
    
        @PostMapping("/person")
        public Map<String, Object> validatePerson(@Validated @RequestBody Person person, BindingResult result) {
            如果有参数校验失败,会将错误信息封装成对象组装在BindingResult}
    }
    
    

Validator + 全局异常处理

  • 在接口方法中利用 BindingResult 处理校验数据过程中的信息是一个可行方案,但在接口众多的情况下,就显得有些冗余,我们可以利用全局异常处理,捕捉抛出的 MethodArgumentNotValidException (参数无效异常)异常,并进行相应的处理。

    定义全局异常处理

    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        /**
         * If the bean validation is failed, it will trigger a MethodArgumentNotValidException.
         如果出现异常进不去全局异常可以都写成 BindException 这时下面异常的父类,也可能是抛出的异常
         */
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex, HttpStatus status) {
            BindingResult result = ex.getBindingResult();
            Map<String, Object> map = new HashMap<>();
            List<String> list = new LinkedList<>();
            result.getFieldErrors().forEach(error -> {
                String field = error.getField();
                Object value = error.getRejectedValue();
                String msg = error.getDefaultMessage();
                list.add(String.format("错误字段 -> %s 错误值 -> %s 原因 -> %s", field, value, msg));
            });
            map.put("msg", list);
            return new ResponseEntity<>(map, status);
        }
    }
    
    

Validated 精确校验到参数字段

  • 有时候,我们只想校验某个字段,并不想校验整个 POJO 对象,我们可以利用 @Validated 精确校验到某个字段。
  • 定义接口
    @RestController
    @Validated
    public class OnlyParamsController {
    
        @GetMapping("/{id}/{name}")
        public String test(@PathVariable("id") @Min(1) Long id,
                           @PathVariable("name") @Size(min = 5, max = 10) String name) {
            return "success";
        }
    }
    
    
  • 当校验生效,但是状态和响应错误信息不太正确,我们可以通过捕获 ConstraintViolationException 修改状态
    @ControllerAdvice
    public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
    
        private static final Logger log = LoggerFactory.getLogger(CustomGlobalExceptionHandler.class);
    
    
        /**
         * If the @Validated is failed, it will trigger a ConstraintViolationException
         */
        @ExceptionHandler(ConstraintViolationException.class)
        public void constraintViolationException(ConstraintViolationException ex, HttpServletResponse response) throws IOException {
            ex.getConstraintViolations().forEach(x -> {
                String message = x.getMessage();
                Path propertyPath = x.getPropertyPath();
                Object invalidValue = x.getInvalidValue();
                log.error("错误字段 -> {} 错误值 -> {} 原因 -> {}", propertyPath, invalidValue, message);
            });
            response.sendError(HttpStatus.BAD_REQUEST.value());
        }
    }
    
    

@Validated 和 @Valid 的不同

  • @Valid :是标准 JSR-303 规范的标记型注解,用来标记验证属性和方法返回值,进行级联和递归校验。
  • @Validated:是 Spring 提供的注解,是标准 JSR-303 的一个变种(补充),提供了一个分组功能,可以在入参验证时,根据不同的分组采取不同的验证机制。
  • 在 Controller 中校验方法参数时,使用 @Valid 和 @Validated 并无特殊差异(若不需要分组校验的话)。
  • @Validated 注解可以用于类级别,用于支持 Spring 进行方法级别的参数校验,@Valid 可以用在属性级别约束,用来表示级联校验
  • @Validated 只能用在类,方法和参数上,而 @Valid 可以用于方法,字段,构造器和参数上。

自定义 Jakarta Bean Validation API 注解

  • Jakarta Bean Validation API 定义了一套标准约束注解,如 @NotNull,@Size等,但是这些内置的约束注解难免不能满足我们的需求,这时我们就可以自定义注解,创建自定义注解需要三步:
  • 创建一个 Constraint annotation

  • 实现一个 Validator

  • 定义一个 default error message

第一步:

/**
 * 自定义注解
 * @author Summerday
 */

@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class) //需要定义CheckCaseValidator
@Documented
@Repeatable(CheckCase.List.class)
public @interface CheckCase {
    String message() default "{CheckCase.message}";

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

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

    CaseMode value();

    @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        CheckCase[] value();
    }
}

第二步

/**
 * 实现ConstraintValidator
 *
 * @author Summerday
 */
public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {

    private CaseMode caseMode;

    /**
     * 初始化获取注解中的值
     */
    @Override
    public void initialize(CheckCase constraintAnnotation) {
        this.caseMode = constraintAnnotation.value();
    }

    /**
     * 校验
     */
    @Override
    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
        if (object == null) {
            return true;
        }

        boolean isValid;
        if (caseMode == CaseMode.UPPER) {
            isValid = object.equals(object.toUpperCase());
        } else {
            isValid = object.equals(object.toLowerCase());
        }

        if (!isValid) {
            // 如果定义了message值,就用定义的,没有则去
            // ValidationMessages.properties中找CheckCase.message的值
            if(constraintContext.getDefaultConstraintMessageTemplate().isEmpty()){
                constraintContext.disableDefaultConstraintViolation();
                constraintContext.buildConstraintViolationWithTemplate(
                        "{CheckCase.message}"
                ).addConstraintViolation();
            }
        }
        return isValid;
    }
}

第三步:在 ValidationMessages.properties 文件中定义

CheckCase.message=Case mode must be {value}.

第四步:在某个字段上加上注解:

@CheckCase(value = CaseMode.UPPER)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值