Validation with Hibernate Validator

Prerequisites

如要完成本指南,您需要:

  • Roughly 15 minutes

  • An IDE

  • 安装了 JDK 17+,已正确配置 JAVA_HOME

  • Apache Maven ${proposed-maven-version}

  • 如果你想使用 Quarkus CLI, 则可以选择使用

  • 如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately

Architecture

本指南中构建的应用程序非常简单。用户在网页上填写表单。网页将表单内容作为 JSON(通过 Ajax)发送到 BookResourceBookResource 验证用户信息,并将 result 作为 JSON 返回。

validation guide architecture

Solution

我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。

克隆 Git 存储库: git clone $${quickstarts-base-url}.git,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。

该解决方案位于 validation-quickstart directory 中。

Creating the Maven project

首先,我们需要一个新项目。使用以下命令创建一个新项目:

CLI
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
    --no-code
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 --gradle--gradle-kotlin-dsl 选项。 有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。

Maven
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
    -DprojectGroupId={create-app-group-id} \
    -DprojectArtifactId={create-app-artifact-id} \
    -DnoCode
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

适用于 Windows 用户:

  • 如果使用 cmd,(不要使用反斜杠 \ ,并将所有内容放在同一行上)

  • 如果使用 Powershell,将 -D 参数用双引号引起来,例如 "-DprojectArtifactId={create-app-artifact-id}"

此命令会生成一个 Maven 结构,导入 Quarkus REST(以前为 RESTEasy Reactive)/Jakarta REST、Jackson 和 Hibernate Validator/Bean Validation 扩展。

如果已配置 Quarkus 项目,可通过在自己的项目基本目录中运行以下命令将 hibernate-validator 扩展添加到自己的项目中:

CLI
quarkus extension add {add-extension-extensions}
Maven
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
Gradle
./gradlew addExtension --extensions='{add-extension-extensions}'

这会将以下内容添加到构建文件中:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-hibernate-validator")

Constraints

在此应用程序中,我们将测试一个基本对象,但我们支持复杂约束并可以验证对象图。使用以下内容创建 org.acme.validation.Book 类:

package org.acme.validation;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Min;

public class Book {

    @NotBlank(message="Title may not be blank")
    public String title;

    @NotBlank(message="Author may not be blank")
    public String author;

    @Min(message="Author has been very lazy", value=1)
    public double pages;
}

约束添加到字段中,当对象得到验证时,将检查其值。getter 和 setter 方法也用于 JSON 映射。

JSON mapping and validation

将以下 REST 资源创建为 org.acme.validation.BookResource

package org.acme.validation;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/books")
public class BookResource {

    @Inject
    Validator validator; 1

    @Path("/manual-validation")
    @POST
    public Result tryMeManualValidation(Book book) {
        Set<ConstraintViolation<Book>> violations = validator.validate(book);
        if (violations.isEmpty()) {
            return new Result("Book is valid! It was validated by manual validation.");
        } else {
            return new Result(violations);
        }
    }
}
1 Validator 实例通过 CDI 注入。

是的,它不会编译,缺少 Result,但我们很快就会添加。

方法参数 (book) 自动从 JSON 负载创建。

该方法使用 Validator 实例来检查负载。它返回违规集。如果该集合为空,则表示对象有效。如果失败,消息将被连接起来并发送回浏览器。

现在,让我们创建 Result 类为内部类:

public static class Result {

    Result(String message) {
        this.success = true;
        this.message = message;
    }

    Result(Set<? extends ConstraintViolation<?>> violations) {
        this.success = false;
        this.message = violations.stream()
             .map(cv -> cv.getMessage())
             .collect(Collectors.joining(", "));
    }

    private String message;
    private boolean success;

    public String getMessage() {
        return message;
    }

    public boolean isSuccess() {
        return success;
    }

}

该类非常简单,仅包含 2 个字段及其关联的 getter 和 setter。由于我们指示生成 JSON,因此会自动执行到 JSON 的映射。

REST end point validation

尽管手动使用 Validator 可能对一些高级用法有用,但如果你只想验证参数或 REST 端点的返回值,你可以直接注释它,用约束 (@NotNull@Digits…​) 或 @Valid (它会将验证逐步应用到 Bean)。

让我们创建一个端点,验证请求中提供的 Book

@Path("/end-point-method-validation")
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Result tryMeEndPointMethodValidation(@Valid Book book) {
    return new Result("Book is valid! It was validated by end point method validation.");
}

正如你所见,由于已自动验证提供的 Book,因此不再需要手动验证。

如果触发验证错误,将生成违规报告,并以 JSON 格式序列化,因为我们的端点会生成 JSON 输出。可以提取和操作它以显示适当的错误消息。

Service method validation

由于它可能会复制一些业务验证,因此将验证规则声明在端点级别可能并不总是方便的。

最佳选择是使用约束(或在我们的特定情况下使用 @Valid)注释业务服务的方法:

package org.acme.validation;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.validation.Valid;

@ApplicationScoped
public class BookService {

    public void validateBook(@Valid Book book) {
        // your business logic here
    }
}

在 REST 端点中调用服务会自动触发 Book 验证:

@Inject BookService bookService;

@Path("/service-method-validation")
@POST
public Result tryMeServiceMethodValidation(Book book) {
    try {
        bookService.validateBook(book);
        return new Result("Book is valid! It was validated by service method validation.");
    } catch (ConstraintViolationException e) {
        return new Result(e.getConstraintViolations());
    }
}

请注意,如果希望将验证错误推送到前端,则必须手动捕获异常并推送信息,因为它们不会自动推送到 JSON 输出。

请记住,你通常不想向公众公开你的服务内部信息 —— 尤其不希望公开违规对象中包含的经验证的值。

A frontend

现在,我们添加一个简单的网页来与我们的 BookResource 进行交互。Quarkus 就会自动提供 META-INF/resources 目录中包含的静态资源。在 src/main/resources/META-INF/resources 目录中,用此 index.html 文件中的内容替换 index.html 文件。

Run the application

现在,让我们看看应用程序在实际应用中的情况。使用以下命令运行它:

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

然后,在浏览器中打开 [role="bare"][role="bare"]http://localhost:8080/:

  1. 输入书籍详情(有效的或无效的)

  2. 单击 Try me&#8230;&#8203; 按钮,通过上述方法之一来检查你的数据是否有效。

validation guide screenshot

可以使用以下命令打包应用程序:

CLI
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

并使用 java -jar target/quarkus-app/quarkus-run.jar 执行。

您还可以使用以下命令构建本机可执行文件:

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.native.enabled=true

Going further

Hibernate Validator extension and CDI

Hibernate Validator 扩展与 CDI 紧密集成。

Configuring the ValidatorFactory

有时,你可能需要配置 ValidatorFactory 的行为,例如使用特定的 ParameterNameProvider

虽然 Quarkus 本身实例化 ValidatorFactory,但你可以非常简单地通过声明替换的 Bean 来调整它,这些 Bean 将注入配置。

如果你在应用程序中创建以下类型 Bean,它将自动注入到 ValidatorFactory 配置中:

  • jakarta.validation.ClockProvider

  • jakarta.validation.ConstraintValidator

  • jakarta.validation.ConstraintValidatorFactory

  • jakarta.validation.MessageInterpolator

  • jakarta.validation.ParameterNameProvider

  • jakarta.validation.TraversableResolver

  • org.hibernate.validator.spi.properties.GetterPropertySelectionStrategy

  • org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider

  • org.hibernate.validator.spi.scripting.ScriptEvaluatorFactory

你不必连接任何东西。

显然,对于每个已列出类型,你只能声明一个 Bean。 大多数情况下,这些 Bean 应该声明为 @ApplicationScoped。 但是,在 ConstraintValidator`s that are dependent of attributes of the constraint annotation (typically when implementing the `initialize(A constraintAnnotation) 方法的情况下,使用 @Dependent 作用域以确保每个注释上下文都有 ConstraintValidator Bean 的单独实例。

如果通过可用的配置属性和上述 CDI Bean 定制 ValidatorFactory ,无法满足你的要求,你可以通过注册 ValidatorFactoryCustomizer Bean 进一步对其定制。

例如,你可以覆盖内置的验证器(强制 @Email 约束)并使用以下类改用 MyEmailValidator

@ApplicationScoped
public class MyEmailValidatorFactoryCustomizer implements ValidatorFactoryCustomizer {

    @Override
    public void customize(BaseHibernateValidatorConfiguration<?> configuration) {
        ConstraintMapping constraintMapping = configuration.createConstraintMapping();

        constraintMapping
                .constraintDefinition(Email.class)
                .includeExistingValidators(false)
                .validatedBy(MyEmailValidator.class);

        configuration.addMapping(constraintMapping);
    }
}

所有实现 ValidatorFactoryCustomizer 的 Bean 都适用,这意味着你可以使用多个 Bean。如果你需要强制某些顺序,可以使用通常的 @jakarta.annotation.Priority 注释 —— 具有较高优先级的 Bean 先予以应用。

Constraint validators as beans

可将约束验证器声明为 CDI bean:

@ApplicationScoped
public class MyConstraintValidator implements ConstraintValidator<MyConstraint, String> {

    @Inject
    MyService service;

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

        return service.validate(value);
    }
}

初始化给定类型的约束验证器时,Quarkus 会检查此类型的 bean 是否可用,如果是,它会使用该 bean,而不是实例化 ConstraintValidator

因此,如示例中演示的那样,可以在约束验证器 bean 中完全使用注入。

ConstraintValidator bean 选择的范围非常重要:

  • 如果整个应用程序可以使用同一 ConstraintValidator bean 的实例,请使用 @ApplicationScoped 范围。

  • 如果 ConstraintValidator bean 实施了 initialize(A constraintAnnotation) 方法,并且依赖于约束注释的状态,请使用 @Dependent 范围以确保每个注释上下文都拥有单独的、配置正确的 ConstraintValidator bean 实例。

Validation and localization

默认情况下,约束违反消息会以构建系统区域设置返回。

可在 application.properties 中添加以下配置来配置此行为:

# The default locale to use
quarkus.default-locale=fr-FR

如果在 Jakarta REST 端点的上下文中使用的是 Quarkus REST 或 RESTEasy Classic,则 Hibernate Validator 将自动解析最佳区域设置,并从 Accept-Language HTTP 头部中使用它,前提是已在 application.properties 中正确指定支持的区域设置:

# The list of all the supported locales
quarkus.locales=en-US,es-ES,fr-FR

或者,可使用 all 使本机镜像可执行文件包含所有可用的区域设置。但这会显著增加可执行文件的体积。仅包含两个或三个区域设置与包含所有区域设置之间的差异至少为 23 MB。

基于 quarkus-smallrye-graphql 扩展程序的 GraphQL 服务也存在类似的机制。

如果此默认机制不够用,并且需要自定义区域设置解析,则可添加其他 org.hibernate.validator.spi.messageinterpolation.LocaleResolver

  • 将考虑实现 org.hibernate.validator.spi.messageinterpolation.LocaleResolver 的任何 CDI bean。

  • LocaleResolver`s are consulted in the order of `@Priority (优先级高的排在前面)。

  • LocaleResolver 如果无法解析区域设置,则可能会返回 null,然后将忽略该区域设置。

  • LocaleResolver 返回的第一个非空区域设置就是已解析的区域设置。

Validation groups for REST endpoint or service method validation

在将类传给不同方法时,有时需要启用相同的验证约束。

例如, Book 在传给 post 方法时可能需要 null 标识符(因为该标识符将被生成),但在传给 put 方法时需要一个非 null 标识符(因为该方法需要标识符来了解要更新的内容)。

为了解决这个问题,您可以利用验证组。验证组是您放在约束上以启用或禁用它们的标记。

首先,定义 PostPut 组,它们只是 Java 接口。

public interface ValidationGroups {
    interface Post extends Default { (1)
    }
    interface Put extends Default { (1)
    }
}
1 让自定义组扩展 Default 组。这意味着只要启用这些组,Default 组也启用。如果有一些约束您希望在 PostPut 方法中都验证,这很有用:您可以在这些约束上简单地使用默认组,例如在以下 title 属性上。

然后,将相关约束添加到 Book,为每个约束分配正确的组:

public class Book {

    @Null(groups = ValidationGroups.Post.class)
    @NotNull(groups = ValidationGroups.Put.class)
    public Long id;

    @NotBlank
    public String title;

}

最后,在您验证的方法中,在您的 @Valid 注释旁边添加一个 @ConvertGroup 注释。

@Path("/")
@POST
@Consumes(MediaType.APPLICATION_JSON)
public void post(@Valid @ConvertGroup(to = ValidationGroups.Post.class) Book book) { (1)
    // ...
}

@Path("/")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void put(@Valid @ConvertGroup(to = ValidationGroups.Put.class) Book book) { (2)
    // ...
}
1 启用 Post 组,这意味着仅对分配给 Post 组(和 Default 组)的约束针对 post 方法的 book 参数进行验证。在这种情况下,这意味着 Book.id 必须是 null,而 Book.title 不能是空白。
2 启用 Put 组,这意味着仅对分配给 Put 组(和 Default 组)的约束针对 put 方法的 book 参数进行验证。在这种情况下,这意味着 Book.id 不能是 null,而 Book.title 不能是空白。

Limitations

META-INF/validation.xml

使用 META-INF/validation.xml 文件配置 ValidatorFactory 在 Quarkus 中受 NOT 支持。

目前,Hibernate Validator 并未公开 API 从该文件中提取信息,以便我们可以注册反射的适当类。

要配置 ValidatorFactory,请使用公开的配置属性和 CDI 集成。

因此,在 Quarkus 中定义约束的唯一方法是通过注释您的类。

ValidatorFactory and native executables

Quarkus 提供了一个您可使用配置属性自定义的默认 ValidatorFactory。此 ValidatorFactory 经过精心初始化,以使用 Quarkus 特有的引导程序来支持原生可执行文件。

在原生可执行文件中,不支持自行创建 ValidatorFactory,如果您尝试执行此操作,则在运行原生可执行文件时,您将得到类似于 jakarta.validation.NoProviderFoundException: Unable to create a Configuration, because no Jakarta Bean Validation provider could be found. Add a provider like Hibernate Validator (RI) to your classpath. 的错误。

因此,您应始终通过注入 ValidatorFactory 实例或使用 CDI 注入直接注入 Validator 实例,使用 Quarkus 管理的 ValidatorFactory

为了支持使用默认引导程序创建 ValidatorFactory 的一些外部库,当调用 Validation.buildDefaultValidatorFactory() 时,Quarkus 返回由 Quarkus 管理的 ValidatorFactory

Hibernate Validator Configuration Reference

Unresolved include directive in modules/ROOT/pages/validation.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-hibernate-validator.adoc[]