当前位置: 首页 > news >正文

如何在Hibernate Validator中实现动态校验组的规则触发 - Violet

在实际项目开发中,我们通常使用 Hibernate Validator 对参数进行校验,利用注解方式简洁直观。然而,这种静态注解配置也带来了一些弊端:

一旦项目上线,校验规则便固定下来,如果业务需要调整(如支持新的手机号号段),就必须修改代码并重新部署。这在生产环境中可能导致较大风险和效率损耗。

为解决这一问题,我们引入了一种支持“动态校验规则”的机制,支持显示触发与规则热更新,核心方案如下:

一、校验工具类封装

我们封装了 ValidationUtils 工具类,简化调用并增强灵活性。它支持:

  • 基于分组的参数校验(支持标准 @Group
  • 字段级别值校验
  • 支持获取详细的错误信息(不抛异常)
  • 提供线程上下文保存校验组信息,便于扩展到动态校验逻辑
package org.example;import jakarta.validation.*;
import jakarta.validation.groups.Default;import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;public final class ValidationUtils {private ValidationUtils() {throw new IllegalStateException("Utility class");}private static final Validator validator;static {ValidatorFactory factory = Validation.buildDefaultValidatorFactory();validator = factory.getValidator();}/*** 校验整个对象(默认组),失败时抛出 ConstraintViolationException*/public static <T> void validate(T object) {validate(object, Default.class);}/*** 支持指定分组的校验,失败时抛出 ConstraintViolationException*/public static <T> void validate(T object, Class<?>... groups) {try {ValidationGroupContext.setGroups(groups);Set<ConstraintViolation<T>> violations = validator.validate(object, groups);if (!violations.isEmpty()) {throw new ConstraintViolationException("参数校验失败", violations);}} finally {ValidationGroupContext.clear();}}/*** 校验信息(默认组)*/public static <T> void validateMessage(T object) {validateMessage(object, Default.class);}/*** 获取指定分组的校验信息,不抛异常*/public static <T> void validateMessage(T object, Class<?>... groups) {try {ValidationGroupContext.setGroups(groups);Set<ConstraintViolation<T>> violations = validator.validate(object, groups);if (!violations.isEmpty()) {String errorMsg = violations.stream().map(v -> v.getPropertyPath() + ": " + v.getMessage()).collect(Collectors.joining("; "));throw new IllegalArgumentException(errorMsg);}} finally {ValidationGroupContext.clear();}}/*** 校验某个字段值是否符合指定组的约束*/public static <T> void validatePropertyValue(Class<T> clazz, String propertyName, Object value, Class<?>... groups) {try {ValidationGroupContext.setGroups(groups);Set<ConstraintViolation<T>> violations = validator.validateValue(clazz, propertyName, value, groups);if (!violations.isEmpty()) {throw new ConstraintViolationException("字段值校验失败", violations);}} finally {ValidationGroupContext.clear();}}/*** 设置当前线程的校验组,留给动态校验组的拓展*/public static class ValidationGroupContext implements AutoCloseable {private static final ThreadLocal<Set<String>> GROUPS = new ThreadLocal<>();public static void setGroups(Class<?>... groups) {if (groups == null || groups.length == 0) {return;}Set<String> groupNames = Arrays.stream(groups).filter(Objects::nonNull) // 过滤掉 null 的 group.map(Class::getName).collect(Collectors.toSet());if (!groupNames.isEmpty()) {GROUPS.set(groupNames);}}public static Set<String> getCurrentGroups() {return GROUPS.get() != null ? GROUPS.get() : Set.of();}public static void clear() {GROUPS.remove();}@Overridepublic void close() {clear();}}
}

✅ 使用示例(含 Group)

1. 定义分组接口:

public interface AddGroup {}
public interface UpdateGroup {}

2. 实体类中使用分组注解:

public class User {@NotBlank(message = "用户名不能为空", groups = {AddGroup.class})private String name;@Min(value = 18, message = "年龄必须大于等于18", groups = {AddGroup.class, UpdateGroup.class})private int age;// getter / setter / constructor ...
}

3. 使用分组进行校验

User user = new User(null, 15);// 校验 AddGroup 分组规则
ValidationUtils.validate(user, AddGroup.class);// 或获取错误信息
ValidationUtils.validateMessage(user, AddGroup.class);

二、✳️ 动态校验的实现机制

1. 自定义注解 @DynamicValidate

为支持动态规则,我们定义了一个类级别注解 @DynamicValidate,该注解通过 Hibernate Validator 标准接口 @Constraint(validatedBy = ...) 与我们自定义的 ConstraintValidator 实现绑定。

注解必须显式包含 groups 参数,否则会因不符合 Hibernate 约定而报错。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DynamicBeanValidator.class)
@Documented
public @interface DynamicValidate {String message() default "动态参数校验失败";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};
}

Hibernate Validator 框架要求约束注解必须包含 groups 参数

否则会报错:HV000074: contains Constraint annotation, but does not contain a groups parameter.

2. 自定义校验器 DynamicBeanValidator

该类实现了 ConstraintValidator<DynamicValidate, Object> 接口,核心逻辑如下:

  • 从缓存中读取目标类对应的校验规则集合 List<ValidationRule>
  • 遍历每条规则,根据当前校验组信息进行条件匹配(仅执行匹配的规则)
  • 支持两种校验方式:
    • 普通规则(如 min/max/null 检查)
    • 表达式校验(如跨字段依赖,使用 MVEL 动态引擎执行)
  • 对失败项构造具体的字段级错误提示

通过 ValidationUtils.ValidationGroupContext 静态上下文对象,我们在 validate 方法入口处传入的 group 信息可以被 DynamicBeanValidator 获取,实现了“从外部传入 -> 动态解析规则 -> 分组匹配执行”这一完整闭环。

package org.example;import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;import java.lang.reflect.Field;
import java.util.List;
import java.util.Set;public class DynamicBeanValidator implements ConstraintValidator<DynamicValidate, Object> {@Overridepublic boolean isValid(Object obj, ConstraintValidatorContext context) {List<ValidationRule> rules = RuleCacheManager.getRules(obj.getClass().getName());if (rules == null || rules.isEmpty()) {return true;}boolean valid = true;context.disableDefaultConstraintViolation();// 从上下文获取当前执行的 groups(由外部 validate 调用时设置)Set<String> currentGroups = ValidationUtils.ValidationGroupContext.getCurrentGroups();for (ValidationRule rule : rules) {try {Field field = obj.getClass().getDeclaredField(rule.getFieldName());field.setAccessible(true);Object value = field.get(obj);// 若该规则未指定 group,默认校验所有. 当前 group 与 ruleGroups 不匹配,跳过Set<String> ruleGroups = rule.getGroups();if (ruleGroups != null && !ruleGroups.isEmpty() && ruleGroups.stream().noneMatch(currentGroups::contains)) {continue;}// 获取对应规则策略RuleCheckerEnum checker = RuleCheckerEnum.getRuleChecker(rule.getRuleType());if (checker == null) {continue;}// 如果规则是Mvel表达式,则直接执行表达式,优先级更高if (checker == RuleCheckerEnum.EXPRESSION && MvelExpressionValidator.evalExpression(rule.getExpression(), obj)) {continue;}// 普通规则校验方式if (checker != RuleCheckerEnum.EXPRESSION && checker.check(value, rule.getRuleValue())) {continue;}context.buildConstraintViolationWithTemplate(rule.getMessage()).addPropertyNode(rule.getFieldName()).addConstraintViolation();valid = false;} catch (Exception e) {context.buildConstraintViolationWithTemplate("字段解析失败: " + e.getMessage()).addConstraintViolation();return false;}}return valid;}
}
package org.example;import lombok.AllArgsConstructor;
import lombok.Getter;@AllArgsConstructor
@Getter
public enum RuleCheckerEnum {MIN("min") {@Overridepublic boolean check(Object val, String ruleVal) {if (val instanceof Number) {return ((Number) val).doubleValue() >= Double.parseDouble(ruleVal);}return true;}},MAX("max") {@Overridepublic boolean check(Object val, String ruleVal) {if (val instanceof Number) {return ((Number) val).doubleValue() >= Double.parseDouble(ruleVal);}return true;}},EXPRESSION("expression");/// --------------------------------------------------------------------------------private final String ruleName;public boolean check(Object value, String param) {return true;}public static RuleCheckerEnum getRuleChecker(String ruleName) {for (RuleCheckerEnum ruleChecker : values()) {if (ruleChecker.getRuleName().equals(ruleName)) {return ruleChecker;}}return null;}
}

✅ 实体类中使用自定义注解

@DynamicValidate
public class UserDTO {private String name;private Integer age;// getter/setter...
}

✅ 控制器中使用标准的 @Valid 校验或者手动调用

@PostMapping("/save")
public String save(@RequestBody @Valid UserDTO userDTO) {return "success";
}
// 校验 AddGroup 分组规则
ValidationUtils.validate(user, AddGroup.class);// 或获取错误信息
ValidationUtils.validateMessage(user, AddGroup.class);

3. 校验规则缓存与动态加载

通过 RuleCacheManager 管理类名与规则列表的映射关系,可实现:

  • 启动时加载数据库中的校验规则
  • 支持后续通过管理界面或接口实现规则动态更新与热加载

规则结构(ValidationRule)包含字段名、校验类型、参数、错误提示、可选表达式及所属 group。

package org.example;import java.util.HashMap;
import java.util.List;
import java.util.Map;public class RuleCacheManager {private RuleCacheManager() {throw new IllegalStateException("Utility class");}private static final Map<String, List<ValidationRule>> ruleCache = new HashMap<>();public static List<ValidationRule> getRules(String className) {return ruleCache.get(className);}public static void putRules(String className, List<ValidationRule> rules) {ruleCache.put(className, rules);}
}

✅ 示例数据库表结构(映射上面的类)

id class_name field_name rule_type rule_value message expression group enabled
1 com.example.User email not_null 邮箱不能为空 null true
2 com.example.User age min 18 年龄必须 ≥ 18 null true
3 com.example.User email expression 管理员必须填写邮箱 `obj.userType != 'admin' (obj.email != null && obj.email.length > 0)`

你可以通过 ORM 工具(如 MyBatis / JPA)将其读取为 ValidationRule 列表,并存入缓存中。

  • 启动时从数据库加载所有规则
  • 也可以做定时刷新 / 动态更新等机制

4. 表达式支持:MVEL

部分校验逻辑无法静态表示(如跨字段约束),我们引入 MVEL 表达式引擎进行动态校验。

优势包括:

  • 与 Java 语法兼容性好,支持对象引用、复杂逻辑判断
  • 轻量级、无须额外脚本引擎依赖,适用于 Java 11+

某些字段的校验规则依赖上下文(如:另一个字段的值),这属于动态规则校验中的高级用法,可以通过表达式 + 动态引擎来实现。

✅ 示例场景

假设有以下业务规则:

userType == "admin" 时,email 必填,否则可为空。

这个规则显然不能只靠单字段判断,而需要跨字段 + 动态表达式

✅ 解决方案:引入表达式引擎(SpEL / MVEL / JavaScript)

我们推荐用 JSR-223 标准支持的脚本引擎,例如 JavaScript 引擎 来动态执行校验表达式,灵活强大且跨字段无压力

✅ 实现步骤
1. 规则格式升级(以 JSON 或数据库存储为例)
{"field": "email","expression": "obj.userType != 'admin' || (obj.email != null && obj.email.length > 0)","message": "管理员必须填写邮箱"
}

说明:表达式中可以引用对象属性,表达式最终返回 true/false

2. 在校验器中使用 JavaScript 引擎执行表达式
import javax.script.*;
import java.util.*;public class JsExpressionValidator {private static final ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");public static boolean evalExpression(String expression, Object obj) {Bindings bindings = engine.createBindings();bindings.put("obj", obj);try {Object result = engine.eval(expression, bindings);return result instanceof Boolean && (Boolean) result;} catch (Exception e) {// 建议记录日志}}
}

支持动态校验的组校验

想要通过校验入口类 ValidationUtils.validateMessage(obj, groupClass)进行动态校验组的规则触发。

三、使用方式

  1. DTO 类上添加 @DynamicValidate
  2. 控制器中使用标准 @Valid 注解触发动态校验,或使用 ValidationUtils.validateMessage(obj, group) 显式调用
  3. 校验器根据上下文 group 与缓存规则完成动态执行
public interface TestUpdateGroup extends Default {
}public interface TestAddGroup extends Default {
}
package org.example.test;import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.example.DynamicValidate;import java.io.Serializable;@DynamicValidate
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@Data
public class TestDTO implements Serializable {@NotNull(message = "id不能为空", groups = TestUpdateGroup.class)private Long id;@NotNull(message = "姓名不能为空", groups = TestAddGroup.class)private String name;@NotNull(message = "年龄不能为空")@Max(100)@Min(0)private Integer age;@NotNull(message = "性别不能为空")@Max(1)@Min(0)private Integer sex;private String address;private String phone;private String email;private String idCard;}
package org.example.test;import org.example.RuleCacheManager;
import org.example.ValidationRule;
import org.example.ValidationUtils;import java.util.Collections;
import java.util.Set;public class TestMain {public static void main(String[] args) {TestDTO testDTO = new TestDTO().setName("测试的名字").setAge(20).setSex(1);ValidationRule validationRule = new ValidationRule();validationRule.setClassName(TestDTO.class.getName());validationRule.setFieldName("address");validationRule.setRuleType("expression");validationRule.setMessage("地址不合法");validationRule.setExpression("obj.age > 19");validationRule.setEnabled(true);validationRule.setGroups(Set.of(TestAddGroup.class.getName()));RuleCacheManager.putRules(TestDTO.class.getName(), Collections.singletonList(validationRule));ValidationUtils.validateMessage(testDTO);}
}

四、常见问题与说明

Q1: ScriptEngineManager().getEngineByName("JavaScript") 返回 null?

✅ 问题根因

JavaScript 引擎依赖 Nashorn,而:

  • Java 8Nashorn 是默认自带的 JavaScript 引擎 ✅
  • Java 11 起:Nashorn 被标记为废弃 ❌
  • Java 15 起:Nashorn 被完全移除 ❌

因此你用的是 Java 11+,就会导致:

new ScriptEngineManager().getEngineByName("JavaScript")  // 返回 null

✅ 解决方案

使用 MVEL 表达式替代 JavaScript

你原来是想动态校验表达式,例如 obj.age > 20,可以使用 MVEL 表达式引擎,轻量高效,兼容 Java 语法:

<dependency><groupId>org.mvel</groupId><artifactId>mvel2</artifactId><version>2.4.12.Final</version></dependency>
package org.example;import org.mvel2.MVEL;import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;public class MvelExpressionValidator {private MvelExpressionValidator() {throw new IllegalStateException("Utility class");}// 缓存表达式对应的编译对象private static final Map<String, Serializable> expressionCache = new ConcurrentHashMap<>();/*** 执行表达式*/public static boolean evalExpression(String expression, Object obj) {try {Serializable compiled = expressionCache.computeIfAbsent(expression, MVEL::compileExpression);Map<String, Object> context = Map.of("obj", obj);Object result = MVEL.executeExpression(compiled, context);return result instanceof Boolean && (Boolean) result;} catch (Exception e) {// 建议记录日志return false;}}
}

Q2: 为什么不能在 @Constraint 中将 groups 默认值设为 {Default.class}

  • Hibernate Validator 会反射校验该注解的结构
  • 一旦你自定义注解上标注了 @Constraint,它就要求 groups() 的默认值必须是空数组;
  • Default.class 是框架默认逻辑的一部分,不允许你直接设置为默认值,会干扰Hibernate 的 group 合并逻辑。

Q3:DynamicBeanValidator 有办法知道当前是需要校验哪些Group吗?

这是个非常好的问题,涉及 Hibernate Validator 在运行时传递 group 信息的机制

简短回答是:

标准的 ConstraintValidator 接口中无法直接获取当前正在使用的校验 groups。

❓ 为什么无法直接拿到 group?

Hibernate Validator 的 ConstraintValidator#isValid() 方法只接收:

boolean isValid(Object value, ConstraintValidatorContext context);

没有传递当前 groups 的参数。这是 JSR 规范的限制 —— groups 是 Validator 引擎在内部处理的一部分,在运行时不会自动传给 ConstraintValidator

✅ 解决方案: 使用自定义的 ThreadLocal 上下文(推荐)

    public class ValidationUtils {public static void validateMessage(Object obj, Class<?>... groups) {try {// 设置当前 group 到上下文中ValidationGroupContext.setGroups(groups);Validator validator = Validation.buildDefaultValidatorFactory().getValidator();Set<ConstraintViolation<Object>> violations = validator.validate(obj, groups);for (ConstraintViolation<Object> violation : violations) {System.out.println("字段:" + violation.getPropertyPath() + " 错误:" + violation.getMessage());}} finally {// 清理上下文ValidationGroupContext.clear();}}/*** 设置当前线程的校验组,留给动态校验组的拓展*/public static class ValidationGroupContext implements AutoCloseable {private static final ThreadLocal<Set<String>> GROUPS = new ThreadLocal<>();public static void setGroups(Class<?>... groups) {if (groups == null || groups.length == 0) {return;}Set<String> groupNames = Arrays.stream(groups).filter(Objects::nonNull) // 过滤掉 null 的 group.map(Class::getName).collect(Collectors.toSet());if (!groupNames.isEmpty()) {GROUPS.set(groupNames);}}public static Set<String> getCurrentGroups() {return GROUPS.get() != null ? GROUPS.get() : Set.of();}public static void clear() {GROUPS.remove();}@Overridepublic void close() {clear();}}
}

动态校验的逻辑调整为

  // 从上下文获取当前执行的 groups(由外部 validate 调用时设置)Set<String> currentGroups = ValidationUtils.ValidationGroupContext.getCurrentGroups();for (ValidationRule rule : rules) {try {Field field = obj.getClass().getDeclaredField(rule.getFieldName());field.setAccessible(true);Object value = field.get(obj);// 若该规则未指定 group,默认校验所有. 当前 group 与 ruleGroups 不匹配,跳过Set<String> ruleGroups = rule.getGroups();if (ruleGroups != null && !ruleGroups.isEmpty() && ruleGroups.stream().noneMatch(currentGroups::contains)) {continue;}.......}}

五、总结

通过上述方式,我们实现了对 Hibernate Validator 的增强:

  • 保留了原生注解式校验的使用方式
  • 引入可配置、热更新的动态规则机制
  • 兼容原有分组校验体系
  • 表达式引擎支持跨字段/复杂逻辑判断

大幅提升了系统参数校验的灵活性、安全性与可维护性。

http://www.sczhlp.com/news/59742/

相关文章:

  • 怎么用php做网站后台程序wordpress查询标签
  • 傻瓜式 建网站企业网站有什么
  • 免费自助建站源码猎头公司面试一般会问什么问题
  • 门户网站cms大庆医院网站建设
  • 北京网站建设公司费用浩森宇特用网盘做网站
  • 网站设计建设公司排行网站群建设系统
  • 商务局网站群建设方案网站下拉箭头怎么做的
  • 深圳外网站建设北京地铁建设的网站
  • 自己建企业网站怎么建人力资源公司起名大全册子
  • 企业如何在网站做认证wordpress 标签 随机
  • codewars Replace With Alphabet Position(c++处理字符串)
  • 比特币简易网站开发WordPress重置密码链接失效
  • 2025.9 模拟赛日志
  • 金泉网做网站电话wordpress没有图片不显示
  • 工装设计方案网站建设网站的目标客户群
  • 网站工程师招聘公司网站建设步骤
  • 做网站如何添加视频最好网页游戏网站
  • 面向对象设计与设计模式实战指南
  • 9.6 sys模块
  • 网站开发开票编码归属网站上面带官网字样怎么做的
  • 服装网站建设竞争对手调查分析固原市住房和城乡建设厅网站
  • 网站建设网址网站制作网站建设首页该放什么
  • 网站建设开发维护唐县住房和城乡建设局网站
  • 上海比较好的网站建设公司wordpress主动推送到Google
  • 张家港杨舍网站建设泉州做网站优化
  • 优秀的手机网站广告机免费投放
  • 数据库 搭建 网站营销网站建设推广
  • 怎么把已经做好的作业中的手写的字去掉?
  • 云南昆明网站建设公司免费做网站可以一直用吗
  • 音乐网站建设规划书wordpress获取标签页