本指南介绍如何使用 Hibernate Validator/Bean Validation,校验 REST 服务的输入输出,以及业务服务方法的参数和返回值。
前提条件
完成指南需要约 15 分钟、一个 IDE、安装 JDK 17 或更高版本并正确配置 JAVA_HOME,以及 Apache Maven 3.9.16。也可以选择使用 Quarkus CLI。若要构建原生可执行文件,可安装并配置 Mandrel 或 GraalVM,或使用 Docker 进行原生容器构建。
架构
应用很简单:用户在网页填写表单,页面通过 Ajax 将内容作为 JSON 发送给 BookResource。它校验用户输入,再以 JSON 返回结果。
完整示例
建议按照下文逐步创建应用,也可以直接查看完整示例。克隆 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。CLI 安装和用法见 Quarkus CLI 指南。
Maven 命令:
mvn io.quarkus.platform:quarkus-maven-plugin:3.40.1: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;
@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);
}
}
}
这里通过 CDI 注入 Validator。此时代码还不能编译,因为尚缺少 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;
}
}
它只有两个字段和相应访问方法。因为资源声明输出 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.");
}
现在不需要手动校验。出现错误时,系统生成约束违反报告,并按端点的 JSON 输出格式序列化。前端可以提取报告并显示合适错误消息。例如原文给出的响应:
{
"title": "Constraint Violation",
"status": 400,
"violations": [
{
"field": "tryMeEndPointMethodValidation.book.title",
"message": "Title cannot be blank"
}
]
}
Quarkus 的内置 jakarta.ws.rs.ext.ExceptionMapper 生成这个响应。如果应用要自定义处理 ValidationException,可提供自己的 mapper:
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。这是因为只有端点参数视为用户输入,可归为 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
然后在浏览器打开 http://localhost:8080/,输入有效或无效的图书信息,点击 Try me 按钮,分别检查上述方法如何校验数据。
打包应用,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
扩展与 CDI 紧密集成。
配置 ValidatorFactory
有时需要改变 ValidatorFactory 的行为,例如指定 ParameterNameProvider。虽然工厂由 Quarkus 创建,但只要声明替代 bean,就可以注入配置,轻松调整行为。以下类型的 bean 会自动注入工厂配置,无需手动连接:
jakarta.validation.ClockProviderjakarta.validation.ConstraintValidatorjakarta.validation.ConstraintValidatorFactoryjakarta.validation.MessageInterpolatorjakarta.validation.ParameterNameProviderjakarta.validation.TraversableResolverorg.hibernate.validator.spi.properties.GetterPropertySelectionStrategyorg.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProviderorg.hibernate.validator.spi.scripting.ScriptEvaluatorFactory
每个类型只能声明一个 bean。多数应为 @ApplicationScoped。但若 ConstraintValidator 依赖约束注解的属性,通常会实现 initialize(A constraintAnnotation),应使用 @Dependent,确保每个注解上下文有独立实例。
配置属性与这些 CDI bean 仍不足时,可注册 ValidatorFactoryCustomizer 进一步定制。例如覆盖内置 @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);
}
}
所有实现该接口的 bean 都会应用,可以有多个。需要顺序时,使用 @jakarta.annotation.Priority,优先级较高的先应用。
把约束校验器声明为 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;存在时直接使用,而不是另行创建 ConstraintValidator。因此可以在校验器里充分使用注入。
作用域很重要:同一实例可供全应用使用时,选择 @ApplicationScoped;实现 initialize 并依赖约束注解状态时,选择 @Dependent,使每个上下文获得独立且正确配置的实例。
注入依赖运行时配置的 bean 时,使用 @Inject Instance<..>。约束在构建时初始化,那时尚无运行时信息,所以不能在 initialize(..) 中完成全部配置。该方法仍可读取注解参数,完成不依赖运行时配置的工作。建议把繁重配置放入注入 bean 的初始化中,并让 ConstraintValidator#isValid(..) 调用的方法尽可能快:
@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);
}
}
校验与本地化
默认违反约束的消息使用构建系统的 locale。可在 application.properties 中设置:
# The default locale to use
quarkus.default-locale=fr-FR
使用 Quarkus REST 或 RESTEasy Classic 时,Jakarta REST 端点上下文里的 Hibernate Validator 可根据 Accept-Language 请求头自动选择最佳语言,但必须正确列出支持语言:
# The list of all the supported locales
quarkus.locales=en-US,es-ES,fr-FR
也可以设置 all,使原生可执行文件包含全部可用语言,但体积会显著增加;原文指出,比只包含两三个语言至少多 23MB。基于 quarkus-smallrye-graphql 的 GraphQL 服务也有类似机制。
默认机制不够时,可增加 org.hibernate.validator.spi.messageinterpolation.LocaleResolver:实现该接口的 CDI bean 都会参与;按 @Priority 从高到低查询;无法解析时返回 null 并忽略;第一个非 null 返回值就是最终 locale。
REST 端点与服务方法的校验组
同一个类传给不同方法时,有时需要启用不同约束。例如 POST 的 Book 标识应为 null,因为会生成新标识;PUT 则应非 null,因为需要确定更新对象。校验组就是加到约束上的标记,用来按需启用或禁用。
先定义两个普通 Java 接口 Post 与 Put:
public interface ValidationGroups {
interface Post extends Default {
}
interface Put extends 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) {
// ...
}
@Path("/")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void put(@Valid @ConvertGroup(to = ValidationGroups.Put.class) Book book) {
// ...
}
POST 启用 Post 与 Default,因此 Book.id 必须为 null,Book.title 必须非空白;PUT 启用 Put 与 Default,因此 id 必须非 null,title 必须非空白。
限制
META-INF/validation.xml
Quarkus 不支持通过 META-INF/validation.xml 配置 ValidatorFactory。目前 Hibernate Validator 没有提供提取该文件信息的 API,以供 Quarkus 注册反射所需类。应使用公开配置属性和 CDI 集成。
不能通过 XML 配置工厂,但可以声明约束:Quarkus 接受类上注解或 validation.xml 中定义的约束。
ValidatorFactory 与原生可执行文件
Quarkus 提供默认工厂,可通过配置属性定制。它使用专属于 Quarkus 的启动过程初始化,以支持原生程序。原生程序不支持自行创建工厂,否则运行时可能出现 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.。
因此,应始终通过 CDI 注入 Quarkus 管理的 ValidatorFactory 或直接注入 Validator。为了兼容使用默认启动过程创建工厂的外部库,调用 Validation.buildDefaultValidatorFactory() 时,Quarkus 会返回自己管理的工厂。
Hibernate Validator 配置参考
原页面标记了构建时固定的属性,其余属性可在运行时覆盖。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
quarkus.hibernate-validator.fail-fast |
boolean | false |
启用快速失败,首次违反约束就停止。环境变量 QUARKUS_HIBERNATE_VALIDATOR_FAIL_FAST。 |
quarkus.hibernate-validator.method-validation.allow-overriding-parameter-constraints |
boolean | false |
是否允许覆盖方法中的参数约束,默认不允许,否则抛出 ConstraintDefinitionException。JSR 380 §4.5.5 规定,子类或接口实现的覆盖方法不得添加参数约束或级联校验,以免加强调用者必须满足的前置条件。环境变量 QUARKUS_HIBERNATE_VALIDATOR_METHOD_VALIDATION_ALLOW_OVERRIDING_PARAMETER_CONSTRAINTS。 |
quarkus.hibernate-validator.method-validation.allow-parameter-constraints-on-parallel-methods |
boolean | false |
是否允许并行类型层级的方法定义参数约束,默认不允许,否则抛出 ConstraintDefinitionException。同一方法来自互不继承的多个接口,或来自类及其未实现接口时,不得声明参数约束或级联校验,以免意外加强前置条件。环境变量 QUARKUS_HIBERNATE_VALIDATOR_METHOD_VALIDATION_ALLOW_PARAMETER_CONSTRAINTS_ON_PARALLEL_METHODS。 |
quarkus.hibernate-validator.method-validation.allow-multiple-cascaded-validation-on-return-values |
boolean | false |
是否允许同一返回值在类型继承链中多次标为级联校验,默认不允许,否则抛出 ConstraintDefinitionException。若父类型的方法返回值已标记,覆盖方法不可再次标记。环境变量 QUARKUS_HIBERNATE_VALIDATOR_METHOD_VALIDATION_ALLOW_MULTIPLE_CASCADED_VALIDATION_ON_RETURN_VALUES。 |
quarkus.hibernate-validator.expression-language.constraint-expression-feature-level |
default、none、variables、bean-properties、bean-methods |
bean-properties |
配置约束消息插值可用的表达式语言特性。只影响约束注解 message 中静态消息,不影响校验器程序化创建的自定义违反约束消息;后者须在校验器实现中直接配置。环境变量 QUARKUS_HIBERNATE_VALIDATOR_EXPRESSION_LANGUAGE_CONSTRAINT_EXPRESSION_FEATURE_LEVEL。 |
原作:Validation with Hibernate Validator,Quarkus 文档贡献者。依据页面标示的 CC BY 3.0转为中文,代码来自同页 HTML,已去除用于解释的可视化编号,程序文本保持原样。文中运行步骤是原文操作说明,本稿未创建、编译或测试项目,也未生成真实应用截图。示例是分步教学片段,部分省略导入或实现,不应把单段直接当作完整可编译项目;校验约束应按业务完善。











暂无评论内容