Java反射机制
什么是Java反射?
Java反射机制是指在运行状态中,对于任意一个类,都能够知道这个类所有的属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能称为Java的反射机制。
反射就是把Java类中的各种成分映射成一个个的Java对象。
在Java中,每个类都对应一个Class对象,其反射机制是通过获取该类的Class对象,然后获取到其类的成员方法(Methods
)、成员变量(Fields
)、构造方法(Constructors
)等信息,同时可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等来体现的。
反射机制
获取Class对象
通过Class类中的静态方法
forName()
1
Class.forName("全类名") // 包名.类名
通过任何数据类型都有的class属性
1
类名.class属性
通过Object类的
getClass()
方法1
对象.getClass()方法
例子:获取Person类对应的Class对象
1 | import domain.Person; |
获取内部类的Classs对象
Java的普通类 C1 中支持编写内部类 C2 ,而在编译的时候,会生成两个文件: C1.class
和 C1$C2.class
,可以把他们看作两个无关的类。即Class.forName("C1$C2")
可以获取到该内部类的Class对象。
类的初始化
默认情况下
Class.forName(className, true, currentLoader)
第二个参数为true表示在获取Class对象时,对类进行初始化。
那么初始化时,其调用的是类的什么方法呢?
1 | package domain; |
获取Class对象时
1 | import domain.Person; |
实例化时
1 | import domain.Person; |
可以看到类的初始化是调用的静态初始块,实例化时会先调用静态初始快、初始块最后是构造函数。
补充:具有父类的类的实例化:父类静态初始块
->子类静态初始块
->父类初始块
->父类构造函数
->子类初始块
->子类构造函数
具有父类的类的初始化:父类静态初始块
->子类静态初始块
利用点:
- 如果程序
Class.forName(name)
中,name
参数可控,那么我们可以编写⼀个恶意类,将恶意代码放置在 static {} 中,从⽽执⾏恶意代码。 - 如果程序中某个类的static代码块可控,那么我们也可以对其修改然后通过
Class.forName(name)
调用。
获取成员变量
Fields[] getFields()
:只能获取所有public修饰的成员变量Fields getField(String name)
:获取特定成员变量Fields[] getDeclaredFields()
:获取所有的成员变量,【不考虑】修饰符Fields getDeclaredField(String name)
:获取特定的成员变量,【不考虑】修饰符
获取构造方法
Constructor<?>[] getConstructors()
: 只能获取所有public修饰的构造方法Constructor getConstructor(类 … parameterTypes)
: 获取特定构造方法Constructor<?> getDeclaredConstructors()
: 获取所有构造方法,【不考虑】修饰符Constructor getDeclaredConstructor(类 … parameterTypes``)
: 获取特定的构造方法,【不考虑】修饰符
获取成员方法
Method[] getMethods()
: 获取所有【public】修饰的方法,父类Object的方法也能看到Method getMethod(String name,类 <?> … parameterTypes)
: 获取特定成员方法Method[] getDeclaredMethods()
: 获取所有声明方法 不考虑修饰符Method getDeclaredMethod(String name,类 <?> … parameterTypes)
:获取特定成员方法,【不考虑】修饰符
创建类实例
1 | Class对象.newInstance() |
执行该方法时会调用该类的==公有无参构造方法==对该类进行实例化。
调用类方法
假设获取到的方法对象为mt
1 | mt.invoke(方法实例对象, 方法参数值,多个参数值用","隔开) |
- 第一个参数必须是类实例对象,如果调用的是
static
方法那么第一个参数值可以传null
或者类名,因为在java中调用静态方法是不需要有类实例的,因为可以直接类名.方法名(参数)
的方式调用。 - 第二个参数不是必须的,如果当前调用的方法没有参数,那么第二个参数可以不传,如果有参数那么就必须严格的
依次传入对应的参数类型
。
修改成员变量/方法权限
假设获取到的变量对象为field
1 | field.set(类实例对象, 修改后的值); |
同理也可以对方法进行访问权限修改,设方法对象为mt
1 | mt.setAccessible(true) |
反射的运用
一般运用
如下一个类
1 | package domain; |
正常实例化并调用其方法
1 | import domain.Person; |
运用反射
1 | import java.lang.reflect.Method; |
其可以简化为
1 | Class clazz = Class.forName("domain.Person"); |
单例模式下
上面写过,Class对象.newInstance()
会调用该类的公有无参构造方法对该类进行实例化,那么如果该类是”单例模式”呢?
单例模式,构造函数为私有,只能通过静态方法去获取该对象。
1 | public class SingleObject { |
实例化对象
1 | public class SingletonPatternDemo { |
此时就不能用上面哪种方法去实例化对象了,但同时也给出了另一种实例化对象的方式,调用其静态方法。
看一个真实的例子,java.lang.Runtime
因为有一个exec
方法可以执行本地命令,所以在很多的payload
中我们都能看到反射调用Runtime
类来执行本地系统命令,但恰好它也是个单例类。
那么如果我要利用它构造payload执行命令,应当进行如下构造
1 | public class Demo { |
指定构造方法实例化
那假设它不是单例模式,而且也没有无参构造方法呢?
此时我们就要指定构造方法对其进行实例化,从而调用其方法,这里以另一个常用来执行命令的ProcessBuilder
类为例。
此类用于创建操作系统进程,它提供一种启动和管理进程(也就是应用程序)的方法。它有两个构造方法,但都是有参构造
1 | public ProcessBuilder(List<String> command) |
可以看下它的一般用法
1 | //利用指定的操作系统程序和参数构造一个进程生成器 |
那么如何利用反射进行调用呢?
我们可以指定构造方法去进行实例化,然后调用start()方法执行传入的命令(程序)。
1 | import java.util.Arrays; |
上面我们是用的第一个构造参数,那么这次用第二个。
可以注意到的是,该构造函数的参数类型应为可变长参数,即同类型数组,在这里是字符串数组。
而且这里需要再提一下,newInstance
方法的参数为Object数组。这意味着我们向newInstance
传参时应当传入一个二维数组。
1 | public class Demo { |
执行私有方法
这里考虑最后一种情况,构造方法为私有的情况,这次依然以Runtime
类为例。
1 | import java.lang.reflect.Constructor; |