类加载器负责把.class文件加载到内存中并生成对应的Class类对象,本小节将讲解类加载器的种类、工作原理以及如何自定义类加载器。
19.2.1类加载机制
类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个Class类的对象实例。一旦一个类被载入内存中,同个类就不会被再次载入了。那么,如何样才算“同一个类”呢?正如一个对象有一个唯一的标识一样,一个载入内存中的类也有一个唯一的标识。 在Java语言中,一个类用包名以及自身的类名作为唯一标识,但在类加载机制中,一个类用包名、类名和其类加载器作为唯一标识。例如,如果在pg的包中有一个名为Person的类,它被类加载器ClassLoader对象k1加载,则该Person类对应的Class对象在表示为(Person、pg、k1)。 如果用ClassLoader对象k2加载这个类,则这个类的Class类对象被表示为(Person、pg、k2),在虚拟机看来,(Person、pg、k1) 和(Person、pg、k2) 是不同的,它们是互不兼容的。
Java虚拟机刚启动时,会有三个类加载器组成一个类加载器组,这个组中的成员包括:
- Bootstrap ClassLoader:根类加载器
- Extension ClassLoader:扩展类加载器
- Application ClassLoader:应用程序类加载器
下面分别介绍这些类加载器的作用。Bootstrap ClassLoader被称为根类加载器、引导类加载器或原始类加载器,它负责加载Java的核心类。Extension ClassLoader被称为扩展类加载器,它负责加载JAVA_HOME\lib\ext目录中的、或者通过java.ext.dirs系统变量指定路径中的类。Application ClassLoade被称为应用程序类加载器,它负责加载程序员所编写的那些类。程序员可以通过
除了Java虚拟机所提供的三个类加载器之外,用户也可以自定义类加载器。用户自定义的类加载器位于以上所有类加载器的下级,因此由虚拟机提供的类加载器以及用户自定义的类加载器可以形成一个自上而下的体系,如图19-2所示。
图19-2 4种类加载器的层次结构
在这个体系中,一个类加载器的上级类加载器也被称为“父类加载器”。类加载的工作方式分为以下三种:
- 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
- 父类委托:所谓父类委托,也称双亲委派,是指先让上一级类加载器试图加载该Class,只有在上级类加载器无法加载该类时才尝试从自己的类路径中加载该类。
- 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,类加载器才会读取该类对应的二进制数据并将其转换成Class对象存入缓存区中。
类加载器加载Class大致要经过如下8个步骤:
- 检测此Class是否载入过(即在缓存区中是否有此Class),如果有则直接进入第8步,否则接着执行第2步
- 如果父类加载器不存在(如果没有父类加载器,则要么parent一定是根类加载器,要么本身就是根类加载器),则跳到第4步执行;如果父类加载器存在,则接着执行第3步
- 请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步
- 请求使用根类加载器来载入目标类,如果成功载入则跳到第8步,否则跳到第7步
- 当前类加载器尝试寻找Class文件(从与此ClassLoader相关的类路径中寻找),如果找到则执行第6步,如果找不到则跳到第7步。
- 从文件中载入Class,成功载入后跳到第8步。
- 抛出ClassNotFoundException异常。
- 返回对应的java.lang.Class对象。
其中,第5、6步允许重写ClassLoader的findClass()方法来实现自己的载入策略,甚至重写loadClass()方法来实现自己的载入过程。
实际开发过程,程序员可以通过ClassLoader类的getSystemClassLoader()静态方法获得应用程序类加载器的引用。下面的【例19_03】展示了如何获得应用程序类加载器的引用并通过这个引用获得其上级类加载器。
【例19_03 应用程序类加载器】
Exam19_03.java
import java.net.URL;
import java.util.Enumeration;
public class Exam19_03 {
public static void main(String[] args) {
try{
//获得应用程序类加载器
ClassLoader appLoader = ClassLoader.getSystemClassLoader();
System.out.println("应用程序类加载器:"+appLoader);
Enumeration<URL> em1 = appLoader.getResources("") ;
System.out.print("应用程序类加载器加载路径:");
while(em1.hasMoreElements()){
System.out.println (em1.nextElement()) ;
}
//获得应用程序类加载器的上级类加载器(即扩展类加载器)
ClassLoader extLoader = appLoader.getParent();
System.out.println("扩展类加载器:"+extLoader);
System.out.print("扩展类加载器的加载路径:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("扩展类加载器的上级加载器:"+extLoader.getParent());
}catch (Exception e){
e.printStackTrace();
}
}
}
【例19_03】的运行结果如图19-3所示。
图19-3【例19_03】运行结果
从图19-3可以看出:程序中无法获得扩展类加载器的加载路径,这是从JDK1.8之后Java语言做出的改变,实际上JDK1.8或更早版本的JDK允许在程序中获得扩展类加载器的加载路径。此外还可以看出:扩展类加载器的上级类加载器,也就是根类加载器也无法获得,是因为根类加载器并没有继承ClassLoader抽象类,并且根类加载器并不是用Java语言,而是用C++语言实现的,所以扩展类加载器的getParent()方法返回null。
19.2.2自定义类加载器
在Java语言中,除根类加载器以外,其他所有的类加载器都是ClassLoader类的子类,因此程序员可以通过继承ClassLoader类并重写其部分方法自定义类加载器。ClassLoader类定义了两个关键的方法,它们分别是:
- loadClas(String name, boolean resolve):该方法为ClassLoader的入口点,它根据指定名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。
- findClass(String name):根据指定名称来查找类。
如果需要实现自定义的类加载器,就可以通过重写以上两个方法来实现。通常推荐重写findClass()方法而不是重写loadClass()方法。loadClass()方法的执行步骤如下:
- 用findLoadedClass(String name) 来检查是否已经加载类,如果已经加载则直接返回
- 在父类加载器上调用loadClass()方法,如果父类加载器为null, 则使用根类加载器来加载
- 调用findClass(String name)方法查找类
从上面步骤中可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略,而如果重写loadClass()方法,则实现逻辑更为复杂。在ClassLoader类中还有一个核心方法是defineClass(),该方法负责将指定类的字节码文件(即Class文件,如Hello.class) 读入字节数组中并把它转换为Class对象,该字节码文件可以来源于文件、网络等。defineClass()方法管理JVM的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等。但程序员无须重写该方法,因为该方法是被final关键字修饰的最终方法。
除此之外,ClassLoader中还包含一些普通方法,如表19-1所示。
表19-1 ClassLoader类的普通方法
方法 | 功能 |
Class<?> findSystemClass(String name) | 从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass()方法将原始字节转换成Class对象 |
static ClassLoader getSystemClassLoader() | 用于返回应用程序类加载器 |
ClassLoader getParent() | 获取该类加载器的父类加载器 |
void resolveClass(Class<?> c) | 链接指定的类,类加载器可以使用此方法来链接类c |
Class<?> findLoadedClass(String name) | 如果此Java虚拟机已加载了名为name的类,则直接返回该类对应的Class实例,否则返回null。 该方法是Java类加载缓存机制的体现 |
下面的【例19_04】展示了一个自定义的类加载器MyClassLoader的实现过程。首先创建了一个MyClassLoader类的对象mcl,mcl调用loadClass()方法对Hello.class文件进行加载,加载过程中会调用findClass()方法找到Hello.java这个类,然后对其进行编译并进行加载。加载完毕后,调用了Hello类的main()方法打印其参数。
【例19_04自定义类加载器】
Hello.java
public class Hello{
public static void main (String[] args)
{
for (String arg : args)
System.out .println(arg) ;//打印参数
}
}
Exam19_04.java
import java.io.*;
import java.lang.reflect.Method;
class MyClassLoader extends ClassLoader {
//定义读取文件内容的方法
private byte[] getBytes(String fileName) throws IOException {
File file = new File(fileName);
long len = file.length();
byte[] raw = new byte[(int) len];
FileInputStream fis = new FileInputStream(file);
// 一次读取Class文件的全部二进制数据
int r = fis.read(raw);
if (r != len) {
throw new IOException("无法读取全部文件:" + r + "!=" + len);
}
return raw;
}
//定义编译指定Java文件的方法
private boolean compile(String fileName) throws IOException {
System.out.println("MyClassLoader正在编译" + fileName);
//调用系统的javac命令
Process p = Runtime.getRuntime().exec("javac " + fileName);
try {
//其他线程都等待这个线程完成
p.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
}
int ret = p.exitValue();
//返回编译是否成功
return ret == 0;
}
//重写ClassLoader的findClass()方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
//将路径中的点(.)替换成斜杠(/)
String fileStub = name.replace(".", "/");
String javaFilename = "./src/"+fileStub + ".java";
String classFilename = "./src/"+fileStub + ".class";//①
File javaFile = new File(javaFilename);
File classFile = new File(classFilename);
//当指定Java源文件存在,且Class文件不存在,或者Java源文件
//的修改时间比Class文件的修改时间更晚时,重新编译
if (javaFile.exists() && (!classFile.exists() || javaFile.lastModified() > classFile.lastModified())) {
try {
//如果编译失败,或者该Class文件不存在
if (!compile(javaFilename) || !classFile.exists()) {
throw new ClassNotFoundException("ClassNotFoundExcetpion:" + javaFilename);
}
} catch (IOException e) {
e.printStackTrace();
}
}
//如果Class文件存在,系统负责将该文件转换成Class对象
if(classFile.exists()){
try{
//将class文件的二进制数据读入数组
byte[] raw = getBytes(classFilename);
//调用ClassLoader的def ineClass()方法将二进制数据转换成Class对象
clazz = defineClass(name,raw,0,raw.length);
}catch (IOException e){
e.printStackTrace();
}
}
//如果clazz为null,表明加载失败,则抛出异常
if (clazz ==null){
throw new ClassNotFoundException (name) ;
}
return clazz ;
}
}
public class Exam19_04 {
public static void main(String[] args) throws Exception{
MyClassLoader mcl = new MyClassLoader();
String progClass = "Hello";
String[] progArgs = {"我喜欢Java","我正在努力学习这门语言"};
//加载需要运行的类
Class<?> clazz = mcl.loadClass (progClass);
// 获取需要运行的类的主方法
Method main = clazz. getMethod ("main", (new String[0]) .getClass()) ;
Object argsArray[] = {progArgs};
main. invoke (null, argsArray) ;
}
}
【例19_04】中,由自定义的类加载器对Java源文件进行编译并加载,由于在默认情况下是父类委托方式加载类,所以都是由应用程序类加载器来加载程序员编写的类。为了不让应用程序类加载器加载Hello类,就要在工程文件夹中按照“out”->“production”->“lesson19”的顺序找到Hello.class文件并把它删掉。需要注意:mcl对象在对Java源文件进行编译时把字节码文件直接生成到了与Java源文件的相同的路径下,因此在程序中的语句①中定义的字节码文件的路径与源文件的路径是相同的,都是src。删掉Hello.class文件后运行【例19_04】的结果如图19-4所示。
图19-4【例19_04】运行结果
运行完【例19_04】后可以看到存放源文件的src文件夹下出现了一个Hello.class,这是因为自定义类加载器mcl对Hello.java进行编译的结果。在Hello.class存在的情况下再次运行【例19_04】,可以看到控制台上不会输出“MyClassLoader正在编译./src/Hello.java”这句话,这是因为字节码文件已经存在的情况下不会被再次编译。需要说明:【例19_04】中使用了反射技术调用Hello类的main()方法,关于反射技术的细节将在19.3小节讲解。
19.2.3类
本书的第15章中曾介绍过URL类,每一个URL类对象就代表一个资源,这个资源可以在网络上,也可以在本地硬盘上。实际上,一个字节码文件或一个jar包也是一个资源,因此一个字节码文件或一个jar包都可以用URL类对象来表示。URLClassLoader类能以URL数组作为构造方法的参数,并且能够把URL数组中的代表字节码文件或jar包的资源加载到内存中。URLClassLoader用于加载类的方法是loadClass(),这个方法在加载一个类之后会生成代表该类的Class类对象。获得了Class类对象后,利用反射技术就能创建出这个类的对象。例如获得了代表A类的Class对象后,利用反射技术就能获得一个A类的对象。下面的【例19_05】利用URLClassLoader加载了用于驱动数据库的Driver接口,紧接着生成了Driver接口的实现类对象,并以此对象创建一个Connection对象。
【例19_05 URLClassLoader类的使用】
Exam19_05.java
import java.net.*;
import java.sql.*;
import java.util.Properties;
public class Exam19_05 {
private static Connection con;
//定义一个获得数据库连接的方法
public static Connection getConnection(String url,String user,String password) throws Exception{
if(con==null){
//以jar包作为资源
URL[] urls = {new URL("file:D://mysql-connector-java-8.0.27.jar")};
//创建URLClassLoader类对象
URLClassLoader ucl = new URLClassLoader(urls);
//加载jar包中的Driver类并用反射技术创建Driver类对象
Driver driver = (Driver) ucl.loadClass("com.mysql.cj.jdbc.Driver").
getConstructor().newInstance();
Properties props = new Properties();
props.setProperty("user",user) ;
props.setProperty ("password",password) ;
//调用Driver对象的connect()方法来取得数据库连接
con = driver.connect (url,props) ;
}
return con;
}
public static void main(String[] args) throws Exception{
Connection con = getConnection("jdbc:mysql://127.0.0.1:3306/mydb","root","123456");
System.out.println(con);
}
}
【例19_05】中URLClassLoader类加载了D盘下mysql-connector-java-8.0.27.jar文件中的Driver接口,因此只有把mysql-connector-java-8.0.27.jar文件提前拷贝到D盘根目录下才能成功的运行程序。此外,读者要把程序中的用户名和密码修改成自己真实的用户名和密码。【例19_05】的运行结果如图19-5所示。
图19-5【例19_05】运行结果
从图19-5可以看出:Connection接口的实现类对象已经被创建,由此可见,只要掌握了类加载技术,即使不把jar包加入IDE的CLASSPATH中也能加载jar包中的类。正如【例19_05】所示,创建URLClassLoader类对象时传入了一个URL数组参数,该ClassLoader就可以从这系列URL指定的资源中加载指定类,这里的URL可以以file:为前缀,表明从本地文件系统加载,也可以以http:为前缀,表明从互联网通过HTTP访问来加载,还可以以ftp:为前缀,表明从互联网通过FTP访问来加载,总之它的功能非常强大。
本文字版教程还配有更详细的视频讲解,小伙伴们可以点击这里观看。