-

开启 java 安全学习第三站,java 反射

0x01 反射的意义

存在即合理,那么为什么要有反射呢?反射能够使得程序在运行时动态地操作类和对象,包括创建对象,访问字段,调用方法等等。这样说可能稍许抽象,我们先反过来理解,正射是不是就是静态地操作类和对象,也就是说正常情况下我们知道一个类的样子,直接实例化即可。

1
2
Apple apple = new Apple(); //直接初始化,「正射」
apple.setPrice(4);

这种直接访问的方式清晰且直观,但在有些场景中需要动态调用调用操作这些成员,比如在程序运行时,根据数据库提供的类名或者方法名,或者基于字符串来动态实例化对象调用方法时就不适用了,就得用到 java 的反射机制。spring/spring boot 或者 mybatis 就应用了大量反射机制,注解以及动态代理也运用到了反射机制。

0x02 反射原理

反射的实现原理其关键在一个特殊的对象,我们称之为类对象,即 Class 对象。Class 对象是由 Java 虚拟机 JVM 在加载类时自动创建的,用于存储类的信息,通过它就能访问类的结构,以及对类本身以及它的实例进行操作。image-20240712113457668

JVM 创建 Class 的过程是这样的,当我们编写一个 .java 类并完成编译后,编译器将其转化为字节码并存储在 .class 文件中。接下来在类的加载中,虚拟机利用 Class Loader 读取 class 文件,将其中的字节码加载到内存中并利用其携带的信息来创建相应的 Class 对象。我们知道每个类在 JVM 中只加载一次,所以每个类都对应唯一的 Class 对象。

image-20240712134939174

有一块镜子我们就能反射光线。那么在我看来 java 的反射中镜子就是 Class 对象。

0x03 反射实现

获取 Class 对象

  1. 使用类字面常量,就是类的名称 + .class

    这是最直接的方式,因为在编译时就确定了具体的类,仍然属于静态引用

    1
    Class<User> userClass = User.class;

    因为在编译时就确定了,已经明确了是个 User 类 的 Class ,所以泛型直接给 User。

  2. 使用对象的 getClass 方法,如果你已知一个对象,那么可以调用它的 getClass 方法来获取它的 Class 对象。

    1
    2
    User user = new User("she11F",18);
    Class<?> clazz = user.getClass();

    这里泛型用通配符是因为这个 Class 对象是在程序运行时从 user 实例获取的, user 实例的具体类型只能在程序运行时才确定,所以我们在编译阶段无法确定 Class 对象的具体类型,所以泛型是通配符。

  3. Class 类的 forName 静态方法

    这种方法用于程序运行时动态加载指定的类,并返回该类的 Class 对象实例。同常用于类名在编译时不可知的场景中

    1
    Class<?> clazz = Class.forName("User");

    这种方法会触发类的初始化,静态块会被执行。

    image-20240712141952855

    image-20240712141931353

获取类的字段

  1. getDeclaredFields()

    返回类型是 Field 类型的数组

    1
    2
    3
    4
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
    System.out.println(field.getName());
    }

    image-20240712143925106

  2. getFields();

    只获取 public 字段(如果有父类的话也可以获得父类的所有 public 字段

    1
    Field[] fields = clazz.getFields();

    image-20240712144301952

​ 继承 Person 类后

image-20240712150114011

​ 如果想获得父类所有字段怎么办?可以先获取父类的 Class 对象,再获取其 getDeclaredFields()

1
Field[] fields = clazz.getSuperclass().getDeclaredFields();

image-20240712150508941

  1. getDeclaredField()

    获取指定字段

    1
    Field field = clazz.getDeclaredField("name");

要获取字段值的话可以用 get 方法,传递的参数是类的实例化对象

1
2
3
Field field = clazz.getDeclaredField("name");
User user = new User("she11F", 18);
System.out.println(field.get(user));

如果是想获得静态变量的话传 null 即可

1
2
Field field = clazz.getDeclaredField("sex");
System.out.println(field.get(null));

访问私有变量的话要设置访问权限为可达

1
2
3
Field field = clazz.getDeclaredField("email");
field.setAccessible(true);
System.out.println(field.get(null));

当然出了获取值也可以设置值,get 换成 set 即可

调用方法

getDeclaredMethods

同理,这样就能获取所有方法名

1
2
3
4
Method[] methods = clazz.getDeclaredMethods();
for(Method method : methods){
System.out.println(method.getName());
}

来看一下一般方法怎么通过反射调用吧。

1
2
3
4
Class<?> clazz = Class.forName("User");
User user = new User();
Method method = clazz.getMethod("getName");
System.out.println(method.invoke(user));

获取方法后用 invoke() 即可执行方法,静态方法或者私有方法的话和获取字段值一样的规则

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

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

如果这个方法是一个静态方法,那么第一个参数是类

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

method.invoke([1], [2], [3], [4]…) 。

1
2
3
4
// 静态
System.out.println(method.invoke(null));
// 私有
method.setAccessible(true);

那么如何传参呢

  1. 获取方法时,第二个参数设置为待调用函数的参数的 Class 对象

    1
    Method method = clazz.getMethod("getName",String.class);
  2. 调用 invoke 时,即可传递参数

    1
    2
    Method method = clazz.getMethod("getName",String.class);
    System.out.println(method.invoke(null,"makka_pakka"));

0x04 反射创建类的实例

在反射中,我们通常获取类的构造器来创建实例。

1
Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class);

实例化

1
Object obj = constructor.newInstance("she11F", 18);

获得变量和调用实例方法

其实和之前是一样的,唯一区别就是对象都换成了 newInstance 实例化出的对象。

1
2
3
4
5
6
7
Class<?> clazz = Class.forName("User");
Constructor constructor = clazz.getConstructor(String.class, int.class);
Object obj = constructor.newInstance("she11F", 18);
Field field = clazz.getDeclaredField("name");
System.out.println(field.get(obj));
Method method = clazz.getMethod("getName",String.class);
System.out.println(method.invoke(obj,"makka_pakka"));

我们可以试着用反射来构造命令执行

1
2
Runtime runtime = Runtime.getRuntime();
runtime.exec("calc");

换成反射写法

1
2
Class<?> clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(),"calc");

这里直接 clazz.newInstance() 只有有无参构造器的时候才能这样写,有参的话就可以用 Constructor(当然无参也可以用

可惜报错了

image-20240715135230230

因为 Runtime 的构造方法是私有的,而 newInstance 不能调用私有的构造方法。正确的做法是使用 Runtime 类提供的静态方法 getRuntime() 来获取唯一的 Runtime 实例,然后调用 exec 方法:

1
2
Class<?> clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"calc");