
故事场景:皇帝需要一位"外来的专家"
我们续接上一个故事。王朝的官员体系(类加载器 )等级分明:皇帝 > 总督 > 县令 。祖宗家法(双亲委派)规定,办事必须层层上报。
新的任务来了:
王朝的"皇家运输署 " (DriverManager
),由皇帝 (启动类加载器)直接管辖,负责管理全国的运输。最近,从西域(第三方厂商)引进了一种全新的交通工具------"MySQL牌飞毯 " (mysql-connector-java.jar
)。这种飞毯需要一位专门的"飞毯驾驶员 " (com.mysql.cj.jdbc.Driver
) 才能操作,而这位驾驶员是一位隐居在某个县城(classpath)里的民间高手。
祖宗家法的困境
"皇家运输署"的主管(DriverManager
的代码)接到了命令:"去,把那位'飞毯驾驶员'给我找来并登记在册!"
主管顿时犯了难。他是皇帝身边的人,按照"向上通报"的祖宗家法,他只能向皇帝汇报,他压根没有渠道、也没有权力去一个偏远县城里直接找人。而县令也没有接到任何请求,自然不会上报他县里有这么一位高手。这成了一个死局。
皇帝的智慧:颁布"招贤令"并派出"钦差"
皇帝(Java的设计者)早已预料到这种情况。他想出了一个绝妙的办法来"打破"常规:
-
- 颁布"招贤令" (SPI 机制):
皇帝下了一道圣旨,传遍天下:"所有身怀绝技的'交通工具驾驶员',不必等朝廷征召,可主动到本地县衙的'专家名录' (
META-INF/services/java.sql.Driver
文件)上登记自己的名号和住址!"于是,那位"飞毯驾驶员"就在他所在县城的名录上写下了自己的大名。
-
- 派出"钦差" (线程上下文类加载器):
现在,"皇家运输署"的主管要找人了。他没有亲自出马,而是采取了以下步骤:
-
• 他查看了当前正在执行的"引进飞毯"这项国家工程(当前线程)。
-
• 他发现,这个工程的发起地是那个偏远县城,因此工程团队里有一位来自当地的联络官------县令本人 (
Thread.currentThread().getContextClassLoader()
)。 -
• 主管(
DriverManager
)于是把皇帝的"招贤令"交给这位联络官,并命令道: "本官乃朝廷命官(由父加载器加载),不便直接去你的地盘上找人。但你,作为本工程的联-络官(线程上下文类加载器),有这个权限。现命你,拿着这份招贤令,去你的县里,按照专家名录上的记载,把那位'飞毯驾驶员'给我请过来!"
-
• 结果:
县令(应用程序类加载器)愉快地接受了来自"上级"的"逆向委托"。他回到自己的地盘,轻松找到了那位"飞毯驾驶员",并成功地将他引荐给了皇家运输署。
故事总结:
概念 | 双亲委派的困境与解决方案 |
---|---|
核心矛盾 | 上级(父)看不见下级(子) 。皇家运输署(由皇帝加载)无法找到民间高手(由县令加载)。 |
解决方案 | SPI + TCCL (招贤令 + 钦差) |
SPI (招贤令) | 提供一个"约定 ",让下级可以主动暴露自己能提供哪些服务。 |
TCCL (钦差) | 提供一个"通道 ",让上级可以临时借用下级的权力,去加载下级才能看到的类。 |
是否"打破" | 它没有修改"向上委托"的家法本身,而是绕过了它 。是一种从上到下的"逆向调用",而非"从下到上"的加载。 |
一句话总结 | 祖宗家法(双亲委派)不许爹找儿子,但爹可以命令跟着自己的"儿子代表"(线程上下文类加载器)回家办事。 |
结论:
JDBC之所以需要"打破"双亲委派,是因为Java的核心API(由父加载器加载)需要动态加载由应用程序提供的、具体的实现类(由子加载器加载)。这种"跨层级"的调用需求,通过线程上下文类加载器这个精巧的设计,得以完美解决。这不仅限于JDBC,在JNDI、JCE等许多需要SPI的场景中,都使用了同样的技术。
技术解析
核心矛盾:谁来加载驱动?
让我们先回顾一下"双亲委派模型"和JDBC的"身份":
-
- 双亲委派模型 : 一个类加载器接到加载任务后,会先向上 委托给父加载器,层层上报,直到顶层的启动类加载器 (Bootstrap ClassLoader)。只有当所有父加载器都找不到时,子加载器才会自己尝试加载。
-
- JDBC的API :
DriverManager
,Connection
,Statement
等核心接口和类,是Java语言的标准组成部分。它们位于java.sql
包下,由最顶层的启动类加载器 (Bootstrap ClassLoader) 加载。
- JDBC的API :
-
- JDBC的驱动 : 比如
mysql-connector-java.jar
里的com.mysql.cj.jdbc.Driver
类,它是一个第三方厂商 实现的。它被放置在你的应用的classpath下,因此它是由应用程序类加载器 (Application ClassLoader) 来加载的。
- JDBC的驱动 : 比如
矛盾出现了:
DriverManager
(由启动类加载器 加载) 需要去加载并管理各种不同的 Driver
实现 (由应用程序类加载器加载)。
按照双亲委派模型,一个父加载器(启动类加载器)是无法看到 也无法加载其子加载器(应用程序类加载器)路径下的类的。这就好比皇帝(父)无法直接调用一个县城里的民间艺人(子),因为正常的流程是县令(子)请求皇帝(父)。这形成了一个无法解决的死循环。
解决方案:SPI + 线程上下文类加载器 (TCCL)
为了解决这个"逆向"加载的难题,Java引入了 SPI (Service Provider Interface) 机制,并通过线程上下文类加载器 (Thread Context Class Loader) 来打破双亲委派的限制。
-
• SPI 约定:
JDBC 4.0 以后,驱动
jar
包会遵循SPI规范,在META-INF/services/
目录下放置一个名为java.sql.Driver
的文件。这个文件的内容就是驱动实现类的全限定名,比如
com.mysql.cj.jdbc.Driver
。 -
• 打破双亲委派的关键动作:
DriverManager
在初始化时,它不会使用自己的加载器(启动类加载器) 去加载这些驱动。相反,它会这样做:// DriverManager 内部的简化逻辑 public class DriverManager { static { // ... loadInitialDrivers(); // ... } private static void loadInitialDrivers() { // 1. 获取当前线程的"上下文类加载器" // 这个加载器通常是 AppClassLoader,它能看到 classpath 下的驱动 jar 包 ClassLoader cl = Thread.currentThread().getContextClassLoader(); // 2. 使用 ServiceLoader 工具类,并传入这个"借来的"加载器 // ServiceLoader 会根据 SPI 约定去 META-INF/services/ 目录下查找驱动 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class, cl); // 3. 遍历找到的驱动实现,并尝试加载和注册它们 for (Driver driver : loadedDrivers) { // ... 注册驱动 ... } } }
Thread.currentThread().getContextClassLoader()
这行代码就是"破局"的关键。它允许一个由父加载器加载的类(DriverManager
),"借用"子加载器的"视野"去加载子加载器才能看到的类。