细说Java反射2值得注意的诸多细节

2020-03-11  本文已影响0人  as_pixar

昨天我编写了一篇关于 Java 反射基础知识的博文,内容挺多的,涉及到了 Class 的获取,Field、Method、Constructor、Array 及 Enum 的获取与操作。如果学会了这些知识,就能阅读或者是编写大多数反射相关代码。

但是,因为反射这一块的内容实在是太多了,编写代码过程中难免会遭遇到各种各样的 Exception,一次改进的机会,一次创造的机会。反射需要耐心与细心地对待它,我们列举编写反射代码中值得注意的一些细节。

获取不存在的对象

比如获取不存在的 Class 对象,比如获取不到一个类中并不存在的 Field、Method 或者是 Constructor。 而 Field、Method 和 Constructor 都是一个 Class 对象中的成员。

获取不到 Class 对象

我们知道获取 Class 对象有 3 种方式。

通过一个对象的 getClass() 方法。
通过 .class 关键字。
通过 Class.forName()。

前两种方式,基本上是没有什么值得注意的地方,需要注意的是第 3 种,因为 Class.forName() 传递进去的参数是一个字符串类型,所以理论上你可以这样编写代码。

Class test = Class.forName("hello world");

显然,在虚拟机中并不会存在这样一个类,所以,Java 提供了一个异常用来在获取不到 Class 文件时进行抛出。这个异常是 ClassNotFoundException。我们应该对于这一块进行处理。

Class cls1 = new String("1").getClass();
Class cls2 = int.class;
try {
    Class test = Class.forName("hello world");
} catch (ClassNotFoundException e1) {
    // TODO Auto-generated catch block
    e1.printStackTrace();
    System.out.println("find class error:"+e1.getMessage());
}

可以看到,只有通过 Class.forName() 的方式获取 Class 的时候才要进行异常的捕获处理,如果查找不到这个 Class 那么程序就会抛出异常。

java.lang.ClassNotFoundException: hello world
    at java.net.URLClassLoader.findClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Unknown Source)

获取不到 Field

获取不到 Field 的情况分两种:

  1. 确实不存在这个 Field
  2. 由于修饰符导致的权限问题。

获取不存在的 Field

我们先定义一个类 Base。

public class Base {
    public String b;
}

我们知道,获取一个 Class 中 Field 方式有 4 种。


public Field getField(String name);

public Field getDeclaredField(String name);

public Field[] getFields();

public Field[] getDeclaredFields()

现在,我们可以用 getField() 方法获取 b 这个 Field。

Class clzBase = Base.class;
try {
    Field fieldBase = clzBase.getField("b");
} catch (NoSuchFieldException e1) {
    // TODO Auto-generated catch block
    e1.printStackTrace();
} catch (SecurityException e1) {
    // TODO Auto-generated catch block
    e1.printStackTrace();
}

但是,我们却没有办法去获取一个不存在的 Field,如 d。

Field fielddBase = clzBase.getField("d");

它会导致程序抛出 NoSuchFieldException 异常。

java.lang.NoSuchFieldException: d
    at java.lang.Class.getField(Unknown Source)

Field 存在,但获取不到

public class Base {
    public String b;

    int d;
}

public class NonExistTest {

    public static void main(String[] args) {

        Field fieldBase = clzBase.getField("d");
        System.out.println(Base.class.getSimpleName()+" has a field "+ fieldBase.getName());

    }

}

可以发现程序报错了。

java.lang.NoSuchFieldException: d
    at java.lang.Class.getField(Unknown Source)

这是根据 API 的定义,getField() 只能获取 public 属性的 Field。这个时候改用 getDeclairedField() 方法就可以了。

Field fieldBase1 = clzBase.getDeclaredField("d");

getDeclaredField() 能够获取一个 Class 中被定义的 Field

有同学可能会想,既然 getDeclaredField() 能够获取所有修饰符类型的 Field,那么为什么还要整出一个 getField() 方法。

其实,getDeclaredField() 并非万能的,有一种属性它是没有办法获取到的,那就是从父类继承下来的 Field。

public class Base {
    public String b;

    int d;
}

public class Sub extends Base{

}

Class clzBase = Sub.class;

Field fieldBase1 = clzBase.getDeclaredField("b");
System.out.println(Sub.class.getSimpleName()+" has a field "+ fieldBase1.getName());

通过 getDeclaredField() 方法去获取 Sub 的父类 Base 中的 Field b,程序会抛出异常。

java.lang.NoSuchFieldException: b
    at java.lang.Class.getDeclaredField(Unknown Source)

但是,通过 getField() 方法却可以获取。

Field fieldBase1 = clzBase.getField("b");

System.out.println(clzBase.getSimpleName()+" has a field "+ fieldBase1.getName());

打印结果是:

Sub has a field b

但是,getField() 也有它的局限性,因为它只能获取被 public 修饰的 Field。例如

Field fieldBase1 = clzBase.getField("d");

System.out.println(clzBase.getSimpleName()+" has a field "+ fieldBase1.getName());

上面代码中,试图用 getField() 方法去获取一个非 public 修饰的 Field,结果自然是获取不到的。

java.lang.NoSuchFieldException: d
    at java.lang.Class.getField(Unknown Source)

所以,我们可以得到一张表,这张表可以说明 getField() 方法和 getDeclaredField() 方法的能力范围。


如何获取一个 Class 中继承下来的非 public 修饰的 Field

public class Base {
    public String b;

    protected int d;
}

public class Sub extends Base{

}

Base 是 Sub 的父类,但是通过 Sub.class 对象是没有办法获取到 Base.class 中的 Field d,因为 d 是默认的修饰符,对于这一点 getField() 只能获取 pulic 属性的 Field 如 b,getDeclaredField() 更是无能为力。

那么,问题来了。

如果非要获取一个 Class 继承下来的非 public 修饰的 Field 要怎么办?

答案是通过获取这个 Class 的 superClass。然后调用这个 superClass 的 getDeclaredField() 方法。

Class clzBase = Sub.class;
Class superClass = clzBase.getSuperclass();

Field fieldBase1 = superClass.getDeclaredField("d");
System.out.println(clzBase.getSimpleName()+" has a field "+ fieldBase1.getName());

获取不到 Method

Method、Constructor 和 Field 一样都是 Class 的成员。所以,有很多共性。因为上一小节讲过 Field 的诸多细节,所以类似的地方我会一笔带过。

获取本身就不存在的 Method

public class Base {
    public String b;

    protected int d;

    void testDefault0(){};

    public void testPublic0(){}

    protected void testProtected0(){}

    private void testPrivate0(){}
}

获取本身就不存在的 Method,程序会抛出一个 NoSuchMethodException 异常。

Class class1 = Base.class;

try {
    Method methodtest = class1.getDeclaredMethod("hellowworld");
} catch (NoSuchMethodException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (SecurityException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}

Method 存在却获取不到。

同 Field 类似,这里给出一张表。


因为参数类型不匹配而找不到。

public class Base {
    public String b;

    protected int d;


    public Base() {
        super();
    }

    void testDefault0(){};

    public void testPublic0(){}

    protected void testProtected0(){}

    private void testPrivate0(){}

    public void test(int a,float b){}
}

现在要获取 test() 对应的 Method。

Method methodtest = class1.getDeclaredMethod("test");

如果不传入参数是获取不到的

java.lang.NoSuchMethodException: com.wwj.test.notfound.Base.test()
    at java.lang.Class.getDeclaredMethod(Unknown Source)

我们应该传入对应类型的参数

public Method getDeclaredMethod(String name, Class<?>... parameterTypes)

值得注意的是,后面接受的是可变参数,也就是参数的个数不定,并且类型都是 Class。

Method methodtest = class1.getDeclaredMethod("test",int.class,float.class);

当然,这样的形式也是可以的。

Method methodtest = class1.getDeclaredMethod("test",new Class[]{int.class,float.class});

如果一个 Method,参数过多的话,推荐使用后面一种方式。

获取 Method 的时候,参数一定要一一匹配,意思是参数的数目与类型都必须对应上,不然还是会抛出 NoSuchMethodException 异常,如下面:

Method methodtest = class1.getDeclaredMethod("test",int.class,double.class);

或者 

Method methodtest = class1.getDeclaredMethod("test",int.class);

获取不到 Constructor

获取本身不存在的构造器。

Class class1 = Base.class;

Constructor constructor = class1.getConstructor(int.class);

需要注意的是,它仍然会抛出 NoSuchMethodException 这个异常。并没有一个 NoSuchConstructorException 的异常,其实想想也是,构造方法本质上其实也就是一个方法,只不过在反射机制中,因为构造方法的特殊性和重要性,要以单独用 Constructor 把它与普通的 Method 区分开来了。

Constructor 存在,却获取不到。

同 Field 和 Method 一样,同样存在

getConstructor()

getDeclaredConstructor()

getConstructors()

getDeclaredConstructors()

不一样的是,getConstructor() 和 getConstructors() 也没有办法获取 SuperClass 的 Constructor。

参数不匹配的问题

这个同 Method 一样。

获取一个 Class 的内部类或者接口

public Class<?>[] getClasses()

public Class<?>[] getDeclaredClasses()

编写代码体会一下:

public class TestMember {

    class enclosingClass{};

    public interface testInterface{}

}


public class MemberTest {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Class clz = TestMember.class;

        Class[] members = clz.getDeclaredClasses();

        for ( Class c : members ) {
            System.out.println(c.toGenericString());
        }
    }

}

测试结果如下:

class com.wwj.test.member.TestMember$enclosingClass
public abstract static interface com.wwj.test.member.TestMember$testInterface

getInterfaces() 的作用

光看名字,大家可能都会觉得 getInterfaces() 的作用是获取一个类中定义的接口,但是其实不是的,getInterfaces() 获取的是一个类所有实现的接口。

public class A implements Runnable,Cloneable{

    @Override
    public void run() {

    }

}

public class MemberTest {
    public static void main(String[] args) {
        Class[] interfaces = A.class.getInterfaces();
        for ( Class c : interfaces ) {
            System.out.println(c.toGenericString());
        }
    }

}

结果是:

public abstract interface java.lang.Runnable
public abstract interface java.lang.Cloneable

反射中的权限问题

操纵非 public 修饰的 Field

public class TestPermission {
    private int value;
     public int getValue() {
        return value;
    }
    public void setValue(int value) {
        this.value = value;
    }
    public TestPermission() {
        super();
    }
}

TestPermission 这个类有一个 int 变量 value,现在用反射的手段获取它对应的 Field 然后操纵它的值。

public class AccessTest {

    public static void main(String[] args) {

        TestPermission test = new TestPermission();
        test.setValue(12);
        System.out.println(" value is :"+test.getValue());

        Class testclass = test.getClass();
        try {
            Field field = testclass.getDeclaredField("value");

            field.set(test, 30);

            System.out.println(" value is :"+test.getValue());
        } catch (NoSuchFieldException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        } catch (SecurityException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

}

结果却报错了。

 value is :12
java.lang.IllegalAccessException: 
Class com.wwj.test.access.AccessTest 
can not access a member of class com.wwj.test.access.TestPermission with modifiers "private"
    at sun.reflect.Reflection.ensureMemberAccess(Unknown Source)
    at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(Unknown Source)
    at java.lang.reflect.AccessibleObject.checkAccess(Unknown Source)
    at java.lang.reflect.Field.set(Unknown Source)
    at com.wwj.test.access.AccessTest.main(AccessTest.java:19)

它抛出来的是一个 IllegalAccessException 异常。
修正方法也很简单。

Field field = testclass.getDeclaredField("value");
field.setAccessible(true);

可以看到结果正常了。

 value is :12
 value is :30

这里需要多说两句:我这里以 private 为例,其实 protected 和 default 也是一样的,但是它们不同于 private 的地方在于,它们在本 package 范围内是可见的,有兴趣的同学可以测试一下,测试代码在一个 package,而测试的类在另外一个 package。

操纵一个 final 类型的 Field

public class TestPermission {

    protected int value;

    public final int value1 = 0;

}

给 TestPermission 这个类新加一个字段 value1,但是它是一个被 final 修饰的属性,如果利用反射来修改它的值会怎么样呢?

TestPermission test = new TestPermission();
Class testclass = test.getClass();
try {
    Field field = testclass.getDeclaredField("value1");

    field.setInt(test,123);
    int a = field.getInt(test);

    System.out.println(" value1 is :"+a);

} catch (NoSuchFieldException e1) {
    // TODO Auto-generated catch block
    e1.printStackTrace();
} catch (SecurityException e1) {
    // TODO Auto-generated catch block
    e1.printStackTrace();
} catch (IllegalArgumentException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (IllegalAccessException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}

结果却遭遇了异常。

java.lang.IllegalAccessException: Can not set final int field com.wwj.test.access.TestPermission.value1 to (int)123
    at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(Unknown Source)
    at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(Unknown Source)
    at sun.reflect.UnsafeQualifiedIntegerFieldAccessorImpl.setInt(Unknown Source)
    at java.lang.reflect.Field.setInt(Unknown Source)

虽然,value1 是被 public 修饰,但是它同样被 final 修饰,这在正常的开发流程说明这个属性不能够再被改变。

如果要解决这个问题,同样可以使用 setAccessible(true) 方法。

Field field = testclass.getDeclaredField("value1");
field.setAccessible(true);
field.setInt(test,123);
int a = field.getInt(test);

System.out.println(" value1 is :"+a);

操纵非 public 修饰的 Method

public class TestPermission {

     public TestPermission() {
        super();
    }

    private void justSay() {
        System.out.println("just for test meothod");
    }
}

public class AccessTest {

    public static void main(String[] args) {
        Class clz = TestPermission.class;

        try {
            Method method = clz.getDeclaredMethod("justSay");
            method.invoke(clz.newInstance(),null);
        } catch (NoSuchMethodException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (SecurityException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InstantiationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

试图通过反射去操作一个 private 方法,结果报错了。

java.lang.IllegalAccessException: Class com.wwj.test.access.AccessTest can not access a member of class com.wwj.test.access.TestPermission with modifiers "private"
    at sun.reflect.Reflection.ensureMemberAccess(Unknown Source)
    at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(Unknown Source)
    at java.lang.reflect.AccessibleObject.checkAccess(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at com.wwj.test.access.AccessTest.main(AccessTest.java:13)

它抛出来的是一个 IllegalAccessException 异常。
修正方法也很简单。

Method method = clz.getDeclaredMethod("justSay");
method.setAccessible(true);
method.invoke(clz.newInstance(),null);

操纵非 public 修饰的 Constructor

同前面两种,同样是通过 setAccessible(true) 来搞定。
所以,在反射中如果要操作被 private 修饰的对象,那么就必须调用它的 setAccessible(true)。

setAccessible() 的秘密

我们已经知道 Field、Method 和 Constructor 都有 setAccessible() 这个方法,至于是什么呢?这是因为它们有共同的祖先 AccessObject。

public class AccessibleObject implements AnnotatedElement {
    public void setAccessible(boolean flag) throws SecurityException {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) sm.checkPermission(ACCESS_PERMISSION);
        setAccessible0(this, flag);
    }

    /* Check that you aren't exposing java.lang.Class.<init> or sensitive
       fields in java.lang.Class. */
    private static void setAccessible0(AccessibleObject obj, boolean flag)
        throws SecurityException
    {
        if (obj instanceof Constructor && flag == true) {
            Constructor<?> c = (Constructor<?>)obj;
            if (c.getDeclaringClass() == Class.class) {
                throw new SecurityException("Cannot make a java.lang.Class" +
                                            " constructor accessible");
            }
        }
        obj.override = flag;
    }

    /**
     * Get the value of the {@code accessible} flag for this object.
     *
     * @return the value of the object's {@code accessible} flag
     */
    public boolean isAccessible() {
        return override;
    }
}

可以看到,主要是设置内部一个 override 变量。

那么,我们再以 Method 的 invoke 方法为例。

public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
{
    if (!override) {
        if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, obj, modifiers);
        }
    }
    MethodAccessor ma = methodAccessor;             // read volatile
    if (ma == null) {
        ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
}

如果一个 Method 的 overide 为 false 的话,它就会根据 Modifiers 判断是否具有访问权限。
[Reflection.java]

public static boolean quickCheckMemberAccess(Class<?> memberClass,
                                                 int modifiers)
{
    return Modifier.isPublic(getClassAccessFlags(memberClass) & modifiers);
}

这个方法主要是简单地判断 modifiers 是不是 public,如果不是的话就返回 false。所以 protected、private、default 修饰符都会返回 false,只有 public 都会返回 true。

而不是 public 修饰的话会执行下面的代码

void checkAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers)
        throws IllegalAccessException
{
    if (caller == clazz) {  // quick check
        return;             // ACCESS IS OK
    }
    Object cache = securityCheckCache;  // read volatile
    Class<?> targetClass = clazz;
    if (obj != null
        && Modifier.isProtected(modifiers)
        && ((targetClass = obj.getClass()) != clazz)) {
        // Must match a 2-list of { caller, targetClass }.
        if (cache instanceof Class[]) {
            Class<?>[] cache2 = (Class<?>[]) cache;
            if (cache2[1] == targetClass &&
                cache2[0] == caller) {
                return;     // ACCESS IS OK
            }
            // (Test cache[1] first since range check for [1]
            // subsumes range check for [0].)
        }
    } else if (cache == caller) {
        // Non-protected case (or obj.class == this.clazz).
        return;             // ACCESS IS OK
    }

    // If no return, fall through to the slow path.
    slowCheckMemberAccess(caller, clazz, obj, modifiers, targetClass);
}

// Keep all this slow stuff out of line:
void slowCheckMemberAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers,
                           Class<?> targetClass)
    throws IllegalAccessException
{
    Reflection.ensureMemberAccess(caller, clazz, obj, modifiers);

    // Success: Update the cache.
    Object cache = ((targetClass == clazz)
                    ? caller
                    : new Class<?>[] { caller, targetClass });

    // Note:  The two cache elements are not volatile,
    // but they are effectively final.  The Java memory model
    // guarantees that the initializing stores for the cache
    // elements will occur before the volatile write.
    securityCheckCache = cache;         // write volatile
}

最终通过 Reflection 这个类的静态方法 ensureMemberAccess() 确认。

public static void ensureMemberAccess(Class<?> currentClass,
                                          Class<?> memberClass,
                                          Object target,
                                          int modifiers)
        throws IllegalAccessException
    {
        if (currentClass == null || memberClass == null) {
            throw new InternalError();
        }

        if (!verifyMemberAccess(currentClass, memberClass, target, modifiers)) {
            throw new IllegalAccessException("Class " + currentClass.getName() +
                                             " can not access a member of class " +
                                             memberClass.getName() +
                                             " with modifiers \"" +
                                             Modifier.toString(modifiers) +
                                             "\"");
        }
    }

如果没有访问权限,程序将会在此抛出一个 IllegalAccessException 的异常。

所以,如果通过反射方式去操作一个 Field、Method 或者是 Constructor,最好先调用它的 setAccessible(true) 以防止程序运行异常。

Class.newInstance() 和 Constructor.newInstance() 的区别

Class.newInstance() 的使用有严格的限制,那就是一个 Class 对象中,必须存在一个无参数的 Constructor,并且这个 Constructor 必须要有访问的权限。

package com.wwj.test.newinstance;

public class TestCreate {

}

public class NewInstanceTest {

    public static void main(String[] args) {
        Class clz = TestCreate.class;

        try {
            Constructor[] constructors = clz.getDeclaredConstructors();
            for ( Constructor c : constructors ) {
                System.out.println(c.toString());
            }
            TestCreate obj = (TestCreate) clz.newInstance();

            System.out.println(obj.toString());
        } catch (InstantiationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

如上面的代码,我们试图通过 Class.newInstance() 的方法创建一个 TestCreate 对象实例。

public com.wwj.test.newinstance.TestCreate()
com.wwj.test.newinstance.TestCreate@15db9742

Java 默认会给每个类加上一个无参的构造方法,并且它的修饰符是 public。现在,我们作一个小小的变动。

public class TestCreate {
    private TestCreate() {}
}

给 TestCreate 添加相应的构造方法,但是是 private 的访问权限,我们再看测试结果。

private com.wwj.test.newinstance.TestCreate()
java.lang.IllegalAccessException: 
Class com.wwj.test.newinstance.NewInstanceTest 
can not access a member of class com.wwj.test.newinstance.TestCreate with modifiers "private"
    at sun.reflect.Reflection.ensureMemberAccess(Unknown Source)
    at java.lang.Class.newInstance(Unknown Source)
    at com.wwj.test.newinstance.NewInstanceTest.main(NewInstanceTest.java:15)

但是,通过 Constructor.newInstance() 却没有这种限制。Constructor.newInstance() 适应任何类型的 Constructor,无论它们有参数还是无参数,只要通过 setAccessible() 控制好访问权限就可以了。

所以,一般建议优先使用 Constructor.newInstance() 去创建一个对象实例。

谨慎使用 Method.invoke() 方法

通过 Method 调用它的 invoke 方法,这应该是整个反射机制中的灵魂了。但是,正因为如此,我们就得小心处理它的很多细节。

静态方法和非静态方法的区别

public class TestMethod {

    static void test1() {
        System.out.println("test1");
    }

    void test2() {
        System.out.println("test1");
    }
}

public class MethodDetailTest {

    public static void main(String[] args) {
        TestMethod.test1();

        new TestMethod().test2();
    }

}

我们知道,在正常流程开发中,调用静态方法直接用 类.方法() 的形式就可以调用,而调用非静态的方法那就必须先创建对象,再通过对象调用相应的方法。

那么,对应到反射中如何正常调用静态方法和非静态方法呢?

try {
    Method method1 = clz.getDeclaredMethod("test1");

    method1.invoke(null);
} catch (NoSuchMethodException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (SecurityException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (IllegalAccessException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (IllegalArgumentException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (InvocationTargetException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}

try {
    Method method2 = clz.getDeclaredMethod("test2");

    method2.invoke(new TestMethod());
} catch (NoSuchMethodException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (SecurityException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (IllegalAccessException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (IllegalArgumentException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (InvocationTargetException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}

关键在于 Method.invoke() 的第一个参数,static 方法因为属于类本身所以不需要对象,那么非静态方法的就必须要传入一个对象了,而且这个对象也必须是与 Method 对应的。

我们可以测试一下,看看随便给 invoke() 方法传递一个对象会如何?

Method method2 = clz.getDeclaredMethod("test2");

method2.invoke(new String("134"));

随便给它传递一个字符串,结果报错了。

java.lang.IllegalArgumentException: object is not an instance of declaring class
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at com.wwj.test.method.MethodDetailTest.main(MethodDetailTest.java:39)

抛出了一个 IllegalArgumentException 的异常,提示说 object 不是声明的 class 的对象实例。

Method.invoke() 参数的秘密

public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException

第一个 Object 参数代表的是对应的 Class 对象实例,这在上面一节已经见识到了。而后面的参数就是可变形参了,它接受多个参数。我们考虑一种特殊情况。

public class TestT<T>{

    public  void test(T t){}

}

这是一个泛型类,T 表示接受任意类型的参数。

public class MethodDetailTest {

    public static void main(String[] args) {

        Class clzT = TestT.class;
        try {
            Method tMethod = clzT.getDeclaredMethod("test",Integer.class);
            tMethod.setAccessible(true);
            try {
                tMethod.invoke(new TestT<Integer>(),1);
            } catch (IllegalAccessException | IllegalArgumentException
                    | InvocationTargetException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        } catch (NoSuchMethodException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        } catch (SecurityException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
    }
}

结果却报错。

java.lang.NoSuchMethodException: com.wwj.test.method.TestT.test(java.lang.Integer)
    at java.lang.Class.getDeclaredMethod(Unknown Source)
    at com.wwj.test.method.MethodDetailTest.main(MethodDetailTest.java:14)

提示找不到这个方法。原因是类型擦除。

当一个方法有泛型参数时,编译器会自动向上转型,T 向上转型是 Object。所以实际上是

void test(Object t);

上面的代码试图去找 test(Integer t) 这个方法,自然是找不到。

Method tMethod = clzT.getDeclaredMethod("test",Object.class);
tMethod.setAccessible(true);

tMethod.invoke(new TestT<Integer>(),1);

这样编写代码才正常。

Method 中处理 Exception

我们平常开发中,少不了与各种异常打交道。

public class TestMethod {

    public static class Super{}

    public static class Sub extends Super{}

    static void test1(Sub sub) {
        System.out.println("test1");
    }

    void test2() throws IllegalArgumentException {
        System.out.println("test2");
        throw new IllegalArgumentException("只是用来测试异常");
    }

}

为了便于测试,给 test2() 方法添加了一个异常。在 Java 反射中,一个 Method 执行时遭遇的异常会被包装在一个特定的异常中,这个异常就是 InvocationTargetException。

try {
    Method method2 = clz.getDeclaredMethod("test2");

    method2.invoke(clz.newInstance());
} catch (NoSuchMethodException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (SecurityException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (IllegalAccessException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (IllegalArgumentException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (InvocationTargetException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
} catch (InstantiationException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}

它会出现异常。

test2
java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at com.wwj.test.method.MethodDetailTest.main(MethodDetailTest.java:63)
Caused by: java.lang.IllegalArgumentException: 只是用来测试异常
    at com.wwj.test.method.TestMethod.test2(TestMethod.java:15)
    ... 5 more

我们如果需要在代码中手动获取被包装在 InvocationTargetException 中的异常,就要通过它的方法手动获取。

catch (InvocationTargetException e) {
    // TODO Auto-generated catch block
    //e.printStackTrace();
    Throwable cause = e.getCause();
    System.out.println(cause.toString());

} 

通过调用 InvocationTargetException 对象的 getCause() 方法,会得到 Throwable 对象,原始的异常就包含在里面。打印结果如下:

java.lang.IllegalArgumentException: 只是用来测试异常

总结

反射给力的地方,在于它可以绕过一定的安全机制,比如操纵 private 修饰的 Field、Method、Constructor。并且它还有能力改变 final 属性的 Field。

但是反射头痛的地方就是它有太多的 Exception 需要处理。

总之,还是那句话:反射有风险,编码需谨慎。你能看到这里很棒!

上一篇 下一篇

猜你喜欢

热点阅读