2-java安全基础——动态类加载(反射)
Posted songly_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2-java安全基础——动态类加载(反射)相关的知识,希望对你有一定的参考价值。
1. 类加载
当程序想要使用一个类时,如果这个类还没加载到内存中,jvm会负责把这个类加载到内存中,那么jvm把类加载到内存中做了哪些事情?一般jvm会按照类加载,类连接,类初始化这三个步骤把这个类加载到内存,完成初始化工作,但这里我们暂时不讨论类连接,类初始化。
什么是类加载?java编译器将写好的.java源文件编译成.class⽂件,然后jvm再将.class文件加载到内存中并为之创建一个java.lang.Class对象的过程称为类加载。
2. java程序的三个阶段
当一个程序文件从磁盘加载到内存运行起来时会经历源码(编译)阶段,Class类对象阶段,Runtime运行阶段,如下所示
这三个阶段我们最熟悉的是Runtime运行阶段,对于源码(编译)阶段和Class类对象阶段可能不是很熟悉。
源码(编译)阶段:这个阶段通常是把一个类的.java文件编译成.class字节码文件的过程(默认一个类对应一个.java源文件)。
Class类对象阶段:这个阶段是将一个类的.class字节码文件加载到内存并创建一个与之对应的Class对象(这个过程也称为反射),jvm的类加载器ClassLoader把.class字节码文件加载进内存后,会把.class字节码文件中的成员属性,构造,成员方法抽取出来封装成一个Class对象,成员属性封装成Field[]数组来表示,构造函数封装成Construction[]来表示,成员方法封装成Method[]数组来表示,但实际上Class类内部封装了很多东西,这里我们只关注这三个。
RunTime运行时阶段:在程序运行阶段,例如使用常规操作new创建Student类的实例对象时,jvm会使用类加载器把.class文件加载到内存中并为之生成一个Class对象,然后这个Class对象与stu1对象有一个映射关系,也就是说,在RunTime运行时阶段只要访问了Person类的内容就会触发Class类对象阶段。
解释一下Class类对象阶段中的Class对象:
当jvm加载类时会在堆内存中产生一个Class类型的对象(一个类只有一个Class对象,并且Class对象的类型为Class,注意:是Class类,不是class关键字),这个对象包含了类内部的完整结构信息(成员属性,构造,成员方法),通过这个Class对象可以得到类内部结构信息,换句话说,类对象就像是一面镜子一样,透过这个镜子看到类的结构,因此称为反射。
3. 反射机制
通过反射可以在程序运行期间动态获取任意一个类的内部结构信息(例如成员变量,构造器,成员方法等等),并能操作对象的属性及方法。
跟反射相关的类:
java.lang.Class表示一个类,Class对象表示某一个类在内存中创建的对象,该类定义了Class对象的相关方法
java.lang.reflect.Field表示类的成员变量,field对象表示某个类的成员变量,Field类中定义了成员变量操作相关的方法
java.lang.reflect.Constructor表示类的构造方法,constructor对象表示某个类的构造器,该类中定义了跟构造方法操作相关的方法
java.lang.reflect.Method表示类的方法,method对象表示某个类的方法,该类定义了跟类的方法操作的相关方法
来看一个简单的反射示例:
package com.test;
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Reflection_Test1 {
public static void main(String[] args)throws Exception{
//为Person类创建一个类对象com.test.Person,对象的类型为class
Class<?> aClass = Class.forName("com.test.Person");
System.out.println(aClass);
//获取类对象aClass的公有成员属性
Field[] fields = aClass.getFields();
//获取类对象aClass的公有构造器
Constructor<?>[] constructors = aClass.getConstructors();
//获取类对象aClass的公有成员方法
Method[] methods = aClass.getMethods();
//获取当前Class对象的类加载器
ClassLoader classLoader = aClass.getClassLoader();
System.out.println(classLoader);
}
}
Reflection_Test1类可以通过Class类的静态方法forName方式把Person类加载到内存并为Person类创建一个Class对象,然后就可以通过Class对象获取Person类的成员属性,构造,成员方法,类加载器。
4. Class类
java程序中每一个类的.class文件在被jvm通过类加载器加载到内存中都会创建一个Class对象,并且Class对象是Class类的实例,也就是说,Class类可以用于描述所有类。
前面在学习类加载的时候说过,当使用new操作创建一个对象时,jvm会通过类加载器把该类的.class文件加载到内存中并为之生成一个Class对象,以Student类为例,可以看到new Student()实际上是调用了ClassLoader类加载器的loadClass方法,并且loadClass方法中的参数name的值就是Student类的全路径,包括反射中Class.forName("com.test.Student")方式同样使用了ClassLoader类加载器。
可能有同学好奇,如果创建多个Student对象那到底存在几个Class对象呢?
当使用new关键字创建stu1和stu2两个student对象时,只会在第一次new操作调用ClassLoader类加载器创建Class对象,接下来的stu2对象则会直接调用构造进行初始化(大家可以自己跟踪调试一下代码),也就是说一个类只会创建一个Class对象,并且Class对象是由jvm来创建的,而stu1和stu2对象是由程序创建的,getClass方法可以在Runtime阶段获取对象所属的Class对象,通过getClass方法发现其实stu1和stu2都是属于同一Class对象class com.test.Student。
获取Class对象的方式:
public static void main(String[] args) throws Exception{
//1. Class.forName()方式
Class<?> stuClass1 = Class.forName("com.test.Student");
System.out.println(stuClass1);
//2. 类名.class方式
Class stuClass2 = Student.class;
System.out.println(stuClass2);
//3. 对象.getClass()方式,需要先创建一个对象
Student student = new Student();
Class stuClass3 = student.getClass();
System.out.print(stuClass3);
}
- 如果我们知道一个类的全路径,且该类在类路径下,可以通过Class类的forName静态方法获取,不过这种方式一般用于加载类,读取类全路径,也就是说在Class类对象阶段可以通过这种方式获取Class对象。
- 每一个类都用一个class属性,通过类名.class也可以获取该类的Class对象,通常在Runtime运行阶段使用这种方式。
- 通过对象.getClass()方法也可以获取到对象的Class对象的类加载器,也是在Runtime运行阶段使用这种方式。
5. 动态加载
java中的反射机制可以实现类的动态加载,但在学习动态加载之前,先来了解一下静态加载,如下所示
Reflection_Test2.java文件中使用new关键字创建了一个test对象,然后在build的时候,程序报了一个找不到Test类的错误,因为java编译器在编译时会优先编译所依赖的类,Reflection_Test2类中使用了一个Test类创建了一个test对象,因此会优先编译Test类,但是在编译过程中没有找到Test类就会报错。
再来看反射的动态加载
可以看到Reflection_Test2.java源文件中通过反射加载了一个Test类,程序也输出了内容,接着执行forName方法调用类加载器去加载Test类
这里会调用ClassLoader类加载器的loadClass方法去加载Test类
loadClass方法内部会调用findLoadedClass方法查找要加载的Test类,但由于Class对象c的内容为null会抛出ClassNotFoundException异常。
最终程序会在控制台输出ClassNotFoundException异常信息
当使用javac命令在编译Reflection_Test2.java源文件时并不会去加载Test类,而是在运行该程序并执行到 Class.forName("com.test.Test")这行代码的时候jvm的类加载器才会去加载Test类,并且程序运行期间并不会立即报错,当类加载器没有找到要加载的类时就会抛出异常错误信息,这就是为什么反射可以实现类的动态加载,因为反射加载类的过程都是在程序运行期间完成的。
总结:所谓静态加载时,程序在编译阶段会加载所依赖的相关类,如果找不到就报错。而动态加载是程序在运行(Runtime)阶段中才会加载类,但也不会立即报错,只有在要使用这个类的时候找不到才报错,动态加载降低了类的依赖性。
因此我们可以总结在RunTime运行阶段时类加载时机如下:
使用new关键字创建对象
访问类或者接口的类变量,或者为该类变量赋值
使用反射方式来强制创建某个类或接口对应的java.lang.Class对象,例如Class.forName()
初始化某个类的子类
类加载器的种类
Bootstrap ClassLoader(启动类加载器):用于将jvm识别的类加载进来
Extension ClassLoader(扩展类加载器):用于加载java的扩展库
Applicaiton ClassLoader(应用程序类加载器):一般用于加载开发者自定义编写的类
Custom ClassLoader(自定义类加载器):用户自定义实现的类加载器,很少用到
6. 类加载过程
其实jvm的类加载过程远没有之前所说的name简单,实际上类加载过程分为:加载,链接(验证,准备,解析),初始化这三个阶段。
加载阶段:jvm类加载器会把.class文件加载到内存中并为之创建一个Class对象
链接阶段:
验证阶段是对.class文件进行一些安全验证,例如文件数据格式,元数据,字节码验
准备阶段是为类变量分配内存,例如对静态变量进行默认初始化,常量则直接分配内存,来看下面这段代码:
public class Test {
//成员变量
public int num1 = 10;
//静态变量
public static int num2 = 20;
public static final int NUM3 = 30;
}
Test类中定义了3个变量,当jvm加载Test类时,在准备阶段会为静态变量和常量分配内存,并且静态变量会默认初始化为0,常量NUM3会直接分配到内存,值为40,并且常量NUM3的值在内存中具有只读属性。而成员变量是属于实例对象的,因此只有在创建实例对象的时候才会通过构造函数初始化,分配内存。
解析阶段是主要为.class文件中具体的类名,变量名,方法名的符号引用解析为具体的内存地址来直接引用
初始化阶段:主要是执行Class类的构造器进行初始化的过程,注意这里只初始化类的静态成员,例如静态变量和静态方法,静态代码块,并且会优先初始化父类。到这一步才开始真正执行类中的代码,这个阶段会有clinit方法执行。
链接阶段中的这三个阶段属于类加载阶段,并且由jvm控制,还没有到Runtime运行阶段,因此这里不会涉及到实例对象的成员变量和方法的操作。
参考资料:
以上是关于2-java安全基础——动态类加载(反射)的主要内容,如果未能解决你的问题,请参考以下文章