Reflection反射-[Javasec]
何为反射?
先创建一个One
类,内容如下:
按照之前的学习的基础知识,使用某个类时必定知道它是什么类,是用来做什么的,通过new一个对象,再去调用Hello
方法
1 | One one = new One(); |
现在通过反射来实现一下:
(刚接触的话可能会有很多疑惑,什么是Class类?这些方法怎么用?下面还会详细解释)
通过上面的例子便可以大致了解Java反射是什么,现在系统的了解一下反射机制:可以参考小阳师傅所画的Java反射机制流程图
反射机制
反射的核心是JVM在运行状态的时候才动态加载类,对于任意一个类都能够知道这个类所有的属性和方法,并且对于任意一个对象,都能够调用它的方法/访问属性。这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。
这段简介总是提到动态,有必要去理解一下什么是Java的动态特性:
P神:一段代码,改变其中的变量,将会导致这段代码产生功能性的变化,称之为动态特性
具体来说的话,Java反射机制可以完成如下这些操作:
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时调用任意一个对象的成员变量和方法
- 生成动态代理
反射常用的API如下:
为什么上面反射的例子不能直接new一个对象再调用方法,原因便可以查询一下Class类的源码,可以看是private
进行修饰的,使用new方法创建实例是无法访问内部信息,但是通过反射机制可以访问
接下来了解一下Class类
Class类
Class类图如下:
其父类为Object
类,Object类似是一切java类的父类,即便不声明,也是默认基础了Object类
Class类的作用:
在程序运行期间,Java运行时系统会始终为所有对象维护一个运动时的类型标识。这个是标识的作用便是跟踪每个对象所属的类,然后
JVM
再利用运行时的信息选择相应的方法执行。保存这些信息的类称为Class
简单来说便是Class
类可以帮助获取类中的值,一般都是配合反射使用的,只向Class
提供一个类或者是一个类的类名,Class
便可以提供很多信息。
这里注意和class
区别开来,不要混淆,class
是java中的关键字,例如:public class xxx
或者 class xxx
,在声明Java类时使用,二者并没有什么关系
Class类对象
那什么是Class类对象?
通过对上面Class类的理解,便可以了解到每个类的运行时的类型信息就是用Class对象
表示的,每一个类都有一个Class对象,编译一个新类便会产生一个Class对象。并且每个类的实例都会记得自己是由哪个Class实例所生成。
Class对象的作用:可以得到一个类的完整结构
通过上面的例子,知道Class对象肯定不是new出来的,而是由系统创建出来的
Class类由
loadClass()
方法完成类加载,生成某个类对应的Class类对象
对于某个类的Class类对象,在内存中只有一份,因为类只会加载一次,例如下面的例子,可以观察到hash值相同
Class类常用方法
Java反射操作的是java.lang.Class
对象,所以我们需要先想办法获取到Class对象
1 | public void execute(String className, String methodName) throws Exception { |
通过P神所举的例子,便可以接触到几个在反射中极为重要的方法:
- 获取类的方法:
forName
- 实例化类对象的方法:
newInstance
- 获取函数的方法:
getMethod
- 执行函数的方法:
invoke
除了用forName
方法获取类,还可以通过如下方法:
- 若已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高,但这种方法不属于反射
1 | #类名.class |
- 已知某个类的实例,通过该实例的
getClass()
方法获取Class对象
1 | Class clazz = Person.getClass(); |
接下来通过一个例子来测试一下class
类的创建方式都有哪些?
还有其他的方法,这里就不再列举了
通过上面的学习,对刚开始所举的例子应该就不会那么云里雾里了,但是要想搞明白Java反射,类加载是一定要理解的,但前提是要先去大致了解一下Java内存方面的知识,因为类的加载过程会涉及到的堆、栈和方法区。
类的加载过程(ClassLoader):
可以参考下韩顺平老师所列举的类加载过程图:
加载阶段:在该阶段,JVM的主要目的是将字节码从不同的数据源转化为二进制字节流加载到内存中,并生成一个代表该类的java.lang.Class对象
连接阶段
- 验证:确保
Class
文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机自身的安全 - 准备:JVM会在该阶段对静态变量,分配内存并默认初始化(对于数据类型的默认初始值,如:0、0L、null、false),这些变量所使用的内存都将在方法区中进行分配
- 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程
- 验证:确保
初始化:
- 到了初始化这个阶段,才真正开始执行类中定义的Java程序代码,这个阶段是执行
<clinit>()
方法的过程 <clinit>()
方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并- 虚拟机会保证一个类的
<clinit>()
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法,其他现场都需要阻塞等待,直到活动线程执行<clinit>()
方法
- 到了初始化这个阶段,才真正开始执行类中定义的Java程序代码,这个阶段是执行
那什么时候会发生类初始化?
- 类的主动引用(一定会发生类的初始化 )
- 当虚拟机启动,先初始化main方法所在的类
- new一个类的对象
- 调用类的静态成员(除了final常量)和静态方法
- 使用java.lang.reflect包的方法对类进行反射调用
- 当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
- 类的被动引用(不会发生类的初始化)
- 当访问一个静态域时,只有真正声明这个域的类才会被初始化。
- 通过数组定义类引用,不会触发此类的初始化
- 引用常量不会触发此类的初始化(常量在链接阶段就已经存入到调用类的常量池中去了)
接下来通过一个例子来测试类什么时候会进行初始化
1 | //测试类什么时候会初始化 |
主动引用产生的结果:
子类调用父类的一个静态方法或者静态的变量,并不会让子类进行加载
通过上面的知识便可以对反射有一个初步的印象,接下来就来学习一下Runtime类,它在Java反射的payload中是非常常见的,通过对这个类的学习来加强对Java反射的理解
Java Runtime类
java.lang.Runtime因为有一个exec方法执行本地命令,所以很多payload中都可以看到反射调用Runtime类来执行本地系统命令
观察一下Runtime类,需要用getRuntime()
方法来获取Runtime
的对象,因此直接new是不行的,因为Runtime类是单例模式,所以只能通过Runtime.getRuntime()
获得Runtime
类的对象,然后再调用Runtime
的内部方法,如下图所示:
这里解释一下什么是单例模式?
单例模式是指在整个程序运行期间,只能初始化某个类一次,然后便一直使用这个实例
若改为反射来执行系统命令,如何调用Runtime
类的exec()
方法,如下图所示:
这里的getMethod
的作用便是通过反射去获取一个类的某个特定的公有方法,invoke
的作用便是执行方法:
- 如果这个方法是一个普通方法,那么第一个参数是类对象
- 如果这个方法是一个静态方法,那么第一个参数便是类
如果要调用私有方法(例如Runtime类中的Runtime()方法),要怎么执行?来看一下反射调用Runtime实现本地命令执行的流程:
1 | 1.首先通过反射获取到Runtime对象 |
上面的代码涉及到getDeclaredConstructor
,它与getConstructor
的区别如下:
getConstructor()
返回指定参数类型public
的构造器getDeclaredConstructor()
返回指定参数类型的private
和public
构造器
因为Runtime这个类的构造函数是私有的,所以需要使用getDeclaredConstructor
来获取这个私有的构造方法来实例化对象,进行执行命令。
这里也简单了解一下Runtime.exec()
的六种重载方法,官方文档如下:envp
指定环境、dir
指定目录
最后列一些Class类的重要方法,便于自己查看
Class类的方法
- 获得类相关的方法
方法 | 用途 |
---|---|
forName(String className) |
根据类名返回类的对象 |
newInstance() |
创建类的实例 |
getClassLoader() |
获得类的加载器 |
getDeclaredClasses() |
返回一个数组,数组中包含该类中所有类和接口类的对象 |
- 获得类中属性相关的方法
方法 | 用途 |
---|---|
getField(String name) |
获得某个公有的属性对象 |
getFields() |
获得所有公有的属性对象 |
getDeclaredField(String name) |
获得某个属性对象 |
getDeclaredFields() |
获得所有属性对象 |
- 获得类中构造器相关的方法
方法 | 用途 |
---|---|
getConstructor(Class...<?> parameterTypes) |
获得该类中与参数类型匹配的公有构造方法 |
getConstructors () |
获得该类的所有公有构造方法 |
getDeclaredConstructor(Class...<?> parameterTypes) |
获得该类中与参数类型匹配的构造方法 |
getDeclaredConstructors() |
获得该类所有构造方法 |
- 获得类中方法相关的方法
方法 | 用途 |
---|---|
getMethod(String name, Class...<?> parameterTypes) |
获得该类某个公有的方法 |
getMethods() |
获得该类所有公有的方法 |
getDeclaredMethod(String name, Class...<?> parameterTypes) |
获得该类某个方法 |
getDeclaredMethods() |
获得该类所有方法 |
参考博客
P神的Java安全漫谈01-03
https://blog.csdn.net/wh710107079/article/details/86294628
https://www.freebuf.com/articles/network/319655.html