本文将从多个方面对Spring Boot本地类和Jar包类加载顺序做详细的阐述,并给出相应的代码示例。
一、类加载机制概述
在介绍Spring Boot本地类和Jar包类加载顺序之前,有必要简要介绍一下Java的类加载机制。
Java虚拟机在程序运行期间动态加载类,并将类的字节码转换成运行时数据结构,形成可执行代码。类的加载过程由ClassLoader类及其子类完成。
Java虚拟机规范将ClassLoader类设计成了一个抽象类,用以描述Java虚拟机中存在的类加载器,用以加载Class文件。ClassLoader类的任务是把class文件字节码转化为java.lang.Class的一个实例。而Class实例通常存放于运行时数据区的方法区内。
二、本地类和Jar包类加载顺序详解
2.1 Jar包优先于本地类
在Java类加载过程中,如果系统中存在多个相同的类文件,则优先加载JVM类路径下的Jar包中的class文件,而忽略本地类路径下的同名class文件。
为了说明这一点,我们可以自己手写一个同名class文件,并同时存放在JVM类路径下和本地类路径下。接着,我们在代码中进行加载并打印该class文件的内容。
// 在JVM类路径下创建SameClass.java package com.example.demo; public class SameClass { public static void printFile() { System.out.println("This is a file in JVM classpath."); } }
// 在本地类路径下创建SameClass.java,内容和上述相同 package com.example.demo; public class SameClass { public static void printFile() { System.out.println("This is a file in local classpath."); } }
// 在代码中进行加载并打印SameClass类文件的内容 package com.example.demo; public class ClassLoadingOrderDemoApplication { public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader = ClassLoadingOrderDemoApplication.class.getClassLoader(); Class<?> clazz = classLoader.loadClass("com.example.demo.SameClass"); clazz.getMethod("printFile").invoke(null, null); } }
运行上面的代码之后,我们发现输出结果是"This is a file in JVM classpath."。
这是因为JVM类路径下的SameClass.class文件优先被加载,而本地类路径下的同名文件被忽略了。
2.2 本地类优先于系统类
对于Jar包中不存在的类文件,Java会首先从本地文件系统中查找,然后才会查找系统类库(如JDK中的rt.jar)。
同样地,我们可以手写一个只在本地类路径下存在的class文件,并在代码中进行加载和打印。
// 在本地类路径下创建OnlyLocalClass.java package com.example.demo; public class OnlyLocalClass { public static void printFile() { System.out.println("This is a file in local classpath, but not in the system classpath."); } }
// 在代码中进行加载并打印OnlyLocalClass类文件的内容 package com.example.demo; public class ClassLoadingOrderDemoApplication { public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader = ClassLoadingOrderDemoApplication.class.getClassLoader(); Class<?> clazz = classLoader.loadClass("com.example.demo.OnlyLocalClass"); clazz.getMethod("printFile").invoke(null, null); } }
运行上述代码之后,我们可以发现打印结果为"This is a file in local classpath, but not in the system classpath."。
由此可见,本地类优先于系统类。
2.3 父ClassLoader优先于子ClassLoader
对于同一份class文件,父ClassLoader的加载顺序优先于子ClassLoader。
我们可以通过自定义ClassLoader来模拟这种情况,并在代码中进行加载和打印。
// 自定义ClassLoader package com.example.demo; import java.io.IOException; import java.io.InputStream; public class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream is = this.getClass().getResourceAsStream(fileName); if (is == null) { return super.loadClass(name); } byte[] b = new byte[is.available()]; is.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { throw new ClassNotFoundException(name); } } }
// 在本地类路径下创建ParentClass.java package com.example.demo; public class ParentClass { public static void printFile() { System.out.println("This is a file in parent classloader."); } }
// 在代码中进行加载并打印ParentClass类文件的内容 package com.example.demo; public class ClassLoadingOrderDemoApplication { public static void main(String[] args) throws ClassNotFoundException { ClassLoader parentClassLoader = new MyClassLoader(); ClassLoader childClassLoader = new MyClassLoader(); parentClassLoader.loadClass("com.example.demo.ParentClass"); Class<?> clazz = childClassLoader.loadClass("com.example.demo.ParentClass"); clazz.getMethod("printFile").invoke(null, null); } }
运行上述代码之后,我们可以发现打印结果为"This is a file in parent classloader.",即父ClassLoader先于子ClassLoader加载了该class文件。
三、小结
本文从类加载机制概述、Jar包与本地类加载顺序、父ClassLoader与子ClassLoader的加载顺序等多个方面,对Spring Boot本地类和Jar包类加载顺序做了详细剖析。
Java虚拟机能够动态加载类,并将类的字节码转换成运行时数据结构形成可执行代码,类的加载过程由ClassLoader类及其子类完成。