编辑此页面

使用 Hibernate Validator 进行验证

本指南介绍如何使用 Hibernate Validator/Bean Validation 来

  • 验证 REST 服务的输入/输出;

  • 验证业务服务方法的参数和返回值。

先决条件

要完成本指南,您需要

  • 大约 15 分钟

  • 一个 IDE

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

  • Apache Maven 3.9.9

  • 如果您想使用它,可以选择 Quarkus CLI

  • 如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置

架构

本指南中构建的应用程序非常简单。用户在一个网页上填写表单。网页将表单内容通过 Ajax 发送给 BookResource 作为 JSON。BookResource 验证用户输入,并将 *结果* 以 JSON 形式返回。

Architecture

解决方案

我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。

克隆 Git 仓库:git clone https://github.com/quarkusio/quarkus-quickstarts.git,或下载一个存档

解决方案位于 validation-quickstart 目录

创建 Maven 项目

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

CLI
quarkus create app org.acme:validation-quickstart \
    --extension='rest-jackson,hibernate-validator' \
    --no-code
cd validation-quickstart

要创建 Gradle 项目,请添加 --gradle--gradle-kotlin-dsl 选项。

有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=validation-quickstart \
    -Dextensions='rest-jackson,hibernate-validator' \
    -DnoCode
cd validation-quickstart

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

对于 Windows 用户

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

  • 如果使用 Powershell,请将 -D 参数用双引号括起来,例如 "-DprojectArtifactId=validation-quickstart"

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

如果您已经配置了 Quarkus 项目,可以通过在项目根目录下运行以下命令将 hibernate-validator 扩展添加到您的项目中

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

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

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

约束

在此应用程序中,我们将测试一个基本对象,但我们支持复杂的约束并可以验证对象图。创建 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 映射和验证

将以下 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 端点验证

虽然手动使用 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 输出。可以提取并操作它以显示适当的错误消息。

一个这样的报告示例可能是

{
    "title": "Constraint Violation",
    "status": 400,
    "violations": [
        {
            "field": "tryMeEndPointMethodValidation.book.title",
            "message": "Title cannot be blank"
        }
    ]
}

此响应由 Quarkus 通过 jakarta.ws.rs.ext.ExceptionMapper 的内置实现生成。如果应用程序代码需要以某种自定义方式处理 ValidationException,它可以像这样提供 jakarta.ws.rs.ext.ExceptionMapper 的实现

import jakarta.validation.ValidationException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

@Provider
public class ResteasyReactiveViolationExceptionMapper implements ExceptionMapper<ValidationException> {

    @Override
    public Response toResponse(ValidationException exception) {
        // TODO: implement
    }
}

服务方法验证

将验证规则声明在端点级别可能并不总是方便,因为它可能重复一些业务验证。

最佳选择是将业务服务的某个方法注解为带有您的约束(或者在本例中为 @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 输出,并且会被视为任何其他内部服务器错误。

请记住,通常您不想向公众公开服务的内部细节 — 尤其是验证对象中包含的已验证值。

默认情况下,只有 REST 端点参数的约束验证失败才会导致“错误请求”响应,并在响应正文中包含验证报告。在任何其他情况下,例如验证服务方法(无论是参数还是返回值)、REST 端点返回值等,都会导致内部服务器错误(响应状态码 5xx),除非应用程序代码显式捕获并处理 ConstraintViolationException(或实现了自定义 ExceptionMapper)。这种行为背后的原因是,只有 REST 端点参数被视为用户输入,因此可以被视为“错误请求”(响应状态码 4xx)。同时,其他地方的约束违例源于应用程序逻辑执行,因此被视为内部错误。

前端

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

运行应用程序

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

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

然后,在浏览器中打开 https://:8080/

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

  2. 点击“*Try me…*”按钮,使用我们上面介绍的任何一种方法检查您的数据是否有效。

Application

可以使用以下方式打包应用程序

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

更进一步

Hibernate Validator 扩展和 CDI

Hibernate Validator 扩展与 CDI 紧密集成。

配置 ValidatorFactory

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

虽然 ValidatorFactory 由 Quarkus 本身实例化,但您可以通过声明将注入到配置中的替换 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,它们依赖于约束注解的属性(通常在实现 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 都会被应用,这意味着您可以有多个。如果您需要强制某些顺序,您可以使用常规的 @jakarta.annotation.Priority 注解 — 优先级更高的 Bean 会先应用。

Bean 作为约束验证器

您可以将约束验证器声明为 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 实例。

注入依赖于运行时配置的 Bean 时,请使用 @Inject Instance<..>。由于约束在构建时初始化,因此无法在 initialize(..) 方法中完全预配置约束,因为此时运行时信息将丢失。在这种情况下,initialize(..) 方法仍可用于读取约束注解参数,并执行任何不依赖于运行时配置的工作。因此,建议将任何繁重的配置工作作为注入 Bean 的初始化部分,并且 ConstraintValidator#isValid(..) 实现中使用的 Méthodes 尽快。

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

    @Inject
    Instance<MyService> service;

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

        return service.get().validate(value);
    }
}

@ApplicationScoped
public class MyService {

	private final Predicate<String> validationFunction;

	@Inject
	public MyService(MyRuntimeConfig config) {
		// perform all possible "initialization" work, e.g.:
		if (config.complexValidationEnabled()) {
			validationFunction = s -> ...
		} else {
			validationFunction = String::isBlank;
		}
	}

	public boolean validate(String value) {
		// perform the validation
		return validationFunction.test(value);
	}
}

验证和本地化

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

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

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

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

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

或者,您可以使用 all 来使 native-image 可执行文件包含所有可用的区域设置。但这会大大增加可执行文件的大小。仅包含两三个区域设置与包含所有区域设置之间的差异至少为 23 MB。

对于基于 quarkus-smallrye-graphql 扩展的 GraphQL 服务,存在类似的机制。

如果此默认机制不足以满足您的需求,并且您需要自定义区域设置解析,您可以添加额外的 org.hibernate.validator.spi.messageinterpolation.LocaleResolver

  • 任何实现 org.hibernate.validator.spi.messageinterpolation.LocaleResolver 的 CDI Bean 都将被考虑在内。

  • LocaleResolvers 将按照 @Priority 的顺序进行查询(优先级高的优先)。

  • LocaleResolver 可能会在无法解析区域设置时返回 null,然后将被忽略。

  • LocaleResolver 返回的第一个非 null 区域设置即为解析出的区域设置。

REST 端点或服务方法验证的验证组

有时需要为同一类在传递给不同方法时启用不同的验证约束。

例如,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 方法的 book 参数,只有分配给 Post(和 Default)组的约束才会被验证。在这种情况下,这意味着 Book.id 必须为 nullBook.title 不能留空。
2 启用 Put 组,这意味着对于 put 方法的 book 参数,只有分配给 Put(和 Default)组的约束才会被验证。在这种情况下,这意味着 Book.id 不能为 nullBook.title 不能留空。

限制

META-INF/validation.xml

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

目前,Hibernate Validator 不暴露 API 来提取此文件中的信息,以便我们可以注册适当的类以进行反射。

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

虽然不能通过 XML 配置验证器工厂,但可以声明约束。Quarkus 接受类中或 validation.xml 中通过注解声明的约束。

ValidatorFactory 和原生可执行文件

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

自己创建 ValidatorFactory 在原生可执行文件中不受支持,如果您尝试这样做,您会收到类似 jakarta.validation.NoProviderFoundException: 无法创建 Configuration,因为找不到 Jakarta Bean Validation 提供程序。请将提供程序(如 Hibernate Validator (RI))添加到您的类路径中。 的错误,当运行您的原生可执行文件时。

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

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

Hibernate Validator 配置参考

构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖

配置属性

类型

默认

启用快速失败模式。启用快速失败后,验证将在检测到第一个约束违例时停止。

环境变量:QUARKUS_HIBERNATE_VALIDATOR_FAIL_FAST

显示更多

布尔值

false

方法验证

类型

默认

定义覆盖约束的方法是否应抛出 ConstraintDefinitionException。默认值为 false,即不允许。

请参阅 JSR 380 规范的第 4.5.5 节,特别是

在子类型中(无论是子类/接口还是接口实现),在被覆盖或实现的方法上不得声明参数约束,也不得将参数标记为级联验证。这将加强调用者需要满足的前提条件。

环境变量:QUARKUS_HIBERNATE_VALIDATOR_METHOD_VALIDATION_ALLOW_OVERRIDING_PARAMETER_CONSTRAINTS

显示更多

布尔值

false

定义定义约束的并行方法是否应抛出 ConstraintDefinitionException。默认值为 false,即不允许。

请参阅 JSR 380 规范的第 4.5.5 节,特别是

如果子类型覆盖/实现了在层次结构中的多个并行类型(例如,两个不扩展彼此的接口,或者一个类和一个未由该类实现的接口)中定义的原始方法,则不得为该方法声明任何参数约束,也不得将参数标记为级联验证。这同样是为了避免调用者需要满足的前提条件不必要地加强。

环境变量:QUARKUS_HIBERNATE_VALIDATOR_METHOD_VALIDATION_ALLOW_PARAMETER_CONSTRAINTS_ON_PARALLEL_METHODS

显示更多

布尔值

false

定义返回值上的多个约束是否可以标记为级联验证。默认值为 false,即不允许。

请参阅 JSR 380 规范的第 4.5.5 节,特别是

在一个类层次结构中,不得将方法返回值标记为级联验证一次以上。换句话说,子类型(无论是子类/接口还是接口实现)上的覆盖方法不能将返回值标记为级联验证,如果该返回值已在超类型或接口的被覆盖方法上标记。

环境变量:QUARKUS_HIBERNATE_VALIDATOR_METHOD_VALIDATION_ALLOW_MULTIPLE_CASCADED_VALIDATION_ON_RETURN_VALUES

显示更多

布尔值

false

表达式语言

类型

默认

配置约束的表达式语言功能级别,允许选择消息插值中可用的表达式语言功能。

此属性仅影响通过约束注解的 message 属性设置的“静态”约束违例消息的 EL 功能级别。

特别地,它不影响为验证器实现中以编程方式创建的自定义违例设置的默认 EL 功能级别。这些功能的特征级别只能在验证器实现中直接配置。

环境变量:QUARKUS_HIBERNATE_VALIDATOR_EXPRESSION_LANGUAGE_CONSTRAINT_EXPRESSION_FEATURE_LEVEL

显示更多

default, none, variables, bean-properties, bean-methods

bean-properties