Drunkmars's Blog

java反射探究

字数统计: 3.3k阅读时长: 13 min
2022/01/14

反射是大多数语言里里都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以调用,总之通过“反射”,我们可以将Java这种静态语⾔言附加上动态特性。

java反射

举一个简单的动态代理的例子

如果使用静态代理,代理类和被代理类在编译期间就确定下来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* @author Drunkmars
* @create 2022-01-08-20:05
*/

/*
静态代理举例
特点:代理类和被代理类在编译期间,就确定下来了。
*/
interface ClothFactory
{
void produceCloth();
}

class ProxyClothFactory implements ClothFactory
{
private ClothFactory factory;

public ProxyClothFactory(ClothFactory factory)
{
this.factory = factory;
}

@Override
public void produceCloth() {
System.out.println("代理工厂正在准备");

factory.produceCloth();

System.out.println("代理工厂做一些后续的收尾工作");
}

}

// 被代理类
class NikeClothFactory implements ClothFactory
{

@Override
public void produceCloth() {
System.out.println("NIKE工厂生产中");
}
}

public class StaticProxyTest
{
public static void main(String[] args) {
// 创建被代理类对象
NikeClothFactory nike = new NikeClothFactory();

// 创建代理类对象
ProxyClothFactory proxyClothFactory = new ProxyClothFactory(nike);

proxyClothFactory.produceCloth();

}
}

输出结果如下

image-20220114101511522

而使用动态代理则需要在运行的时候才能确定代理类和被代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
* @author Drunkmars
* @create 2022-01-08-22:46
*/


interface Human{

String getBelief();

void eat(String food);

}
//被代理类
class SuperMan implements Human{


@Override
public String getBelief() {
return "I believe I can fly!";
}

@Override
public void eat(String food) {
System.out.println("我喜欢吃" + food);
}
}

class HumanUtil{

public void method1(){
System.out.println("====================通用方法一====================");

}

public void method2(){
System.out.println("====================通用方法二====================");
}

}

/*
要想实现动态代理,需要解决的问题?
问题一:如何根据加载到内存中的被代理类,动态的创建一个代理类及其对象。
问题二:当通过代理类的对象调用方法a时,如何动态的去调用被代理类中的同名方法a。


*/

class ProxyFactory{
//调用此方法,返回一个代理类的对象。解决问题一
public static Object getProxyInstance(Object obj){//obj:被代理类的对象
MyInvocationHandler handler = new MyInvocationHandler();

handler.bind(obj);

return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),handler);
}

}

class MyInvocationHandler implements InvocationHandler{

private Object obj;//需要使用被代理类的对象进行赋值

public void bind(Object obj){
this.obj = obj;
}

//当我们通过代理类的对象,调用方法a时,就会自动的调用如下的方法:invoke()
//将被代理类要执行的方法a的功能就声明在invoke()中
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

HumanUtil util = new HumanUtil();
util.method1();

//method:即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
//obj:被代理类的对象
Object returnValue = method.invoke(obj,args);

util.method2();

//上述方法的返回值就作为当前类中的invoke()的返回值。
return returnValue;

}
}

public class ProxyTest {

public static void main(String[] args) {
SuperMan superMan = new SuperMan();
//proxyInstance:代理类的对象
Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
//当通过代理类对象调用方法时,会自动的调用被代理类中同名的方法
String belief = proxyInstance.getBelief();
System.out.println(belief);
proxyInstance.eat("四川麻辣烫");

System.out.println("*****************************");

NikeClothFactory nikeClothFactory = new NikeClothFactory();

ClothFactory proxyClothFactory = (ClothFactory) ProxyFactory.getProxyInstance(nikeClothFactory);

proxyClothFactory.produceCloth();

}
}

输出结果如下

image-20220114101625367

在反射中最重要的几个方法无非:

  • 获取类的方法: forName

  • 实例化类对象的方法: newInstance

  • 获取函数的方法: getMethod

  • 执行函数的方法: invoke

在反射中我们一般有四种方法来获取类对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 方式一:调用运行时类的属性:.class
Class<PersonTest> aClass = PersonTest.class;
System.out.println(aClass);

// 方式二:通过运行时类的对象,调用getClass()
PersonTest p = new PersonTest();
Class<? extends PersonTest> aClass1 = p.getClass();
System.out.println(aClass1);

// 方式三:调用Class的静态方法:forName(String classPath)
Class<?> aClass2 = Class.forName("PersonTest");
System.out.println(aClass2);

// 方式四:使用类的加载器:ClassLoader
ClassLoader loader = ClassTest.class.getClassLoader();
Class<?> aClass3 = loader.loadClass("PersonTest");
System.out.println(aClass3);

image-20220114101905548

我们一般最经常使用的获取类方法是第三种:调用Class的静态方法:forName(String classPath)

forName有两个函数重载,其中第一种方式可以理解为第二种方式的封装

  • Class<?> forName(String name)
  • Class<?> forName(String name, boolean initialize, ClassLoader loader)

默认情况下, forName 的第一个参数是类名;第二个参数表示是否初始化;第三个参数就是 ClassLoader

展开说一下ClassLoader,我们首先看一下类的加载过程

image-20220114102415547

而其中类的加载器的作用就是把类装载进内存,JVM规范了几种扩展类的加载器,分别为引导类加载器、扩展类加载器以及系统类加载器

  • 引导类加载器:用C++编写的,是JVM自带的类加载器,负责Java平台核心库,用来装载核心类库。该加载器无法直接获取
  • 扩展类加载器:负责jre/liblext目录下的jar包或-java.ext.dirs指定目录下的jar包装入工作库
  • 系统类加载器:负责java -classpath或-Djava.class.path所指的目录下的类与jar包装入工作,是最常用的加载器

那么我们这里可以对这几种加载器进行一下测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test3()
{
// 对于自定义类,使用系统类加载器进行加载
ClassLoader loader = ClassTest.class.getClassLoader();
System.out.println(loader);

// 调用系统类加载器的getParent():获取扩展类加载器
ClassLoader loader1 = loader.getParent();
System.out.println(loader1);

// 调用扩展类加载器的getParent():无法获取引导类加载器
// 引导类加载器主要负责加载java的核心类库,无法加载自定义类
ClassLoader loader2 = loader1.getParent();
System.out.println(loader2);
}

如下所示,引导类加载器是不能够获取的

image-20220114102640298

继续回到上面,第二个参数 initialize 常常被人误解,使用功能”.class”来创建Class对象的引用时,不会自动初始化该Class对象,使⽤用forName()会自动初始化该Class对象

其实在 forName 的时候,构造函数并不会执行,即使我们设置 initialize=true 。可以将这个“初始化”理理解为类的初始化。

这里又要说到初始化方法的调用过程了,借用p牛代码

1
2
3
4
5
6
7
8
9
10
11
public class TrainPrint {
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
static {
System.out.printf("Static initial %s\n", TrainPrint.class);q
}
public TrainPrint() {
System.out.printf("Initial %s\n", this.getClass());
}
}

在上面的三种初始化方法中,static方法是会最先被调用的,因为static方法在类进行初始化之前就已经调用,而 {} 中的代码会放在构造函数的 super() 后面,但在当前构造函数内容的前面。所以说, forName 中的 initialize=true 其实就是告诉Java虚拟机是否执行’’类初始化’’

newInstance():调用此方法,创建对应的运行时类的对象,内部调用了运行时类的空参的构造器

要想此方法正常的创建运行时类的对象,要求:

1.运行时类必须提供空参的构造器
2.空参的构造器的访问权限得够。通常,设置为public

在javabean中要求提供一个public的空参构造器。原因:

1.便于通过反射,创建运行时类的对象
2.便于子类继承此运行时类时,默认调用super()时,保证父类有此构造器

那么我们这里写个测试,这里是有空参的构造器且权限可以访问的情况,是能够创建对象的

image-20220114110009496

再看一下newInstance不成功的情况,这里调用Runtime会报错java.lang.IllegalAccessException,就是因为他的构造方法为private

1
2
3
4
5
6
7
8
public class RETest1
{
public static void main(String[] args) throws Exception{
Class clazz = Class.forName("java.lang.Runtime");
Object invoke = clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");
System.out.println(invoke);
}
}

image-20220114110343599

这里的Runtime构造方法为私有,是因为Runtime的设计模式为单例模式,对于Web应用来说,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连接,此时作为开发者你就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来获取。这样,只有类初始化的时候会执行一次构造函数,后面只能通过 getInstance 获取这个对象,避免建立多个数据库连接。

1
2
3
4
5
6
7
8
9
public class TrainDB {
private static TrainDB instance = new TrainDB();
   public static TrainDB getInstance() {
     return instance;
  }
   private TrainDB() {
    // 建立连接的代码...
  }
}

Runtime类就是单例模式,我们只能通过 Runtime.getRuntime() 来获取到 Runtime 对象,修改后即可

1
2
3
4
5
6
public static void main(String[] args) throws Exception{
Class clazz = Class.forName("java.lang.Runtime");
//clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");

clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");
}

image-20220114111821205

getMethod 的作用是通过反射获取一个类的某个特定的公有方法,Java中支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用 getMethod 的时候,我们需要传给他你需要获取的函数的参数类型列表。

invoke 的作用是执行方法,它的第一个参数是:

  • 如果这个方法是一个普通方法,那么第一个参数是类对象
  • 如果这个方法是一个静态方法,那么第一个参数是类

这也比较好理解了,我们正常执行方法是 [1].method([2], [3], [4]...) ,其实在反射里就是 method.invoke([1], [2], [3], [4]...)

所以我们将上述命令执行的Payload分解一下就是:

1
2
3
4
5
Class clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc.exe");

这里就会衍生出两个问题

如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
如果一个方法或构造方法是私有方法,我们是否能执行它呢?

那么这时候我们就需要获取可使用的构造器,使用到getConstructors

getConstructors()∶获取当前运行时类中声明为public的构造器

getDecLaredConstructors():获取当前运行时类中声明的所有的构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void test1()
{
Class<PersonTest2> aClass = PersonTest2.class;
// getConstructors()∶获取当前运行时类中声明为public的构造器
Constructor<?>[] constructors = aClass.getConstructors();

for(Constructor c : constructors)
{
System.out.println(c);
}

System.out.println();

//getDecLaredConstructors():获取当前运行时类中声明的所有的构造
Constructor<?>[] constructors1 = aClass.getDeclaredConstructors();

for (Constructor c : constructors1)
{
System.out.println(c);
}
}

image-20220114122125171

那么我们可以先使用getConstructor获取函数之后再调用newInstance

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();

image-20220114122639843

ProcessBuilder有两个构造函数:

  • public ProcessBuilder(List command)
  • public ProcessBuilder(String… command)

上面用到了第一个形式的构造函数,在 getConstructor 的时候传入的是 List.class

但是,我们看到,前面这个用到了Java里的强制类型转换,有时候我们利用漏洞的时候(在表达式上下文中)是没有这种语法的。所以,我们仍需利用反射来完成这一步。

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));

image-20220114123635833

通过 getMethod("start") 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是ProcessBuilder Object

那么如果没有public权限的函数,我们是不是就不能进行反射了呢,也不是,可以通过getDeclaredField方法来进行操作

调用运行时类的指定结构的过程中,必须要声明属性为public,那么我们看一下使用常规方法是否会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void test4() throws Exception
{
Class<PersonTest2> aClass = PersonTest2.class;

// 创建运行时类的对象
PersonTest2 p = aClass.newInstance();

// 获取指定的属性:要求运行时类中属性声明为public,所以一般不是用这种方法
Field id = aClass.getField("id");
Field id = aClass.getField("age");

/*
设置当前属性的值

set():参数1:指明设置哪个对象的属性 参数2:将此属性设置为多少
*/

id.set(p,10);
System.out.println(id.get(p));
}

image-20220114124020198

我们再试一下getDeclaredField方法,这里需要用到setAccessible(true)保证当前属性是可访问的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test5() throws Exception
{
Class<PersonTest2> aClass = PersonTest2.class;

// 1.getDeclaredField(String fieLdName):获取运行时类中指定变量名的属性
PersonTest2 p = aClass.newInstance();
Field name = aClass.getDeclaredField("name");

//2.保证当前属性是可访问的
name.setAccessible(true);

//3.获取、设置指定对象的此属性值
name.set(p, "neymar");
System.out.println(name.get(p));
}

image-20220114124149094

再使用下getDeclaredField进行命令执行

1
2
3
4
Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");

image-20220114124247618

CATALOG
  1. 1. java反射