Quarkus 集成 Validation 参数验证

Quarkus 集成 Validation 参数验证
MeteorCat官方文档: validation
Quarkus 集成 Jakarta Validation(原 Bean Validation) 能优雅地完成请求参数和方法入参/返回值的合法性校验, 无需手写大量逻辑.
除了挂载 Rest 相关组件需要引入以下依赖:
1 | <!-- 其他略 --> |
官方简单的例子, 需要先定义验证结构体, 这里推荐采用高版本的 record 特性, 能节省大量编写样板代码:
1 | import jakarta.validation.constraints.*; |
这里用到 quarkus-rest 设计验证, 这里面有两种参数验证方式:
-
@Valid: Jakarta 原生注解, 支持基本的验证功能 -
@Validated: 扩展的验证特性, 包括SpringBoot也支持的扩展, 用于定制高级的扩展功能
最终测试例子如下, 注意这里可能会报错, 可以先把这些异常错误放着后面说明:
1 | import jakarta.validation.Valid; |
这里测试模拟 POST 请求数据:
1 | 我这边采用监听端口为 9099 |
注意: 这里可能会异常没办法解析出结构体, 这是因为 record 是高级特性, 需要额外引入组件依赖
1 | <!-- 其他略 --> |
quarkus-hibernate-validator 涉及验证注解如下:
| 注解 | 适用数据类型 | 核心校验规则 | 典型应用场景 |
|---|---|---|---|
@NotNull |
所有类型(对象/基本类型包装类) | 校验对象不能为 null(对空字符串 ""、空集合、0 等不生效) |
1. 实体主键(如 id)、关联对象(如 user)2. 非空的数值型参数(如 age price)3. 非空的布尔值(如 isVip) |
@NotBlank |
仅 String 类型 | 校验字符串不能为 null + 去除首尾空格后长度 > 0(即非空且非空白) | 1. 用户名、密码、手机号、邮箱(需实际字符) 2. 订单备注、用户昵称(不允许纯空格) 3. 接口查询的关键词参数 |
@NotEmpty |
String/Collection/Map/数组 | 1. 字符串:非 null + 长度 > 0(不去除空格) 2. 集合/数组:非 null + 元素个数 > 0 |
1. 字符串(允许首尾空格,如地址 address)2. 集合(如 roles 角色列表、ids 批量ID数组)3. Map(如请求参数 params) |
@Null |
所有类型 | 校验对象必须为 null(极少用,多用于特殊业务规则) | 1. 新增场景下强制 id 为 null(防止手动传ID)2. 特定状态下某字段必须为空 |
@Min |
数值型(byte/short/int/long/浮点型/大数)、String(可转数字) | 校验数值大于等于指定最小值 | 1. 年龄(@Min(18))、商品库存(@Min(0))2. 订单金额( @Min(0.01))、用户等级(@Min(1)) |
@Max |
同 @Min |
校验数值小于等于指定最大值 | 1. 年龄(@Max(120))、商品限购数量(@Max(10))2. 接口分页大小( @Max(100))、金额上限(@Max(999999.99)) |
@DecimalMin |
同 @Min(更精准的小数控制) |
校验数值≥指定最小值(支持指定 inclusive=false 表示「大于」) |
1. 支付金额(@DecimalMin(value = "0.01", inclusive = true))2. 利率( @DecimalMin("0.001")) |
@DecimalMax |
同 @DecimalMin |
校验数值≤指定最大值(支持 inclusive=false 表示「小于」) |
1. 折扣率(@DecimalMax("1.0", inclusive = true))2. 税率( @DecimalMax("0.25", inclusive = true)) |
@Positive |
数值型(同 @Min) |
校验数值严格大于 0(不包含 0) | 1. 商品单价(@Positive)、提现金额(@Positive)2. 积分兑换数量( @Positive) |
@PositiveOrZero |
数值型(同 @Min) |
校验数值大于等于 0(包含 0) | 1. 商品销量(@PositiveOrZero)、退款金额(@PositiveOrZero)2. 库存余量( @PositiveOrZero) |
@Negative |
数值型(同 @Min) |
校验数值严格小于 0(不包含 0) | 1. 账户欠费金额(@Negative)、扣减积分(@Negative) |
@NegativeOrZero |
数值型(同 @Min) |
校验数值小于等于 0(包含 0) | 1. 退款金额(反向记账,@NegativeOrZero)、积分扣减上限(@NegativeOrZero) |
@Digits |
数值型/字符串(可转数字) | 校验数字的整数位+小数位总长度不超过指定值 格式: @Digits(integer=整数位, fraction=小数位) |
1. 手机号(@Digits(integer=11, fraction=0))2. 金额( @Digits(integer=8, fraction=2),如最大 99999999.99) |
@Size |
String/Collection/Map/数组 | 1. 字符串:长度在 min-max 之间2. 集合/数组:元素个数在 min-max 之间 |
1. 用户名长度(@Size(min=4, max=20))2. 角色列表( @Size(min=1, max=5))3. 密码长度( @Size(min=8, max=32)) |
@Pattern |
仅 String 类型 | 字符串匹配指定的正则表达式 | 1. 手机号(@Pattern(regexp="^1[3-9]\\d{9}$"))2. 邮箱( @Pattern(regexp="^\\w+@\\w+\\.\\w+$"))3. 用户名格式(字母+数字+下划线) |
@Email |
仅 String 类型 | 校验字符串为合法邮箱格式(支持 regexp 自定义正则) |
1. 用户邮箱(@Email(message="邮箱格式错误"))2. 联系邮箱( @Email(regexp="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")) |
@Future |
Date/Calendar/Instant/LocalDate 等日期类型 | 校验日期晚于当前系统时间 | 1. 预约时间(@Future)、活动结束时间(@Future)2. 过期时间( @Future) |
@FutureOrPresent |
同 @Future |
校验日期晚于或等于当前系统时间 | 1. 优惠券生效时间(@FutureOrPresent)、任务执行时间(@FutureOrPresent) |
@Past |
同 @Future |
校验日期早于当前系统时间 | 1. 出生日期(@Past)、订单创建时间(@Past)2. 登录时间( @Past) |
@PastOrPresent |
同 @Future |
校验日期早于或等于当前系统时间 | 1. 商品上架时间(@PastOrPresent)、退款申请时间(@PastOrPresent) |
@AssertTrue |
布尔型(Boolean/boolean) | 校验布尔值必须为 true | 1. 用户同意协议(@AssertTrue(message="必须同意用户协议"))2. 数据有效性标记( @AssertTrue) |
@AssertFalse |
布尔型(Boolean/boolean) | 校验布尔值必须为 false | 1. 禁用标记(@AssertFalse(message="该功能已禁用,不可提交"))2. 删除标记( @AssertFalse) |
@Valid |
复杂对象(嵌套实体/集合) | 触发嵌套对象的递归校验(如 UserDTO 中的 AddressDTO 字段) | 1. 订单DTO中的收货地址(@Valid private AddressDTO address)2. 批量用户列表( @Valid List<UserDTO> users) |
另外还有其他在 quarkus 的 application.properties 配置文件修改的参数:
1 | ## Quarkus Hibernate |
其他配置基本不太需要关注, 如果需要就可以去官方查询下就可以了.
异常拦截
目前已经能够实现拦截参数, 但是没办法拦截异常并返回自定义的参数对象, 这里就依赖 ExceptionMapper + @Provider
拦截全局异常:
1 | import jakarta.validation.ConstraintViolation; |
只要触发 ConstraintViolationException 异常就会被拦截, 不过如果你传递以下内容会抛出怪异错误:
1 | { |
这里其实是内部 jackson 序列化处理异常问题, 但是没有被 Quarkus 内部异常拦截, 官方的 issues 也有反馈:
可能这部分是源于 Hibernate Validator 内部自身的异常, 开启 DEBUG 模式发现是
com.fasterxml.jackson.databind.exc.InvalidFormatException 抛出的异常, 所以要拦截异常
1 | import com.fasterxml.jackson.databind.exc.InvalidFormatException; |
这样就能处理对应的 Map|List 的 JSON 字段内容, 方便客户端做更加丰富的 JSON 提交.
自定义拦截器
可以先参考 @Email 格式定义手段, 这里需要自定义注解和实现 ConstraintValidator 来构建自己的拦截处理器.
可以编写个自定义的 货币代码(CurrencyCode) 验证器来验证用户传入参数是否 USD/CNY/HKD/JPY 合法值, 需要定义以下功能类:
-
@CurrencyCode: 提供给验证结构的注解对象 -
CurrencyCodeValidator: 具体的检查验证器, 拦截参数之后回调该对象
@CurrencyCode 注解对象声明如下:
1 | import jakarta.validation.Constraint; |
CurrencyCodeValidator 拦截器验证类功能如下:
1 | import jakarta.validation.ConstraintValidator; |
还需要定制写本地 i18n 异常模板, 这里采用比较常用的中英文错误码表即可.
需要将以下文件创建于 src/main/resources 之下, 应用启动的时候会自动加载系统之中:
-
ValidationMessages_en.properties: 英文错误表 -
ValidationMessages_zh.properties: 中文错误表
/ValidationMessages_en.properties 内容如下:
1 | currency.code.invalid=Unsupported currency code, please use ISO 4217 standard |
ValidationMessages_zh.properties 内容如下:
1 | # 自定义货币码校验提示 |
这样系统异常的时候会默认会去加载这部分码表获取错误消息, 最后就是引用该注解:
1 | /** |
最后就可以做具体的功能验证, 直接测试请求:
1 | 传递不成立的参数 |
测试完成通过即可, 后续可以去编写衍生出多种专属的参数拦截器.




