Java拾遺:006 - Java反射與內省

反射

Java中提供了一套精心設計的工具集,以便編寫可以動態操縱Java代碼的程序,這套工具集就是反射(reflective)。html

反射做用

可以分析類能力的程序稱爲反射,反射機制功能很是強大,甚至能破壞程序可見性(訪問並操縱私有變量和方法)。 反射機制能夠用來:java

  • 在運行中分析類的能力(好比第三方類庫裏的類,手上又沒有源碼的狀況下)
  • 在運行中查看、操縱對象,如:編寫一個通用的toString方法供全部類使用
  • 實現通用的數組操做代碼
  • 利用Method對象,實現相似C++中的方法指針功能(表示筆者沒用過)

反射API

反射的API內容比較多,類在運行時的全部信息都有對應的反射類表示,這些類大部分在java.langjava.lang.reflect包下: 下面示例代碼中少一部分代碼,所有都在:https://github.com/zlikun-jee/effective-java 中(com.zlikun.jee.j006包下)。git

  • 表示類的java.lang.Class
@Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({TYPE})
    @interface MyAnnotation {
        String value() default "Hello !";
    }

    @MyAnnotation("我是一個內部類!")
    static class MyType {

        private String name;

        static {
            System.out.println("靜態代碼塊");
        }

        {
            System.out.println("非靜態代碼塊");
        }

        public MyType() {
            System.out.println("無參構造方法");
        }
    }

    @Test
    public void clazz() throws ClassNotFoundException, IllegalAccessException, InstantiationException {

        Class<MyType> clazz = MyType.class;

        // 使用 Class.forName() 加載類的時候,只會執行靜態代碼塊(不會構造類實例)
        // 實際上使用 Class.forName() 加載類返回類實例等價於直接使用類.class返回類實例,區別在於後者不會執行靜態代碼塊
        assertEquals(clazz, Class.forName(clazz.getName()));

        // 返回類全名(在使用內部類、匿名類時,中間使用$分隔,若是是外部類則是.,與getCanonicalName方法一致)
        assertEquals("com.zlikun.jee.j006.ReflectiveTest$MyType", clazz.getName());
        // 返回類標準名稱
        assertEquals("com.zlikun.jee.j006.ReflectiveTest.MyType", clazz.getCanonicalName());
        // 返回類名(簡稱,只包含類名,不包含包名)
        assertEquals("MyType", clazz.getSimpleName());

        // 使用無參構造方法構造一個實例(若是要使用參數構造實例,須要使用Constructor實例來實現)
        // 構造實例時會執行非靜態代碼塊和無參構造方法
        MyType mt = clazz.newInstance();
        assertNull(mt.name);

    }
  • 表示註解的java.lang.annotation.Annotation
@Test
    public void annotations() {
        Class<MyType> clazz = MyType.class;

        // 獲取類註解列表
        Annotation[] annotations = clazz.getAnnotations();

        // @com.zlikun.jee.j006.ReflectiveTest$MyAnnotation(value=我是一個內部類!)
        for (Annotation annotation : annotations) {
            System.out.println(annotation);
        }

        // 獲取指定類註解
        MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);
        // @com.zlikun.jee.j006.ReflectiveTest$MyAnnotation(value=我是一個內部類!)
        System.out.println(annotation);

        // interface com.zlikun.jee.j006.ReflectiveTest$MyAnnotation
        System.out.println(annotation.annotationType());
        // @com.zlikun.jee.j006.ReflectiveTest$MyAnnotation(value=我是一個內部類!)
        System.out.println(annotation.toString());
        // class com.zlikun.jee.j006.$Proxy4
        System.out.println(annotation.getClass());

        // 獲取該註解屬性值
        // 我是一個內部類!
        System.out.println(annotation.value());

    }
  • 表示ClassLoader的java.lang.ClassLoader
@Test
    public void classLoader() {
        Class<MyType> clazz = MyType.class;

        // 返回加該類的ClassLoader
        // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(clazz.getClassLoader());

        // 其與當前測試類使用了同一個ClassLoader
        assertEquals(this.getClass().getClassLoader(), clazz.getClassLoader());
    }
  • 表示包的java.lang.Package
@Test
    public void package0() {
        Class<MyType> clazz = MyType.class;
        Package package0 = clazz.getPackage();
        // package com.zlikun.jee.j006
        System.out.println(package0);
    }
  • 表示構造方法的java.lang.reflect.Constructor
@Test
    public void constructors() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<Employee> clazz = Employee.class;

        System.out.println("--getConstructors--");
        // 返回構造方法列表,只返回當前類的public構造方法,不包含私有構造方法和其父類的構造方法
        Constructor[] constructors = clazz.getConstructors();
        // public com.zlikun.jee.j006.Employee(java.lang.String,java.lang.Long)
        for (Constructor<Employee> constructor : constructors) {
            System.out.println(constructor);
        }

        System.out.println("--getDeclaredConstructors--");
        // 返回當前類定義的所有構造方法(包含private和protected及包級訪問權限的),不包含其父類的構造方法
        constructors = clazz.getDeclaredConstructors();
        // public com.zlikun.jee.j006.Employee(java.lang.String,java.lang.Long)
        // protected com.zlikun.jee.j006.Employee()
        // private com.zlikun.jee.j006.Employee(java.lang.Long)
        for (Constructor<Employee> constructor : constructors) {
            System.out.println(constructor);
        }

        // 若是要獲取父類構造方法,須要先獲取父類的類對象
        // class com.zlikun.jee.j006.Person
        Class<? super Employee> superClazz = clazz.getSuperclass();
        System.out.println(superClazz);
        // 獲取父類的構造方法
        System.out.println("--superClazz.getDeclaredConstructors--");
        // public com.zlikun.jee.j006.Person()
        // public com.zlikun.jee.j006.Person(java.lang.String)
        for (Constructor<?> constructor : superClazz.getDeclaredConstructors()) {
            System.out.println(constructor);
        }
        System.out.println("--end--");

        // 根據參數類型列表返回構造方法,無參構造方法不用傳入類型參數
        Constructor<Employee> constructor = clazz.getConstructor(String.class, Long.class);
        // public com.zlikun.jee.j006.Employee(java.lang.String,java.lang.Long)
        System.out.println(constructor);

        // 構造一個實例
        Employee employee = constructor.newInstance("zlikun", 350000L);
        // zlikun salary is 350000 fen
        System.out.printf("%s salary is %d fen%n", employee.getName(), employee.getSalary());

        // 可否使用private構造方法構造一個類實例
        // 首先使用getDeclaredConstructor方法才能獲得這個實例
        constructor = clazz.getDeclaredConstructor(Long.class);
        // 其次修改其訪問權限爲true,才能使用該構造方法對象構造實例,這一點對屬性、方法也適用
        constructor.setAccessible(true);
        employee = constructor.newInstance(240000L);
        assertEquals(Long.valueOf(240000L), employee.getSalary());
    }
  • 表示字段的java.lang.reflect.Field
@Test
    public void fields() throws NoSuchFieldException, IllegalAccessException {
        Employee employee = new Employee("Ashe", 180000L);
        Class<? extends Employee> clazz = employee.getClass();

        // 同構造方法相似,只能取得僅有屬性,且不包含父類屬性
        Field[] fields = clazz.getFields();
        for (Field field : fields) {
            System.out.println(field);
        }

        // 取得全部屬性,但不包含父類屬性
        fields = clazz.getDeclaredFields();
        // private java.lang.Long com.zlikun.jee.j006.Employee.salary
        for (Field field : fields) {
            System.out.println(field);
        }

        // 獲取單個屬性
        Field field = clazz.getDeclaredField("salary");
        assertEquals("salary", field.getName());
        assertEquals("private java.lang.Long com.zlikun.jee.j006.Employee.salary", field.toString());
        assertEquals("private java.lang.Long com.zlikun.jee.j006.Employee.salary", field.toGenericString());

        // 獲取字段值(字段是私有的,因此須要設置訪問權限)
        field.setAccessible(true);
        assertEquals(Long.valueOf(180000L), field.get(employee));

    }
  • 表示方法的java.lang.reflect.Method
@Test
    public void methods() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Employee employee = new Employee("Ashe", 180000L);
        Class<? extends Employee> clazz = employee.getClass();

        // 與構造方法、屬性不一樣,getMethods返回了全部public方法,包含從父類繼承的(固然也包含Object類的)
        /* ----------------------------------------------------------------------------------------------
            public java.lang.Long com.zlikun.jee.j006.Employee.raise(long)
            public void com.zlikun.jee.j006.Employee.setSalary(java.lang.Long)
            public java.lang.Long com.zlikun.jee.j006.Employee.getSalary()
            public java.lang.String com.zlikun.jee.j006.Person.getName()
            public void com.zlikun.jee.j006.Person.setName(java.lang.String)
            public final void java.lang.Object.wait() throws java.lang.InterruptedException
            public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
            public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
            public boolean java.lang.Object.equals(java.lang.Object)
            public java.lang.String java.lang.Object.toString()
            public native int java.lang.Object.hashCode()
            public final native java.lang.Class java.lang.Object.getClass()
            public final native void java.lang.Object.notify()
            public final native void java.lang.Object.notifyAll()
        ---------------------------------------------------------------------------------------------- */
        System.out.println("--getMethods--");
        for (Method method : clazz.getMethods()) {
            System.out.println(method);
        }

        // getDeclaredMethods則仍只獲取了當前類的方法(與訪問範圍無關)
        /* ----------------------------------------------------------------------------------------------
            public java.lang.Long com.zlikun.jee.j006.Employee.raise(long)
            public java.lang.Long com.zlikun.jee.j006.Employee.getSalary()
            public void com.zlikun.jee.j006.Employee.setSalary(java.lang.Long)
            private void com.zlikun.jee.j006.Employee.tip(long)
        ---------------------------------------------------------------------------------------------- */
        System.out.println("--getDeclaredMethods--");
        for (Method method : clazz.getDeclaredMethods()) {
            System.out.println(method);
        }

        System.out.println("--end--");

        // 獲取父類裏的單個方法
        Method method = clazz.getMethod("setName", String.class);
        // 執行這個方法
        method.invoke(employee, "Peter");
        // 查找執行效果
        assertEquals("Peter", employee.getName());

        // 獲取當前類裏的私有方法
        method = clazz.getDeclaredMethod("tip", long.class);
        method.setAccessible(true);
        // Peter偷偷跟客戶要了小費(反正我是私有的,老闆看不到^_^)。
        Object r = method.invoke(employee, 1200L);
        // 1200
        System.out.println(r);

    }

反射性能

關於反射機制會影響性能,這點毋庸置疑,但影響到底有多大,這個沒有一個統一的說法,我作了一個簡單的測試github

@Test
    public void test() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        // 2,147,483,647
        int loop = Integer.MAX_VALUE;

        Employee employee = new Employee("Ashe", 120000L);

        long time = System.currentTimeMillis();
        for (int i = 0; i < loop; i++) {
            employee.setName("Peter");
        }
        // 不用反射程序執行耗時:5455毫秒
        System.out.printf("不用反射程序執行耗時:%d毫秒%n", System.currentTimeMillis() - time);

        time = System.currentTimeMillis();
        Class<Employee> clazz = Employee.class;
        Method method = clazz.getMethod("setName", String.class);
        for (int i = 0; i < loop; i++) {
            method.invoke(employee, "Peter");
        }
        // 不用反射程序執行耗時:9555毫秒
        System.out.printf("不用反射程序執行耗時:%d毫秒%n", System.currentTimeMillis() - time);


    }

測試能夠看出,使用反射設置屬性比直接調用setter慢了接近一倍的樣子,但在實際應用中,這點開銷能夠忽略不計(慢一倍還能忽略不計?那得看基準是什麼,平均一微秒執行約224.7次,你沒看錯,我說的是微秒,因此可不就是忽略不計麼)。 關於以上說法我留一點保留意見,在高併發的場景下,仍是要謹慎使用反射API。緣由是在JDK1.6版的Method類中invoke方法有使用synchronized加鎖,但在JDK1.7及之後的版本中移除了。 因此是否在高併發下使用反射要視你的JDK版本而定,最好是作一下壓力測試再決定是否使用。spring

內省

內省(Introspector)是 Java 語言對 Bean 類屬性、事件的一種缺省處理方法,對是反射API的高級封裝。其相比反射API更爲易用,但功能相對受限,僅針對JavaBean使用。apache

內省做用

內省的做用主要體如今對JavaBean的動態操縱上,通常用於屬性值的讀取和設置,經常使用在屬性複製、對象克隆等場景裏。數組

內省API

內省的API相對比反射的簡單一些(只是上層封裝部分,內部操做還是反射)。緩存

public class IntrospectionTest {

    @Test
    public void testBeanDescriptor() {
        BeanDescriptor descriptor = new BeanDescriptor(Employee.class);

        assertEquals(Employee.class, descriptor.getBeanClass());
        assertEquals(null, descriptor.getCustomizerClass());
        assertEquals("Employee", descriptor.getDisplayName());
        assertEquals("Employee", descriptor.getName());
        assertEquals("Employee", descriptor.getShortDescription());
        assertFalse(descriptor.isHidden());
        System.out.println(descriptor.hashCode());
        Enumeration<String> enums = descriptor.attributeNames();
        while (enums.hasMoreElements()) {
            System.out.println(enums.nextElement());
        }
    }

    @Test
    public void testPropertyDescriptor() throws IntrospectionException, InvocationTargetException, IllegalAccessException {


        // PropertyDescriptor 類表示JavaBean類經過存儲器導出一個屬性
        PropertyDescriptor descriptor = new PropertyDescriptor("name", Employee.class);

        // name
        System.out.println(descriptor.getName());
        // name
        System.out.println(descriptor.getDisplayName());
        // class java.lang.String
        System.out.println(descriptor.getPropertyType());
        // public java.lang.String com.zlikun.jee.j006.Person.getName()
        System.out.println(descriptor.getReadMethod());
        // public void com.zlikun.jee.j006.Person.setName(java.lang.String)
        System.out.println(descriptor.getWriteMethod());
        // 541214437
        System.out.println(descriptor.hashCode());
//        descriptor.setReadMethod(null);
//        descriptor.setWriteMethod(null);


        // 傳入一個實例
        Employee employee = new Employee("Ashe", 180000L);

        // 操做類屬性
        // setter
        Method writeMethod = descriptor.getWriteMethod();
        // 若是setter是不可見的,設置其可見性
        writeMethod.setAccessible(true);
        // 執行setter方法
        writeMethod.invoke(employee, "Peter");

        // getter
        Method readMethod = descriptor.getReadMethod();
        readMethod.setAccessible(true);
        Object result = readMethod.invoke(employee);
        // 測試其返回結果是否與類實例一致
        assertEquals(result, employee.getName());

    }

    @Test
    public void testBeanInfo() throws IntrospectionException {

        // 這是一個工具類,用於獲取Bean信息實例
        BeanInfo info = Introspector.getBeanInfo(Employee.class);

        BeanDescriptor beanDescriptor = info.getBeanDescriptor();
        System.out.println(beanDescriptor);

        PropertyDescriptor[] propertyDescriptors = info.getPropertyDescriptors();
        System.out.println(propertyDescriptors);

        MethodDescriptor[] methodDescriptors = info.getMethodDescriptors();
        System.out.println(methodDescriptors);

    }

}

應用

反射和內省一般用於底層工具或框架開發,應用開發幾乎用不到,因此專一於應用開發的童鞋能夠忽略(我固然是建議多少要懂一些的)。下面實現了幾個案例,主要是測試反射和內省API,實際開發中不建議直接使用(有不少專業的第三方類庫提供了一樣功能,如:commons-beanutilsSpring FrameworkGuava等)。併發

通用ToString實現

public class ToStringHelper {

    public static final <T> String genericToString(T t) {
        if (t == null) return null;

        // 獲取類信息
        Class<?> clazz = t.getClass();

        // 獲取全部字段
        Set<Field> fields = new HashSet<>();
        fields.addAll(Arrays.stream(clazz.getDeclaredFields()).collect(Collectors.toSet()));
        fields.addAll(Arrays.stream(clazz.getFields()).collect(Collectors.toSet()));

        // 獲取字段,組裝字符串
        StringBuilder builder = new StringBuilder(clazz.getCanonicalName());
        builder.append(" : {");
        for (Field field : fields) {
            if (!field.isAccessible()) {
                field.setAccessible(true);
            }

            try {
                Object value = field.get(t);
                if (value != null) {
                    builder.append(field.getName()).append(" = ")
                            .append(field.getType().isPrimitive() ? value : value.toString())
                            .append(", ");
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return builder.substring(0, builder.length() - 2) + "}";
    }

    /**
     * 使用Introspection機制實現ToString
     *
     * @param t
     * @param <T>
     * @return
     */
    public static final <T> String genericToString2(T t) {
        try {
            // 獲取Bean信息
            BeanInfo info = Introspector.getBeanInfo(t.getClass());

            // 提取所有屬性
            PropertyDescriptor[] descriptors = info.getPropertyDescriptors();
            // 遍歷屬性描述符,取得屬性值
            StringBuilder builder = new StringBuilder(t.getClass().getCanonicalName());
            builder.append(" : {");

            for (PropertyDescriptor descriptor : descriptors) {
                if (descriptor.getName().equals("class")) {
                    continue;
                }
                Method method = descriptor.getReadMethod();
                if (!method.isAccessible()) {
                    method.setAccessible(true);
                }
                try {
                    Object value = method.invoke(t);
                    if (value != null) {
                        builder.append(descriptor.getName()).append(" = ")
                                .append(method.getReturnType().isPrimitive() ? value : value.toString())
                                .append(", ");
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }

            return builder.substring(0, builder.length() - 2) + "}";
        } catch (IntrospectionException e) {
            e.printStackTrace();
        }
        return null;
    }

}

屬性拷貝工具類(Spring)源碼解讀

public class BeanHelper {

    /**
     * 從Spring源碼(org.springframework.beans.BeanUtils)中抄來的(去除了外部組件依賴,和不重要的參數),這裏對其進行解讀
     *
     * @param source
     * @param target
     * @throws Exception
     */
    public static final void copyProperties(Object source, Object target) throws Exception {


        // 獲取目標類型屬性描述符列表
        PropertyDescriptor[] targetPds = getPropertyDescriptors(target.getClass());

        // 遍歷屬性描述符列表
        for (PropertyDescriptor targetPd : targetPds) {

            // 判斷其是否包含讀方法(getter)
            Method writeMethod = targetPd.getWriteMethod();
            if (writeMethod != null) {
                // 根據屬性名,從源類中找到對應的屬性及其屬性描述符
                PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
                // 若是存在則進行屬性複製
                if (sourcePd != null) {
                    // 獲取源類型的屬性讀方法,判斷其是否存在
                    Method readMethod = sourcePd.getReadMethod();
                    if (readMethod != null &&
                            isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                        try {
                            // 判斷、修改讀方法可見性
                            if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                                readMethod.setAccessible(true);
                            }
                            // 調用讀方法,返回屬性值
                            Object value = readMethod.invoke(source);
                            // 判斷、修改寫方法可見性
                            if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                                writeMethod.setAccessible(true);
                            }
                            // 調用寫方法,寫入屬性值
                            writeMethod.invoke(target, value);
                        } catch (Throwable ex) {
                            throw new Exception(
                                    "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                        }
                    }
                }
            }
        }
    }

    private static PropertyDescriptor getPropertyDescriptor(Class<?> clazz, String name) throws IntrospectionException {
        return new PropertyDescriptor(name, clazz);
    }

    private static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) throws IntrospectionException {
        return Introspector.getBeanInfo(clazz).getPropertyDescriptors();
    }

    private static final Map<Class<?>, Class<?>> primitiveWrapperTypeMap = new IdentityHashMap<>(8);
    private static final Map<Class<?>, Class<?>> primitiveTypeToWrapperMap = new IdentityHashMap<>(8);

    private static boolean isAssignable(Class<?> lhsType, Class<?> rhsType) {
        if (lhsType.isAssignableFrom(rhsType)) {
            return true;
        }
        if (lhsType.isPrimitive()) {
            Class<?> resolvedPrimitive = primitiveWrapperTypeMap.get(rhsType);
            if (lhsType == resolvedPrimitive) {
                return true;
            }
        } else {
            Class<?> resolvedWrapper = primitiveTypeToWrapperMap.get(rhsType);
            if (resolvedWrapper != null && lhsType.isAssignableFrom(resolvedWrapper)) {
                return true;
            }
        }
        return false;
    }

}

代碼挺多,但思路其實很簡單(Spring內部作了不少優化,好比使用緩存,這部分代碼被刪掉了),就使用內省API獲取源對象類屬性和目標對象類屬性,讀取源對象屬性值,寫入到目標對象屬性中。 雖然Spring提供的工具類已經很好用,但實際應用中仍是有改進空間,好比我曾經在應用中遇到複製屬性時須要過濾空值(默認實現未提供這一特性),我在調用讀方法獲得屬性值後面加了一句判斷,若是值爲空,則跳事後面的邏輯,避免源對象的空值覆蓋了目標對象同名屬性(可能有值)。app

結語

關於反射和內省,通常仍是寫框架、庫或者工具類的時候會用得多,應用開發不多會用到(不排除有喜歡用的),因此通常不要求必須掌握,但我的仍是建議能掌握儘可能掌握,不能掌握也要了解,至少別人寫了你得能看得懂。

代碼倉庫:

相關文章
相關標籤/搜索