序言
看试手,补天裂。
今天来总结一下Java反射,在它面前,任何事物没有任何隐私。
什么是Java反射?
将类的各个组成部分封装为其他对象,这就是反射机制。
Java中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。
让Java可以在运行时,根据传入的类名字符串,去执行这个类存在的方法。
举个例子,Java代码在计算机中经历的三个阶段:
source源代码阶段:此时刚刚编译为字节码,仍然保存在硬盘上。
Class类对象阶段:类加载器把Person.class字节码加载进内存。
对于java来说,万物皆对象,那么这时内存里会有一个Person这个类的类对象,也就是Class类对象,这个东西就很抽象。
在Java里面有个类叫做Class,它是来描述所有字节码物理文件的一些共同特征和行为。例如成员变量,构造方法,普通的成员方法。
Class类对象会对重点内容进行封装成成员变量:
成员变量 ——-> Field对象,Field[] fields
构造方法 ——-> Constructor对象,Constructor[] cons
成员方法 ——-> Method对象,Method[] methods
运行时阶段:这个时候类对象已经实例化成为了一个对象
好处:
- 可以在程序运行过程中,操作对象。例如:获取,设置
- 可以降低程序耦合性,提高程序的扩展性
如何获取Class类对象?
字节码文件阶段
Class.forname(“全类名”)//包名.类名
将字节码文件加载进内存 返回Class类对象
内存阶段
类名.class
当class字节码加载进内存,那么这个Class类对象就有一个类名属性,可以用来索引
运行时阶段
对象.getClass()
所有对象的父类都是Object类,这个类有自己的getClass方法,所有对象都可以拿来用
注意⚠️
类名.class 最安全 性能最好
Class.forName() 属于动态加载类。将字节码文件加载进内存,参数需要类的全限定名
使用类名.class来创建Class对象的引用时,不会自动初始化该Class对象
使用Class.forName()会自动初始化该Class对象
Class.forName() 方法 当类加载进了内存,只有静态初始块得到了执行。
getDeclaredxxx 不能获取父类的方法
实例demo
字节码阶段
1 | //Class.forName("全类名") |
内存阶段
1 | //类名.class属性 |
运行时阶段
1 | //对象.getClass()方法 |
小细节
1 | //用==去比较三个class类对象,==比较的是对象的内存地址,如果内存地址相同,那么就是同一个对象 |
实例对照
下面看一个demo:
1 | public class Person { |
获取Fields
获取成员变量们:
- Fields[] getFields():只能获取所有public修饰的成员变量
- Fields getField(String name):获取特定成员变量
- Fields[] getDeclaredFields():获取所有的成员变量,【不考虑】修饰符,不考虑继承
- Fields getDeclaredField(String name):获取特定的成员变量,【不考虑】修饰符,不考虑继承
1 | Class personClass = Person.class; |
1 | //这里新增测试字段 public a |
1 | //这里新增测试字段private d |
1 | Class personClass = Person.class; |
获取Constructor
获取构造方法们:
Constructor[] getConstructors()
Constructor getConstructor(类 <?> … parameterTypes)
Constructor getDeclaredConstructors()
Constructor getDeclaredConstructor(类 <?> … parameterTypes)
1 | Class personClass = Person.class; |
1 | Class personClass = Person.class; |
获取Methods
获取成员方法们:
Method[] getMethods()//获取所有【public】修饰的方法,父类Object的方法也能看到
Method getMethod(String name,类 <?> … parameterTypes)
Method[] getDeclaredMethods()//获取所有声明方法 不考虑修饰符 不考虑继承的方法
Method getDeclaredMethod(String name,类 <?> … parameterTypes) 不考虑修饰符 不考虑继承的方法
1 | Class personClass = Person.class; |
1 | Class personClass = Person.class; |
1 | Class personClass = Person.class; |
灵魂体现
这里引入一个实际利用中的小demo:
1 | public void execute(String className, String methodName) throws Exception { |
这里分步解读:
- 首先两个参数:
className
、methodName
分别代表了传入的类名和方法名。 - 首先
Class clazz = Class.forName(className);
这里的意思是在字节码层面,先获取你的Class类对象clazz
。 - 接下来
clazz.getMethod(methodName).invoke(clazz.newInstance());
这里的含义可以分步解读:clazz.getMethod(methodName)
这里我先根据传进来的方法名methodName
获取你的Method对象;- 接下来
.invoke(clazz.newInstance())
这个含义是对于我获取的method方法对象,我要执行这个方法,那么问题来了,我如何才能执行这个方法呢??? - 这里我先
clazz.newInstance()
,这一手的含义是我要先生成一个指定类的对象,那么如何生成呢?之前我已经获取了Class类对象,那么就可以用这个类对象来生成这个类的实例化对象 clazz.newInstance()
这样就可以生成一个实例化对象了- 那么有了类的实例化对象之后,直接放入invoke参数里面,就可以凭借我们之前的Method对象来执行这个特殊的
className
方法了!
说了这么多嗷,我分步拆开来写一下,会更清晰一些:
1 | public void execute(String className, String methodName) throws Exception { |
以下一步步使用反射机制实现Runtime.getRuntime().exec("calc.exe");
这个语句
- getRuntime():其实就是Runtime类获取对象的方式,等于new一个Runtime类。之所以封装成一个函数是为了不调用一次建立一个对象,只获取一个对象来执行操作。
- exec():调用exec函数
- calc.exe:调用计算器程序
类的初始化
这里我再写一个小demo:
1 | package domain; |
首先我写一个测试类,对于这个测试类,它有初始块,静态初始块,构造函数。
那么下面我来获取它的Class对象,想看看当这个TrainPrint
测试类加载进内存之后,哪些部分执行了,好主意,说写就写:
首先类的初始化:
1 | package reflect; |
执行结果:Static initial class domain.TrainPrint
这说明当类加载进了内存,只有静态初始块得到了执行。
那么类的实例化呢:
1 | package reflect; |
执行结果:
1 | Static initial class domain.TrainPrint |
这也暴露了顺序:静态初始块
->初始块
->构造函数
补充:
具有父类的类的实例化:父类静态初始块
->子类静态初始块
->父类初始块
->父类构造函数
->子类初始块
->子类构造函数
以上对于类初始化的说明其实就是说,单独一个Class.forName(),在类静态初始块可控的情况下,可以执行恶意代码。
调用内部类
Java的普通类 C1 中支持编写内部类 C2 ,而在编译的时候,会生成两个文件: C1.class
和 C1$C2.class
,可以把他们看作两个无关的类。
Class.forName("C1$C2")
可以调用这个内部类。
我们可以通过Class.forName("java.lang.Runtime")
来获取类(java.lang.Runtime是Runtime类的完整路径)
getMethod
Java中支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用 getMethod 的时候,我们需要传给他你需要获取的函数的参数类型列表,如下:Class.forName("java.lang.Runtime").getMethod("exec", String.class)
invoke
invoke方法位于Method类下,其作用是传入参数,执行方法,public Object invoke(Object obj, Object... args)
它的第一个参数是执行method的实例化对象:
- 如果这个方法是一个普通方法,那么第一个参数是类对象。
- 如果这个方法是一个静态方法,那么第一个参数是类(之后会提到,这里其实不用那么死板,null也行),它接下来的参数才是需要传入的参数。
由于我们的exec函数是一个普通方法,需要传入类对象,即invoke(类对象,exec方法传入的参数)
。
之前说到Runtime的类对象不能通过newInstance()来获取对象(class.newInstance等于new class),是因为Runtime的类构造函数是一个private构造函数,只能通过getRuntime方法返回一个对象。
获取类对象:
1 |
|
再简化一下:
1 | Class clazz = Class.forName("java.lang.Runtime"); |
详细解读,这里是一个疯狂套娃的过程:
- 首先先要找到类对象,clazz,不多说了
- 接下来我要告诉这个类对象我要执行的方法是
exec
,这个exec方法接受的参数类型是String
类型,当然就是恶意命令字符串 - 那么我们的invoke函数需要的操作就是
invoke(类对象,exec方法等待接收的参数)
,那么问题来了:Runtime类不允许newInstance()
来实例化对象!!!但是他允许通过getRuntime
方法返回一个对象,那么我就用getRuntime方法来返回一个对象! - 现在开始疯狂套娃!
- 首先既然你不允许instance,那么我就在那个类对象的位置“套”出来一个对象!思路其实就是既然我没办法给你在那个位置“放”上一个实例化对象,那么我就在你的位置上使用反射invoke执行
getRuntime
方法,不就可以了么! - 那么就有了简化版代码!
- 这里仔细看,可以看到,其实
getRuntime
方法它也是一个无参构造方法!
指定的构造方法生成类的实例
继续举一个演化成反射机制的执行命令payload的例子:
1 | List<String> paramList = new ArrayList<>(); |
可见,其构造函数是写入了一个字符串,不是无参构造方法,接下来我们会一步步进行转化。
ProcessBuilder有两个构造函数:
public ProcessBuilder(List command)
public ProcessBuilder(String... command)
(此处,String...
这种语法表示String参数数量是可变的,与String[]一样)
getConsturctor()
函数可以选定指定接口格式的构造函数(由于构造函数也可以根据参数来进行重载),getConsturctor(参数类型)
选定后我们可以通过newInstance()
,并传入构造函数的参数执行构造函数,即newInstance(传入的构造函数参数)
。
start函数不是一个静态函数,需要传入类的实例,所以这里可以继续使用反射:
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
这里还有一种方式:
1 | //这个可以传入多个字符串 |
那么payload就这样写吧:
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
但是实际上我们这样调用是会报错的,因为newInstance函数接受参数是一个Object..
也就是Object数组,它会完美契合我们提供的String[],剥去一层数组。
那就再套一层:
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
执行私有方法
以上都是方法或构造方法是public函数,但是如果是私有方法,该如何调用?
之前用的都是getMethod、getConstructor,接下来需要使用getDeclaredMethod、getDeclaredConstructor:
- getMethod等方法获取的是当前类中所有公共方法,包括从父类继承的方法
- getDeclared等方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了
之前说到Runtime的构造方式是一个私有方法,从而不能直接调用,那么接下来我就来调用Runtime的构造方法来获取一个实例来执行计算器弹出:
1 | Class clazz = Class.forName("java.lang.Runtime"); |
在获取到私有方法后,通过setAccessible(true)
可以打破私有方法访问限制,从而进行调用。
从Commons-collections收获的一点反思
对于正常的反射模板:
1 | Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")) |
但其实我们很容易忽略反射机制中调用的函数实际上可以在两个不同的class中调用,就像好比有一个函数可以在两个class中调用。
在Commons-Collections的环境中,我们是没法得到Class.forName("java.lang.Runtime").getMethod(...)
的,但是可以得到Class.forName("java.lang.Class").getMethod(...)
。
可以理解为我们可以抓到Runtime
的父类,那么就可以得到以下途径进行变形:
- 用反射机制去调用反射机制中使用的函数getMethod
- 使用invoke传入的obj去指定getMethod的当前的调用环境(在实际代码执行中是this变量的区别)
继续看一下Class类中getMethod方法的接口:
1 |
|
那么我们传数组,来拿到getmethod
这个方法不就行了么:
1 | Method method1= Class.forName("java.lang.Class") |
以下套娃警告!
这里的Class类,它有所有方法,我们就是先拿到getmethod
这个方法,这里是为什么呢?因为对于想用反射来调用方法其他类的方法,你必须会用到getmethod
方法(忘了的向上翻模板),这里就相当于我先把梯子拿到!
那么有了getmethod
方法之后,我需要一个obj对象来调用呀,这个obj其实就是java.lang.Runtime
。之前也说了,这个对象很有脾气,需要执行这个类下面的Runtime
方法,才能实例化一个Runtime
对象。
结合上面刚刚获取的getmethod
梯子方法,继续写吧:
1 | //以下语句执行结果等同于Class.forName("java.lang.Runtime").getMethod(getRunime) |
合起来就是:
1 | // 1.正常的反射调用 |
new Class[0]
其实就是占位,因为阅读源码能看到invoke方法参数规范,是硬性标准需要两个参数的,第二个是一个Object
类的数组:
1 |
|
所以现在我们只是以Class.forName("java.lang.Class")
开头获取到了Runtime类下的getRuntime方法。还没有执行。
神奇的invoke参数
自己写完总觉得invoke参数奇奇怪怪的,淦,研究一下。
invoke有一个非常神奇的特性,它不会那么严格地校验obj。回顾之前总结的invoke传参规则:
它的第一个参数是执行method的对象obj:
- 如果这个方法是一个普通方法,那么第一个参数是类对象
- 如果这个方法是一个静态方法,那么第一个参数是类
它接下来的参数才是需要传入的参数。
但是,传入的第一个参数其实不一定要是正确的类或者类对象!
下面一个小例子:
1 | //main函数 |
按照规则,print函数是一个静态方法,实际上我们应该invoke传入一个a的类。但是以上代码的执行结果却是成功的:
但是如果print
方法不是静态防范时就会执行失败:
这是因为invoke函数null抛出报错的机制导致的:
1 | * NullPointerException |
当method是一个普通函数时,传入obj不能为null,并且其类对象要与方法匹配;但是当method是一个静态函数时,就很随便了(可能是因为压根不会被用到吧)。
这里我迷茫了好久,知道我看到这样一句话才弄懂:
类实例是其他类的实例,类实际上是Class.class这个类的实例。
这边之前的疑问是,getMethod不是静态方法而invoke中传入的是类而不是类实例。
实际上因为getMethod本来就是class类中的方法,而Class.forName("java.lang.Runtime")
获取到的class类的实例
我们调用getMethod传入的不是之前的模糊类的概念,而是class类的实例(类实例),所以这里是没毛病的调用class类下的非静态方法,传入class类实例。
回来继续构造调用
上面我们只是获取到了getRuntime方法,我们还没有调用这个方法获取其Runtime对象
1 | //普通调用形式 |
搞懂invoke函数
直接上源码:
1 | package java.lang.reflect; |
override参数
它是父类AccessibleObject的一个属性,AccessibleObject这个类有三个子类:构造函数、属性、方法。
override这个值默认是false,但是我们可以通过method.setAccessible(true)来改掉它的值。
Reflection.quickCheckMemberAccess(clazz, modifiers)
如果是override是默认值false,那么继续往下走。
我们这里假设:m.invoke(obj,args)
参数:
clazz:m所属类的Class对象
args:m的所需参数
这里quickCheckMemberAccess先检查Class对象是不是public的。
如果是public,那么就跳出本方法;
如果不是public,那继续来到Reflection.getCallerClass(1);
这是一个native方法,返回的是获取调用这个方法的Class对象,赋值给caller
1
public static native Class<?> getCallerClass();
checkAccess
然后通过checkAccess(checkAccess(caller, clazz, obj, modifiers)做一次快速的权限校验
不难看出,还进行了一个很简单的缓冲机制,只适用于一个类的重复调用。
MethodAccessor ma = methodAccessor;
接下来是重头戏:
1 |
|
首先要了解Method对象的基本构成,每个Java方法有且只有一个Method对象作为root,它相当于根对象,对用户不可见。当我们创建Method对象时,我们代码中获得的Method对象都相当于它的副本(或引用)。root对象持有一个MethodAccessor对象,所以所有获取到的Method对象都共享这一个MethodAccessor对象,因此必须保证它在内存中的可见性。
当第一次调用一个Java方法对应的Method对象的invoke()方法之前,实现调用逻辑的MethodAccessor对象还没有创建(第一次调用,methodAccessor属性为null),所以通过reflectionFactory
创建MethodAccessor并更新给root,然后调用MethodAccessor.invoke()
完成反射调用。
具体细看,可以看到invoke方法实际是委派给了MethodAccessor类型的ma对象来处理。MethodAccessor是一个接口,主要有两个实现类。
一个委派实现(DelegatingMethodAccessorImpl
),一个本地实现(NativeMethodAccessorImpl
)。这里调用的委派实现主要是为了在本地实现和动态实现之间做切换。考虑到许多反射调用仅会执行一次,Java虚拟机设置了一个阈值15(是从0开始计算,>15):
- 当某个反射调用的调用次数<=15 时,采用本地实现;
- 当大于15时,便开始动态生成字节码,并将委派实现的委派对象切换至动态实现。这个过程我们称之为Infation。
这里我们以NativeMethodAccessorImpl
本地实现为例:
每次调用其invoke时会做一个累加,判断是否到达阙值,如果没有则调用native的invoke0方法,当超过时则调用MethodAccessorGenerator.generateMethod()
,并将其设置到DelegatingMethodAccessorImpl
的delegate,这样下次就会直接调用到动态实现的位置。
m.invoke(o,args)
invoke0参数:
- this.method 就是m
- var1就是o,如果m是静态方法的话,这里写null也可以
- var2就是args参数
我们重点分析一下invoke0这个native方法:
1 | JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0 |
借用ruilin的图,继续看JVM_InvokeMethod方法:
发现提到了oop,简单写一下oop-klass理解:
JVM就是用这种方式,将一个对象的数据和对象模型进行分离。普遍意义上来说,我们说持有一个对象的引用,指的是图中的handle(存放在栈区),它是oop(存放在堆区)的一个封装。
关键点是:
1 | oop result = Reflection::invoke_method(method_handle(), receiver, args, CHECK_NULL); |
跟进hotspot/src/share/vm/runtime/reflection.cpp
Reflection::invoke_method()中接受的method_mirror(oop)就是我们要反射调用的方法。然后代码调用了Reflection::invoke(),跟进之后最终到JavaCalls::call()执行
最后的os_exception_wrapper
其实就是调用了call_help
,也就是说本地实现的反射最终的方法执行是通过JavaCalls::call_helper
方法来完成的。
总结
反射,永远滴神!