关于反射
通过Class实例获取class信息的方法称为反射。Java的反射机制提供为Java工程师的开发提供了相当多的便利性,同样也带来了潜在的安全风险。反射机制的存在使得我们可以越过Java本身的静态检查和类型约束,在运行期直接访问和修改目标对象的属性和状态,极有可能给恶意代码提供可乘之机。想要揭开JAVA反序列化漏洞的秘密,就必须学习JAVA的反射机制。
获取Class
JVM在执行过程中,每读到一个类class
,就会在内存中为其创建一个Class实例
,这里的Class
其实也是一种类,而Class实例
就是这种Class类
的对象,只不过它记录的是对应类的类名、包名、父类、实现的接口、所有方法、字段等信息,可以说是类的个人资料。
比如创建了个Person类
Person Wkai =new Person()
当JVM读取到这行时,首先会读取Person.class
,然后在内存为其创建个Person
的Class实例
并关联起来,这个实例是由JVM
内部创建的,如果我们查看源码,可以发现Class
的构造方法是private
,只有JVM
才能创建,无法人为地创建。
所以每个Class实例都对应着一种数据类型
───────────────────────────┐
│ Class Instance │──────> String
├───────────────────────────┤
│name = "java.lang.String" │
├───────────────────────────┤
│package = "java.lang" │
├───────────────────────────┤
│super = "java.lang.Object" │
├───────────────────────────┤
│interface = CharSequence...│
├───────────────────────────┤
│field = value[],hash,... │
├───────────────────────────┤
│method = indexOf()... │
└───────────────────────────┘
那么如何获取一个Class实例
呢?
有三种方法
第一种:直接通过一个class
的静态变量class
获取
Class cls = Person.class;
第二种:如果已经有了一个实例变量,可以通过该实例变量的getClass()
方法获取
Person Wkai =new Person()
Class cls = Wkai.getClass();
第三种:如果知道一个class的完整类名,可以通过静态方法Class.forName()
获取
Class cls = Class.forName("java.lang.String");
小结
JVM
为每个加载的class
及interface
创建了对应的Class实例
来保存class
及interface
的所有信息;
获取一个class
对应的Class实例
后,就可以获取该class
的所有信息;
通过Class实例
获取class
信息的方法称为反射(Reflection)
;
JVM
总是动态加载class
,可以在运行期根据条件来控制加载class
。
获取字段和值
对任意的一个Object实例,只要我们获取了它的Class,就可以获取它的一切信息。
Class提供了以下几种方法来获取字段
Field getField(name)
:根据字段名获取某个public的field(包括父类)Field getDeclaredField(name)
:根据字段名获取当前类的某个field(不包括父类)Field[] getFields()
:获取所有public的field(包括父类)Field[] getDeclaredFields()
:获取当前类的所有field(不包括父类)
首先我们来看看
getField(name)
和getDeclaredField(name)
他们的区别是这样的
getDeclaredField
是可以获取一个类的所有字段,getField
只能获取类的public 字段
.
那么有人问了
Field
又是啥玩意- 具体来说
Field
也是一种类,只不过这种类实例化后的对象是用来记录某个字段所有的值
- 具体来说
具体用法是这样的
- 首先我定义了一个
Person类
- 通过
Person类
创建了个子类Student
,并且拓展了两个字段 - 那么我通过这个
student类
获取一个关联它的Class实例stdClass
- 接着通过这个实例
stdClass
,来获取student类的指定字段的Field实例 - 获取方式就是
getField(name)
和getDeclaredField(name)
- 首先我定义了一个
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public字段"score":
System.out.println(stdClass.getField("score"));
// 获取继承的public字段"name":
System.out.println(stdClass.getField("name"));
// 获取private字段"grade":
System.out.println(stdClass.getDeclaredField("grade"));
}
}
class Student extends Person {
public int score;
private int grade;
}
class Person {
public String name;
}
- 运行结果
public int Student.score
public java.lang.String Person.name
private int Student.grade
再看看
Field类
Field类
中也定义了许多方法getName()
:返回字段名称,例如,”name”;getType()
:返回字段类型,也是一个Class实例,例如,String.class;getModifiers()
:返回字段的修饰符,它是一个int
,不同的bit
表示不同的含义。get()
:获取指定实例在Field实例
中的值
接着看看代码
public final class String {
private final byte[] value;
}
Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class [B 表示byte[]类型
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false
- 这里先是定义了一个
String类
- 然后调用
String类
的Class实例
里的获取字段名函数返回了一个Field类型实例 - 通过Field类型实例的
getName()
方法可以获取字段名 - 通过Field类型实例的
getType()
方法可以获取字段类型 - 通过
getModifiers()
函数可以判断字段是public还是Private等类型的字段
- 那么如何获取字段的值呢
- 对于一个Person实例,我们可以先拿到name字段对应的Field,再获取这个实例的name字段的值
public class Main {
public static void main(String[] args) throws Exception {
Object p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true); //这里要注意通过main类是无法直接访问private类型的字段值的,通过setAccessible(true)可以设置任意访问
Object value = f.get(p); //这里不知道为啥返回的字段值类型得是Object
System.out.println(value); // "Xiao Ming"
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
- 这里先是通过Person实例p获取Person类的Class实例
- 然后通过Person类的Class实例获取关于字段name的Field实例
- 再通过Field实例的
get()
函数来获取指定实例对应的字段值
既然可以查看字段值,那么可以设置字段值吗
- 通过Field实例既然可以获取到指定实例的字段值,自然也可以设置字段的值。
设置字段值是通过
Field.set(Object, Object)
实现的过程和查询字段值类似,只不过调用的方法变成了
set()
public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person("Xiao Ming");
System.out.println(p.getName()); // "Xiao Ming"
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true); //同样的修改private类型的字段值也是需要设置这一项
f.set(p, "Xiao Hong");
System.out.println(p.getName()); // "Xiao Hong"
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
总结
- Java的反射API提供的Field类封装了字段的所有信息:
- 通过Class实例的方法可以获取Field实例:
getField()
,getFields()
,getDeclaredField()
,getDeclaredFields()
; - 通过Field实例可以获取字段信息:
getName()
,getType()
,getModifiers()
; - 通过Field实例可以读取某个对象的字段
get()
,如果存在访问限制,要首先调用setAccessible(true)
来访问非public字段。同样设置某个对象的字段set()
,也要调用setAccessible(true)
- 通过反射读写字段是一种非常规方法,它会破坏对象的封装。
调用方法
上面说到Class实例可以返回一个Field的实例
而Class实例除了记录字段,还记录了方法
那么可不可以返回关于方法的信息,并且直接调用方法呢
Class类提供了以下几个方法来获取Method:
Method getMethod(name, Class...)
:获取某个public的Method(包括父类)Method getDeclaredMethod(name, Class...)
:获取当前类的某个Method(不包括父类)Method[] getMethods()
:获取所有public的Method(包括父类)Method[] getDeclaredMethods()
:获取当前类的所有Method(不包括父类)
示例代码
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public方法getScore,参数为String:
System.out.println(stdClass.getMethod("getScore", String.class));
// 获取继承的public方法getName,无参数:
System.out.println(stdClass.getMethod("getName"));
// 获取private方法getGrade,参数为int:
System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
}
}
class Student extends Person {
public int getScore(String type) {
return 99;
}
private int getGrade(int year) {
return 1;
}
}
class Person {
public String getName() {
return "Person";
}
}
Method也是一种类,本身包含了返回对应类方法信息的方法
getName()
:返回方法名称,例如:”getScore”;getReturnType()
:返回方法返回值类型,也是一个Class实例,例如:String.class;getParameterTypes()
:返回方法的参数类型,是一个Class数组,例如:{String.class, int.class};getModifiers()
:返回方法的修饰符,它是一个int,不同的bit表示不同的含义。invoke(指定实例, 调用方法的参数)
:直接调用方法,必须指定实例,除非调用的方法是静态的
public class Main { public static void main(String[] args) throws Exception { // String对象: String s = "Hello world"; // 获取String substring(int)方法,参数为int: Method m = String.class.getMethod("substring", int.class); // 在s对象上调用该方法并获取结果: String r = (String) m.invoke(s, 6); // 打印调用结果: System.out.println(r); } }
- 当访问静态方法的时候,是没有实例给你指定的,所以
invoke()
第一个参数为null
就好了
public class Main { public static void main(String[] args) throws Exception { // 获取Integer.parseInt(String)方法,参数为String: Method m = Integer.class.getMethod("parseInt", String.class); // 调用该静态方法并获取结果: Integer n = (Integer) m.invoke(null, "12345"); // 打印调用结果: System.out.println(n); } }
- 类似的,存在私有变量,同时也存在私有方法,访问私有方法的时候同样需要设置
setAccessible(true)
public class Main { public static void main(String[] args) throws Exception { Person p = new Person(); Method m = p.getClass().getDeclaredMethod("setName", String.class); m.setAccessible(true); //不设置就会报错 m.invoke(p, "Bob"); System.out.println(p.name); } } class Person { String name; private void setName(String name) { this.name = name; } }
- 最后再来看看这段代码
public static void main(String[] args) throws Exception {
Object runtime=Class.forName("java.lang.Runtime") .getMethod("getRuntime",new Class[]{}).invoke(null);
//因为Runtime类的实例是不能由我们创建的,所以需要通过getRuntime返回一个实例
Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(runtime,"calc.exe");
//需要指定实例,才能调用方法
}
之前一直疑惑,为啥我调用个计算器需要这么多步骤,感觉怪怪的,给人一种多此一举的感觉
原来是因为Runtime类的实例必须是JVM虚拟机创建的,不能人为地创建,对比上边的Person的Method实例,可以直接创建个Person类的对象,然后invoke()马上就能用,而Runtime类还得通过
getRuntime()
来返回一个实例再来说说什么是Runtime类
该类主要代表了应用程序的运行环境。一个Runtime就代表一个运行环境。
常用的方法有:
getRuntime()
:该方法用于返回当前应用程序的运行环境对象。exec(String command)
:该方法用于根据指定的路径执行对应的可执行文件。
参考
[1]https://www.freebuf.com/vuls/170344.html
[2]https://www.liaoxuefeng.com/wiki/1252599548343744/1255945147512512