You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通用校验Java对象中的null值及空字符串?

可复用的对象null/空字符串校验方案

逐字段手写校验逻辑的维护成本极高,只要类新增字段、新增嵌套对象就要改校验代码,完全没有复用性。以下两种是工业界常用的最优实现,完全适配任意自定义对象的校验需求,未来新增类不需要修改校验框架代码。


方案1:Jakarta Bean Validation(生产级首选,推荐)

这是Java生态标准的声明式校验框架,已经经过十余年的生产验证,自动支持任意层级的嵌套对象、集合、数组的递归校验,不需要自己写反射处理边界逻辑。

主流框架(Spring Boot、Quarkus、Micronaut等)默认集成该能力,普通Java项目只需引入Jakarta Validation API和Hibernate Validator实现即可使用。

  1. 给待校验类的字段加对应注解,标记校验规则:
    • 字符串字段加@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;
    }
    
  2. 写一次通用校验工具类,所有对象都可以复用:
    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();
        }
    }
    
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 14:03:20