如何通用校验Java对象中的null值及空字符串?
可复用的对象null/空字符串校验方案
逐字段手写校验逻辑的维护成本极高,只要类新增字段、新增嵌套对象就要改校验代码,完全没有复用性。以下两种是工业界常用的最优实现,完全适配任意自定义对象的校验需求,未来新增类不需要修改校验框架代码。
方案1:Jakarta Bean Validation(生产级首选,推荐)
这是Java生态标准的声明式校验框架,已经经过十余年的生产验证,自动支持任意层级的嵌套对象、集合、数组的递归校验,不需要自己写反射处理边界逻辑。
主流框架(Spring Boot、Quarkus、Micronaut等)默认集成该能力,普通Java项目只需引入Jakarta Validation API和Hibernate Validator实现即可使用。
- 给待校验类的字段加对应注解,标记校验规则:
- 字符串字段加
@NotBlank:自动校验字段不为null、不是空串(如果允许全空格字符串,替换为@NotNull+@NotEmpty即可) - 嵌套对象、集合类型字段加
@Valid:触发递归校验内部元素 - 引用类型字段如果本身不允许为null,加
@NotNull
基本类型(int/boolean/long等)本身不存在null值,不需要加校验注解
import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.experimental.FieldDefaults; import java.util.List; @Data @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Builder public class Child { @NotBlank String name; int age; } @Data @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Builder public class Car { @NotBlank String brand; @NotBlank String model; } @Data @AllArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE) @Builder public class Parent { @NotBlank String name; int age; @NotNull List<@Valid Child> children; @NotNull @Valid Car car; } - 字符串字段加
- 写一次通用校验工具类,所有对象都可以复用:
import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; import java.util.Set; import java.util.stream.Collectors; public class GlobalBeanValidator { private static final Validator VALIDATOR; static { try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { VALIDATOR = factory.getValidator(); } } /** * 通用校验入口,传入任意待校验对象,返回所有不通过的原因 */ public static <T> Set<String> validate(T target) { Set<ConstraintViolation<T>> violations = VALIDATOR.validate(target); return violations.stream() .map(v -> String.format("路径[%s]非法,值[%s]:%s", v.getPropertyPath(), v.getInvalidValue(), v.getMessage())) .collect(Collectors.toSet()); } /** * 直接返回校验是否通过,适合单元测试断言 */ public static <T> boolean isValid(T target) { return validate(target).isEmpty(); } } - JUnit测试中直接调用即可,后续新增任意待校验类,只要给字段加对应注解,不需要修改工具类代码:
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class AlgorithmOutputTest { @Test void testOutputNoNullNoEmpty() { Parent result = // 调用你的业务/算法逻辑得到输出对象 Set<String> errors = GlobalBeanValidator.validate(result); assertTrue(errors.isEmpty(), "输出存在非法null或空串:" + String.join(";", errors)); } }
方案2:通用反射递归校验(无额外依赖,适合纯测试场景)
如果项目不允许引入第三方校验依赖,可以实现一个基于反射的通用校验工具,自动遍历对象所有字段、递归处理嵌套对象和集合,不需要给类加任何注解,一次编写所有对象通用。
import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.*; public class GenericFieldValidator { // 标记不需要递归拆解的简单值类型,避免无限递归 private static final Set<Class<?>> SIMPLE_TYPES = Set.of( Boolean.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Character.class, String.class, Date.class, LocalDate.class, LocalDateTime.class, BigDecimal.class ); public static boolean isValid(Object target) { return validate(target, new HashSet<>()).isEmpty(); } public static List<String> validate(Object target) { return validate(target, new HashSet<>()); } private static List<String> validate(Object target, Set<Object> visited) { List<String> errors = new ArrayList<>(); if (target == null) { errors.add("值为null"); return errors; } // 循环引用检测,避免双向关联对象导致栈溢出 if (visited.contains(target)) return errors; visited.add(target); Class<?> clazz = target.getClass(); // 简单类型直接校验规则 if (clazz.isPrimitive() || SIMPLE_TYPES.contains(clazz)) { if (target instanceof String s && s.isEmpty()) { errors.add("字符串为空"); } return errors; } // 处理数组 if (clazz.isArray()) { int len = Array.getLength(target); for (int i = 0; i < len; i++) { Object item = Array.get(target, i); if (item == null) { errors.add("数组第" + i + "位元素为null"); continue; } errors.addAll(validate(item, visited)); } return errors; } // 处理集合 if (target instanceof Collection<?> collection) { int idx = 0; for (Object item : collection) { if (item == null) { errors.add("集合第" + idx + "位元素为null"); idx++; continue; } errors.addAll(validate(item, visited)); idx++; } return errors; } // 处理Map(按需保留,没有Map场景可以删除这段) if (target instanceof Map<?, ?> map) { for (Map.Entry<?, ?> entry : map.entrySet()) { Object key = entry.getKey(); Object val = entry.getValue(); if (key == null) { errors.add("Map存在null key"); } else { errors.addAll(validate(key, visited)); } if (val == null) { errors.add("Map key[" + key + "]对应value为null"); } else { errors.addAll(validate(val, visited)); } } return errors; } // 自定义POJO,遍历所有字段递归校验 while (clazz != null && clazz != Object.class) { Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { // 跳过静态、瞬态字段 if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) { continue; } field.setAccessible(true); try { Object fieldVal = field.get(target); if (fieldVal == null) { errors.add(clazz.getSimpleName() + "." + field.getName() + "为null"); continue; } List<String> fieldErrors = validate(fieldVal, visited); for (String err : fieldErrors) { errors.add(clazz.getSimpleName() + "." + field.getName() + " -> " + err); } } catch (IllegalAccessException e) { errors.add("无法访问字段" + clazz.getSimpleName() + "." + field.getName() + ":" + e.getMessage()); } } clazz = clazz.getSuperclass(); } return errors; } }
这个工具的使用方式和方案1完全一致,JUnit中直接调用GenericFieldValidator.isValid(yourObject)即可,不需要修改任何待校验类的代码。
注意:反射方案的性能和鲁棒性弱于方案1,更适合测试场景使用,生产环境优先选择方案1。
内容的提问来源于stack exchange,提问作者Paul Marcelin Bejan
相关产品推荐
相关产品推荐

