前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java: Context ClassLoader加载器机制及spring boot打包运行可能导致类已存在,但运行时加载找不到

Java: Context ClassLoader加载器机制及spring boot打包运行可能导致类已存在,但运行时加载找不到

作者头像
崔认知
发布2023-06-19 16:04:41
7260
发布2023-06-19 16:04:41
举报
文章被收录于专栏:nobodynobody

Context ClassLoader的出现


JVM既然有了双亲委派模型来加载类,为什么又出现了上下文类加载器,去打破双亲委派模型呢

java双亲委派类加载模型

举例说明应用场景:java中的SPI机制是扩展java功能的扩展点。比如JDBC驱动的实现,java只实现接口定义,定义的类当然是由能加载java平台api的Platform class loader类加载器加载(java17环境下,本博文)。

第三方实现的驱动,则是在我们的class path目录下,由System class loader即application class loader 加载

目前java17内置的类有:

接口与实现类如果是由不同的类加载器加载,在运行时,由于双亲委派模型,父类加载器加载的类是找不到子类加载器加载的类,导致实现类是找不到的。

我们可以验证一下:

代码语言:javascript
复制
  System.out.println("java.sql.Connection :==>  " + Connection.class.getClassLoader());
  System.out.println("java.sql.DriverManager :==>  " + DriverManager.class.getClassLoader());
  System.out.println("java.sql.Driver :==>  " + Driver.class.getClassLoader());
  System.out.println("com.mysql.cj.jdbc.ConnectionImpl :==>  " + com.mysql.cj.jdbc.ConnectionImpl.class.getClassLoader());

运行结果:

java平台定义的JDBC接口是由Platform class loader类加载器加载,而驱动的实现由由System class loader即application class loader 加载。

所以运行时动态加载JDBC实现类时,双亲委派机制就不行了,Platform class loader类加载器此时加载不到驱动的实现类,此时Context ClassLoader就派上用场了。

Context ClassLoader的加载机制


类加载时,我们可以指定类加载器,如下方法:

代码语言:javascript
复制
java.lang.Class#forName(java.lang.String, boolean, java.lang.ClassLoader)

而当前上下文的类加载器我们可以从当前线程获取:

代码语言:javascript
复制
java.lang.Thread#getContextClassLoader

我们以ServiceLoader示例,JDBC驱动实现类的加载是ServiceLoader实现的。

代码语言:javascript
复制
java.sql.DriverManager#ensureDriversInitialized
代码语言:javascript
复制
 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator<Driver> driversIterator = loadedDrivers.iterator();

                    /* Load these drivers, so that they can be instantiated.
                     * 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) {
                        // Do nothing
                    }
                    return null;

ServiceLoader加载类:

代码语言:javascript
复制
public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
    }

类加载时使用的类加载器是当前线程上下文类加载器:

代码语言:javascript
复制
Thread.currentThread().getContextClassLoader();

用当前上下文类加载器打破双亲委派模型,最终类加载的方法:

代码语言:javascript
复制
java.lang.Class#forName(java.lang.String, boolean, java.lang.ClassLoader)

用的是同一个线程上下文类加载器去加载和查找类。

spring boot不打包运行与打包运行jar的区别


spring boot 不打包,即开发模式IDE直接运行,应用中的类是由application class loader 加载的,线程上下文类加载器默认也是application class loader 。

我们来验证一下:

代码语言:javascript
复制
package com.example.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.*;

/**
 * @author 认知科技技术团队
 * 微信公众号:认知科技技术团队
 */
@Component
@Slf4j
public class ThreadPoolConfig {

    @PostConstruct
    void init() throws ExecutionException, InterruptedException {
        log.info("当前类加载器:{}", getClass().getClassLoader());
        log.info("启动类加载器:{}", Demo1Application.class.getClassLoader());
        log.info("{} ===> {}", "Thread.currentThread().getContextClassLoader", Thread.currentThread().getContextClassLoader());

        Thread thread = new Thread(
                () ->
                        log.info("Thread: {} ===> {}", "Thread.currentThread().getContextClassLoader",
                                Thread.currentThread().getContextClassLoader())
        );

        thread.start();
        thread.join();

        //CompletableFuture 默认线程池注意上下文类加载器
        CompletableFuture<Void> future = CompletableFuture.runAsync(
                () ->
                        log.info("CompletableFuture: {} ===> {}", "Thread.currentThread().getContextClassLoader",
                                Thread.currentThread().getContextClassLoader())
        );
        future.get();

    }

}

本地不打包运行结果:

而spring boot打包运行,即java -jar demo.jar

spring boot 打包运行所用的类加载器是

代码语言:javascript
复制
org.springframework.boot.loader.LaunchedURLClassLoader

spring boot打包运行,使用了自己实现的类加载器。

spring boot的LaunchedURLClassLoader有什么坑


来源 https://docs.spring.io/spring-boot/docs/2.3.12.RELEASE/reference/htmlsingle/#executable-jar-restrictions

第二点,大部分第三方jar包的类加载器使用线程上下文类加载器(Thread.getContextClassLoader()),少数使用系统类加载器即应用类加载器ClassLoader.getSystemClassLoader()(即application class loader),此时类加载就会失败。

java.util.Logging上面提到了总是使用系统类加载器。

而且细心的读者,可以看到上面的示例中

CompletableFuture异步提交任务,使用默认的ForkJoinPool线程池时,会使系统类加载器即应用类加载器,成为了当前线程上下文加载器。此时遇到第三方jar包,在CompletableFuture提交的异步任务内加载时,同时在spring jar包运行下,使用线程上下文类加载器加载类导致失败。

运行环境:

代码语言:javascript
复制
java version "17.0.2" 2022-01-18 LTS
Java(TM) SE Runtime Environment (build 17.0.2+8-LTS-86)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.2+8-LTS-86, mixed mode, sharing)
代码语言:javascript
复制
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.7</version>

小结


线程上下文类加载器打破了双亲委派类加载机制,这个在模块化框架中会经常遇到。

线程上下文类加载器使得SPI机制顺利加载到第三方jar包的类。

spring boot 以jar包运行环境下,使用的是spring自己实现的类类加载器LaunchedURLClassLoader,并且存在一下类加载坑(第三方jar包不是以当前线程上下文类加载器加载,或者使用了类ForkJoinPool的线程池,使得当前线程上下文类加载器改变)。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 认知科技技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档