Java SPI 机制是如何破坏双亲委派模型的?
Java SPI(Service Provider Interface)机制破坏双亲委派模型,主要是因为核心类库(由顶层类加载器加载)需要去加载和实例化由第三方提供的具体实现类(由底层类加载器加载),这违背了双亲委派模型中“子类加载器向上委托”的单向依赖原则。
为了让你清晰地理解这个过程,我们分步骤来剖析:
1. 回顾:什么是正常的“双亲委派模型”?
在正常的双亲委派模型中,类加载的顺序是自下而上的(向上委托):
- Application ClassLoader(应用类加载器)遇到类加载请求,委托给父加载器。
- Extension ClassLoader(扩展类加载器)继续向上委托。
- Bootstrap ClassLoader(启动类加载器)尝试加载。如果加载不到,再逐层向下交由子加载器尝试加载。
核心规则:父类加载器对子类加载器是不可见的。也就是说,启动类加载器只能加载核心类(如 rt.jar),它无法去调用或加载位于应用 Classpath 下的用户类。
2. 冲突产生:SPI 面临的困境
SPI 的核心思想是:Java 官方定义接口,第三方厂商提供实现。最典型的例子就是 JDBC。
- 接口在哪里?
java.sql.Driver接口存在于 Java 核心类库rt.jar中。它是由 Bootstrap ClassLoader(启动类加载器) 加载的。 - 实现类在哪里? MySQL 提供的实现类
com.mysql.cj.jdbc.Driver存在于用户引入的第三方 jar 包中。它应该由 Application ClassLoader(应用类加载器) 加载。
困境出现了:
当我们在代码中调用 DriverManager.getConnection() 时,DriverManager 类(由 Bootstrap 加载)需要去寻找并实例化所有实现了 Driver 接口的第三方类。
根据类加载机制的默认规则:一个类加载其引用的其他类时,会默认使用自身的类加载器。
这就意味着,DriverManager 会尝试用 Bootstrap ClassLoader 去加载 MySQL 的驱动类。但是,Bootstrap ClassLoader 根本找不到存放在 Classpath 下的 MySQL 驱动包!
3. 如何破坏(解决思路):线程上下文类加载器 (TCCL)
既然父加载器没法向下找子加载器帮忙,Java 的设计者就引入了一个“作弊”工具:线程上下文类加载器(Thread Context ClassLoader,简称 TCCL)。
这个加载器可以通过 Thread.currentThread().setContextClassLoader() 来设置。如果在创建线程时没有设置,它会从父线程继承;如果在应用程序的全局范围都没有设置过的话,它默认就是 Application ClassLoader(应用类加载器)。
4. 完整的破坏过程(以 ServiceLoader 为例)
Java 提供了 java.util.ServiceLoader 来实现 SPI 机制。当 DriverManager 或 ServiceLoader 需要加载第三方实现时,它是这样做的:
ServiceLoader是核心类,由 Bootstrap ClassLoader 加载。- 当
ServiceLoader.load(Driver.class)被调用时,它实际上并没有使用 Bootstrap ClassLoader 去加载实现类。 - 相反,它去获取了当前线程的上下文类加载器:
Thread.currentThread().getContextClassLoader()(这里拿到的通常是 Application ClassLoader)。 ServiceLoader拿着这个底层获取到的 AppClassLoader,去扫描META-INF/services/目录,并把第三方厂商的实现类加载进来。
5. 为什么说这是一种“破坏”?
因为在这个过程中,启动类加载器(父)实际上委托了应用类加载器(子)去加载类,完成了一次“逆向委托”(自顶向下)。
这就打破了双亲委派模型中严格的“自底向上委托,父加载器无法访问子加载器可见类”的原则。
总结
Java SPI 破坏双亲委派模型,本质上是基础框架(如 JDBC)需要调用用户代码(如 MySQL 驱动)时引发的类加载器可见性问题。通过引入线程上下文类加载器(TCCL),允许父类加载器“借用”子类加载器来加载类,从而巧妙(且刻意)地打破了双亲委派的限制。这种“破坏”并不是设计的失误,而是为了实现灵活的插件化扩展而必须做出的妥协。