使用 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 形式返回。
解决方案
我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。
克隆 Git 仓库:git clone https://github.com/quarkusio/quarkus-quickstarts.git
,或下载一个存档。
解决方案位于 validation-quickstart
目录。
创建 Maven 项目
首先,我们需要一个新项目。使用以下命令创建一个新项目
对于 Windows 用户
-
如果使用 cmd,(不要使用反斜杠
\
并将所有内容放在同一行上) -
如果使用 Powershell,请将
-D
参数用双引号括起来,例如"-DprojectArtifactId=validation-quickstart"
此命令将生成一个 Maven 结构,导入 Quarkus REST (原 RESTEasy Reactive)/Jakarta REST、Jackson 和 Hibernate Validator/Bean Validation 扩展。
如果您已经配置了 Quarkus 项目,可以通过在项目根目录下运行以下命令将 hibernate-validator
扩展添加到您的项目中
quarkus extension add hibernate-validator
./mvnw quarkus:add-extension -Dextensions='hibernate-validator'
./gradlew addExtension --extensions='hibernate-validator'
这会将以下内容添加到您的构建文件中
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
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 端点返回值等,都会导致内部服务器错误(响应状态码 |
前端
现在让我们添加一个简单的网页来与我们的 BookResource
进行交互。Quarkus 会自动服务 META-INF/resources
目录中包含的静态资源。在 src/main/resources/META-INF/resources
目录中,将 index.html
文件替换为这个 index.html 文件中的内容。
运行应用程序
现在,让我们看看我们的应用程序的运行情况。使用以下命令运行它
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
然后,在浏览器中打开 https://: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
更进一步
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 应声明为 但是,对于 |
如果通过可用的配置属性和上述 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 中完全使用注入。
您为
|
注入依赖于运行时配置的 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 都将被考虑在内。 -
LocaleResolver
s 将按照@Priority
的顺序进行查询(优先级高的优先)。 -
LocaleResolver
可能会在无法解析区域设置时返回 null,然后将被忽略。 -
LocaleResolver
返回的第一个非 null 区域设置即为解析出的区域设置。
REST 端点或服务方法验证的验证组
有时需要为同一类在传递给不同方法时启用不同的验证约束。
例如,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 方法的 book 参数,只有分配给 Post (和 Default )组的约束才会被验证。在这种情况下,这意味着 Book.id 必须为 null ,Book.title 不能留空。 |
2 | 启用 Put 组,这意味着对于 put 方法的 book 参数,只有分配给 Put (和 Default )组的约束才会被验证。在这种情况下,这意味着 Book.id 不能为 null ,Book.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 配置参考
构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖
配置属性 |
类型 |
默认 |
---|---|---|
启用快速失败模式。启用快速失败后,验证将在检测到第一个约束违例时停止。 环境变量: 显示更多 |
布尔值 |
|
类型 |
默认 |
|
定义覆盖约束的方法是否应抛出 请参阅 JSR 380 规范的第 4.5.5 节,特别是
环境变量: 显示更多 |
布尔值 |
|
定义定义约束的并行方法是否应抛出 请参阅 JSR 380 规范的第 4.5.5 节,特别是
环境变量: 显示更多 |
布尔值 |
|
定义返回值上的多个约束是否可以标记为级联验证。默认值为 请参阅 JSR 380 规范的第 4.5.5 节,特别是
环境变量: 显示更多 |
布尔值 |
|
类型 |
默认 |
|
配置约束的表达式语言功能级别,允许选择消息插值中可用的表达式语言功能。 此属性仅影响通过约束注解的 特别地,它不影响为验证器实现中以编程方式创建的自定义违例设置的默认 EL 功能级别。这些功能的特征级别只能在验证器实现中直接配置。 环境变量: 显示更多 |
|
|