本篇章源码基于 Java 8
版本,不同版本可能存在差异。🚀本篇章代码 Demo
# 类加载执行流程我们每编写一个 .java
文件的时候都储蓄着需要执行的程序与逻辑。 这些 .java
文件经过 Java
编译器后会生成它对应的类加载文件也就是 .class
文件。 此时 .class
文件中保存着 Java
代码转换后的虚拟机指令。 当需要使用某个类时,虚拟机将会加载它的 .class
文件,并创建对应的 class
对象。 并将 class
文件加载到虚拟机的内存中,这便是类加载的执行流程,流程图如下,其中验证、准备、解析统一归属于链接。 加载:通过一个类的完全限定名查找此类的字节码文件,并利用字节码文件创建一个 Class
对象 验证:确保 Class
文件的字节流中包含信息符合当前虚拟机的要求,并检测会不会对虚拟机产生危害,主要包括四种验证: 准备:为类变量 (既 static
修饰的字段变量) 分配内存并设置该类变量的初始值为 0
(如: private static int num = 5
这里只是将 num
初始化为 0
,至于 5
的值将会在初始化时赋值),这里不包含 final
修饰的 static
,因为 final
在编译的时候就会分配了,值得注意的是:这里不会实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java
堆中。 解析:将常量池中的符号引用替换为直接引用的过程,符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标地址,相对偏移量或一个间接定位到目标的句柄,如: 初始化:类加载的最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量,如:前面只初始化了默认值的 static
,变量将会在这个阶段进行赋值,成员变量也将会被初始化。 这便是类加载的全部过程,而类加载器的任务是根据一个类的全限定名来读取此类二进制字节流到 JVM
中的,然后转换为一个与目标类对应的 java.lang.Class
对象实例。 虚拟机提供了三种方式的类加载器,如下: # 三种方式类加载器# 引导 (Bootstrap) 类加载器由 C/C++
语言实现,启动类加载器,属于最高层, JVM
启动时创建,通常由于 os 相关的本地代码实现,是最根基的类加载器,没有对应的 Java
对象,因此在 Java
中只能用 null
来指代。
# Java8启动类加载器主要加载的是 JVM
自身需要的类,这个类加载使用 C++
来实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib
路径下的核心类库或 -Xbootclasspath
参数指定的路径下的 jar
包加载到内存中,注意由于虚拟机是按照文件名识别加载 jar
包的,如果文件名不被虚拟机识别,即使将 jar
包放在 lib
目录下也没有任何作用,出于安全考虑 Bootstrap
启动类加载器只加载包名为 java
、 javax
、 sun
等开头的类。 总结:引导类加载器使用 C/C++
语言实现,嵌套在 JVM
内部。 用于加载 Java
核心类库。 JVM
启动时创建,通常由于 os 相关的本地代码实现,是最根基的类加载器。没有对应的 Java
对象,因此在 Java
中只能用 null
来指代。 不继承自 java.lang.ClassLoader
,没有父加载器。 还可用于加载扩展类加载器和应用程序类加载器,并指定为它们的父类加载器。 只加载包名为 java
javax
sun
开头的类文件。 在 JVM
启动时通过 Bootstrap ClassLoader
加载 rt.jar
, 并初始化 sun.misc.Launcher
从而创建 Extension ClassLoader
和 Application ClassLoader
的实例,下面代码是查看 Bootstrap ClassLoader
初始化了那些类库。
GetBootstrapClassLoader.java 版本请选择JAVA 8 package top. rem. rain ; import sun. misc. Launcher ; import java. net. URL; * @Author: LightRain * @Description: 查看引导类加载器到底加载了那些核心类库 * @DateTime: 2023-12-03 00:21 * @Version:1.0 **/ public class GetBootstrapClassLoader { public static void main ( String [ ] args) { URL[ ] urLs = Launcher . getBootstrapClassPath ( ) . getURLs ( ) ; for ( URL urL : urLs) { System . out. println ( urL. toExternalForm ( ) ) ; } } } 打印结果: file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/lib/resources.jar file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/lib/rt.jar file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/lib/sunrsasign.jar file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/lib/jsse.jar file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/lib/jce.jar file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/lib/charsets.jar file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/lib/jfr.jar file:/C:/LightRainData/IDEA/JDK/JDK-1.8/jre/classes */
# Java9自从 Java 9
引入了模块特性后,负责加载启动时的基础模块类有以下几种:java.base
java.management
java.xml
除了启动类加载器之外,其它的类加载器都是 java.lang.ClassLoader
的子类,因此有对应的 Java
对象。 这些类加载器需要先由另一个类加载器,比如说启动类加载器,加载至 Java
虚拟机中方能执行类加载。 # Java8 - 扩展 (Extension)扩展类加载器是 Sun
公司实现的,现已被 Oracle
收购, sun.misc.Launcher$ExtClassLoader
类由 Java
语言实现。 它是 Launcher
的静态内部类,它负责加载 <JAVA_HOME>/lib/ext
目录下或者由系统变量 -Djava.ext.dirs
路径中的类库。 负责加载一些扩展的系统类,如: Xml
加密
压缩
相关的功能类等。 可以供开发者直接使用标准扩展类加载器来加载。 Launcher.java 参考JAVA 8 package sun. misc ; * This class is used by the system to launch the main application. */ public class Launcher { static class ExtClassLoader extends URLClassLoader { private static File [ ] getExtDirs ( ) { String s = System . getProperty ( "java.ext.dirs" ) ; File [ ] dirs; if ( s != null ) { StringTokenizer st = new StringTokenizer ( s, File . pathSeparator) ; int count = st. countTokens ( ) ; dirs = new File [ count] ; for ( int i = 0 ; i < count; i++ ) { dirs[ i] = new File ( st. nextToken ( ) ) ; } } else { dirs = new File [ 0 ] ; } return dirs; } } }
从 Java 9
替换为了平台类加载器,为何要替换? Java 8
主要加载 jre/lib
下的 ext
扩展 jar
包时使用,这样的操作并不推荐,而 Java 9
更新了模块化就更无需这样扩展的类加载器了。
只要负责加载一些相对次要,但又通用的类。 加载一些平台相关模块如:java.scripting
java.compiler.*
java.corba.*
# 系统 (System) 类加载器 & 应用 (App) 类加载器系统类加载器
也被称为 应用程序加载器
是 Sun
公司实现的 sun.misc.Launcher$AppClassLoader
。它负责加载系统类路径 java -classpath
或 -D java.class.path
指定路径下的类库,也就是我们经常用到的 classpath
路径。 开发者可以直接使用 系统类加载器
,一般情况下该类加载是程序中默认的类加载器,通过 ClassLoader.getSystemClassLoader()
方法可以获取到该类加载器。 在 Java
日常应用程序开发中,类的加载几乎由上述 3
种类加载器相互配合执行的,在必要时我们还可以自定义类加载器,需要注意的是 Java
虚拟机对 class
文件采用的是按需加载的方式。 就是说当需要使用该类时才会将它的 class
文件加载到内存中生成主要分为以下 class
对象,而且加载某个类的 class
文件时, Java
虚拟机采用的是 双亲委派模式
,即把当前请求交由父类处理,它是一种任务委派模式。 # 双亲委派模式# 双亲委派工作原理双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器, 注意
:双亲委派模式中的父子关系并非通常的类继承关系,而是采用组合关系来复用父类加载器的相关代码。
双亲委派模式是 Java 1.2
后引入的,其工作原理就是如果:一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。 如果父类加载器还存在其父类加载器,则进一步向上委托,以此类推,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回。 倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。 俗话说就是每个儿子都很懒,有活就先给父亲干,直到父亲说这事我也干不了时,儿子才会自己想办法去做。 # 双亲委派主要用途双亲委派的好处就是 Java
类随着它的类加载器一起具备了一种带有优先级的层级关系,通过这种层级关系可以避免类的重复加载。 当父类加载了该类时,其子类就没必要再加载了,其次考虑到安全因素 Java
核心 API
中定义类型不会被随意替换。 假设通过网络传递了一个名为 java.lang.Integer
类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心 Java API
中发现这个名称的类已被加载,之后并不会重新加载传递过来的 java.lang.Integer
类,而是直接返回已加载过的 Integer.class
对象。 这样做的原因还有一个就是防止核心 API
被随意篡改,如果:我们在 classpath
路径下自定义一个名为 java.lang.Student
类呢?该类在 java.lang
包下并不存在,经过双亲委派模式传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会被加载。 这将会反向委托给子类加载器来加载,最终会通过系统类加载器加载该类,但这样做是不被允许的,因为 java.lang
是核心 API
,是需要访问权限的,强制加载将会抛出 java.lang.SecurityException: Prohibited package name: java.lang
的异常。 所以是无论如何都无法加载成功的,下面是用代码在 Java
中定义的类加载器和 双亲委派模式
的实现。
# 双亲委派关系图
上图中可以看出顶层类加载器是 ClassLoader
类,它是一个抽象类,所有的类加载器都继承自 ClassLoader
(不包括启动类加载器)。 ClassLoader
中的几个比较重要的方法如下有:上述 4 个方法都 ClassLoader
类中的比较重要的方法,也是我们可能会经常用到的方法。 # ClassLoader 常用方法# loadClass(Sting)该方法加载指定名称 (包括包名) 的二进制类型,该方法在 JDK 1.2
之后不在建议开发者重写但开发者可以直接调用该方法。 loadClass()
方法是 ClassLoader
类自己实现的,该方法中的逻辑就是 双亲委派模式
的实现。下面是 loadClass(String name, boolean resolve)
方法的源码,它是一个重载方法, resolve
参数代表是否生成 class
对象的同时进行解析相关操作。 ClassLoader.java 参考JAVA 8 package java. lang ; public abstract class ClassLoader { protected Class < ? > loadClass ( String name, boolean resolve) throws ClassNotFoundException { synchronized ( getClassLoadingLock ( name) ) { Class < ? > c = findLoadedClass ( name) ; if ( c == null ) { long t0 = System . nanoTime ( ) ; try { if ( parent != null ) { c = parent. loadClass ( name, false ) ; } else { c = findBootstrapClassOrNull ( name) ; } } catch ( ClassNotFoundException e) { } if ( c == null ) { long t1 = System . nanoTime ( ) ; c = findClass ( name) ; sun. misc. PerfCounter. getParentDelegationTime ( ) . addTime ( t1 - t0) ; sun. misc. PerfCounter. getFindClassTime ( ) . addElapsedTimeFrom ( t1) ; sun. misc. PerfCounter. getFindClasses ( ) . increment ( ) ; } } if ( resolve) { resolveClass ( c) ; } return c; } } }
当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,不存在则交给该类加载器的父类加载器去加载。 若没有父类加载器则交给顶级启动类加载器去加载,最后倘若扔没有找到,则使用 findClass()
方法去加载。 从 loadClass
实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,我们可以直接使用 this.getClass().getClassLoader().loadClass('className')
这样就可以直接调用 ClassLoader
的 loadClass
方法获取到 class
对象了。
# findClass(Sting)JDK 1.2
之前,在自定义类加载时,总会去继承 ClassLoader
类并重写 loadClass
方法,从而实现自定义的类加载。JDK 1.2
之后不再建议调用者去覆盖 loadClass()
方法,而是建议把自定义的类加载逻辑写在 findClass()
方法中。findClass()
方法是在 loadClass()
方法中被调用的,当 loadClass()
方法中父类加载器加载失败后,则会调用自己的 findClass()
方法来完成类加载。这样就可以保证自定义的类加载器也符合 双亲委派模式
了,但需要注意的是 ClassLoader
类中并没有实现 findClass()
方法的具体逻辑,取而代之的是抛出 ClassNotFoundException
异常。 同时应该知道的是 findClass()
方法通常是和 defineClass()
方法一起使用的。 ClassLoader
类中的 findClass()
方法源码如下:ClassLoader.java 参考JAVA 8 package java. lang ; public abstract class ClassLoader { * 实现自定义类的加载器需要子类重写此方法 * 默认抛出 ClassNotFoundException */ protected Class < ? > findClass ( String name) throws ClassNotFoundException { throw new ClassNotFoundException ( name) ; } }
# defineClass(byte[] b, int off, int len)defineClass()
方法是用来将 byte
字节流解析成 JVM
能够识别的 Class
对象 ( ClassLoader
中已实现该方法逻辑)通过这个方法不仅能够通过 class
文件实例化 class
对象,还可以通过其它方式实例化 class
对象,如:通过网络接收一个类的字节码,然后转换为 byte
字节流创建对应的 class
对象。 defineClass()
方法通常与 findClass()
方法一起使用,一般情况下,在自定义类加载器时会直接覆盖 ClassLoader
的 findClass()
方法并编写加载规则。最后取得要加载类的字节码后转换成流,然后调用 defineClass()
方法生成类的 class
对象,下面是简单案例代码。 OverrideFindClass.java 版本请选择JAVA 8 package top. rem. rain ; import java. io. ByteArrayOutputStream ; import java. io. File ; import java. io. IOException ; import java. io. InputStream ; import java. nio. file. Files ; * @Author: LightRain * @Description: 重写 FindClass 方法示例代码 * @DateTime: 2023-12-03 00:13 * @Version:1.0 **/ public class OverrideFindClass extends ClassLoader { * 文件扩展名 */ private final static String FILE_EXTENSION = ".class" ; * .class 文件路径 */ private final String filePath; * 构造方法 * * @param filePath .class 文件路径 */ public OverrideFindClass ( String filePath) { this . filePath = filePath; } * 重写获取类的字节码并创建 class 对象的逻辑 * * @param className 全限定类名称 * @return Class<?> 任意 class 类型对象 */ @Override protected Class < ? > findClass ( String className) { byte [ ] data = this . getClassData ( className) ; return this . defineClass ( className, data, 0 , data. length) ; } * 读取字节码流的方法 * * @param className 对象全限定名称 * @return byte [] 字节数组 */ private byte [ ] getClassData ( String className) { InputStream inputStream = null ; byte [ ] data = null ; ByteArrayOutputStream byteArrayOutputStream = null ; try { className = className. replace ( '.' , '\\' ) ; inputStream = Files . newInputStream ( new File ( filePath + className + FILE_EXTENSION) . toPath ( ) ) ; byteArrayOutputStream = new ByteArrayOutputStream ( ) ; int ch = 0 ; while ( - 1 != ( ch = inputStream. read ( ) ) ) { byteArrayOutputStream. write ( ch) ; } data = byteArrayOutputStream. toByteArray ( ) ; } catch ( Exception e) { e. printStackTrace ( ) ; } finally { try { assert inputStream != null ; inputStream. close ( ) ; assert byteArrayOutputStream != null ; byteArrayOutputStream. close ( ) ; } catch ( IOException e) { e. printStackTrace ( ) ; } } return data; } }
注意:如果直接调用 defineClass()
方法生成类的 class
对象,这个类的 class
对象并没有解析 (可以理解为链接阶段,解析是链接的最后一步),其解析操作需要等待初始化阶段进行。
# resolveClass(Class<?> c)使用该方法可以使用类的 Class
对象创建完成并同时被解析,前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值,同时将字节码文件中的符号引用替换为直接引用。
上述 4 个方法都 ClassLoader
类中的比较重要的方法,也是我们可能会经常用到的方法。
# SercureClassLoader 扩展 - ClassLoader# URLClassLoader&URLClassPath来看 SercureClassLoader
扩展了 ClassLoader
新增了几个与使用相关的代码源 (对代码源的的位置及证书的验证) 和权限定义类验证 (主要指对 class
源码的访问权限) 的方法。 我们更多是与它的子类 URLClassLoader
有所关联, ClassLoader
是一个抽象类,很多方法是空方法并没有实现,如: findClass()
findResource()
等方法。 而 URLClassLoader
类为这些方法提供了具体实现,新增了 URLClassPath
类协助取得 Class
字节码流等功能。 在编写自定义类加载器时如果没有过于复杂的需求,可以直接继承 URLClassLoader
类,就可以避免自己编写 findClass()
方法及其获取字节码流的方法了。 从类图结构来看 URLClassLoader
中存在一个 URLClassPath
类,通过这个类就可以找到要加载的字节码流。 就是说 URLCLassPath
类负责找到要加载的字节码后再读取字节流,最后通过 defineClass()
方法创建类的 class
对象。 从 URLClassLoader
类的结构图可以看出其构造方法都有一个必须传递的参数 URL[]
,该参数的元素是代表字节码文件的路径。 在创建 URLClassLoader
对象时必须要指定这个类加载器要到哪个目录下寻找要加载的 class
文化。 同时应该注意: URL[]
是 URLCLassPath
类的必传参数,在创建 URLClassPath
对象时,会根据传递的 URL
数组中的路径判断是文件还是 jar
包。 根据不同的路径创建 FileLoader
或者 JarLoader
或默认 Loader
类去加载相应路径下的 class
文件。 而当 JVM
调用 findClass()
方法时就由 Loader
FileLoader
JarLoader
这三个加载器中的一个将 class
字节码流文件加载到内存中,最后利用字节码流创建类的 class
对象。 注意事项: 如果我们在定义类加载器时选择继承 ClassLoader
类而非 URLClassLoader
时,必须自己实现 findClass()
方法的具体加载逻辑以及获取字节码流的逻辑。了解完 URLClassLoader
后下面来看剩余的两个类加载器: SystemClassLoader(系统类加载器)
也被称为 AppClassLoader(应用程序加载器)
和 ExtClassLoader(扩展类加载器)
扩展类加载器是 Java 8
中的, Java 9
之后将扩展改为了平台 PlatformClassLoader(平台类加载器)
,此处使用 Java 8
版本
# App(System)ClassLoader&ExtClassLoader这两个类都继承自 URLClassLoader
类, ExtClassLoader
和 AppClassLoader
是 sun.misc.Launcher
中的静态内部类。 sun.misc.Launcher
主要被系统用于启动主应用程序, ExtClassLoader
和 AppClassLoader
都是由 sun.misc.Launcher
创建的,来看结构图如下。我们发现 ExtClassLoader
并没有重写 loadClass()
方法,这足矣证明其遵循 双亲委派模式
。 而 AppClassLoader
重写了 loadClass()
方法,但最终调用的还是父类的 loadClass()
方法,因此也遵循 双亲委派模式
,重写方法源码如下。 Launcher.java 参考JAVA 8 package sun. misc ; public class Launcher { * The class loader used for loading from java.class.path. * runs in a restricted security context. */ static class AppClassLoader extends URLClassLoader { * Override loadClass so we can checkPackageAccess. */ public Class < ? > loadClass ( String name, boolean resolve) throws ClassNotFoundException { int i = name. lastIndexOf ( '.' ) ; if ( i != - 1 ) { SecurityManager sm = System . getSecurityManager ( ) ; if ( sm != null ) { sm. checkPackageAccess ( name. substring ( 0 , i) ) ; } } if ( ucp. knownToNotExist ( name) ) { Class < ? > c = findLoadedClass ( name) ; if ( c != null ) { if ( resolve) { resolveClass ( c) ; } return c; } throw new ClassNotFoundException ( name) ; } return ( super . loadClass ( name, resolve) ) ; } } }
无论是 ExtClassLoader
还是 AppClassLoader
都继承自 URLClassLoader
类,因此它们都遵守 双亲委派模式
。 到此处我们对 CLassLoader
URLClassLoader
ExtClassLoader
AppClassLoader
及 Launcher
类之间的关系有了比较清晰地了解。 下面我们将通过代码来阐明,上面的每个类加载器的父类到底是谁。
# 类加载器之间的关系类加载器间的关系并非指继承关系,主要分为以下 4
点: CustomClassLoader.java 版本请选择JAVA 8 package top. rem. rain ; import java. io. * ; import java. nio. file. Files ; * @Author: LightRain * @Description: 自定义类加载器 * @DateTime: 2023-12-02 15:22 * @Version:1.0 **/ public class CustomClassLoader extends ClassLoader { * 文件扩展名 */ private final static String FILE_EXTENSION = ".class" ; * .class 文件路径 */ private final String filePath; * 构造方法 * * @param filePath .class 文件路径 */ public CustomClassLoader ( String filePath) { this . filePath = filePath; } * 重写获取类的字节码并创建 class 对象的逻辑 * * @param className 全限定类名称 * @return Class<?> 任意 class 类型对象 */ @Override protected Class < ? > findClass ( String className) { byte [ ] data = this . getClassData ( className) ; return this . defineClass ( className, data, 0 , data. length) ; } * 读取字节码流的方法 * * @param className 对象全限定名称 * @return byte [] 字节数组 */ private byte [ ] getClassData ( String className) { InputStream inputStream = null ; byte [ ] data = null ; ByteArrayOutputStream byteArrayOutputStream = null ; try { className = className. replace ( '.' , '\\' ) ; inputStream = Files . newInputStream ( new File ( filePath + className + FILE_EXTENSION) . toPath ( ) ) ; byteArrayOutputStream = new ByteArrayOutputStream ( ) ; int ch = 0 ; while ( - 1 != ( ch = inputStream. read ( ) ) ) { byteArrayOutputStream. write ( ch) ; } data = byteArrayOutputStream. toByteArray ( ) ; } catch ( Exception e) { e. printStackTrace ( ) ; } finally { try { assert inputStream != null ; inputStream. close ( ) ; assert byteArrayOutputStream != null ; byteArrayOutputStream. close ( ) ; } catch ( IOException e) { e. printStackTrace ( ) ; } } return data; } }
CustomClassLoaderTest.java 版本请选择JAVA 8 package top. rem. rain ; * @Author: LightRain * @Description: 自定义加载器测试类 * @DateTime: 2023-12-02 15:32 * @Version:1.0 **/ public class CustomClassLoaderTest { public static void main ( String [ ] args) { CustomClassLoader loader = new CustomClassLoader ( CustomClassLoaderTest . class . getName ( ) ) ; System . out. println ( "自定义类加载器的父加载器: " + loader. getParent ( ) ) ; System . out. println ( "系统默认的AppClassLoader: " + ClassLoader . getSystemClassLoader ( ) ) ; System . out. println ( "AppClassLoader的父类加载器: " + ClassLoader . getSystemClassLoader ( ) . getParent ( ) ) ; System . out. println ( "ExtClassLoader的父类加载器: " + ClassLoader . getSystemClassLoader ( ) . getParent ( ) . getParent ( ) ) ; 执行结果: 自定义类加载器的父加载器: sun.misc.Launcher$AppClassLoader@18b4aac2 系统默认的 AppClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2 AppClassLoader 的父类加载器: sun.misc.Launcher$ExtClassLoader@4554617c ExtClassLoader 的父类加载器: null */ } }
在上述代码中我们自定义了一个 CustomClassLoader
并继承了 ClassLoader
类,而非继承自 URLClassLoader
。 因此需要自己编写 findClass()
方法的逻辑以及加载字节码逻辑,在这里我们仅需要知道 CustomClassLoader
是自定义加载器即可。 接着在 main
方法之后通过 ClassLoader.getSystemClassLoader()
方法获取到系统默认类加载器,通过获取其父类加载器及父类的父类加载器,同时还获取了自定义类加载器的父类加载器,打印结果正如我们所预料的。 AppClassLoader
的父类加载器为 ExtClassLoader
,而 ExtClassLoader
并没有父类加载器。自定义类加载器默认情况下它的父类加载器都是 AppClassLoader
,我们来看 Lancher
构造器的源码。 Launcher.java 参考JAVA 8 package sun. misc ; public class Launcher { public Launcher ( ) { ClassLoader extcl; try { extcl = ExtClassLoader . getExtClassLoader ( ) ; } catch ( IOException e) { throw new InternalError ( "Could not create extension class loader" , e) ; } try { loader = AppClassLoader . getAppClassLoader ( extcl) ; } catch ( IOException e) { throw new InternalError ( "Could not create application class loader" , e) ; } Thread . currentThread ( ) . setContextClassLoader ( loader) ; String s = System . getProperty ( "java.security.manager" ) ; if ( s != null ) { sun. nio. fs. DefaultFileSystemProvider. create ( ) ; SecurityManager sm = null ; if ( "" . equals ( s) || "default" . equals ( s) ) { sm = new java. lang. SecurityManager( ) ; } else { try { sm = ( SecurityManager ) loader. loadClass ( s) . newInstance ( ) ; } catch ( IllegalAccessException e) { } catch ( InstantiationException e) { } catch ( ClassNotFoundException e) { } catch ( ClassCastException e) { } } if ( sm != null ) { System . setSecurityManager ( sm) ; } else { throw new InternalError ( "Could not create SecurityManager: " + s) ; } } } }
在 Lancher
初始化时首先会创建 ExtClassLoader
类加载器,然后再创建 AppClassLoader
并把 ExtClassLoader
传递给 AppClassLoader
作为父类加载器。 后面还把 AppClassLoader
默认设置为线程上下文加载器,关于线程上下文类加载器后面分析。 最后就是 ExtClassLoader
类加载器为什么是 null
??? 来看下面的源码中的创建过程就会明白了。 在创建 ExtClassLoader
时强制设置了其父类加载器为 null
。 Launcher.java 参考JAVA 8 package sun. misc ; public class Launcher { public Launcher ( ) { ClassLoader extcl; try { extcl = ExtClassLoader . getExtClassLoader ( ) ; } catch ( IOException e) { throw new InternalError ( "Could not create extension class loader" , e) ; } } * ExtClassLoader 继承自 URLClassLoader * 用于加载已安装扩展的类加载器 * The class loader used for loading installed extensions. */ static class ExtClassLoader extends URLClassLoader { public static ExtClassLoader getExtClassLoader ( ) throws IOException { if ( instance == null ) { synchronized ( ExtClassLoader . class ) { if ( instance == null ) { instance = createExtClassLoader ( ) ; } } } return instance; } private static ExtClassLoader createExtClassLoader ( ) throws IOException { try { return AccessController . doPrivileged ( new PrivilegedExceptionAction < ExtClassLoader > ( ) { public ExtClassLoader run ( ) throws IOException { final File [ ] dirs = getExtDirs ( ) ; int len = dirs. length; for ( int i = 0 ; i < len; i++ ) { MetaIndex . registerDirectory ( dirs[ i] ) ; } return new ExtClassLoader ( dirs) ; } } ) ; } catch ( java. security. PrivilegedActionException e) { throw ( IOException ) e. getException ( ) ; } } * 为指定的目录创建一个新的 ExtClassLoader。 * Creates a new ExtClassLoader for the specified directories. */ public ExtClassLoader ( File [ ] dirs) throws IOException { super ( getExtURLs ( dirs) , null , factory) ; SharedSecrets . getJavaNetAccess ( ) . getURLClassPath ( this ) . initLookupCache ( this ) ; } } }
上述 super
跟入进去后便是下面的 URLClassLoader
构造器源码。
URLClassLoader.java 参考JAVA 8 package java. net ; public class URLClassLoader extends SecureClassLoader implements Closeable { public URLClassLoader ( URL[ ] urls, ClassLoader parent, URLStreamHandlerFactory factory) { super ( parent) ; SecurityManager security = System . getSecurityManager ( ) ; if ( security != null ) { security. checkCreateClassLoader ( ) ; } acc = AccessController . getContext ( ) ; ucp = new URLClassPath ( urls, factory, acc) ; } }
仔细分析后很显然 ExtClassLoaer
的父类为 null
,而 AppClassLoader
的父加载器为 ExtClassLoader
。 所有自定义类加载器默认情况下其父加载器都是 AppClassLoader
,注意: 这里所指的父类加载器并不是 Java
继承关系中的那种父子关系。 # 类与类加载器在 JVM
中表示两个 class
对象是否为同一个类对象时,存在两个必要条件:类的全限定名称需要保持一致。 加载这个类的 ClassLoader
实例对象必须相同。 就是说在 JVM
中即使这两个类对象 ( class
对象) 来源于同一个 Class
文件,被同一个虚拟机所加载,但只要加载它们的 ClassLoader
实例对象不同,那么这两个类对象也是不相等的。 这是因为不同的 ClassLoader
实例对象都拥有不同的独立类名空间,所以加载的 class
对象也会存在不同的类名空间中,注意:前提是需要重写 loadClass()
方法 。 就从前面的 双亲委派模式
对 loadClass()
方法源码分析中可得知,在方法第一步会通过 Class<?> c = findLoadedClass(name)
从缓存中查找,类的完全限定名相同则不会再一次被加载。 因此我们必须绕过缓存查询才能重新加载 class
对象,也可以直接调用 findClass()
方法,下面是避免从缓存中查找的代码示例如下。 NotEqualClassTest.java 版本请选择JAVA 8 package top. rem. rain ; * @Author: LightRain * @Description: 不相等对象测试 * @DateTime: 2023-12-02 22:11 * @Version:1.0 **/ public class NotEqualClassTest { public static void main ( String [ ] args) { String classPath = "D:\\项目\\gitee\\untitled\\out\\production\\untitled\\" ; CustomClassLoader classLoader1 = new CustomClassLoader ( classPath) ; CustomClassLoader classLoader2 = new CustomClassLoader ( classPath) ; Class < ? > aClass1 = classLoader1. findClass ( "top.rem.rain.NotEqualClassTest" ) ; Class < ? > aClass2 = classLoader2. findClass ( "top.rem.rain.NotEqualClassTest" ) ; System . out. println ( "findClass -> aClass1:" + aClass1. hashCode ( ) ) ; System . out. println ( "findClass -> aClass2:" + aClass2. hashCode ( ) ) ; 执行结果: findClass -> aClass1:1836019240 findClass -> aClass2:325040804 生成不同实例对象 */ } }
如果调用父类的 loadClass()
方法,结果如下,除非重写 loadClass()
方法去掉缓存查找步骤,不过现在一般都不建议重写 loadClass()
方法。
EqualClassTest.java 版本请选择JAVA 8 package top. rem. rain ; * @Author: LightRain * @Description: 相等对象测试 * @DateTime: 2023-12-03 00:55 * @Version:1.0 **/ public class EqualClassTest { public static void main ( String [ ] args) throws ClassNotFoundException { String classPath = "D:\\项目\\gitee\\untitled\\out\\production\\untitled\\" ; CustomClassLoader classLoader1 = new CustomClassLoader ( classPath) ; CustomClassLoader classLoader2 = new CustomClassLoader ( classPath) ; Class < ? > aClass1 = classLoader1. loadClass ( "top.rem.rain.EqualClassTest" ) ; Class < ? > aClass2 = classLoader2. loadClass ( "top.rem.rain.EqualClassTest" ) ; System . out. println ( "findClass -> aClass1:" + aClass1. hashCode ( ) ) ; System . out. println ( "findClass -> aClass2:" + aClass2. hashCode ( ) ) ; System . out. println ( "Class -> aClass3:" + EqualClassTest . class . hashCode ( ) ) ; 直接调用 loadClass 方法的输出结果,注意并没有重写 loadClass 方法 执行结果: findClass -> aClass1:685325104 findClass -> aClass2:685325104 Class -> aClass3:685325104 生成的都是同一个实例对象 */ } }
所以如果不想从缓存查询相同完全类名的 class
对象,那么只有 ClassLoader
的实例对象不同,同一个字节码文件创建的 class
对象自然也不会相同。
# class 文件显示 & 隐式加载概念了解
所谓的 class
文件的显示加载与隐式加载的方式是指 JVM
加载 class
文件到内存中的方式。 显示加载指的是在代码中通过调用 ClassLoader
加载 class
对象,如直接调用 Class.forName(name)
或 this.getClass().getClassLoader().loadClass()
加载 class
对象。 隐式加载指的是不直接在代码中调用 ClassLoader
方法加载 class
对象,而是通过虚拟机自动加载到内存中,如在加载某个类的 class
文件时,该类的 class
文件中引用了另外一个类对象,此时额外引用的类将通过 JVM
自动加载到内存中。 以上两种方式一般会混合使用。 # 实现自定义类加载器想实现自定义类加载器需要继承 ClassLoader
或者 URLClassLoader
类。 继承 ClassLoader
则需要自己重写 findClass()
方法并编写加载逻辑。 继承 URLClassLoader
则不用自己编写 findClass()
方法的加载逻辑以及 class
文件加载转换成字节码流的代码。 为什么要编写自定义类加载器其意义何在? 当 class
文件不在 ClassPath
路径下,默认系统类加载器 (应用类加载器) 无法找到该 class
文件,在这种情况下我们需要自己实现一个自定义的 ClassLoader
来加载特定路径下的 class
文件生成 class
对象。 当一个 class
文件是通过网络传输并且可能会进行相应加密操作时,需要先对 class
文件进行相应的解密后再加载到 JVM
内存中,这种情况下也需要自己编写自定义的 ClassLoader
并实现相应的逻辑。 当需要实现热部署功能时 (一个 class
文件通过不同的类加载器产生不同 class
对象从而实现热部署功能) 也想要自己实现自定义 ClassLoader
的逻辑。 # 自定义 File 类加载器我们继承 ClassLoader
实现自定义的特定路径下的文件类加载器并加载编译后的 Student.class
源码如下。
Student.java 版本请选择JAVA 8 package top. rem. rain ; * @Author: LightRain * @Description: 学生类对象 * @DateTime: 2023-12-03 12:42 * @Version:1.0 **/ public class Student { @Override public String toString ( ) { return "top.rem.rain.Student{ " + "name=" + "LightRain" + " age=" + "17" + " sex=" + "woman " + '}' ; } }
FileClassLoader.java 版本请选择JAVA 8 package top. rem. rain ; import java. io. ByteArrayOutputStream ; import java. io. File ; import java. io. IOException ; import java. io. InputStream ; import java. nio. file. Files ; * @Author: LightRain * @Description: 自定义 FileClassLoader * @DateTime: 2023-12-03 12:50 * @Version:1.0 **/ public class FileClassLoader extends ClassLoader { * 文件扩展名 */ private final static String FILE_EXTENSION = ".class" ; * .class 文件路径 */ private final String filePath; public FileClassLoader ( String filePath) { this . filePath = filePath; } @Override protected Class < ? > findClass ( String className) { byte [ ] data = this . getClassData ( className) ; return this . defineClass ( className, data, 0 , data. length) ; } * 读取字节码流的方法 * * @param className 对象全限定名称 * @return byte [] 字节数组 */ private byte [ ] getClassData ( String className) { InputStream inputStream = null ; byte [ ] data = null ; ByteArrayOutputStream byteArrayOutputStream = null ; try { className = className. replace ( '.' , '\\' ) ; inputStream = Files . newInputStream ( new File ( filePath + className + FILE_EXTENSION) . toPath ( ) ) ; byteArrayOutputStream = new ByteArrayOutputStream ( ) ; int ch = 0 ; while ( - 1 != ( ch = inputStream. read ( ) ) ) { byteArrayOutputStream. write ( ch) ; } data = byteArrayOutputStream. toByteArray ( ) ; } catch ( Exception e) { e. printStackTrace ( ) ; } finally { try { assert inputStream != null ; inputStream. close ( ) ; assert byteArrayOutputStream != null ; byteArrayOutputStream. close ( ) ; } catch ( IOException e) { e. printStackTrace ( ) ; } } return data; } }
FileClassLoaderTest.java 版本请选择JAVA 8 package top. rem. rain ; * @Author: LightRain * @Description: FileClassLoader 测试 * @DateTime: 2023-12-03 12:56 * @Version:1.0 **/ public class FileClassLoaderTest { public static void main ( String [ ] args) throws InstantiationException , IllegalAccessException , ClassNotFoundException { String filePath = "D:\\项目\\gitee\\untitled\\out\\production\\untitled\\" ; FileClassLoader fileClassLoader = new FileClassLoader ( filePath) ; Class < ? > aClass = fileClassLoader. loadClass ( "top.rem.rain.Student" ) ; System . out. println ( "aClass.newInstance().toString() = " + aClass. newInstance ( ) . toString ( ) ) ; 执行结果:aClass.newInstance ().toString () = top.rem.rain.Student { name=LightRain age=17 sex=woman } */ } }
我们通过 getClassData()
方法找到了 class
文件并转换为字节流重写了 findClass()
方法,利用 defineClass()
方法创建了类的 class
对象。 在 main
方法中调用 loadClass()
方法加载指定路径下的 class
文件,由于启动类加载器、扩展类加载器以及系统类加载器都无法在其路径下找到该类,因此最终将由自定义类加载器加载,即调用 findClass()
方法进行加载。 如果继承 URLClassLoader
类来实现自定义类加载器,代码将会更加简洁,代码如下。
FileUrlClassLoader.java 版本请选择JAVA 8 package top. rem. rain ; import java. net. URL; import java. net. URLClassLoader ; import java. net. URLStreamHandlerFactory ; * @Author: LightRain * @Description: 继承自 URLClassLoader 的自定义类加载器 * @DateTime: 2023-12-03 14:37 * @Version:1.0 **/ public class FileUrlClassLoader extends URLClassLoader { public FileUrlClassLoader ( URL[ ] urls, ClassLoader parent) { super ( urls, parent) ; } public FileUrlClassLoader ( URL[ ] urls) { super ( urls) ; } public FileUrlClassLoader ( URL[ ] urls, ClassLoader parent, URLStreamHandlerFactory factory) { super ( urls, parent, factory) ; } }
FileUrlClassLoaderTest.java 版本请选择JAVA 8 package top. rem. rain ; import java. io. File ; import java. net. MalformedURLException ; import java. net. URL; * @Author: LightRain * @Description: FileUrlClassLoader 测试 * @DateTime: 2023-12-03 14:38 * @Version:1.0 **/ public class FileUrlClassLoaderTest { public static void main ( String [ ] args) throws MalformedURLException , ClassNotFoundException , InstantiationException , IllegalAccessException { String filePath = "D:\\项目\\gitee\\untitled\\out\\production\\untitled\\" ; File file = new File ( filePath) ; FileUrlClassLoader fileUrlClassLoader = new FileUrlClassLoader ( new URL[ ] { file. toURI ( ) . toURL ( ) } ) ; Class < ? > aClass = fileUrlClassLoader. loadClass ( "top.rem.rain.Student" ) ; System . out. println ( "aClass.newInstance().toString() = " + aClass. newInstance ( ) . toString ( ) ) ; 执行结果:aClass.newInstance ().toString () = top.rem.rain.Student { name=LightRain age=17 sex=woman } */ } }
除了需要重写构造器外无需编写 findClass()
方法及其 class
文件字节码流的转换逻辑。
# 自定义网络类加载器自定义网络类加载器,主要用于读取通过网络传递的 class
文件
NetClassLoader.java 版本请选择JAVA 8 package top. rem. rain ; import java. io. ByteArrayOutputStream ; import java. io. InputStream ; import java. net. URL; * @Author: LightRain * @Description: 自定义网络类加载器 * @DateTime: 2023-12-03 15:43 * @Version:1.0 **/ public class NetClassLoader extends ClassLoader { private String url; public NetClassLoader ( String url) { this . url = url; } @Override protected Class < ? > findClass ( String name) throws ClassNotFoundException { byte [ ] classData = getClassDataFromNet ( name) ; if ( classData == null ) { throw new ClassNotFoundException ( ) ; } else { return defineClass ( name, classData, 0 , classData. length) ; } } * 从网络获取 class 文件 * @param className * @return */ private byte [ ] getClassDataFromNet ( String className) { String path = classNameToPath ( className) ; try { URL url = new URL ( path) ; ByteArrayOutputStream baos; try ( InputStream ins = url. openStream ( ) ) { baos = new ByteArrayOutputStream ( ) ; int bufferSize = 4096 ; byte [ ] buffer = new byte [ bufferSize] ; int bytesNumRead = 0 ; while ( ( bytesNumRead = ins. read ( buffer) ) != - 1 ) { baos. write ( buffer, 0 , bytesNumRead) ; } } return baos. toByteArray ( ) ; } catch ( Exception e) { e. printStackTrace ( ) ; } return null ; } private String classNameToPath ( String className) { return url + "/" + className. replace ( '.' , '/' ) + ".class" ; } }
主要是在获取字节码流时的区别,从网络直接获取到字节流再转成字节数组然后利用 defineClass()
方法创建 class
对象。 如果继承自 URLClassLoader
类则和前面文件路径的实现是类似的,无需担心路径是 filePath
还是 url
,因为 URLClassLoader
内的 URLClassPath
对象会根据传递过来的 URL
数组中的路径去判断是文件还是 jar
包,然后根据不同的路径创建 FileLoader
或 JarLoader
或默认类 Loader
去读取对应的路径或 url
下的 class
文件。 # 实现热部署类加载器热部署就是利用同一个 class
文件,使用不同的类加载器在内存中创建出两个不同的 class
对象 (即使用不同的类加载器加载实例),由于 JVM
在加载类之前会检测请求的类是否已被加载过 (即在 loadClass()
方法中调用 findLoadedClass()
方法)。 如果被加载过则直接从缓存获取,不会重新再加载, 注意:
同一个类加载器的实例和同一个 class
文件只能被加载器加载一次,多次加载将会报错。 因此我们实现热部署必须让同一个 class
文件,可以根据不同的类加载器重复加载,以实现所谓的热部署。 实际上前面实现的 FileClassLoader
和 FileUrlClassLoader
已具备这个功能了,但前提是直接调用 findClass()
方法,而不是调用 loadClass()
方法,因为我们重写了 findClass()
方法,在方法中我们并没有去查询缓存中是否已被加载。 因此在 ClassLoader
中的 loadClass()
方法体中调用的 findLoadedClass()
方法进行了检测是否已被加载。 因此我们直接调用 findClass()
方法就可以绕过这个问题,当然也可以重写 loadClass()
,只不过是不推荐这么干仅此而已。 利用 FileClassLoader
类测试代码如下: FileClassLoaderTest2.java 版本请选择JAVA 8 package top. rem. rain ; * @Author: LightRain * @Description: FileClassLoader 测试 -- 热部署 * @DateTime: 2023-12-03 12:56 * @Version:1.0 **/ public class FileClassLoaderTest2 { public static void main ( String [ ] args) throws InstantiationException , IllegalAccessException , ClassNotFoundException { String filePath = "D:\\项目\\gitee\\untitled\\out\\production\\untitled\\" ; FileClassLoader fileClassLoader1 = new FileClassLoader ( filePath) ; FileClassLoader fileClassLoader2 = new FileClassLoader ( filePath) ; Class < ? > aClass1 = fileClassLoader1. loadClass ( "top.rem.rain.Student" ) ; Class < ? > aClass2 = fileClassLoader2. loadClass ( "top.rem.rain.Student" ) ; System . out. println ( "loadClass -> aClass1:" + aClass1. hashCode ( ) ) ; System . out. println ( "loadClass -> aClass2:" + aClass2. hashCode ( ) ) ; System . out. println ( "--------------------------------" ) ; Class < ? > aClass3 = fileClassLoader1. findClass ( "top.rem.rain.Student" ) ; Class < ? > aClass4 = fileClassLoader2. findClass ( "top.rem.rain.Student" ) ; System . out. println ( "findClass -> aClass3:" + aClass3. hashCode ( ) ) ; System . out. println ( "findClass -> aClass4:" + aClass4. hashCode ( ) ) ; 执行结果: loadClass -> aClass1:1163157884 loadClass -> aClass2:1163157884 -------------------------------- findClass -> aClass3:325040804 findClass -> aClass4:1173230247 */ } }
# 线程上下文类加载器在 Java
应用中存在着很多服务提供者接口 ( Service Provider Interface
简称: SPI
),这些接口允许第三方为它们提供具体实现。 常见的 SPI
有 JDBC
、 JNDI
等,这些 SPI
的接口属于 Java
核心库,一般存在 rt.jar
包中,由 Bootstrap
类加载器加载。 而 SPI
的第三方具体实现的代码则是作为 Java
应用所依赖的 jar
包被存放在 classpath
路径下。 由于 SPI
接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但 SPI
的核心接口类是由引导类加载器 来加载的。 而 Bootstrap
类加载器无法直接加载 SPI
的具体实现类,同时由于 双亲委派模式
的存在, Bootstrap
类加载器也无法反向委托给 AppClassLoader
加载器来加载 SPI
的具体实现类。 这种情况下我们就需要一种特殊的类加载器来加载第三方类库,而线程上下文类加载器就是个很好的选择。 线程上下文类加载器 ( ContextClassLoader
) 是从 Java 1.2
开始引入的,我们可以通过 java.lang.Thread
类中的 getContextClassLoader()
和 setContextClassLoader(ClassLoader cl)
方法来获取和设置线程的上下文类加载器。 如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是应用类加载器 ( AppClassLoader
),在线程中运行的代码可以通过此类加载器来加载类和资源,以 jdbc.jar
加载为例,如图所示。 从图中可以看出 rt.jar
核心包是由 Bootstrap
类加载器加载的,其内包含 SPI
核心接口类,由于 SPI
中的类经常需要调用外部实现类的方法,而 jdbc.jar
包含外部实现类 ( jdbc.jar
存在于 classpath
路径下) 无法通过 Bootstrap
类加载器加载,因此只能委托线程上下文类加载器把 jdbc.jar
中的实现类加载到内存以便 SPI
相关类使用。 显然这种线程上下文类加载器的加载方式破坏了 双亲委派模式
,它在执行过程中抛弃了 双亲委派模式
,使程序可以逆向使用类加载器,当然这也使得 Java
类加载器变得更加灵活。 接下来看 DriverManager
类的源码, DriverManager
是 Java
核心 rt.jar
包中的类,该类用来管理不同数据库的实现驱动 (即 Driver
),它们都实现了 Java
核心包中的 java.sql.Driver
接口,如: mysql
驱动包中的 com.mysql.jdbc.Driver
,在这里只要看看如何加载外部实现类,在 DriverManager
初始化时将会执行如下代码。
DriverManager.java 参考JAVA 8 package java. sql ; public class DriverManager { * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the {@code ServiceLoader} mechanism */ static { loadInitialDrivers ( ) ; println ( "JDBC DriverManager initialized" ) ; } private static void loadInitialDrivers ( ) { String drivers; try { drivers = AccessController . doPrivileged ( new PrivilegedAction < String > ( ) { public String run ( ) { return System . getProperty ( "jdbc.drivers" ) ; } } ) ; } catch ( Exception ex) { drivers = null ; } AccessController . doPrivileged ( new PrivilegedAction < Void > ( ) { public Void run ( ) { ServiceLoader < Driver > loadedDrivers = ServiceLoader . load ( Driver . class ) ; Iterator < Driver > driversIterator = loadedDrivers. iterator ( ) ; * It may be the case that the driver class may not be there * i.e. there may be a packaged driver with the service class * as implementation of java.sql.Driver but the actual class * may be missing. In that case a java.util.ServiceConfigurationError * will be thrown at runtime by the VM trying to locate * and load the service. * * Adding a try catch block to catch those runtime errors * if driver not available in classpath but it's * packaged as service and that service is there in classpath. */ try { while ( driversIterator. hasNext ( ) ) { driversIterator. next ( ) ; } } catch ( Throwable t) { } return null ; } } ) ; println ( "DriverManager.initialize: jdbc.drivers = " + drivers) ; if ( drivers == null || drivers. equals ( "" ) ) { return ; } String [ ] driversList = drivers. split ( ":" ) ; println ( "number of Drivers:" + driversList. length) ; for ( String aDriver : driversList) { try { println ( "DriverManager.Initialize: loading " + aDriver) ; Class . forName ( aDriver, true , ClassLoader . getSystemClassLoader ( ) ) ; } catch ( Exception ex) { println ( "DriverManager.Initialize: load failed: " + ex) ; } } } }
在 DriverManager
类初始化执行了 loadInitialDrivers()
方法,在该方法中通过 ServiceLoader.load(Diver.class)
去加载外部实现的驱动类, ServiceLoader
类会去读取 mysql
的 jdbc.jar
下的 META-INF
文件的内容如下:
而 com.mysql.jdbc.Driver
继承类如下:
Driver.java 参考JAVA 8 package com. mysql. jdbc ; import java. sql. SQLException ; public class Driver extends com. mysql. cj. jdbc. Driver { public Driver ( ) throws SQLException { } static { System . err. println ( "Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary." ) ; } }
从注释中可以看出平常我们使用 com.mysql.jdbc.Driver
已经被丢弃了,取而代之的是 com.mysql.cj.jdbc.Driver
,也就是说官方不再建议我们使用如下代码注册 mysql
驱动。
RegisterDriver.java 版本请选择JAVA 8 package top. rem. rain ; import java. sql. Connection ; import java. sql. SQLException ; * @Author: LightRain * @Description: 注册 MySQL 驱动 * @DateTime: 2023-12-04 21:58 * @Version:1.0 **/ public class RegisterDriver { public Connection registerMysql ( ) throws SQLException , ClassNotFoundException { Class . forName ( "com.mysql.jdbc.Driver" ) ; String url = "jdbc:mysql://localhost:3306/cckfs?characterEncoding=UTF-8" ; Connection conn = java. sql. DriverManager. getConnection ( url, "root" , "123456" ) ; return conn; } }
而是去掉注册步骤,如下即可。
RegisterDriver.java 版本请选择JAVA 8 package top. rem. rain ; import java. sql. Connection ; import java. sql. SQLException ; * @Author: LightRain * @Description: 注册 MySQL 驱动 * @DateTime: 2023-12-04 21:58 * @Version:1.0 **/ public class RegisterDriver { public Connection registerMysql ( ) throws SQLException , ClassNotFoundException { String url = "jdbc:mysql://localhost:3306/cckfs?characterEncoding=UTF-8" ; Connection conn = java. sql. DriverManager. getConnection ( url, "root" , "123456" ) ; return conn; } }
这样 ServiceLoader
就会帮助我们处理一切,并最终通过 load()
方法加载,来看看具体方法实现。
ServiceLoader.java 参考JAVA 8 package java. util ; public final class ServiceLoader < S > implements Iterable < S > { public static < S > ServiceLoader < S > load ( Class < S > service) { ClassLoader cl = Thread . currentThread ( ) . getContextClassLoader ( ) ; return ServiceLoader . load ( service, cl) ; } }
很显然确实是通过线程上下文类加载器加载的,实际上核心包的 SPI
类对外部实现类的加载都是线程上下文类加载器执行的,通过这种方式实现了 Java
核心代码内部去调用外部实现类。 我们知道线程上下文类加载器默认情况下就是 AppClassLoader
,那为什么不直接通过 getSystemClassLoader()
方法获取类加载器来加载 classpath
路径下的类呢?是可以的,但这种直接使用 getSystemClassLoader()
方法获取 AppClassLoader
加载类会有一个缺点,那就是代码部署到不同服务器上时会出现问题。 如果代码部署到 Java Web
应用服务或者 EJB
之类的服务将会出现问题,以为这些服务器使用的线程上下文类加载器并非 AppClassLoader
,而是 Java Web
应用服务器自家的类加载器,类加载器不同,所以我们应该少用 getSystemClassLoader()
。 总之不同服务使用的可能默认 ClassLoader
是不同的,但使用线程上下文类加载器总能获取到当前程序执行相同的 ClassLoader
,从而避免不必要的问题。