跳至主要內容

JDK validation 功能

Steven大约 3 分钟javajdk

Java EE 规范中用接口定义了 Java Bean 的校验方式,即 Java Bean Validationopen in new window

相关信息

常用的 Java EE 规范接口在 jdk 的 javax 包下,如:

  • javax.sql —— 数据库访问接口。实现厂商有 mysql/sqlserver/oracle/...
  • javax.servlet —— tomcat/jetty
  • java.xml —— jaxp(java api for xml processing)/jaxb
  • javax.persistence —— hibernate
  • javax.transaction —— 分布式实务
  • javax.jms —— activemq

使用案例

一:引入校验接口

Java Bean Validation 接口不默认包含在 JDK 中,需要主动引入:

Java Bean Validation
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

提示

Java 与 Jakarta 的区别?

Java EE 规范由 JCP(Java Community Process)open in new window 通过 JSR(Java Specification Requests,Java 规范提案) 规定。 但 Oracle 收购 Java 后,虽然将 Java EE 规范捐献给 eclipse 基金会管理,但要求更改 java 相关的命名,其中包括 Java EE 代码所在的 javax 包名。 因此,后续 javax 包下的代码统一移动到了 jakarta 下。 所以,可以说 java 与 jakarta 没太大区别,或者说 jakarta 是新版的 java。

参考: https://blogs.oracle.com/theaquarium/opening-up-java-eeopen in new window

相关信息

规范版本:

提案版本
jsr303beanvalidation 1.0
jsr349beanvalidation 1.1
jsr380beanvalidation 2.0

二:引入校验实现(hibernate-validator)

引入 validation-api 的具体实现之一 hibernate-validatoropen in new window

Java Bean Validation
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.0.Final</version>
</dependency>
<!-- fix: javax.validation.ValidationException: HV000183: Unable to initialize 'javax.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead -->
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-el</artifactId>
    <version>9.0.65</version>
</dependency>

注意

validation-api/jakarta.validation-api 属于接口,没有具体实现,不能直接运行,否则出现如下告警:

javax.validation.NoProviderFoundException: Unable to create a Configuration, because no Bean Validation provider could be found. Add a provider like Hibernate Validator (RI) to your classpath.

	at javax.validation.Validation$GenericBootstrapImpl.configure(Validation.java:291)
	at javax.validation.Validation.buildDefaultValidatorFactory(Validation.java:103)

三:使用校验规则

类限定
package org.example.entity;

import lombok.Builder;
import lombok.Data;
import org.example.entity.validation.MyUrl;

import javax.validation.constraints.*;
import java.time.LocalDateTime;
import java.util.List;

@Builder
@Data
public class User {
    public static interface Insert {}
    public static interface Update {}

    @NotNull(groups = {
            Update.class, // 全部注解默认 Default 组。如果校验时指定组,则只执行对应组的校验。
    })
    private Long id;
    @NotBlank(groups = {
            Insert.class
    })
    private String username;
    @Min(value = 0, message = "年龄大于{value}岁")
    private Integer age;
    @PastOrPresent
    private LocalDateTime birthDay;
    @Email
    private String email;
    @Pattern(regexp = "^\\p{Print}+$", message = "手机号输入错误")
    private String phone;
    /**
     * 个人网站
     */
    private List<@MyUrl String> urls;
}

常用注解

Bean Validation Constraint

注解说明
@Null/@NotNull/@NotEmpty/NotBlank非空
@AssertTrue/@AssertFalse布尔
@Min(value)/@Max(value)/@DecimalMin(value)/@DecimalMax(value)/@Size(max, min)/@NegativeOrZero/@Digits(integer, fraction)数值
@Past/@PastOrPresent/@Future时间
@Pattern(value)/@Email正则

Hibernate Validation Constraint

注解说明
@Length长度
@Range范围
@URL正则

实现原理分析

有多种 bean validation 实现,下面以 hibernate 为例。

一:校验实现的加载

上面例子中,我们业务类上只调用了校验接口(javax.validation.Validator),运行时就自动调用了校验器的实现(org.hibernate.validator.internal.engine.ValidatorImpl)。 有这种现象是因为 beanvalidation 使用了 SPI 技术。

SPI(service provider interface,服务提供接口) 是 JDK 提供的一种服务发现机制。 同样使用 SPI 的有 jdbc/slf4j/...

在 beanvalidation 这里具体有两个关键的配置点:

  1. 在接口中调用服务接口的类加载方法 —— ServiceLoader<ValidationProvider> loader = ServiceLoader.load( ValidationProvider.class, classloader );
  2. 在实现中配置服务接口的实现类名 —— 在 META-INF/services/javax.validation.spi.ValidationProvider 中写入 org.hibernate.validator.HibernateValidator

这样,运行时就可以获取到服务接口的具体实现了。

二:校验实现的绑定

校验注解会绑定一个校验器实现。当要校验值时,会找到校验器进行校验。

如: @NotBlankorg.hibernate.validator.internal.constraintvalidators.hv.NotBlankValidator 实现

绑定过程在 org.hibernate.validator.internal.metadata.core.ConstraintHelper 中:

putConstraint( tmpConstraints, NotBlank.class, NotBlankValidator.class );

List<Class<? extends ConstraintValidator<NotEmpty, ?>>> notEmptyValidators = new ArrayList<>( 11 );
notEmptyValidators.add( NotEmptyValidatorForCharSequence.class );
notEmptyValidators.add( NotEmptyValidatorForCollection.class );
notEmptyValidators.add( NotEmptyValidatorForArray.class );
notEmptyValidators.add( NotEmptyValidatorForMap.class );
notEmptyValidators.add( NotEmptyValidatorForArraysOfBoolean.class );
notEmptyValidators.add( NotEmptyValidatorForArraysOfByte.class );
notEmptyValidators.add( NotEmptyValidatorForArraysOfChar.class );
notEmptyValidators.add( NotEmptyValidatorForArraysOfDouble.class );
notEmptyValidators.add( NotEmptyValidatorForArraysOfFloat.class );
notEmptyValidators.add( NotEmptyValidatorForArraysOfInt.class );
notEmptyValidators.add( NotEmptyValidatorForArraysOfLong.class );
notEmptyValidators.add( NotEmptyValidatorForArraysOfShort.class );
putConstraints( tmpConstraints, NotEmpty.class, notEmptyValidators );

putConstraint( tmpConstraints, NotNull.class, NotNullValidator.class );
putConstraint( tmpConstraints, Null.class, NullValidator.class );

自定义注解

注解
package org.example.entity.validation;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

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

/**
 * 自定义校验注解:判断是 ip or domain or host
 */
@Documented
@Constraint(validatedBy = { MyUrlValidator.class })
@Target({ FIELD, TYPE_USE })
@Retention(RUNTIME)
public @interface MyUrl {
    String message() default "请输入正确的地址";

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

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


参考: