开启 java 安全学习第三站,java 反射
0x01 反射的意义
存在即合理,那么为什么要有反射呢?反射能够使得程序在运行时动态地操作类和对象,包括创建对象,访问字段,调用方法等等。这样说可能稍许抽象,我们先反过来理解,正射是不是就是静态地操作类和对象,也就是说正常情况下我们知道一个类的样子,直接实例化即可。
1 | Apple apple = new Apple(); //直接初始化,「正射」 |
这种直接访问的方式清晰且直观,但在有些场景中需要动态调用调用操作这些成员,比如在程序运行时,根据数据库提供的类名或者方法名,或者基于字符串来动态实例化对象调用方法时就不适用了,就得用到 java 的反射机制。spring/spring boot 或者 mybatis 就应用了大量反射机制,注解以及动态代理也运用到了反射机制。
0x02 反射原理
反射的实现原理其关键在一个特殊的对象,我们称之为类对象,即 Class 对象。Class 对象是由 Java 虚拟机 JVM 在加载类时自动创建的,用于存储类的信息,通过它就能访问类的结构,以及对类本身以及它的实例进行操作。
JVM 创建 Class 的过程是这样的,当我们编写一个 .java 类并完成编译后,编译器将其转化为字节码并存储在 .class 文件中。接下来在类的加载中,虚拟机利用 Class Loader 读取 class 文件,将其中的字节码加载到内存中并利用其携带的信息来创建相应的 Class 对象。我们知道每个类在 JVM 中只加载一次,所以每个类都对应唯一的 Class 对象。

有一块镜子我们就能反射光线。那么在我看来 java 的反射中镜子就是 Class 对象。
0x03 反射实现
获取 Class 对象
使用类字面常量,就是类的名称 + .class
这是最直接的方式,因为在编译时就确定了具体的类,仍然属于静态引用
1
Class<User> userClass = User.class;
因为在编译时就确定了,已经明确了是个 User 类 的 Class ,所以泛型直接给 User。
使用对象的 getClass 方法,如果你已知一个对象,那么可以调用它的 getClass 方法来获取它的 Class 对象。
1
2User user = new User("she11F",18);
Class<?> clazz = user.getClass();这里泛型用通配符是因为这个 Class 对象是在程序运行时从 user 实例获取的, user 实例的具体类型只能在程序运行时才确定,所以我们在编译阶段无法确定 Class 对象的具体类型,所以泛型是通配符。
Class 类的 forName 静态方法
这种方法用于程序运行时动态加载指定的类,并返回该类的 Class 对象实例。同常用于类名在编译时不可知的场景中
1
Class<?> clazz = Class.forName("User");
这种方法会触发类的初始化,静态块会被执行。


获取类的字段
getDeclaredFields()
返回类型是 Field 类型的数组
1
2
3
4Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName());
}
getFields();
只获取 public 字段(如果有父类的话也可以获得父类的所有 public 字段
1
Field[] fields = clazz.getFields();

继承 Person 类后

如果想获得父类所有字段怎么办?可以先获取父类的 Class 对象,再获取其 getDeclaredFields()
1 | Field[] fields = clazz.getSuperclass().getDeclaredFields(); |

getDeclaredField()
获取指定字段
1
Field field = clazz.getDeclaredField("name");
要获取字段值的话可以用 get 方法,传递的参数是类的实例化对象
1 | Field field = clazz.getDeclaredField("name"); |
如果是想获得静态变量的话传 null 即可
1 | Field field = clazz.getDeclaredField("sex"); |
访问私有变量的话要设置访问权限为可达
1 | Field field = clazz.getDeclaredField("email"); |
当然出了获取值也可以设置值,get 换成 set 即可
调用方法
getDeclaredMethods
同理,这样就能获取所有方法名
1 | Method[] methods = clazz.getDeclaredMethods(); |
来看一下一般方法怎么通过反射调用吧。
1 | Class<?> clazz = Class.forName("User"); |
获取方法后用 invoke() 即可执行方法,静态方法或者私有方法的话和获取字段值一样的规则
invoke 的作用是执行方法,它的第一个参数是:
如果这个方法是一个普通方法,那么第一个参数是类对象
如果这个方法是一个静态方法,那么第一个参数是类
这也比较好理解了,我们正常执行方法是 [1].method([2], [3], [4]…) ,其实在反射里就是
method.invoke([1], [2], [3], [4]…) 。
1 | // 静态 |
那么如何传参呢?
获取方法时,第二个参数设置为待调用函数的参数的 Class 对象
1
Method method = clazz.getMethod("getName",String.class);
调用 invoke 时,即可传递参数
1
2Method 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 | Class<?> clazz = Class.forName("User"); |
我们可以试着用反射来构造命令执行
1 | Runtime runtime = Runtime.getRuntime(); |
换成反射写法
1 | Class<?> clazz = Class.forName("java.lang.Runtime"); |
这里直接 clazz.newInstance() 只有有无参构造器的时候才能这样写,有参的话就可以用 Constructor(当然无参也可以用
可惜报错了

因为 Runtime 的构造方法是私有的,而 newInstance 不能调用私有的构造方法。正确的做法是使用 Runtime 类提供的静态方法 getRuntime() 来获取唯一的 Runtime 实例,然后调用 exec 方法:
1 | Class<?> clazz = Class.forName("java.lang.Runtime"); |



