Java反射笔记3
在正常情况下,除了Java自带的类,我们要想使用一个类,就需要我们导入即import才能使用,所以forName就显得特别重要,因为我们可以拿这个来加载任意类。
在一些情况中,类名的部分会有美元$这个符号,$的作用是查找内部类。
在Java中,普通类c1中支持编写内部类c2,在编译时,会生成两个文件:c1.class和c1$c2.class,可以把他们看做两个不相关的类,通过class.forName就可以加载这个内部类
获取了这个类,就能接着通过反射来获取属性,方法,以及进行实例化。
newInstance()是调用类的无参构造函数,不过有时使用这个方法时会不成功,主要有两点
- 想调用的类没有无参构造函数
- 想调用的类构造函数是私有的
举个例子,在构造命令执行相关Payload时,一般会使用Runtime这个类,但不能直接这样执行命令
1 | Class clazz = Class.forName("java.lang.Runtime"); |
会有报错!
原因是Runtime类的构造方法是私有的,而我们是不能直接使用的,所以会有报错,这里其实就使用了单例设计模式。
比如说,在Web网站中会使用数据库进行大量数据交互,但数据库链接只需要一次链接就行,不是每次使用时还要重新建立一个新的链接,此时将建立数据库链接的相关构造类的构造函数设置为私有,编写一个静态方法,使用单例模式
1 | public class TrainDB { |
代码如图所示,只有类初始化时才执行一次构造函数,否则只能通过getInstance来获取这个对象,避免建立多个数据库链接
Runtime类是很明显的一个单例模式设计例子,只能通过Runtime.getRuntime()来获取Runtime对象,所以要将上面的payload进行修改
1 | Class clazz = Class.forName("java.lang.Runtime"); |
getMethod和invoke都是反射中很熟悉的方法,又因为在Java语法中类是可以进行重载的,不能仅仅通过函数名来确定一个函数。所以,调用getMethod的时候,需要传给需要获取的函数的参数类型列表,这里使用的Runtime.exec这个方法就有6个重载
在这里我们使用最简单的,只有一个参数类型是String,所以使用getMethod(“exec”,String.class)来获取Runtime.exec方法
invoke()作用是执行方法:
- 如果这个方法是一个普通方法,第一个参数就是类对象
- 如果这个方法是一个静态方法,第一个参数就是类
所以将上述代码进行简化
1 | Class clazz = Class.forName("java.lang.Runtime"); |
这下会清楚很多
那如果一个类没有无参构造方法,也没有单例模式里的静态方法,如何通过反射实例化该类?
为了解决这个问题,我们需要引入一个新的反射方法getConstructor
与getMethod类似,getConstructor接收的参数是构造函数列表类型,因为构造函数也支持重载,有无参数即是一个体现。
获取想要的构造函数,那我们就要用newInstance来执行
比如另一种执行命令的方式ProcessBuilder,用反射来获取其构造函数,调用start()来执行命令
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
ProcessBuilder拥有两个构造函数
- public ProcessBuilder(List command)
- public ProcessBuilder(String… command)
这里用到了第一个形式的构造函数,所以在getConstructor传入的是List.class,但这里很明显使用了一个强制类型转换,所以需要一个反射来完成
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
通过 getMethod(“start”) 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是 ProcessBuilder Object了。
那如果采取使用第二个构造函数呢?这里面涉及了可变长参数,在Java里如果定义函数时不确定参数数量的时候,可以使用…这样的语法,来表示这个函数的参数个数是可变的。对于可变长参数,java其实在编译的时候会编译成一个数组
1 | public void hello(String[] names) {} |
所以我们如果有一个数组,就可以直接传给我们想使用的函数,对于反射来说同理,在这里把字符串数组的类String[].class传给getConstructor,获取第二种构造函数
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
在调用 newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给 ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
如果一个方法或构造方法是私有的呢?
这里就要使用getDeclared系列的反射,与普通的区别在于
- getMethod系列方法获取的是当前类中所有公共方法,包括继承的方法
- getDeclaredMethod系列方法获取的是当前类中的声明过的方法,是确确实实写在类里的,包括私有的,但是继承的类不包括
具体使用方法是与getMethod等是类似的
在前面说过Runtime这个类的构造函数是私有的,就要用Runtime.getRuntime()来获取对象,现在也可以用getDeclareConstructor来获取这个私有的构造方法进行实例化
1 | Class clazz = Class.forName("java.lang.Runtime"); |
这里使用了一个方法 setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用 setAccessible 修改它的作用域,否则仍然不能调用。