利用Java反射机制降低代码圈复杂度

简介

  • 在实际的工作中,我遇到了项目里老代码存在圈复杂度过高的问题,在提交代码的时候通不过CI(代码检查)的Lizard复杂度检查,所以迫切需要解决这个问题,运用Java反射是一个很好的解决思路。

什么是Java反射

  • Reflection(反射) 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序对自身进行检查。被private封装的资源只能类内部访问,外部是不行的,但反射能直接操作类私有属性。反射可以在运行时获取一个类的所有信息,(包括成员变量,成员方法,构造器等),并且可以操纵类的字段、方法、构造器等部分。 一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把一个个组成部分映射成一个个对象。
  • 我们知道Spring框架可以帮我们创建和管理对象。需要对象时,我们无需自己手动new对象,直接从Spring提供的容器中的Beans获取即可。Beans底层其实就是一个Map<String,Object>,最终通过getBean(“user”)来获取。而这其中最核心的实现就是利用反射技术。
  • Java反射需要用到Class类
  • Class 类的实例表示正在运行的 Java 应用程序中的类和接口。也就是jvm中有N多的实例每个类都有该Class对象。(包括基本数据类型)

为什么要用Java反射

  • 一开始学习Java的时候,我们都是通过new一个对象然后再对其进行操作,如果每个对象有特定的属性值,那么对一种场景进行校验的时候,就需要创建多个对象,然后对每个对象进行校验,在代码量上就会体现的十分繁琐。
public class DeviceInfo {
    private Integer id;
    private String deviceName;
    private String deviceType;
    private String deviceId;
    private String devicePort;
    private String userName;
    private String userId;
 
//此处省略get、set方法,下面的代码都是写在这一个类中

}
  • 对于上面的DeviceInfo的实体类,有一个场景,需要在它与另一个对象进行比较前先校验部分属性值是否为空,即在.euqals()方法内加入校验的代码,如果按照每个属性校验一次的思路,则需要写很多代码,显得十分繁琐。
@Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        DeviceInfo other = (DeviceInfo) obj;
        boolean deviceNameResult =       checkDeviceName(this.deviceName, other);
        if (deviceNameResult == false)
            return false;
        boolean deviceIdResult =       checkDeviceId(this.deviceId, other);
        if (deviceIdResult == false)
            return false;
        boolean devicePortResult =       checkDevicePort(this.devicePort, other);
        if (devicePortResult == false)
            return false;
        boolean UserIdResult =       checkUserId(this.userId, other);
        if (devicePortResult == false)
            return false;
        return true;
    }

    /**
     * 校验deviceName是否为空
     */
    private boolean checkDeviceName(String deviceName, DeviceInfo other)
    {
        if (deviceName == null) {
            if (other.deviceName != null)
                return false;
        } 
       else if  (!deviceName.equals(other.deviceName))
            return false;
        return true;
    }

    /**
     * 校验deviceId是否为空
     */
    private boolean checkDeviceId(String deviceId, DeviceInfo other)
    {
        if (deviceId == null) {
            if (other.deviceId != null)
                return false;
        } 
       else if  (!deviceId.equals(other.deviceId))
            return false;
        return true;
    }

    /**
     * 校验devicePort是否为空
     */
    private boolean checkDevicePort(String devicePort, DeviceInfo other)
    {
        if (devicePort == null) {
            if (other.devicePort != null)
                return false;
        } 
       else if  (!devicePort.equals(other.devicePort))
            return false;
        return true;
    }

    /**
     * 校验userId是否为空
     */
    private boolean checkUserId(String userId, DeviceInfo other)
    {
        if (userId == null) {
            if (other.userId != null)
                return false;
        } 
       else if  (!userId.equals(other.userId))
            return false;
        return true;
    }
  • 上述代码每个需要校验的属性都要写一遍重复的代码,不仅十分繁琐,代码的圈复杂度也很高(一个方法中有很多个if)

怎么使用Java反射降低代码圈复杂度

  • 使用class类
//获取类对应的字节码对象
Class<?> clazz = Class.forName("类的全路径");
//通过类的字节码对象获取类中的成员变量
Field[] fs = clazz.getFields();//field顾名思义:属性
//通过类对应的字节码对象获取类中的成员方法们
Method[] ms = clazz.getMethods();
//通过字节码对象获取目标类的构造方法们
Constructor<?>[] cs = clazz.getConstructors();
//遍历数组,获取类中的每个成员变量的具体信息
for(Field f : fs){
    //通过本轮循环到的字段对象获取字段名
            System.out.println(f.getName());
    //通过本轮循环到的字段对象获取字段的类型
            System.out.println(f.getType());
        }
  • 使用Class类中的forName()静态方法(最安全,性能最好)

场景实例

  • 对于上面提到的场景,
  • 我们先将需要校验的属性写入Set容器,更方便后面代码维护,需要添加哪个就在容器里面写入
/**
     * 包含需要校验是否为空的参数
     */
    private static final Set<String> includeFields = new HashSet<>();

    static {
        includeFields.add("deviceIp");
        includeFields.add("deviceName");
        includeFields.add("devicePort");
        includeFields.add("userId");
    }
  • 利用Java反射将重复代码提取出来
/**
     * 校验单个参数是否为空
     *
     * @param field
     * @param DeviceInfo
     * @return
     */
    private boolean checkParam(Field field, DeviceInfo DeviceInfo) {
        String fieldName = field.getName();
        field.setAccessible(true);
        try {
            Object thisField = field.get(this);
            Object otherField = field.get(DeviceInfo);
            if (thisField == null) {
                return otherField == null;
            } else return thisField.equals(otherField);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return false;
    }
/**
     * 校验参数是否为空,有一个参数为空就返回false
     *
     * @param other
     */
    private boolean check(DeviceInfo other) {
    Field[] fields =      DeviceInfo.class.getDeclaredFields();
        for (Field field : fields) {
            if (includeFields.contains(field.getName())) {
                if (!checkParam(field, other)) {
                    return false;
                }
            }
        }
        return true;
    }
  • 然后在.equals()内调用check
@Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof DeviceInfo)) return false;
        DeviceInfo that = (DeviceInfo) obj;
        if (!check(that)) return false;
        return true;

    }
  • 上述代码也用到了封装的思想,即把实现一个功能的代码封装成一个个的方法,而不是写在同一个方法内,这样能使代码解耦,具有更高的维护性和可读性(逻辑清晰)