【JVM原理详解】07-打破双亲委派-SPI与线程上下文类加载器
打破双亲委派SPI与线程上下文类加载器上一篇我们详细讲解了双亲委派模型——它保证了核心类库的安全性和类的唯一性。但双亲委派并不是万能的它有一个根本性的局限父加载器无法访问子加载器加载的类。这个限制在某些场景下会导致严重问题。本篇将以JDBC为切入点讲解SPI机制如何通过线程上下文类加载器打破双亲委派并扩展到OSGi的网状类加载结构。为什么需要打破双亲委派先来看一个典型的矛盾场景┌─────────────────────────────────────────────────────┐ │ Bootstrap ClassLoader (加载 rt.jar / java.base) │ │ ┌──────────────────────┐ │ │ │ java.sql.DriverManager│ ← JDBC管理类在核心库中 │ │ │ java.sql.Driver │ ← 驱动接口在核心库中 │ │ └──────────────────────┘ │ └──────────────────────────┬──────────────────────────┘ │ 双亲委派 ┌──────────────────────────▼──────────────────────────┐ │ Application ClassLoader (加载 classpath) │ │ ┌──────────────────────┐ │ │ │ mysql-connector.jar │ ← MySQL驱动实现在这里 │ │ │ com.mysql.cj.jdbc. │ │ │ │ Driver │ │ │ └──────────────────────┘ │ └─────────────────────────────────────────────────────┘问题来了java.sql.DriverManager由Bootstrap ClassLoader加载它需要实例化com.mysql.cj.jdbc.Driver这个驱动实现类而该实现类在classpath下由Application ClassLoader加载。按照双亲委派模型DriverManager在Bootstrap ClassLoader中发出的类加载请求会委派给Bootstrap ClassLoader自己去加载但Bootstrap ClassLoader根本看不到classpath下的com.mysql.cj.jdbc.Driver——父加载器无法向下委托给子加载器。这就是双亲委派模型的根本局限它是一个自上而下的单向委托模型只允许子委派父不允许父访问子。SPI机制什么是SPISPIService Provider Interface是Java提供的一种服务发现机制。它将接口定义和服务实现分离接口由核心库定义如java.sql.Driver由Bootstrap ClassLoader加载实现由第三方提供如MySQL驱动由Application ClassLoader加载SPI机制允许核心库在运行时动态发现和加载第三方实现而不需要在核心库中硬编码实现类。SPI的约定SPI有严格的目录约定META-INF/services/ └── 接口的全限定名文件名 └── 实现类的全限定名文件内容每行一个以JDBC为例MySQL驱动的jar包中包含META-INF/services/java.sql.Driver文件内容com.mysql.cj.jdbc.DriverSPI核心APIServiceLoaderjava.util.ServiceLoader是SPI机制的实现核心。它负责读取META-INF/services目录下的配置文件并加载其中声明的实现类。// ServiceLoader的核心逻辑简化版publicfinalclassServiceLoaderSimplementsIterableS{privatestaticfinalStringPREFIXMETA-INF/services/;// 加载某个接口的所有实现publicstaticSServiceLoaderSload(ClassSservice){// 关键使用当前线程的上下文类加载器ClassLoaderclThread.currentThread().getContextClassLoader();returnServiceLoader.load(service,cl);}// 遍历配置文件并加载实现类privateSnextService(){// 读取 META-INF/services/接口全限定名 文件// 对文件中的每个实现类全限定名通过类加载器加载并实例化StringclassName...;// 从配置文件读取Class?cClass.forName(className,false,loader);// loader就是上下文类加载器Sp(S)c.newInstance();returnp;}}注意load方法的关键一行它使用的是线程上下文类加载器而不是ServiceLoader自身的类加载器ServiceLoader本身在java.util包中由Bootstrap ClassLoader加载。这正是打破双亲委派的关键。线程上下文类加载器概念线程上下文类加载器Thread Context ClassLoaderTCCL是java.lang.Thread类的一个成员变量contextClassLoader每个线程都持有一个。publicclassThreadimplementsRunnable{privateClassLoadercontextClassLoader;// 获取当前线程的上下文类加载器publicClassLoadergetContextClassLoader(){returncontextClassLoader;}// 设置当前线程的上下文类加载器publicvoidsetContextClassLoader(ClassLoadercl){this.contextClassLoadercl;}}默认值线程创建时如果没有显式设置上下文类加载器默认继承自父线程。主线程main线程的上下文类加载器默认是Application ClassLoader。publicclassTCCLDemo{publicstaticvoidmain(String[]args){// 主线程的上下文类加载器ClassLoadertcclThread.currentThread().getContextClassLoader();System.out.println(tccl);// 输出: jdk.internal.loader.ClassLoaders$AppClassLoaderxxx// ServiceLoader就在java.util包中由Bootstrap ClassLoader加载System.out.println(ServiceLoader.class.getClassLoader());// 输出: nullBootstrap ClassLoader// 但ServiceLoader加载实现类时用的是TCCL而不是自己所在的Bootstrap CL}}TCCL如何打破双亲委派回到JDBC的问题。有了TCCL解决方案就很清晰了1. DriverManager由Bootstrap CL加载需要加载Driver实现类 2. DriverManager无法通过自己的类加载器Bootstrap CL找到classpath下的Driver实现 3. 解决方案从当前线程获取TCCL默认是Application CL 4. 用TCCL去加载Driver实现类 5. Application CL可以看到classpath下的MySQL驱动 → 加载成功本质上TCCL充当了一个逆向的桥梁父加载器通过TCCL借用子加载器的加载能力从而实现了对子加载器类的访问。这不是修改双亲委派的加载流程而是在需要时绕过它使用另一个类加载器。JDBC驱动的加载过程详解下面我们完整追踪JDBC驱动加载的全过程看看SPI和TCCL如何协作。代码示例importjava.sql.Connection;importjava.sql.DriverManager;importjava.sql.SQLException;publicclassJdbcDemo{publicstaticvoidmain(String[]args)throwsSQLException{// 这一行触发DriverManager类的初始化// 在JDBC 4.0 (JDK 6)中驱动会自动注册Stringurljdbc:mysql://localhost:3306/test;ConnectionconnDriverManager.getConnection(url,root,password);// DriverManager如何知道用哪个驱动// 答案就是SPI TCCL}}完整加载流程第一步DriverManager类初始化DriverManager位于java.sql包由Bootstrap ClassLoader加载。在JDBC 4.0中DriverManager的静态代码块会通过SPI加载所有驱动实现// DriverManager源码简化static{loadInitialDrivers();println(JDBC DriverManager initialized);}privatestaticvoidloadInitialDrivers(){// 使用ServiceLoader加载所有java.sql.Driver的实现// 这里使用了TCCLServiceLoaderDriverloadedDriversServiceLoader.load(Driver.class);IteratorDriverdriversIteratorloadedDrivers.iterator();while(driversIterator.hasNext()){driversIterator.next();// 逐个实例化驱动类// 实例化时Driver实现类的static块会自动向DriverManager注册自己}}第二步ServiceLoader使用TCCL// ServiceLoader.load(Driver.class) 内部publicstaticSServiceLoaderSload(ClassSservice){ClassLoaderclThread.currentThread().getContextClassLoader();// ↑ 关键获取TCCL此时是Application CLreturnServiceLoader.load(service,cl);}第三步加载MySQL驱动实现类ServiceLoader读取META-INF/services/java.sql.Driver文件找到com.mysql.cj.jdbc.Driver然后// ServiceLoader内部加载实现类Class?driverClassClass.forName(com.mysql.cj.jdbc.Driver,false,tccl// ← 使用TCCLApplication CL而非Bootstrap CL);第四步MySQL Driver自动注册// com.mysql.cj.jdbc.Driver 源码简化publicclassDriverimplementsjava.sql.Driver{static{try{// 自动向DriverManager注册自己DriverManager.registerDriver(newDriver());}catch(SQLExceptione){thrownewRuntimeException(Cant register driver!);}}}流程图DriverManager.getConnection() (Bootstrap CL加载) │ ▼ DriverManager.static{} 触发初始化 │ ▼ ServiceLoader.load(Driver.class) │ ▼ Thread.currentThread().getContextClassLoader() │ ← 获取TCCL Application CL ▼ 读取 META-INF/services/java.sql.Driver │ ← Application CL能访问classpath下的资源 ▼ Class.forName(com.mysql.cj.jdbc.Driver, false, Application CL) │ ← 用Application CL加载驱动实现类 ▼ com.mysql.cj.jdbc.Driver.static{} │ ▼ DriverManager.registerDriver(new Driver()) │ ← 驱动注册完成 ▼ getConnection() 成功返回连接手动验证TCCL的作用我们可以通过修改TCCL来验证它的作用publicclassTCCLVerification{publicstaticvoidmain(String[]args){// 先确保MySQL驱动不在classpath中但放在自定义路径下StringcustomPathD:/libs/mysql-connector-j.jar;URLClassLoadercustomCLnewURLClassLoader(newURL[]{newFile(customPath).toURI().toURL()},ClassLoader.getSystemClassLoader().getParent()// parent设为Platform CL);// 将TCCL设置为自定义类加载器Thread.currentThread().setContextClassLoader(customCL);// 现在ServiceLoader会使用customCL来加载驱动ServiceLoaderDriverloadersServiceLoader.load(Driver.class);for(Driverdriver:loaders){System.out.println(发现驱动: driver.getClass().getName());System.out.println(驱动加载器: driver.getClass().getClassLoader());// 输出: 自定义URLClassLoader}// 恢复TCCLThread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());}}OSGi的网状类加载结构SPI和TCCL只是在特定场景下绕过双亲委派而OSGiOpen Services Gateway initiative则从根本上颠覆了双亲委派模型采用了一种网状Network类加载结构。OSGi模块化与Bundle在OSGi中每个模块称为一个Bundle每个Bundle有自己的类加载器。与双亲委派的树形结构不同OSGi的Bundle之间可以声明相互依赖关系形成网状结构。OSGi类加载规则OSGi的类加载规则远比双亲委派复杂Bundle自己的类java.*开头的类委派给父加载器否则先在自己Bundle内查找。Import包如果类属于已声明的Import-Package则委托给导出该包的Bundle加载器。Export包自己导出的包可以被其他Bundle通过Import引用。Require-Bundle声明依赖的其他Bundle可以访问其导出的类。Fragment Bundle片段Bundle的类会合并到宿主Bundle的类空间中。Dynamic Import动态导入运行时按需查找。OSGi网状结构图┌─────────┐ Import ┌─────────┐ │ Bundle A │◀───────────────│ Bundle B │ │CL_A │ (com.foo) │CL_B │ └────┬─────┘ └────┬─────┘ │ │ Require-Bundle Require-Bundle │ │ ┌────▼─────┐ Import ┌────▼─────┐ │ Bundle C │──────────────▶ │ Bundle D │ │CL_C │ (com.bar) │CL_D │ └──────────┘ └──────────┘ ┌──────────────────────────────┐ │ OSGi Framework ClassLoader │ └──────────────┬───────────────┘ │ ┌──────────────▼───────────────┐ │ Bootstrap ClassLoader │ └──────────────────────────────┘OSGi vs 双亲委派特性双亲委派OSGi结构树形网状类可见性父加载器对子加载器可见通过Import/Export精确控制同一类的多版本不支持同一类只加载一次支持不同Bundle可加载同一类的不同版本复杂度低高类查找可能涉及多个Bundle的协商OSGi的网状类加载结构提供了极强的灵活性但也带来了复杂性和性能开销。Java 9引入的JPMSJava Platform Module System在某种程度上是OSGi模块化理念的简化版但JPMS仍然基于双亲委派模型没有OSGi那样灵活的网状加载。实践要点JDBC 4.0不需要手动Class.forName由于SPI机制只要驱动jar包存在于classpath中包含正确的META-INF/services/java.sql.Driver文件驱动就会自动注册。手动Class.forName(com.mysql.cj.jdbc.Driver)是JDBC 4.0之前的做法现在已非必需。TCCL的线程继承性线程池中工作线程的TCCL继承自创建它的线程。如果在Web容器中主线程的TCCL可能是容器类加载器而工作线程的TCCL可能是某个WebApp类加载器。在异步场景中需要注意TCCL的传递。TCCL与线程池配合如果在线程池中执行的任务依赖TCCL如使用SPI、JDBC等需要确保TCCL设置正确。可以使用以下模式ClassLoaderoriginalTCCLThread.currentThread().getContextClassLoader();try{Thread.currentThread().setContextClassLoader(targetClassLoader);// 执行需要特定TCCL的任务serviceLoaderLoad();}finally{Thread.currentThread().setContextClassLoader(originalTCCL);}SPI在JDK 9模块化中的变化JDK 9之后除了META-INF/servicesSPI还可以通过module-info.java中的provides和uses声明来注册和发现服务。ServiceLoader会同时查找这两种来源。SPI的性能注意ServiceLoader是惰性加载的iterator().next()才会真正加载和实例化实现类。但它不做缓存——每次调用ServiceLoader.load()都会重新扫描配置文件。在高频调用场景下应缓存ServiceLoader实例。小结双亲委派模型的根本局限是父加载器无法访问子加载器加载的类这在SPI场景下会导致核心库无法加载第三方实现。SPI机制通过META-INF/services目录约定实现接口与实现的解耦由ServiceLoader负责发现和加载实现类。线程上下文类加载器TCCL是打破双亲委派的关键——父加载器通过TCCL借用子加载器的加载能力实现逆向访问。JDBC是最典型的SPI应用案例DriverManagerBootstrap CL加载通过TCCLApplication CL加载classpath下的驱动实现。OSGi采用网状类加载结构通过Import/Export/Require等声明实现模块间精确的类可见性控制支持同类的多版本共存。下一篇我们将把理论付诸实践深入剖析Tomcat的类加载架构——一个工业级的、打破双亲委派模型的经典案例。

相关新闻