反射工具類,如斯優雅

blankj發表於2018-01-17

logo

Foreword

反射的作用我在這就不多說了,每次用到反射都是那麼一坨程式碼丟進去,總是讓人覺得很不優雅,如今有了我這個反射工具類,那麼大家就可以一句話優雅地來完成反射的工作,該工具類是站在 jOOR 的肩膀上進行改造,修復了它沒有完成的工作,至於修復了什麼,後面原始碼分析會詳述,至於這個工具類在哪,現已加入至 1.12.0 版本的 AndroidUtilCode,下面來介紹下其功能。

Functions

其 APIs 如下所示:

反射相關 -> ReflectUtils.java -> Test

reflect    : 設定要反射的類
newInstance: 例項化反射物件
field      : 設定反射的欄位
method     : 設定反射的方法
get        : 獲取反射想要獲取的
複製程式碼

Use

例項化反射物件

比如,我們例項化一個 String 物件可以這樣做:

String str1 = ReflectUtils.reflect(String.class).newInstance().get();
// equals: String str1 = new String();

String str2 = ReflectUtils.reflect("java.lang.String").newInstance("abc").get();
// equals: String str2 = new String("abc");

String str3 = ReflectUtils.reflect(String.class).newInstance("abc".getBytes()).get();
// equals: String str3 = new String("abc".getBytes());
複製程式碼

設定反射的方法

比如,我們想要呼叫 Stringsubstring 函式可以這樣做:

String str1 = ReflectUtils.reflect((Object) "1234").method("substring", 2).get();
// equals: String str1 = "1234".substring(2);

String str2 = ReflectUtils.reflect((Object) "1234").method("substring", 0, 2).get();
// equals: String str1 = "1234".substring(0, 2);
複製程式碼

設定反射的欄位

比如,TestPrivateStaticFinal.java 如下所示:

public class TestPrivateStaticFinal {
    private static final int     I1 = new Integer(1);
    private static final Integer I2 = new Integer(1);
}
複製程式碼

我們要設定其 I1I2 值為 2,可以如下操作:

ReflectUtils.reflect(TestPrivateStaticFinal.class).field("I1", 2);
ReflectUtils.reflect(TestPrivateStaticFinal.class).field("I2", 2);
複製程式碼

要獲取其 I1I2 值的話,可以如下操作:

ReflectUtils.reflect(TestPrivateStaticFinal.class).field("I1").get()
ReflectUtils.reflect(TestPrivateStaticFinal.class).field("I2").get()
複製程式碼

當然,欄位操作也有更高階的操作,比如 Test1.java 測試類如下所示:

public class Test1 {
    public static int     S_INT1;
    public static Integer S_INT2;
    public        int     I_INT1;
    public        Integer I_INT2;

    public static Test1 S_DATA;
    public        Test1 I_DATA;
}
複製程式碼

我對其進行的單元測試如下所示:

@Test
public void fieldAdvanced() throws Exception {
    ReflectUtils.reflect(Test1.class)
            .field("S_DATA", ReflectUtils.reflect(Test1.class).newInstance())// 設定 Test1.class 中 S_DATA 欄位 為 new Test1()
            .field("S_DATA")// 獲取到 Test1.class 中 S_DATA 欄位
            .field("I_DATA", ReflectUtils.reflect(Test1.class).newInstance())// 獲取到 Test1.class 中 S_DATA 欄位 的 I_DATA 為 new Test1()
            .field("I_DATA")// 獲取到 Test1.class 中 S_DATA 欄位 的 I_DATA 欄位
            .field("I_INT1", 1)// 設定 Test1.class 中 S_DATA 欄位 的 I_DATA 欄位的 I_INT1 值為 1
            .field("S_INT1", 2);// 設定 Test1.class 中 S_DATA 欄位 的 S_INT1 欄位的 I_INT1 值為 2
    assertEquals(2, Test1.S_INT1);// 靜態變數就是最後設定的 2
    assertEquals(null, Test1.S_INT2);// 沒操作過就是 null
    assertEquals(0, Test1.S_DATA.I_INT1);// 沒操作過就是 0
    assertEquals(null, Test1.S_DATA.I_INT2);// 沒操作過就是 0
    assertEquals(1, Test1.S_DATA.I_DATA.I_INT1);// 倒數第二步操作設定為 1
    assertEquals(null, Test1.S_DATA.I_DATA.I_INT2);// 沒操作過就是 null
}
複製程式碼

根據如上註釋相信大家也可以理解一二了,如果還想了解更多使用方式,可以檢視我寫的單元測試類 ReflectUtilsTest,其使用方式就介紹到這裡,下面介紹其實現方式。

Achieve

實現的話是站在 jOOR 的肩膀上進行改造,其內部封裝了一個 private final Object object; 變數,每次進行反射操作時都會重新例項化一個變數並把結果賦予該變數,最終 get() 就是獲取其值,比如我們來看一下 newInstance 的操作,其涉及的程式碼如下所示:

/**
 * 例項化反射物件
 *
 * @param args 例項化需要的引數
 * @return {@link ReflectUtils}
 */
public ReflectUtils newInstance(Object... args) {
    Class<?>[] types = getArgsType(args);
    try {
        Constructor<?> constructor = type().getDeclaredConstructor(types);
        return newInstance(constructor, args);
    } catch (NoSuchMethodException e) {
        List<Constructor<?>> list = new ArrayList<>();
        for (Constructor<?> constructor : type().getDeclaredConstructors()) {
            if (match(constructor.getParameterTypes(), types)) {
                list.add(constructor);
            }
        }
        if (list.isEmpty()) {
            throw new ReflectException(e);
        } else {
            sortConstructors(list);
            return newInstance(list.get(0), args);
        }
    }
}

private Class<?>[] getArgsType(final Object... args) {
    if (args == null) return new Class[0];
    Class<?>[] result = new Class[args.length];
    for (int i = 0; i < args.length; i++) {
        Object value = args[i];
        result[i] = value == null ? NULL.class : value.getClass();
    }
    return result;
}

private void sortConstructors(List<Constructor<?>> list) {
    Collections.sort(list, new Comparator<Constructor<?>>() {
        @Override
        public int compare(Constructor<?> o1, Constructor<?> o2) {
            Class<?>[] types1 = o1.getParameterTypes();
            Class<?>[] types2 = o2.getParameterTypes();
            int len = types1.length;
            for (int i = 0; i < len; i++) {
                if (!types1[i].equals(types2[i])) {
                    if (wrapper(types1[i]).isAssignableFrom(wrapper(types2[i]))) {
                        return 1;
                    } else {
                        return -1;
                    }
                }
            }
            return 0;
        }
    });
}

private ReflectUtils newInstance(final Constructor<?> constructor, final Object... args) {
    try {
        return new ReflectUtils(
                constructor.getDeclaringClass(),
                accessible(constructor).newInstance(args)
        );
    } catch (Exception e) {
        throw new ReflectException(e);
    }
}

private final Class<?> type;

private final Object object;

private ReflectUtils(final Class<?> type, Object object) {
    this.type = type;
    this.object = object;
}
複製程式碼

jOOR 所沒有做到的就是沒有對多個符合的 Constructor 進行排序,而是直接返回了第一個與之匹配的。這樣說有點抽象,我舉個例子應該就明白了,比如說有兩個建構函式如下所示:

public class Test {

    public Test(Number n) {
    }

    public Test(Object n) {
    }
}
複製程式碼

jOOR 反射呼叫建構函式引數傳入 Long 型別,很可能就會走 Test(Object n) 這個建構函式,而我修改過後就是對多個符合的 Constructor 進行排序,匹配出與之最接近的父類,也就是會走 Test(Number n) 這個建構函式,同理,在後面的 method 中的引數匹配 jOOR 也是存在這個問題,我也已經對其修復了。

還有就是 jOORprivate static final 欄位先 getset 會報異常 java.lang.IllegalAccessException 異常,是因為對 private static final 欄位 get 的時候沒有去除 final 屬性,如果在 get 時就把 final 去掉即可解決,那樣在 set 的時候就不會報錯。然而,在 Android 的 SDK 中是沒有 Field.class.getDeclaredField("modifiers") 這個欄位的,所以會報 NoSuchFieldException 異常,這方面我做了容錯處理,相關程式碼如下所示:

/**
 * 設定反射的欄位
 *
 * @param name 欄位名
 * @return {@link ReflectUtils}
 */
public ReflectUtils field(final String name) {
    try {
        Field field = getField(name);
        return new ReflectUtils(field.getType(), field.get(object));
    } catch (IllegalAccessException e) {
        throw new ReflectException(e);
    }
}

/**
 * 設定反射的欄位
 *
 * @param name  欄位名
 * @param value 欄位值
 * @return {@link ReflectUtils}
 */
public ReflectUtils field(String name, Object value) {
    try {
        Field field = getField(name);
        field.set(object, unwrap(value));
        return this;
    } catch (Exception e) {
        throw new ReflectException(e);
    }
}

private Field getField(String name) throws IllegalAccessException {
    Field field = getAccessibleField(name);
    if ((field.getModifiers() & Modifier.FINAL) == Modifier.FINAL) {
        try {
            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        } catch (NoSuchFieldException ignore) {
            // runs in android will happen
        }
    }
    return field;
}

private Field getAccessibleField(String name) {
    Class<?> type = type();
    try {
        return accessible(type.getField(name));
    } catch (NoSuchFieldException e) {
        do {
            try {
                return accessible(type.getDeclaredField(name));
            } catch (NoSuchFieldException ignore) {
            }
            type = type.getSuperclass();
        } while (type != null);
        throw new ReflectException(e);
    }
}

private Object unwrap(Object object) {
    if (object instanceof ReflectUtils) {
        return ((ReflectUtils) object).get();
    }
    return object;
}
複製程式碼

所以該工具類既完美支援 Java,也完美支援 Android。

Conclusion

好了,這次反射工具類就介紹到這了,是不是覺得如斯優雅,如果覺得好的話以後遇到反射的問題,那就快用我這個工具類吧,這麼好的東西藏著不用真的是可惜了哦。

關於安卓核心常用工具類我已經差不多都封裝了,今後應該也不會在核心的裡面新增了,除非確實很需要我才會再新增某個工具類,其餘不常用的我都會放在 subutil 中,感謝大家一直陪伴著 AndroidUtilCode 的成長。

相關文章