如何更优雅的判断参数不为空?

自定义一个ValidationUtil参数验证工具类,它通过反射机制实现了对数据传输对象(DTO)中字段的自动验证。可以省去在 service 层中对参数不为 null 的校验

1. 实现原理

ValidationUtil主要通过以下技术实现参数验证:

1.1 核心验证方法 validateParameters

package com.yixueji.utils;

import com.yixueji.exception.ParameterException;
import com.yixueji.constant.MessageConstant;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

/**
 * 参数验证工具类
 * 提供通用的参数验证方法,用于检查数据传输对象(DTO)中的字段是否有效
 */
public class ValidationUtil {
    

    /**
     * 验证指定字段的参数是否有效
     * 该方法通过反射调用dto中的getter方法来检查指定字段的值是否为null或空字符串
     * 如果任何一个字段的值为null或空字符串,将抛出ParameterException异常
     *
     * @param dto 数据传输对象,包含了需要验证的字段
     * @param fields 需要验证的字段名数组
     * @throws ParameterException 如果任何一个字段的值为null或空字符串
     */
    public static void validateParameters(Object dto, String... fields) {
        // 遍历需要验证的字段数组
        for (String field : fields) {
            try {
                // 构造getter方法名,JavaBean规范通常使用get作为前缀
                String getterMethodName = "get" + field.substring(0, 1).toUpperCase() + field.substring(1);
                // 使用反射调用getter方法获取字段值
                //invoke(dto) 的意思是:
                //“在 dto 这个对象上,调用之前通过反射找到的方法”。
                //相当于直接调用 dto.方法名(),只不过是通过反射实现的。
                Object value = dto.getClass().getMethod(getterMethodName).invoke(dto);
                // 检查字段值是否为null或空字符串
                if (value == null || (value instanceof String && ((String) value).isEmpty())) {
                    // 如果值为null或空字符串,抛出ParameterException异常
                    throw new ParameterException(MessageConstant.PARAMETER_ERROR+":"+field);
                }
            } catch (NoSuchMethodException e) {
                throw new ParameterException("字段 " + field + " 没有对应的 getter 方法");
            } catch (IllegalAccessException e) {
                throw new ParameterException("无权访问字段 " + field + " 的 getter 方法");
            } catch (InvocationTargetException e) {
                throw new ParameterException("获取字段 " + field + " 的值时出错: " + e.getCause().getMessage());
            }
        }
    }
    
    /**
     * 验证字符串是否为空
     * 
     * @param value 需要验证的字符串
     * @return 如果字符串为null或空,返回true;否则返回false
     */
    public static boolean isEmpty(String value) {
        return value == null || value.trim().isEmpty();
    }

    /**
     * 验证对象是否为空
     *
     * 此方法通过反射检查对象的所有字段是否为空,用于判断一个对象是否真的“空”
     * 它不仅检查对象是否为null,还检查对象的每个字段是否为null或空字符串
     *
     * @param obj 需要验证的对象
     * @return 如果对象为null或所有字段都为空,返回true;否则返回false
     */
    public static boolean isObjectEmpty(Object obj) {
        if (obj == null) {
            return true;
        }

        // 获取对象的所有字段(包括私有字段)
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            try {
                // 设置可访问(避免 private 字段报错)
                field.setAccessible(true);

                // 获取字段值
                Object value = field.get(obj);

                // 检查字段是否非空
                if (value != null && !value.toString().trim().isEmpty()) {
                    return false; // 只要有一个字段非空,就不是空对象
                }
            } catch (IllegalAccessException e) {
                throw new RuntimeException("反射访问字段失败", e);
            }
        }
        return true; // 所有字段都为空
    }
    /**
     * 验证对象是否为null
     * 
     * @param obj 需要验证的对象
     * @return 如果对象为null,返回true;否则返回false
     */
    public static boolean isNull(Object obj) {
        return obj == null;
    }
    
    /**
     * 验证集合或数组是否为空
     * 
     * @param collection 需要验证的集合或数组
     * @return 如果集合为null或空,返回true;否则返回false
     */
    public static boolean isEmpty(Object[] collection) {
        return collection == null || collection.length == 0;
    }
} 

这个方法使用了Java反射API来动态调用DTO对象的getter方法,具体步骤:

  1. 接收一个DTO对象和需要验证的字段名数组
  2. 遍历字段名数组,为每个字段构造标准的JavaBean getter方法名
  3. 通过反射获取并调用getter方法获取字段值
  4. 检查字段值是否为null或空字符串
  5. 如果验证失败,抛出自定义的ParameterException异常

1.2 辅助验证方法

工具类还提供了多个辅助验证方法:

  • isEmpty(String): 验证字符串是否为空
  • isObjectEmpty(Object): 通过反射检查对象的所有字段是否为空
  • isNull(Object): 验证对象是否为null
  • isEmpty(Object[]): 验证集合或数组是否为空

2. 封装成工具类的好处

2.1 代码复用与简化

原始代码:

if (userRegisterDTO.getUserName() == null ||
    userRegisterDTO.getUserName().isEmpty() ||
    userRegisterDTO.getPassword() == null ||
    userRegisterDTO.getPassword().isEmpty() ||
    userRegisterDTO.getPhone() == null ||
    userRegisterDTO.getPhone().isEmpty()) {
    throw new ParameterException(MessageConstant.PARAMETER_ERROR);
}

使用工具类后:

ValidationUtil.validateParameters(userRegisterDTO, "userName", "password", "phone");

显而易见,代码量大幅减少,可读性显著提高。

2.2 统一异常处理

  • 所有参数验证失败都抛出统一的ParameterException异常
  • 异常消息格式统一,包含具体的错误字段信息
  • 便于全局异常处理器进行捕获和处理

2.3 灵活性与可扩展性

  • 支持任意类型的DTO对象验证
  • 可以验证任意数量的字段
  • 易于扩展新的验证规则和方法

2.4 提高代码质量

  • 减少重复代码,符合DRY原则(Don't Repeat Yourself)
  • 提高代码可维护性,验证逻辑集中在一处
  • 降低出错几率,统一的验证规则减少人为错误

2.5 增强安全性

  • 确保关键参数不会为空,防止空指针异常
  • 防止恶意用户提交空参数进行攻击
  • 提供详细的错误信息,便于问题定位

2.6 提高开发效率

  • 开发者无需编写重复的参数检查代码
  • 减少样板代码,专注于业务逻辑实现
  • 统一的验证方式,降低学习成本

3. 实际应用场景

3.1 用户登录参数验证

// 验证用户登录参数的有效性
ValidationUtil.validateParameters(userLoginDTO, "userName", "password");

3.2 用户注册参数验证

ValidationUtil.validateParameters(userRegisterDTO, "userName", "password", "phone");

3.3 密码修改参数验证

ValidationUtil.validateParameters(userPasswordDTO, "oldPassword", "newPassword");

4. 技术亮点

4.1 反射机制的灵活应用

通过Java反射API动态调用getter方法,避免了硬编码字段访问,使工具类具有通用性。

4.2 可变参数的使用

使用String... fields可变参数,允许验证任意数量的字段,提高了API的灵活性。

4.3 异常处理的完善

不仅捕获了反射可能产生的各种异常,还将它们转换为业务异常,提供了更友好的错误信息。

4.4 符合面向对象设计原则

  • 单一职责原则:工具类专注于参数验证
  • 开闭原则:易于扩展新的验证方法,无需修改现有代码
  • 依赖倒置原则:通过接口(Object)而非具体实现进行操作

5. 潜在的改进空间

  1. 添加更复杂的验证规则,如邮箱格式、手机号格式等
  2. 支持嵌套对象的验证
  3. 考虑使用注解驱动的验证方式,如Bean Validation API
  4. 添加批量验证并收集所有错误的能力,而非首次失败就抛出异常

总结

ValidationUtil工具类通过反射机制实现了对DTO对象参数的自动验证,大大简化了代码,提高了开发效率和代码质量。它将分散在各处的参数验证逻辑集中管理,使代码更加简洁、可维护,并提供了统一的异常处理机制。这种设计不仅符合软件工程的最佳实践,也体现了"高内聚、低耦合"的设计思想。