serviceloader.load(driver.class) 必须绕过双亲委派,因driver接口由bootstrap加载而实现类(如com.mysql.cj.jdbc.driver)由appclassloader加载;它通过thread.currentthread().getcontextclassloader()获取tccl(默认appclassloader)来加载驱动,依赖meta-inf/services/java.sql.driver文件声明实现类,jdbc 4.0+由此实现自动注册,无需class.forname()。

ServiceLoader.load(Driver.class) 为什么必须绕过双亲委派
因为 Driver 接口定义在 java.sql 包里,由启动类加载器(Bootstrap ClassLoader)加载;而像 com.mysql.cj.jdbc.Driver 这样的实现类,打包在用户引入的 mysql-connector-java.jar 中,只能由应用程序类加载器(AppClassLoader)加载。按双亲委派规则,启动类加载器绝不会委托子加载器去加载类——它连 CLASSPATH 都看不见。所以 DriverManager 在静态初始化时若直接用自身类加载器去加载驱动实现,必然失败。
Thread.currentThread().getContextClassLoader() 是关键破口
ServiceLoader.load(Driver.class) 内部不使用调用者的类加载器,而是显式读取当前线程的上下文类加载器:ClassLoader cl = Thread.currentThread().getContextClassLoader()。JVM 启动时,主线程的 TCCL 默认被设为 AppClassLoader。这就让本该由 Bootstrap 加载的代码,能“反向”借道子加载器完成实际类加载。这不是 bug,是 JDK 明确设计的协作机制。
META-INF/services/java.sql.Driver 文件是触发点
破坏不是无条件发生的,它依赖 SPI 的约定路径和内容:
- 驱动 JAR 必须在
META-INF/services/java.sql.Driver文件中写入实现类全限定名(如com.mysql.cj.jdbc.Driver) -
ServiceLoader会扫描所有 JAR 的该路径,但只用 TCCL 去加载其中列出的类 - 如果没这个文件,或文件名拼错(比如写成
java.sql.driver小写),ServiceLoader就找不到实现,也不会触发 TCCL 加载逻辑
JDBC 4.0+ 不再需要 Class.forName() 的真正原因
旧写法 Class.forName("com.mysql.cj.jdbc.Driver") 是手动触发驱动类的静态块注册,走的是标准双亲委派——由当前类(比如你写的 Main)的类加载器去加载驱动类,没问题但冗余。而 ServiceLoader 方式把发现、加载、注册全交给 DriverManager 初始化阶段自动完成,前提是 TCCL 已就位且驱动 JAR 符合 SPI 规范。一旦你在线程中手动改了 TCCL(比如设为 null),即使有驱动 JAR,ServiceLoader 也会因无法加载类而静默失败——这点极易被忽略。










