目录
我们还是从原理到代码,来详细理解java的反射机制。
Java反射原理
(一) *.class文件是什么?
众所周知, Java 是一种平台无关的语言。实现这一步的原理就是:通过javac编译器将 *.java代码编译成字节码(*.class) 然后通过JVM来加载。
除了java 像是JRuby等语言,也是通过自己语言的编译器,将其转化成.class文件。然后通过jvm加载运行。
那么大家可能会想到我们的C语言。从开始的.c文件到最后的.out文件。
其实。.class文件是二进制的字节码。由JVM识别 分析 执行。
.out 文件是二进制的机器指令。由操作系统加载运行。
所以我们只要有了JVM,无论我们是windows unix 或者mac系统,都可以运行咱们的程序。
(二) 反射的原理
咱们说了那么多字节码.class文件。跟咱们反射有什么关系呢?
我们反射的第一步就是要获取到咱们想要的Class类。这跟咱们的.class文件就有很大关系了。
我们获取Class类一般有三种方法:
1. Class.forName(className)
2. 类名.class
3. this.getClass()
其实这三种方法还是有些区别的。Class.forName会触发类的静态初始化块的执行。其他两种不会。
反射的应用场景
使用场景一:编程工具 IDEA 或 Eclipse 等,在写代码时会有代码(属性或方法名)提示,就是因为使
用了反射;
使用场景二:很多知名的框架,如 Spring、MyBatis 等,为了让程序更优雅更简洁,也会使用到反射。
例如,Spring 可以通过配置来加载不同的类,调用不同的方法,代码如下所示:
"'"java
<bean id="person" class="com.spring.beans.Person" init-method="initPerson">
</bean>
"'"
例如,MyBatis 在 Mapper 使用外部类的 Sql 构建查询时,代码如下所示:
"'"java
@SelectProvider(type = PersonSql.class, method = "getListSql")
List<Person> getList();
class PersonSql {
public String getListSql() {
String sql = new SQL() {{
SELECT("*");
FROM("person");
}}.toString();
return sql;
}
}
"'"
使用场景三:数据库连接池,也会使用反射调用不同类型的数据库驱动,代码如下所示:
"'"java
String url = "jdbc:mysql://127.0.0.1:3306/mydb";
String username = "root";
String password = "root";
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(url, username, password);
"'"
but...我主要还是想说反射跟反序列化漏洞的关系。我们先说完反射再说。(这里就cue一下我的另一个文章)。
Class的三种获取方法:
public class Apple {
private int price;
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public static void main(String[] args) throws Exception{
Apple apple = new Apple();
Class clz = Class.forName("com.text.reflect.Apple");
Class clz1 = Apple.class;
Class clz2 = apple.getClass();
}
}
这里注意第三种创建Class的方法是需要一个实例的。
好啦,我们获得了我们心心念念的Class类。
通过这个Class类。我们可以获得其对应的:
Field类(表示类的属性)
Method类(表示类的方法)
Constructor类(表示类的构造)
怎么获得这三个类呢?
通过Class实例的 .getField(String name) .getFields()
.getDeclaredField(String name) .getDeclaredFields()方法来获得
这里添加了Declared的是可以获取包括私有在内的所有属性(方法,构造),而没有Declared的方法是只能获取公有的。在使用Declared的方法后面要加一行.setAccessible(true);来设置权限。
比如咱们Apple类中有一个属性是 private int price;
我们就可以通过 Field ff = clz.getDeclaredField("price");
ff.setAccessible(true); 来获取这个私有属性。
把Field改成Method和Constructor 就是其他两类的获取方法。值得一提的是:这两类
getMethod(String name,Class...<?>parameterTypes)
getConstructor(Class...<?>parameterTypes)
传入的参数有些不同,不过我们通过下面的这个例子能很好理解。
public class Apple {
private int price;
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public static void main(String[] args) throws Exception{
//正常调用
Apple apple = new Apple();
apple.setPrice(5);
System.out.println("Apple Price:" + apple.getPrice());
//使用反射调用
Class clz = Class.forName("com.text.reflect.Apple");
//Class clz1 = Apple.class;
//Class clz2 = apple.getClass();
//获取名叫setPrice的方法,并且传入的参数类型为int
Method setPriceMethod = clz.getMethod("setPrice", int.class);
//获取无参的构造方法
Constructor appleConstructor = clz.getConstructor();
//通过这个构造方法创建一个appleObj对象
Object appleObj = appleConstructor.newInstance();
//通过一个invoke传入 对象和参数 给这个方法
setPriceMethod.invoke(appleObj, 14);
Method getPriceMethod = clz.getMethod("getPrice");
System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));
}
}
咱们看完这个代码之后。我来梳理一下。
1.我们正常创建一个Apple对象。就是Apple apple = new Apple();
而我们通过反射获取一个对象。我们可以通过:
Apple apple = (Apple)clz.getConstructor().newInstance();
或者Apple apple = (Apple)clz.newInstance();
我们一般还是用Constructor的newInstance方法来构造,毕竟Constructor就是为了构造嘛。Class 对象则只能使用默认的无参数构造方法。而Constructor有参无参都可以,而且从JDK9开始就弃用Class直接构造了。因为不是很灵活。
值得一提的是,newInstance的效率不如new,也很好理解,比较多经过一层反射嘛,需要运行时动态地获取和调用方法或构造函数。
2.我们正常调用 apple.setprice(5);
而我们反射就需要 先通过
Method setPriceMethod = clz.getMethod("setPrice", int.class); 获取名叫setPrice的方法
setPriceMethod.invoke(appleObj, 14);
再通过Invoke将我们需要调用setPrice方法的appleObj对象,以及setPrice所需的参数传入进去。
这里Invoke就是Method中的方法 专门实现方法功能的。
那么,咱们绕那么大圈,意义是什么呢?
除了实现依赖注入(DI)和控制反转(IoC)。许多依赖注入框架(如 Spring)使用反射来动态地将依赖注入到对象中。实现调试工具和测试框架等许多功能以外。
其实我们发现,我们正向调用这些方法,其实我们是知道我们这个Apple类有什么方法。
而我们通过反射,只需要一个class字节码的名字,就可以知道里面所有的属性和方法。
// 获取所有声明的方法
Method[] declaredMethods = clz.getDeclaredMethods();
for (Method method : declaredMethods) {
System.out.println("Declared Method: " + method.getName());
}
在我们反序列化漏洞中。我们可以通过反射定制需要的对象。
通过invoke调用除了同名函数以外的函数 (invoke可以通过字符串来调用 灵活) 通过Class类创建对象,引入不能序列化的类(Class类可以序列化,可以通过这个来操作不能实例化的类) eg:Runtime 不能实例化 Runtime.class 中的 getruntime可以作为一个参数传入一个类中。
还有比如动态代理InvocationHandler中的invoke,有函数调用的时候自动执行。就类似于readObject,在反序列化的时候自动执行。
这篇文字主要还是告诉大家反射的基础知识。有错误希望大家指正。