Validation with Hibernate Validator
本指南介绍了如何使用 Hibernate Validator/Bean Validation:
-
对 REST 服务的输入/输出进行验证;
-
对业务服务的方法的参数和返回值进行验证。
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)发送到 BookResource
。BookResource
验证用户信息,并将 result 作为 JSON 返回。
Solution
我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。
克隆 Git 存储库: git clone $${quickstarts-base-url}.git
,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。
该解决方案位于 validation-quickstart
directory 中。
Creating the Maven project
首先,我们需要一个新项目。使用以下命令创建一个新项目:
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 指南。
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
扩展添加到自己的项目中:
quarkus extension add {add-extension-extensions}
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
./gradlew addExtension --extensions='{add-extension-extensions}'
这会将以下内容添加到构建文件中:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
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
现在,让我们看看应用程序在实际应用中的情况。使用以下命令运行它:
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
然后,在浏览器中打开 [role="bare"][role="bare"]http://localhost:8080/:
-
输入书籍详情(有效的或无效的)
-
单击 Try me…​ 按钮,通过上述方法之一来检查你的数据是否有效。
可以使用以下命令打包应用程序:
quarkus build
./mvnw install
./gradlew build
并使用 java -jar target/quarkus-app/quarkus-run.jar
执行。
您还可以使用以下命令构建本机可执行文件:
quarkus build --native
./mvnw install -Dnative
./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
标识符(因为该方法需要标识符来了解要更新的内容)。
为了解决这个问题,您可以利用验证组。验证组是您放在约束上以启用或禁用它们的标记。
首先,定义 Post
和 Put
组,它们只是 Java 接口。
public interface ValidationGroups {
interface Post extends Default { (1)
}
interface Put extends Default { (1)
}
}
1 | 让自定义组扩展 Default 组。这意味着只要启用这些组,Default 组也启用。如果有一些约束您希望在 Post 和 Put 方法中都验证,这很有用:您可以在这些约束上简单地使用默认组,例如在以下 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
。