Java反射
1.反射机制简述
1.1.引入
一般情况下,我们在使用一个类的时候必定知道它是什么类,作用是什么,所以正常使用类的方式为实例化一个类,然后便可以调用或访问类中的方法与属性。
eg:
public static void main(String[] args) {
HelloWorld helloWorld = new HelloWorld();
helloWorld.hello();
}
类HelloWorld:
public class HelloWorld{
public static void hello(){
System.out.println("hello");
}
}
而反射则是事先不知道要初始化的类是什么,无法使用new去为类创建实例时,使用JDK提供的反射API去实现反射创建并调用
public static void main(String[] args) {
// HelloWorld helloWorld = new HelloWorld();
// helloWorld.hello();
try{
String className = "HelloWorld";
Class c = Class.forName(className);
Constructor constructor = c.getConstructor();
Object o = constructor.newInstance();
Method m =c.getMethod("hello");
m.invoke(o);
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
上面这段代码和注释中的代码实际作用是一样的,只是思路完全不同,反射创建调用中我们是通过字符串才知道了要实例化的类名。
1.2.反射的概念
将一个类的各个部分:类、构造方法、方法、属性都映射成一个新的对象,这就是反射。
JAVA的反射机制使得在运行状态中,我们可以获取到任何一个类的所有属性和方法,对于任意一个对象,我们都能够调用它的所有方法和获取所有属性。
反射是Java非常重要的动态特性,也是各种Java框架底层实现的灵魂
1.3.反射优缺点
了解反射的优缺点之前,先了解一下动静态的区别:
- 静态编译:在编译时确定类型、绑定对象。
- 动态编译:运行时确定类型、绑定对象。动态编译最大限度地发挥了JAVA的灵活性,体现了多态这一特性的应用,减少了类之间的耦合。
反射正是利用了动态编译的特性来实现,因此优缺点也与动态特性息息相关
优点:
- 可以实现动态创建对象和编译,使得反射在实际应用中体现出极强的扩展性,如某个软件的某个功能需要更新,采用静态编译的话需要用户卸载软件再重新编译整个代码才能实现,而反射则可以不用卸载,在运行时去动态的创建地修改该功能即可
缺点
- 执行效率较低,由于反射涉及了动态类型解析,JVM无法对其代码做自动优化,所有反射的执行效率总是比直接操作来的更低,在经常使用或对性能要求较高的场景中应该避免使用反射。
- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行,否则反射的功能将会受到限制,而由于反射允许代码执行一些在正常情况下无法执行的操作(如获取、修改类的私有属性和方法),所以使用反射可能会带来一些内部的安全问题。
2.获取Class对象
通过上面的描述,与代码实例,我们不难发现,要通过反射去进行一个类的实例化与调用,首先要获取到该类的Class对象,而获取Class对象的方式主要有以下四种,我们以自己编写的HelloWorld类为例:
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(HelloWorld.class);//通过类名.class获取
System.out.println(Class.forName("HelloWorld"));//通过Class类的forName()方法获取
HelloWorld helloWorld = new HelloWorld();
System.out.println(helloWorld.getClass());//通过getClass()方法获取
System.out.println(ClassLoader.getSystemClassLoader().loadClass("HelloWorld"));//通过ClassLoader.getSystemClassLoader().loadClass()方法获取
}
运行结果为

ps:通过Class.forName()方法获取数组的Class对象时注意描述符:
Class<?> doubleArray = Class.forName("[D");
//相当于double[].class
Class<?> cStringArray = Class.forName("[[Ljava.lang.String;");
//相当于String[][].class
3.Java反射相关操作
我们通过上述方式获取到Class对象之后,就可以通过以下对象对获取到的对象进行创建,方法调用等操作
- Method:获取成员方法
- FIeld:获取成员变量
- Constructor:获取构造方法
- Object:创建实例
eg:java.lang.Runtime类中exec()方法可以执行本地命令,所以在很多Payload中我们可以看到调用了Runtime类的exec()方法去本地系统命令,我们通过Runtime这个类来理解反射的基本使用方法
首先我们直接正向调用Runtime.exec()方法
public static void main(String[] args) throws ClassNotFoundException, IOException {
BufferedReader bf = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec("whoami").getInputStream()));
System.out.println(bf.readLine());//输出命令执行结果
}
代码执行结果:

同样的,我们也可以通过反射去创建和调用Runtime实例,代码实现如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class TestReflect {
public static void main(String[] args) {
try{
Class c = Class.forName("java.lang.Runtime");//获取Class对象
Constructor constructor = c.getDeclaredConstructor();//获取Runtime的构造方法,因为该构造方法权限为private,故使用getDeclaredConstructor()
constructor.setAccessible(true);//设置访问权限为允许
Object o = constructor.newInstance();//创建一个Runtime实例
Method m = c.getMethod("exec",String.class);//获取exec方法
String cmd = "whoami";
Process process = (Process)m.invoke(o,cmd);
BufferedReader bf = new BufferedReader(new InputStreamReader(process.getInputStream()));
System.out.println(bf.readLine());
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行结果:

以上过程中:
- 先通过Class.forName()方法获取到java.lang.Runtime类的Class对象
- 随后我们获取Runtime类的构造方法,因为Runtime类的构造方法权限为private,直接使用getConstructor()方法无法获取,故通过getDeclaredConstructor()方法获取到权限为private的构造方法,随后通过setAccessible()方法设置参数为true取得该构造方法的访问权限
- 调用上一步得到的构造方法constructor的newInstance()方法创建一个Runtime实例
- 通过Class对象的getMethod()方法获取Runtime类的exec()方法
- 通过Method类的invoke()方法调用上一步获取到的exec()方法并输出执行结果
通过反射创建并调用类大致都为以上过程,当然在Runtime类中,实例化的步骤我们也可以通过Runtime类中的getRuntime方法去完成以此省略获取构造方法的过程
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) {
try{
Class c = Class.forName("java.lang.Runtime");
Method m = c.getMethod("getRuntime");
Method m1 = c.getMethod("exec", String.class);
Object o = m.invoke(m);
Process p = (Process) m1.invoke(o,"whoami");
BufferedReader bf = new BufferedReader(new InputStreamReader(p.getInputStream()));
System.out.println(bf.readLine());
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
接下来我们对每个步骤使用的类以及对应方法做一个详细解释:
3.1.反射创建类实例
我们知道,Java中任何一个类都有自己的构造方法,如果一个类代码中没有构造方法,则在编译时会为该类自动创建一个无参构造方法,而实例化类的过程实际上就是调用了类的构造方法。
还是以Runtime类为例:
publicclass Runtime {
/** Don't let anyone else instantiate this class */
private Runtime() {}
}
可以看到Runtime类的构造方法权限为private,而且注释中也表明了不希望其他人去实例化这个类。所以我们无法使用new去实例化一个Runtime类,但在上述代码中我们通过反射修改了这个构造方法的访问权限成功的创建了一个Runtime对象。
在反射过程我们获取类的构造方法时,一般会使用getConstructor() 方法或getDeclaredConstructor()方法,二者的区别在于前者只能获取到权限为public的构造方法,而后者可以获取到权限为public和private的构造方法,且可以通过setAccessible()方法传入参数true来修改构造方法的权限为public,以下为JavaSE官方文档中对Class类中两种方法的描述


故我们在获取构造方法时一般使用getDeclaredConstructor()方法,同时我们也可以使用getDeclaredConstructors()方法去获取到一个类的所有构造方法(一个类可以有多个重载构造方法)。
获取到构造方法后,我们便可以调用构造方法来为类创建一个实例了。
3.2.反射调用类方法
Class类中也包含了获取某个类的成员方法的方法


以上方法可以获取到某个类中指定或全部的方法,区别为前两个方法只能获取权限为public的方法,而后者可以类中所有方法(除了父类中的方法)
获取到类中的方法后我们就可以调用这些方法了,Method类中提供了invoke方法来调用特定对象中的Method对象指定的方法并传入特定参数。
eg:
Method m1 = c.getMethod("exec", String.class);
Object o = m.invoke(m);
Process p = (Process) m1.invoke(o,"whoami");

invoke()方法第一个参数必须传入一个类实例,如果Method指向的方法是一个static方法,则可以传入null,因为static方法不需要实例化即可调用,直接用类名.方法名()调用即可。invoke()后续的参数根据Method指向的方法所需的参数传入,若Method指向的方法无参也可以不传入。
3.3.反射获取类成员变量
Class类中也提供了获取成员变量的方法:


区别依然是前两种方法只能获取权限为public的变量,而后者可以获取除了父类中变量的全部变量。
获取到成员变量之后,我们甚至可以对成员变量进行修改,如:
若成员变量权限不为public,则可以通过Field类中的setAccessible()方法传入参数true修改权限,修改权限后可以使用Field类中的set()方法去修改变量的值,以下是上述两个方法的官方文档描述


4.总结
Java反射机制是Java动态特性的一大重要体现,同时也是大多数Java开发框架实现的底层机制,而对于安全领域来说,反射机制在编写漏洞利用代码、代码审计、绕过RASP方法限制等中起到了至关重要的作用