Apache BVal 快速入门与集成示例

Apache BVal 3.0.2 快速入门:Maven 与手动双 jar 两种引入,示例覆盖内置约束回显、分组与级联、容器元素校验、bval-extras 扩展约束,JDK 17 实测。

Apache BValBVal2026/10/13

Apache BVal 快速入门与集成示例

Apache BVal 是 Jakarta Bean Validation 的开源实现:在 JavaBean 上标注约束注解,调用 Validator 即可拿到约束违反清单。本篇以 BVal 3.0.2 为例,覆盖 Maven 与手动双 jar 两种引入方式、内置约束与 bval-extras 扩展约束的实测回显,JDK 17 下全部真实运行。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS,x86_64
  • JDK:OpenJDK 17.0.20.1;构建工具:Apache Maven 3.6.3
  • 组件版本:bval-jsr 3.0.2、bval-extras 3.0.2、jakarta.validation-api 3.0.2(Maven Central 最新稳定版,2026-10 实测)
  • 运行统一带 LANG=C.UTF-8 与 -Dfile.encoding=UTF-8(中文输出需要)
  • 实验工程:/opt/bval-demo

二、前置条件

java -version 2>&1 | head -1; mvn -version 2>&1 | head -1

回显(实测):

openjdk version "17.0.20.1" 2026-08-18
Apache Maven 3.6.3

JDK 11 及以上即可(BVal 3.0.2 字节码为 Java 11,MANIFEST 实测 Build-Jdk-Spec: 11)。

三、依赖引入

3.1 方式 A:Maven(推荐)

<dependency>
  <groupId>org.apache.bval</groupId>
  <artifactId>bval-jsr</artifactId>
  <version>3.0.2</version>
</dependency>
<dependency>
  <groupId>org.apache.bval</groupId>
  <artifactId>bval-extras</artifactId>
  <version>3.0.2</version>
</dependency>
<dependency>
  <groupId>jakarta.validation</groupId>
  <artifactId>jakarta.validation-api</artifactId>
  <version>3.0.2</version>
</dependency>

实测依赖树(mvn dependency:tree):

demo:bval-demo:jar:1.0.0
+- org.apache.bval:bval-jsr:jar:3.0.2:compile
+- org.apache.bval:bval-extras:jar:3.0.2:compile
\- jakarta.validation:jakarta.validation-api:jar:3.0.2:compile

jakarta.validation-api 必须显式声明:bval-jsr 把它标为 provided(预期由容器提供),独立应用不写就编译不过。运行期最小只需 bval-jsr + jakarta.validation-api 双 jar(bval-extras 仅在使用其扩展约束时需要,实测见第五节)。

3.2 方式 B:手动双 jar

从 Maven Central 下载 bval-jsr-3.0.2.jar(748810 字节)与 jakarta.validation-api-3.0.2.jar(93298 字节):

javac -encoding UTF-8 -cp lib/jakarta.validation-api-3.0.2.jar -d classes src/demo/BvalDemo.java
LANG=C.UTF-8 java -Dfile.encoding=UTF-8 -cp "classes:lib/bval-jsr-3.0.2.jar:lib/jakarta.validation-api-3.0.2.jar" demo.BvalDemo

编译只需规范 API jar,运行需实现 jar(bval-jsr 自带 META-INF/services/jakarta.validation.spi.ValidationProvider,buildDefaultValidatorFactory() 经 SPI 自动发现 ApacheValidationProvider,无需手写注册文件)。完整下载与 sha1 校验流程见《Ubuntu 22.04 安装 Apache BVal 指南》第五节。

四、核心 API 速览

类/接口作用
jakarta.validation.Validation引导入口,构建 ValidatorFactory(SPI 自动发现或 byProvider 显式指定)
ValidatorFactory工厂,创建 Validator,线程安全、应用级单例
Validator执行校验:validate(obj)(整对象)、validateProperty、validateValue
ConstraintViolation<T>单条违反:getPropertyPath() 路径、getMessage() 消息、getInvalidValue() 无效值
jakarta.validation.constraints.*内置约束注解(NotNull NotBlank Size Min Max Pattern Email 等)
org.apache.bval.extras.constraints.*BVal 扩展约束(银行卡、IBAN、ISBN、IP 等 18 个)

内置约束三要素:message(消息模板,支持 {min}/{value} 等属性插值)、groups(分组)、payload(元数据)。

五、集成示例:两个类跑通核心能力

5.1 示例一:内置约束全流程

package demo;

import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.constraints.*;
import java.util.Set;

public class BvalDemo {
    public static class User {
        @NotNull(message = "用户名不能为空")
        @Size(min = 3, max = 20, message = "用户名长度须在 {min}-{max} 之间")
        private String username;

        @NotBlank(message = "邮箱不能为空")
        @Email(message = "邮箱格式不合法")
        private String email;

        @Min(value = 18, message = "年龄不能小于 {value}")
        @Max(value = 120, message = "年龄不能大于 {value}")
        private Integer age;

        @Pattern(regexp = "1[3-9]\\d{9}", message = "手机号格式不合法")
        private String phone;

        public User(String username, String email, Integer age, String phone) {
            this.username = username; this.email = email; this.age = age; this.phone = phone;
        }
        public String getUsername() { return username; }
        public String getEmail() { return email; }
        public Integer getAge() { return age; }
        public String getPhone() { return phone; }
    }

    public static void main(String[] args) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        System.out.println("ValidatorFactory 实现类: " + factory.getClass().getName());

        User bad = new User("ab", "not-an-email", 15, "12345");
        Set<ConstraintViolation<User>> violations = validator.validate(bad);
        System.out.println("非法对象约束违反数: " + violations.size());
        for (ConstraintViolation<User> v : violations) {
            System.out.println("  [" + v.getPropertyPath() + "] " + v.getMessage()
                    + " | 无效值 = " + v.getInvalidValue());
        }

        User good = new User("zhangsan", "zhangsan@example.com", 30, "13800138000");
        System.out.println("合法对象约束违反数: " + validator.validate(good).size());

        factory.close();
    }
}

运行回显(实测):

ValidatorFactory 实现类: org.apache.bval.jsr.ApacheValidatorFactory
非法对象约束违反数: 4
  [age] 年龄不能小于 18 | 无效值 = 15
  [username] 用户名长度须在 3-20 之间 | 无效值 = ab
  [phone] 手机号格式不合法 | 无效值 = 12345
  [email] 邮箱格式不合法 | 无效值 = not-an-email
合法对象约束违反数: 0

要点:一个对象五类约束同时校验、四条违反全部命中且消息模板插值正确;validate() 返回 Set,条目顺序不固定(上面输出即两次采集顺序不同的实例);合法对象零违反。

5.2 示例二:分组、级联与容器元素

package demo;

import jakarta.validation.*;
import jakarta.validation.constraints.*;
import java.util.List;
import java.util.Set;

public class QuickstartDemo {
    public interface Basic {}
    public interface Strict {}

    public static class Order {
        @NotNull(groups = {Basic.class}, message = "订单号不能为空")
        @Pattern(regexp = "ORD-\\d{6}", groups = {Strict.class}, message = "订单号须形如 ORD-999999")
        private String orderNo;

        @NotEmpty(groups = {Basic.class}, message = "商品列表不能为空")
        private List<@NotBlank(message = "商品名不能为空白") String> items;

        @Valid
        @NotNull(groups = {Basic.class}, message = "收货地址不能为空")
        private Address addr;

        public Order(String orderNo, List<String> items, Address addr) {
            this.orderNo = orderNo; this.items = items; this.addr = addr;
        }
        public String getOrderNo() { return orderNo; }
        public List<String> getItems() { return items; }
        public Address getAddr() { return addr; }
    }

    public static class Address {
        @NotBlank(message = "城市不能为空")
        private String city;

        public Address(String city) { this.city = city; }
        public String getCity() { return city; }
    }

    public static void main(String[] args) {
        // 显式指定 BVal Provider(classpath 有多个实现时消歧)
        ValidatorFactory factory = Validation.byProvider(
                org.apache.bval.jsr.ApacheValidationProvider.class)
                .configure().buildValidatorFactory();
        Validator validator = factory.getValidator();

        Order bad = new Order(null, List.of("", "咖啡"), new Address(""));

        Set<ConstraintViolation<Order>> v1 = validator.validate(bad, Basic.class);
        System.out.println("[Basic 组] violations = " + v1.size());
        for (ConstraintViolation<Order> v : v1) {
            System.out.println("  [" + v.getPropertyPath() + "] " + v.getMessage());
        }

        Set<ConstraintViolation<Order>> v2 = validator.validate(bad, Strict.class);
        System.out.println("[Strict 组] violations = " + v2.size());

        Set<ConstraintViolation<Order>> v3 = validator.validate(bad, Default.class, Basic.class);
        System.out.println("[Default+Basic 组] violations = " + v3.size());

        factory.close();
    }
}

运行回显(实测):

[Basic 组] violations = 1
  [orderNo] 订单号不能为空
[Strict 组] violations = 0
[Default+Basic 组] violations = 3

三个实测结论:orderNo=null 只触发 Basic 组的 @NotNull,null 不触发 @Pattern(Pattern 对 null 视为有效,规范行为);联合校验 Default.class + Basic.class 时,级联对象(addr.city)与容器元素(items[0])上的 Default 组约束同时生效,共 3 条。级联与容器元素的逐条路径回显(addr.city、items[0].<list element>)见《Apache BVal 配置参数详解》5.2 节。

5.3 示例三:bval-extras 扩展约束

package demo;

import jakarta.validation.*;
import jakarta.validation.ConstraintViolation;
import org.apache.bval.extras.constraints.creditcard.Visa;
import org.apache.bval.extras.constraints.checkdigit.IBAN;
import org.apache.bval.extras.constraints.net.InetAddress;
import org.apache.bval.extras.constraints.file.Directory;

import java.io.File;
import java.util.Set;

public class ExtrasDemo {
    public static class Payment {
        @Visa(message = "不是合法的 Visa 卡号")
        private String card;

        @IBAN(message = "不是合法的 IBAN")
        private String iban;

        @InetAddress(message = "不是合法的 IP 地址")
        private String ip;

        @Directory(message = "路径不是目录")
        private File dir;   // file 包约束只支持 java.io.File,不支持 String

        public Payment(String card, String iban, String ip, File dir) {
            this.card = card; this.iban = iban; this.ip = ip; this.dir = dir;
        }
        public String getCard() { return card; }
        public String getIban() { return iban; }
        public String getIp() { return ip; }
        public File getDir() { return dir; }
    }

    public static void main(String[] args) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        File home = new File(System.getProperty("user.home"));
        Payment bad = new Payment("1234567890123456", "DE8937040044053201300X", "999.999.1.1", home);
        Set<ConstraintViolation<Payment>> v1 = validator.validate(bad);
        System.out.println("[bad] violations = " + v1.size());
        for (ConstraintViolation<Payment> v : v1) {
            System.out.println("  [" + v.getPropertyPath() + "] " + v.getMessage());
        }

        Payment good = new Payment("4111111111111111", "DE89370400440532013000", "192.168.1.1", home);
        System.out.println("[good] violations = " + validator.validate(good).size());

        factory.close();
    }
}

运行回显(实测):

[bad] violations = 4
  [card] {org.apache.bval.extras.constraints.checkdigit.Luhn.message}
  [card] must match the following regular expression: ^(4)(\d{12}|\d{15})$
  [ip] 不是合法的 IP 地址
  [iban] 不是合法的 IBAN
[good] violations = 0

要点:@Visa 内部由「正则 + Luhn 校验位」两个子约束组合而成,卡号两道都不满足时各报一条——注意即使 @Visa 上写了自定义 message,产出消息的也是内部子约束(Luhn 的默认消息 key {...Luhn.message} 原样输出、正则报其固定英文消息),这与常规约束「message 即所得」不同;合法 Visa 测试号、标准德国 IBAN、内网 IP 与真实目录路径全部通过。据此对银行卡类场景更建议用 @Luhn + 业务正则自行组合,消息可控。

六、注意事项

  1. jakarta vs javax:3.0.x 用 jakarta.validation.*,2.0.x 用 javax.validation.*,版本、依赖、import 三者必须配套。
  2. Factory 单例:ValidatorFactory/Validator 创建成本高且线程安全,全局建一次复用,不要每请求新建。
  3. 消息插值限制:BVal 默认插值器支持 {min}/{value} 模板参数,但不求值 EL——${validatedValue} 会原样输出(实测),展示无效值用 getInvalidValue()。
  4. bval-extras 的类型限制:file 包约束(@Directory 等)只接受 java.io.File,标注 String 启动即报 No compliant ... ConstraintValidator found(实测)。
  5. Set 无序:违反清单顺序不稳定,展示前按 getPropertyPath() 排序。
  6. 中文环境:运行带 LANG=C.UTF-8 与 -Dfile.encoding=UTF-8;BVal 无内置中文语言包,中文消息靠注解 message 或应用侧 ValidationMessages_zh_CN.properties。

七、参考链接

  1. Apache BVal 项目主页:https://bval.apache.org/
  2. Maven 中央仓库 bval-jsr:https://repo1.maven.org/maven2/org/apache/bval/bval-jsr/
  3. Maven 中央仓库 bval-extras:https://repo1.maven.org/maven2/org/apache/bval/bval-extras/
  4. Jakarta Bean Validation 3.0 规范:https://jakarta.ee/specifications/bean-validation/3.0/
  5. 系列篇:《Ubuntu 22.04 安装 Apache BVal 指南》《Apache BVal 配置参数详解》《Apache BVal 常用 CLI 命令与工具详解》