点击上方“凌天实验室”,“星标或置顶公众号”
漏洞、技术还是其他,我都想第一时间和你分享
本章为新手向零基础 Java 反射学习笔记。
截取部分本实验室发起的项目javaweb-sec。
Gitbook地址:https://javasec.org/
反射的基本定义
官方文档:
https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/index.html
Reflection(反射)赋予了 Java 代码从已加载的类中发现成员变量(Fields)、成员方法(Methods)和构造方法(Constructors)的能力,并可以在安全限制内使用这些反射得到的成员变量、成员方法、构造方法来操作他们对应的底层对象。
简而言之,你可以在运行状态中通过反射机制做到:
对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性。
这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
不得不说,反射是一种十分具有 "Hacker" 精神的机制。
反射的基础
从源代码到程序运行,大致经历的步骤如下:
创建源文件后,程序会被编译器编译为后缀名为 .class
的文件;
通过类加载器系统将字节码.class
加载入 JVM 的内存中,类的加载是通过 ClassLoader 及其子类来完成的;在加载阶段,虚拟机要完成三件事情:通过一个类的全限定名来获取其定义的二进制字节流,将这个字节流所代表的的静态储存结构转化为方法区的运行时数据结构,在 Java 堆中生成一个代表这个类的 java.lang.Class
对象,作为对方法区中这些数据的访问入口。在加载的过程中将使用双亲委派模型进行类的加载;
对字节码进行验证;
解析类、接口、字段,是虚拟机将常量池中的符号引用转化为直接引用的过程;
类初始化,这里需要注意的是,在使用 Java.lang.reflect
包的方法对类进行反射调用时,如果类还没有进行过初始化,则需要先触发其初始化;
执行。
可以看到,在加载的过程中,当一个 class 被加载,或当加载器(classloader)的 defineClass()
被 JVM 调用时,JVM 将自动产生一个 Class 对象,并且这个对象会保存在同名的 .class
文件里,当我们 new 一个新对象或者引用一个静态成员变量时,JVM 中的加载器系统会将对应的 Class 对象加载到 JVM 中,然后 JVM 再根据这个类型信息相关的 Class 对象创建我们需要实例对象或者提供静态变量的引用值。
因此,这些在程序加载过程中产生的类的 Class 对象就是反射的基础。
Class 类位于包 java.lang
下:
public final class Class<T> implements java.io.Serializable,GenericDeclaration,Type,AnnotatedElement
可以看到,这是个 final 类,实现了 4 个接口。
这个类十分特殊,它同样的继承自 Object ,它的实例能用来表达 Java 程序运行时的 classes 和 interfaces,也能用来表达 enum、array、Java 基础类型(boolean、byte、char、short、int、long、double、float)以及关键词 void。
Class 没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机以及通过调用类加载器中的 defineClass
方法自动构造的。
获取 Class 对象的方法
通过上面的了解,我们知道,如果想使用反射,必须得获得 Class 对象。
除了 java.lang.reflect.ReflectPermission
以外,java.lang.reflect
中的其他类都没有 public 的构造函数,也就是说要得到这些类,我们必须通过 Class 。
下面列举了能够获取 Class 对象的方法:
第一种方法是通过类的实例来获取对应的 Class 对象。
byte[] bytes = new byte[1024];
Class<?> c = bytes.getClass();
但是对于基础数据类型不能使用这种方式。
通过类的类型获取 Class 对象,基本类型同样可以使用这种方法。
Class<?> c = boolean.class;
Class<?> c = String.class;
通过类的全限定名获取Class对象, 基本类型无法使用此方法。
try {
Class<?> c = Class.forName("java.lang.String");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
对于数组比较特殊:
Class<?> doubleArray = Class.forName("[D"); //相当于double[].class
Class<?> cStringArray = Class.forName("[[Ljava.lang.String;"); //相当于String[][].class
如果在调用Class.forName()
方法时,没有在编译路径下(classpath)找到对应的类,那么将会抛出ClassNotFoundException
。
基本类型和 void 类型的包装类可以使用 TYPE 字段获取。
Class c = Double.TYPE; //等价于 double.class
Class c = Void.TYPE;
另外还有一些反射方法可以获取 Class 对象,但前提是你已经获取了一个 Class 对象。
比如说你已经获取了一个类的 Class 对象,就可以通过Class.getSuperclass()
方法获取这个类的父类的 Class 对象。
Class<?> c = javax.swing.JButton.class.getSuperclass();
类似能够返回 Class 对象的方法还有:
Class.getClasses()
Class.getDeclaredClasses()
Class.getDeclaringClass()
Class.getEnclosingClass()
java.lang.reflect.Field.getDeclaringClass()
java.lang.reflect.Method.getDeclaringClass()
java.lang.reflect.Constructor.getDeclaringClass()
还有部分类能够返回 Class 对象,但基本上都是调用 Class.forName()
,此处不再列举。
从 Class 中获取信息
在获取了 Class 对象后,就可以通过 Class 类提供的方法来获取其中的信息和进行操作了。
获取内容 | 方法签名 |
---|---|
类名 | String getName() |
类名简称 | String getSimpleName() |
规范化类名 | String getCanonicalName() |
类加载器 | ClassLoader getClassLoader() |
泛型的参数类型 | TypeVariable<Class<T>>[] getTypeParameters() |
直接继承的父类 | Class<? super T> getSuperclass() |
直接继承的父类(包含泛型信息) | Type getGenericSuperclass() |
包含的方法 | Method getMethod(String name, Class<?>... parameterTypes) |
内部类 | Class<?>[] getDeclaredClasses() |
构造器 | Constructor<T> getConstructor(Class<?>... parameterTypes) |
包含的属性 | Field getField(String name) |
修饰符 | int getModifiers() |
类的标记 | Object[] getSigners() |
数组的 Class 对象 | Class<?> getComponentType() |
所在包 | Package getPackage() |
所实现的接口 | Class<?>[] getInterfaces() |
所实现的接口(包含泛型信息) | Type[] getGenericInterfaces() |
包含的Annotation | <A extends Annotation> A getAnnotation(Class<A> annotationClass) |
在 Class 类中,可以看到类似 getEnclosing*、getDeclared* 与 getDeclaringClass,可以理解为在不同”域“中获取信息,由于篇幅的原因不再进行罗列:
get*:返回当前类和继承层次中的所有父类的成员
getEnclosing*:返回内部或匿名的封闭成员
getDeclared*:返回当前类中的成员(不包含父类)
getDeclaringClass:返回当前类声明所在的类
Class 类提供了一些判断类本身信息的方法,通常命名为 isXxxxx ,返回类型均为 boolean。
判断内容 | 方法签名 |
---|---|
是否为注解类型 | boolean isAnnotation() |
是否使用了该Annotation修饰 | boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) |
是否为匿名类 | boolean isAnonymousClass() |
是否为数组类型 | boolean isArray() |
是否为枚举类型 | boolean isEnum() |
判断两个类的是否关联 | boolean isAssignableFrom() |
是否为接口 | boolean isInterface() |
obj 是否是该 Class 的实例 | boolean isInstance(Object obj) |
该类是否为局部类 | boolean isLocalClass() |
该类是否为成员类 | boolean isMemberClass() |
是否为基础类型 | boolean isPrimitive() |
是否由Java编译器引入 | boolean isSynthetic() |
Class 类提供了四个 public 方法,用于获取某个类的构造方法。
方法 | 描述 |
---|---|
Constructor getConstructor(Class[] params) | 根据构造函数的参数,返回一个具体的具有public属性的构造函数 |
Constructor getConstructors() | 返回所有具有public属性的构造函数数组 |
Constructor getDeclaredConstructor(Class[] params) | 根据构造函数的参数,返回一个具体的构造函数(不分public和非public属性) |
Constructor getDeclaredConstructors() | 返回该类中所有的构造函数数组(不分public和非public属性) |
如果想要反射出无参数的构造方法,可以直接使用 newInstanse()
方法创建新实例,因为该方法的本质即调用类的无参数构造方法。
方法 | 描述 |
---|---|
Method getMethod(String name, Class[] params) | 根据方法名和参数,返回一个具体的具有public属性的方法 |
Method[] getMethods() | 返回所有具有public属性的方法数组 |
Method getDeclaredMethod(String name, Class[] params) | 根据方法名和参数,返回一个具体的方法(不分public和非public属性) |
Method[] getDeclaredMethods() | 返回该类中的所有的方法数组(不分public和非public属性) |
方法 | 描述 |
---|---|
Field getField(String name) | 根据变量名,返回一个具体的具有public属性的成员变量 |
Field[] getFields() | 返回具有public属性的成员变量的数组 |
| 根据变量名,返回一个成员变量(不分public和非public属性) |
Field[] getDelcaredFields() | 返回所有成员变量组成的数组(不分public和非public属性) |
上面列举的部分方法中还存在部分方法的重载方法,不再赘述,按需使用。
反射操作
能够反射得到类、属性、方法之后,应该如何操作呢?
首先我们先看反射的包 java.lang.reflect
下的 Member 接口,顾名思义,这是一个标识为成员的接口,这个接口有若干个实现:
其中我们首先关注的是:
java.lang.reflect.Field
:对应类变量。
java.lang.reflect.Method
:对应类方法。
java.lang.reflect.Constructor
:对应类构造函数。
反射就是通过这三个类才能在运行时改变对象状态。
而 Class 对象的 getXXX()
方法返回的成员方法,成员属性,构造方法就是 reflect 包中相对应的类。
其中 Method 类和 Constructor 类继承自 Executable 类,Executable 类实现了 Member 接口,而 Field 类则直接实现了Member 接口。
从 JDK 1.8 开始,java.lang.reflect.Executable.getParameters
为我们提供了获取普通方法或者构造方法的名称的能力,因此 Method 类和 Constructor 类也具有这个能力。
每个成员变量有类型和值。java.lang.reflect.Field
为我们提供了获取当前对象的成员变量的类型,和重新设值的方法。
具体方法如下:
获取变量的类型:
方法 | 描述 |
---|---|
Class<?> getType() | 返回这个变量的类型 |
Type getGenericType() | 如果当前属性有签名属性就返回,否则返回getType() |
这里再复习一下类型,类中的变量分为两种类型:基本类型和引用类型:
基本类型( 8 种)
整数:byte, short, int, long
浮点数:float, double
字符:char
布尔值:boolean
引用类型
类,枚举,数组,接口都是引用类型
java.io.Serializable 接口,基本类型的包装类(比如 java.lang.Double
)也是引用类型
获取和修改成员变量的值:
拿到一个对象后,我们可以在运行时修改它的成员变量的值,对运行时来说,反射修改变量值的操作和类中修改变量的结果是一样的。
基本类型的获取方法:
方法 | 描述 |
---|---|
byte getByte(Object obj) | 获取一个静态或实例 byte 字段的值 |
int getInt(Object obj) | 获取 int 类型或另一个通过扩展转换可以转换为 int 类型的基本类型的静态或实例字段的值 |
short getShort(Object obj) | 获取 short 类型或另一个通过扩展转换可以转换为 short 类型的基本类型的静态或实例字段的值 |
long getLong(Object obj) | 获取 long 类型或另一个通过扩展转换可以转换为 long 类型的基本类型的静态或实例字段的值 |
float getFloat(Object obj) | 获取 float 类型或另一个通过扩展转换可以转换为 float 类型的基本类型的静态或实例字段的值 |
double getDouble(Object obj) | 获取 double 类型或另一个通过扩展转换可以转换为 double 类型的基本类型的静态或实例字段的值 |
boolean getBoolean(Object obj) | 获取一个静态或实例 boolean 字段的值 |
char getChar(Object obj) | 获取 char 类型或另一个通过扩展转换可以转换为 char 类型的基本类型的静态或实例字段的值 |
基本类型的设置方法:
方法 | 描述 |
---|---|
void setByte(Object obj, byte b) | 将字段的值设置为指定对象上的一个 byte 值 |
void setShort(Object obj, short s) | 将字段的值设置为指定对象上的一个 short 值 |
void setInt(Object obj, int i) | 将字段的值设置为指定对象上的一个 int 值 |
void setLong(Object obj, long l) | 将字段的值设置为指定对象上的一个 long 值 |
void setFloat(Object obj, float f) | 将字段的值设置为指定对象上的一个 float 值 |
void setDouble(Object obj, double d) | 将字段的值设置为指定对象上的一个 double 值 |
void setBoolean(Object obj, boolean z) | 将字段的值设置为指定对象上的一个 boolean 值 |
void setChar(Object obj, char c) | 将字段的值设置为指定对象上的一个 char 值 |
引用类型的获取和设置方法:
方法 | 描述 |
---|---|
Object get(Object obj) | 返回指定对象上此 Field 表示的字段的值 |
void set(Object obj, Object value) | 将指定对象变量上此 Field 对象表示的字段设置为指定的新值 |
继承的方法(包括重载、重写和隐藏的)会被编译器强制执行,这些方法都无法反射。因此,反射一个类的方法时不考虑父类的方法,只考虑当前类的方法。
每个方法都由修饰符、返回值、参数、注解和抛出的异常组成。
java.lang.reflect.Method
方法为我们提供了获取上述部分的 API。
重写 Object 类的方法不再描述,重写 Executable 的方法将在后面部分描述,在此只列举一些 Method 类自己的方法。
方法 | 描述 |
---|---|
Object getDefaultValue() | 返回由此方法实例表示的注释成员的默认值 |
Type getGenericReturnType() | 获取目标方法返回类型对应的 Type 对象 |
Class<?> getReturnType() | 获取目标方法返回类型对应的 Class 对象 |
boolean isBridge() | 判断是否是桥接方法 |
boolean isDefault() | 如果此方法是默认方法,则返回 true ; 否则返回 false |
Object invoke(Object obj, Object... args) | 使用反射执行方法 |
首先来看一下 getReturnType()
和 getGenericReturnType()
的异同。
getReturnType()
返回类型为 Class,getGenericReturnType()
返回类型为 Type ; Class 实现 Type 。
返回值为普通简单类型如 Object , int , String 等,getGenericReturnType()
返回值和getReturnType()
一样。
例如 public String function1()
那么各自返回值为:getReturnType() : class java.lang.String
getGenericReturnType() : class java.lang.String
返回值为泛型
例如public T function2()
那么各自返回值为:getReturnType() : class java.lang.Object
getGenericReturnType() : T
返回值为参数化类型
例如public Class function3()
那么各自返回值为:getReturnType() : class java.lang.Class
getGenericReturnType() : java.lang.Class
其实反射中所有形如getGenericXXX()
的方法规则都与上面所述类似。
然后就是非常重要的invoke()
方法,
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
第一个 Object 参数代表的是对应的 Class 对象实例,第二个参数是可变形参,能够接受多个参数。
这里需要注意的是,invoke 方法的两个参数均为 Object 类型。
简单来说,通过 invoke()
方法可以让我们调用反射得到的类的方法。那么具体是怎么实现的呢?
invoke过程图解:
此处不进行过多描述,有兴趣可以跟一下源码。
同样地,我们也只列举 Constructor 自己的 public 方法:
方法 | 描述 |
---|---|
T newInstance(Object ... initargs) | 调用构造方法创建新实例 |
方法使用此 Constructor 对象表示的构造函数,使用指定的初始化参数来创建和初始化构造函数的声明类的新实例。个别参数自动展开以匹配原始形式参数,原始参考参数和参考参数都需要进行方法调用转换。
通过 Class 对象也可以创建新实例,但是两者的异同在于:
Class.newInstance()
只能反射无参数的构造器,也就是使用无参数构造方法创建新实例,而 Constructor.newInstance()
可以反射任何构造器;
Class.newInstance()
需要构造器可见(visible),Constructor.newInstance()
可以反射私有构造器;
Class.newInstance()
对于捕获或者未捕获的异常均由构造器抛出,Constructor.newInstance()
通常会把抛出的异常封装成InvocationTargetException
抛出;
因此,还是建议直接使用 Constructor.newInstance()
反射构造方法,几乎全部的框架都是使用此种模式进行反射的。
Executable 抽象类继承 AccessibleObject 类,实现了 Member 接口,并有两个子类分别为 Constructor 和 Method,如下:
该类中定义了多个方法,能够在通过反射获得的成员方法或构造方法中使用:
该抽象类声明的方法有:
方法 | 描述 |
---|---|
Class<?>[] getParameterTypes() | 按照声明顺序返回 Class 对象的数组,这些对象描述了此 Method/Constructor 对象所表示的方法的形参类型。 |
Class<?>[] getExceptionTypes() | 返回 Class 对象的数组,这些对象描述了声明将此 Method/Constructor 对象表示的底层方法抛出的异常类型。 |
Type[] getGenericParameterTypes() | 按照声明顺序返回 Type 对象的数组,这些对象描述了此 Method/Constructor 对象所表示的形参类型的。 |
Type[] getGenericExceptionTypes() | 返回 Type 对象数组,这些对象描述了声明由此 Method/Constructor 对象抛出的异常的类型。 |
String toGenericString() | 返回描述此 Method/Constructor 的字符串,包括类型参数。 |
Annotation[][] getParameterAnnotations() | 返回表示按照声明顺序对此 Method/Constructor 对象所表示方法的形参进行注释的那个数组的数组。 |
AnnotatedType getAnnotatedReturnType() | 返回一个AnnotatedType对象,该对象表示使用一个类型来指定由该可执行文件表示的方法/构造函数的返回类型 |
Type[] getGenericExceptionTypes() | 返回一个AnnotatedType对象数组,这些对象表示使用类型来指定由该可执行文件表示的方法/构造函数声明的异常 |
AnnotatedType getAnnotatedReceiverType() | 返回一个AnnotatedType对象,该对象表示使用一个类型来指定该可执行对象表示的方法/构造函数的接收者类型 |
AnnotatedType[] getAnnotatedParameterTypes() | 返回一个AnnotatedType对象数组,这些对象表示使用类型来指定由该可执行文件表示的方法/构造函数的形式参数类型 |
int getParameterCount() | 获取参数的个数(无论是显式声明的还是隐式声明的或不声明的 |
Parameter[] getParameters() | 返回一个参数对象数组,该数组表示该方法对象的所有参数 |
boolean isVarArgs() | 是否是可变参数 |
在 java.lang.reflect
包中,存在一个 AccessibleObject 类,细心的同学已经发现了,该类是Method、Field、Constructor 类的基类,它提供了标记反射对象的能力,以抑制在使用时使用默认 Java 语言访问控制检查,从而能够任意调用被私有化保护的方法、域和构造函数。
此类实现了 AnnotatedElement 接口,主要是注解相关的操作。
声明的方法:
方法 | 说明 |
---|---|
boolean isAccessible() | 获取此对象的accessible标志的值(布尔类型) |
void setAccessible(AccessibleObject[] array, boolean flag) | 使用单一安全检查来设置对象数组的可访问标志的一个方便的方法(为了效率),静态方法 |
void setAccessible(boolean flag) | 将对象的可访问标志设置为指示的布尔值 |
通过调用 setAccessible()
方法会关闭反射访问检查,这行代码执行之后不论是私有的、受保护的以及包访问的作用域,你都可以在任何地方访问,即使你不在他的访问权限作用域之内。
当isAccessible()
的结果是 false 时不允许通过反射访问该字段;
当该字段时 private 修饰时isAccessible()
得到的值是 false ,必须要改成 true 才可以访问;
所以 f.setAccessible(true)
得作用就是让我们在用反射时访问私有变量。
所以你其实并不是把 private 方法改成了 public ,也没有更改任何的权限,你只是关闭了反射访问的检查,这种情况下,其实可以导致一些效率上的提升,与此同时带来的就是安全性的下降。
此接口位于包 java.lang.reflect
下。
这个接口的对象代表了在当前 JVM 中的一个“被注解元素”。在 Java 语言中,所有实现了这个接口的“元素”都是可以“被注解的元素”。
使用这个接口中声明的方法可以读取(通过 Java 的反射机制)“被注解元素”的注解。
这个接口中的所有方法返回的注解都是不可变的、并且都是可序列化的。这个接口中所有方法返回的数组可以被调用者修改,而不会影响其返回给其他调用者的数组。
此类具有以下的实现类:
AccessibleObject(可访问对象,如:方法、构造器、属性等)
Class
Constructor
Executable(可执行的,如构造器和方法)
Field(属性,类中属性的类型)
Method(方法,类中方法的类型)
Package(包)
Parameter(参数,主要指方法或函数的参数,其实是这些参数的类型)
接口声明的方法有:
方法 | 描述 |
---|---|
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) | 如果指定类型的注解出现在当前元素上,则返回 true ,否则将返回false。这种方法主要是为了方便地访问一些已知的注解。 |
<T extends Annotation> T getAnnotation(Class<T> annotationClass) | 如果在当前元素上存在参数所指定类型(annotationClass)的注解,则返回对应的注解,否则将返回 null 。 |
Annotation[] getAnnotations() | 返回在这个元素上的所有注解。如果该元素没有注释,则返回值是长度为0的数组。该方法的调用者可以自由地修改返回的数组;它不会对返回给其他调用者的数组产生影响。 |
<T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) | 返回与该元素相关联的注解。如果没有与此元素相关联的注解,则返回值是长度为0的数组。这个方法与getAnnotation(Class) 的区别在于,该方法检测其参数是否为可重复的注解类型( JLS 9.6 ),如果是,则尝试通过“ looking through ”容器注解来查找该类型的一个或多个注解。该方法的调用者可以自由地修改返回的数组;它不会对返回给其他调用者的数组产生影响。 |
<T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) | 如果参数中所指定类型的注解是直接存在于当前元素上的,则返回对应的注解,否则将返回null。这个方法忽略了继承的注解。(如果没有直接在此元素上显示注释,则返回null。) |
<T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) | 如果参数中所指定类型的注解是直接存在或间接存在于当前元素上的,则返回对应的注解。这种方法忽略了继承的注解。如果没有直接或间接地存在于此元素上的指定注解,则返回值是长度为0的数组。这个方法和getDeclaredAnnotation(Class) 的区别在于,这个方法检测它的参数是否为可重复的注释类型( JLS 9.6 ),如果是,则尝试通过“ looking through ”容器注解来查找该类型的一个或多个注解。该方法的调用者可以自由地修改返回的数组;它不会对返回给其他调用者的数组产生影响。 |
Annotation[] getDeclaredAnnotations() | 返回直接出现在这个元素上的注解。这种方法忽略了继承的注解。如果在此元素上没有直接存在的注解,则返回值是长度为0的数组。该方法的调用者可以自由地修改返回的数组;它不会对返回给其他调用者的数组产生影响。 |
Member 接口标识类或接口的所有公共成员的集合,包括继承的成员。Constructor / Filed / Method 类都直接或间接的继承此类。
Member 接口中定义的方法为:
方法 | 描述 |
---|---|
Class<?> getDeclaringClass() | 返回表示该类或接口的 Class 对象 |
String getName() | 返回此成员表示的基础成员或构造函数的简单名称 |
int getModifiers() | 以整数形式返回此成员表示的成员或构造函数的 Java 语言修饰符 |
boolean isSynthetic() | 是否由 Java 编译器引入 |
通过以上知识的学习,可以知道:
AnnotatedElement 接口提供获取注解相关能力。
Member 接口提供通用成员属性获取能力。
GenericDeclaration 接口提供给 Class 获取泛型类型的能力。
Executable 抽象类提供获取可执行对象相关信息的能力。
AccessibleObject 抽象类提供判断可更改可访问标识的能力。
Constructor、Method、Field各自基础或实现上述接口和抽象类,赋予了他们获取相关信息的能力。
以上类或接口之间的关系为:
9. 数组和枚举
数组和枚举也是对象,但是在反射中,对数组和枚举的创建、访问和普通对象有些不同,所以 Java 反射为数组和枚举提供了一些特定的API接口。
数组类型
数组类型:数组本质是一个对象,所以它也有自己的类型。例如对于int[] intArray
,数组类型为class [I
。数组类型中的[
个数代表数组的维度,例如[
代表一维数组,[[
代表二维数组;[
后面的字母代表数组元素类型,I
代表int
,一般为类型的首字母大写( long 类型例外,为 J )。
class [B //byte类型一维数组
class [S //short类型一维数组
class [I //int类型一维数组
class [C //char类型一维数组
class [J //long类型一维数组,J代表long类型,因为L被引用对象类型占用了
class [F //float类型一维数组
class [D //double类型一维数组
class [Lcom.dada.Season //引用类型一维数组
class [[Ljava.lang.String //引用类型二维数组
//获取一个变量的类型
Class<?> c = field.getType();
//判断该变量是否为数组
if (c.isArray()) {
//获取数组的元素类型
c.getComponentType()
}
创建和初始化数组
Java反射为我们提供了java.lang.reflect.Array
类用来创建和初始化数组。
//创建数组, 参数componentType为数组元素的类型,后面不定项参数的个数代表数组的维度,参数值为数组长度
Array.newInstance(Class<?> componentType, int... dimensions)//设置数组值,array为数组对象,index为数组的下标,value为需要设置的值
Array.set(Object array, int index, int value)
//获取数组的值,array为数组对象,index为数组的下标
Array.get(Object array, int index)
例子,用反射创建 int[] array = new int[]{1, 2}
Object array = Array.newInstance(int.class, 2);
Array.setInt(array , 0, 1);
Array.setInt(array , 1, 2);
注意:反射支持对数据自动加宽,但不允许数据
narrowing
(变窄?真难翻译)。意思是对于上述set方法,你可以在int类型数组中 set short类型数据,但不可以set long类型数据,否则会报IllegalArgumentException
。
多维数组
Java反射没有提供能够直接访问多维数组元素的API,但你可以把多维数组当成数组的数组处理。
Object matrix = Array.newInstance(int.class, 2, 2);
Object row0 = Array.get(matrix, 0);
Object row1 = Array.get(matrix, 1);Array.setInt(row0, 0, 1);
Array.setInt(row0, 1, 2);
Array.setInt(row1, 0, 3);
Array.setInt(row1, 1, 4);
或者
Object matrix = Array.newInstance(int.class, 2);
Object row0 = Array.newInstance(int.class, 2);
Object row1 = Array.newInstance(int.class, 2);Array.setInt(row0, 0, 1);
Array.setInt(row0, 1, 2);
Array.setInt(row1, 0, 3);
Array.setInt(row1, 1, 4);
Array.set(matrix, 0, row0);
Array.set(matrix, 1, row1);
枚举隐式继承自java.lang.Enum
,Enum继承自Object,所以枚举本质也是一个类,也可以有成员变量,构造方法,方法等;对于普通类所能使用的反射方法,枚举都能使用;另外java反射额外提供了几个方法为枚举服务。
方法 | 描述 |
---|---|
Class.isEnum() | 此类是否为枚举类型 |
Class.getEnumConstants() | 返回按照声明顺序索引由枚举类型定义的枚举常量的列表 |
java.lang.reflect.Field.isEnumConstant() | 此字段是否表示枚举类型的元素 |
造成这种异常的可能有:
方法本身不存在
传入的参数类型不匹配(也可能是泛型擦除导致)
传入的参数个数不匹配
反射调用泛型方法时,由于运行前编译器已经把泛型擦除,参数类型会被擦除为上边界(默认 Object)。
此时再试图传入特定类型的参数会导致NoSuchMethodException
异常。
解决的方式是使用 Object 作为参数类型即可。
当你访问 private 的方法或者 private 的类中的方法,会抛出IllegalAccessException
异常。
也有可能是试图操作 final 修饰的 Field 导致。
解决方法就是给该 method 设置 setAccessible(true)
。
如果一个方法没有参数,但是我们反射时传入参数,就会导致 llegalArgumentException
。
被调用的方法本身所抛出的异常在反射中都会以 InvocationTargetException
抛出。
换句话说,反射调用过程中如果异常 InvocationTargetException
抛出,说明反射调用本身是成功的,因为这个异常是目标方法本身所抛出的异常。
通过调用 InvocationTargetException
对象的 getCause()
方法,会得到 Throwable 对象,原始的异常就包含在里面。
Class.forName()
传入的包名有误,或 Class 本身不存在。将会引发此异常。
Field 名称不正确,或对 getField()
和 getDeclaredField()
的使用不正确将会引发此异常。
实例化出错,可能是 Class.newInstance()
或者 Constructor.newInstance()
出现了异常。
简单使用反射
我们了解的反射的基本情况,能够获取 Class 对象,又能从 Class 中获取相应的信息,记下来记录如何使用反射。
通过反射创建类对象主要有两种方式:通过 Class 对象的 newInstance()
方法,或通过 Constructor 对象的 newInstance()
方法。前面提到过, newInstanse()
方法就是调用无参数的构造方法。
第一种:Class 对象的 newInstance()
方法。
Class clz = Apple.class;
Apple apple = (Apple)clz.newInstance();
第二种:Constructor 对象的 newInstance()
方法。
Class clz = Apple.class;
Constructor constructor = clz.getConstructor();
Apple apple = (Apple)constructor.newInstance();
通过 Constructor 对象创建类对象可以选择特定构造方法,而通过 Class 对象则只能使用默认的无参数构造方法。下面的代码就调用了一个有参数的构造方法进行了类对象的初始化。
Class clz = Apple.class;
Constructor constructor = clz.getConstructor(String.class, int.class);
Apple apple = (Apple)constructor.newInstance("红富士", 15);
下面例子展示了使用 getDeclaredFields()
获取全部属性:
Class clz = Apple.class;
Field[] fields = clz.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName());
}
package org.su18;import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Reflection {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// Class<?> testClass = TestReflection.class;
Class<?> testClass = Class.forName("org.su18.TestReflection");
Method funTwoReflect = testClass.getDeclaredMethod("funTwo", String.class, String.class, String.class);
funTwoReflect.setAccessible(true);
funTwoReflect.invoke(null, "Kiki", "Pants", "Dance");
System.out.println("\n");
Method funOneReflect = testClass.getDeclaredMethod("funOne");
funOneReflect.invoke(null);
System.out.println("\n");
Method funThreeReflect = testClass.getDeclaredMethod("funThree", Integer[].class);
funThreeReflect.setAccessible(true);
Integer[] nums = {1,2,3,4,5,6,123,141};
int result = (int) funThreeReflect.invoke(new TestReflection(), (Object) nums);
System.out.println(result);
}
}
class TestReflection {
static void funOne() {
System.out.println("Let's dance");
}
private static void funTwo(String name, String clothes, String action) {
System.out.printf("The First Man Name Is %s .\n", name);
System.out.printf("He Wears Such Little %s .\n", clothes);
System.out.print("His Brother Was A Champion .\n");
System.out.printf("But %s Loves To %s .\n", name, action);
}
private int funThree(Integer... numbers) {
int sum = 0;
for (Integer number : numbers) {
sum += number;
}
return sum;
}
}
上例演示了在各种不同情况下调用的方式的不同结果如下:
4. 调用内部类
假设com.reflect.Outer
类,有一个内部类 inner 和静态内部类 StaticInner 。那么静态内部类的构造函数为Outer$StaticInner();
而普通内部类的构造函数为Outer$Inner(Outer outer)
,多了一个 final 的 Outer 类型属性,即Outer$Inner.this$0
,用于存储外部类的属性值,也就是说非static内部类保持了外部类的引用。
直接实例化内部类方法如下:
// 静态内部类
Outer.StaticInner sInner = new Outer.StaticInner();
// 非静态内部类
Outer.Inner inner = new Outer().new Inner();
内部类的类名使用采用 $ 符号,来连接外部类与内部类,格式为outer$Inner
。
String className = "com.reflect.Outer$Inner";
Class.forName(className);
除了格式了差异,关于内部类的属性和方法操作基本相似,下面以调用该静态类的静态方法为例:
public static Object invokeMethod(String methodName, Class[] argsType, Object... args) {
Class clazz = Class.forName("com.reflect.Outer$StaticInner");
Method method = clazz.getDeclaredMethod(methodName, argsType);
method.setAccessible(true);
return method.invoke(null, args);
反射的作用
有了如此强大的反射特性,我们可以进行:
为所欲为:理论上可以访问任意类任意方法任意成员变量,这一步能做的就很多,直接调用某些类的方法、在运行中获取某些类的某些成员变量、通过获取注解对web框架获取到类和地址的映射关系等等。
框架实现:大部分的框架基本都是基于反射实现的。
防护绕过:针对 RASP 技术防护的站,可以反射调用底层方法进行封装,来绕过一些拦截位置。
还有诸多作用等着大家发掘吧~
反射的缺点
性能开销
反射涉及类型动态解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
安全限制
使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。
内部曝光
由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用--代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
虽然大家都在说反射性能低,是因为无法通过 JIT 进行优化,但现在的 JDK 已经非常强悍了,并不至于因为性能开销大而无法使用强大的反射功能。
凌天实验室,是安百科技旗下针对应用安全领域进行攻防研究的专业技术团队,其核心成员来自原乌云创始团队及社区知名白帽子,团队专业性强、技术层次高且富有实战经验。实验室成立于2016年,发展至今团队成员已达35人,在应用安全领域深耕不辍,向网络安全行业顶尖水平攻防技术团队的方向夯实迈进。