JVM 线程上下文类加载器 ServiceLoader

 2023-02-08
原文作者:KittyGuy 原文地址:https://juejin.cn/post/6998354027310219301

先从一段代码说起

前提要导入mysql的驱动

  • 代码
    public class ServiceLoaderTest {
        public static void main(String[] args) {
            ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
            for (Driver driver : loader) {
                System.out.println("diverName :" +driver.getClass().getName()+" , loadDriver's Loader: "+driver.getClass().getClassLoader());
            }
            System.out.println("当前线程上下文类加载器 : "+Thread.currentThread().getContextClassLoader());
            System.out.println("加载ServiceLoader的类加载 : " +loader.getClass().getClassLoader());
        }
    }
  • 结果

202301011551558371.png

疑问

  • 为什么什么都没提供给ServLoader,就提供了一个Driver.class它就能加载所有Driver.class,它是这么做到的呢?
  • 它怎么知道哪些路径有Driver呢?

这就得先从ServiceLoader的jdk源码的doc注释说起了

202301011551564042.png

  • 一个简单的服务提供商(service-provider)装载设施。
  • 服务是一组众所周知的接口和(通常是抽象的)类。 服务提供者是服务的特定实现。 提供者中的类通常实现接口 并将服务本身定义的类子类化。 服务提供者可以 以扩展的形 式安装在 Java 平台的实现中,即将 jar 文件放置在 任何常用的扩展目录中(就会由ExtClassLoader加载) 。 提供者也可以通过将它们添加到应用程序的 类路径(就会由AppClassLoader加载) 或通过其他一些特定于平台的方式来提供。
  • 出于加载的目的, 服务由单一类型表示 ,即单一接口或抽象类。 (可以使用具体类, 但不建议这样做。 )给定服务的提供者 包含(就是服务只有一个,但服务里面有多个类) 一个或多个具体类,这些类使用特定于提供者的数据和代码扩展该服务类型。 提供者类通常不是整个提供者本身,而是一个代理它包含足够的信息来决定提供者是否能够满足特定请求以及可以按需创建实际提供者的代码。 提供者类的细节往往是高度特定于服务的; 没有任何一个类或接口可以统一它们,所以这里没有定义这样的类型。 此工具强制执行的唯一要求是 提供程序类必须具有零参数构造函数,以便它们可以在加载期间实例化。
  • 通过在资源目录META-INF/services 中放置提供者配置文件来标识服务提供者。 该文件的名称是服务类型的完全限定二进制名称(binary name)。 该文件包含具体提供者类的完全限定二进制名称列表,每行一个。 每个名称周围的 空格和制表符以及空行都将被忽略 。 注释字符是'#' ( '\u0023' , NUMBER SIGN ); 在每一行中,第一个注释字符之后的所有字符都将被忽略。 该文件必须以 UTF-8 编码
  • 如果特定的具体提供程序类在 多个配置文件中命名,或者在同一个配置文件中多次命名,则忽略重复项 。 命名特定提供程序的配置文件 不必与提供程序本身位于同一 jar 文件或其他分发单元中 。 提供者必须可以从最初查询并定位配置文件的同一个类加载器访问(就找到配置文件的类加载器也可以访问提供者); 请注意,这不一定是实际加载文件的类加载器。
  • 提供者被延迟地定位和实例化,即按需。 一个服务加载器维护一个到目前为止已经加载的提供者的缓存。 iterator方法的每次调用都会返回一个迭代器,该迭代器首先按实例化顺序生成缓存的所有元素,然后懒惰(lazy,必要的时候)地定位 并实例化任何剩余的提供者 ,依次将每个提供者添加到缓存中。 可以通过 reload方法清除缓存
  • 服务加载器总是在调用者的安全上下文中执行。 受信任的系统代码通常应该从特权安全上下文中调用此类中的方法以及它们返回的迭代器的方法。
  • 此类的实例对于多个并发线程使用是不安全的。
  • 除非另有说明,否则向此类中的任何方法传递空参数将导致抛出NullPointerException 。
  • 示例假设我们有一个服务类型com.example.CodecSet ,它旨在表示某些协议的编码器/解码器对集。 在这种情况下,它是一个具有 两个抽象方法的抽象类
        public abstract Encoder getEncoder(String encodingName);
        public abstract Decoder getDecoder(String encodingName);
  • 如果提供者不支持给定的编码,则每个方法都返回一个适当的对象或null 。 典型的提供者支持不止一种编码。
  • 如果com.example.impl.StandardCodecs是CodecSet服务的实现, 那么它的 jar 文件还包含一个名为 META-INF/services/com.example.CodecSet
  • 此文件包含单行:
        com.example.impl.StandardCodecs    # Standard codecs
  • CodecSet类在初始化时创建并保存单个服务实例:
        private static ServiceLoader<CodecSet> codecSetLoader
             = ServiceLoader.load(CodecSet.class);
  • 为了 定位 给定编码名称的编码器,它定义了一个静态工厂方法,该方法遍历已知和可用的提供者,仅在找到合适的编码器或用完提供者时才返回。
        public static Encoder getEncoder(String encodingName) {
            for (CodecSet cp : codecSetLoader) {
                Encoder enc = cp.getEncoder(encodingName);
                if (enc != null)
                    return enc;
            }
            return null;
        }
  • getDecoder方法的定义类似。
  • 使用说明:如果用于提供程序加载的类加载器的类路径包括远程网络 URL,那么在搜索提供程序配置文件的过程中将取消引用这些 URL。
  • 此行为是正常的,尽管它可能会导致在 Web 服务器日志中创建令人费解的条目。 但是,如果未正确配置 Web 服务器,则此活动可能会导致提供程序加载算法错误地失败。
  • 当请求的资源不存在时,Web 服务器应返回 HTTP 404(未找到)响应。 然而,有时,在这种情况下,Web 服务器被错误地配置为返回 HTTP 200 (OK) 响应以及有用的 HTML 错误页面。 当此类尝试将 HTML 页面解析为提供者配置文件时,这将导致抛出ServiceConfigurationError 。 此问题的最佳解决方案是修复错误配置的 Web 服务器以返回正确的响应代码 (HTTP 404) 以及 HTML 错误页面。

也就是说?

  • 服务提供者要遵循一些规范。

  • 通过在资源目录META-INF/services 中放置提供者配置文件来标识服务提供者

    202301011551572433.png

    202301011551578074.png

  • 所以ServiceLoader就单单根据Driver.class就找到的了类。

稍稍阅读一下其源码

  • 看一下它的load方法

202301011551584165.png

  • 看它的构造方法

202301011551590726.png

用-XX:+TraceClassLoading看看

202301011551597197.png

都是从驱动的jar包里加载的

上面的代码的线程上下文类加载器改一下

  • 代码
    public class ServiceLoaderTest {
        public static void main(String[] args) {
            //设置线程上下文类加载器
            Thread.currentThread().setContextClassLoader(ServiceLoaderTest.class.getClassLoader().getParent());
            ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
            for (Driver driver : loader) {
                System.out.println("diverName :" +driver.getClass()+" , loadDriver's Loader: "+driver.getClass().getClassLoader());
            }
            System.out.println("当前线程上下文类加载器 : "+Thread.currentThread().getContextClassLoader());
            System.out.println("加载ServiceLoader的类加载 : " +loader.getClass().getClassLoader());
        }
    }

202301011552002338.png

  • 首先,服务提供者由调用类的线程上下文类加载器加载,源码已经很写的很清楚了。
  • 其次,在这里上下文类加载器已经手动修改了,为ExtClassLoader。
  • 然后,ExtClassLoader显然无法加载mysql驱动中的类,因为它只能加载ext目录下的类。

小结

  • 当前线程上下文类加载器,是用来弥补(打破)双亲委托模型的,让SPI可以被正常加载和使用
  • 要明确,线程上下文类加载器是在程序运行的时候才有的,并去加载类的。

JDBC

  • 通过阅读DriverManager源码,它就是ServiceLoader加载的驱动。

    202301011552010339.png

  • 通过阅读其源码发现使用驱动的时候并不需要

        Class.forName("com.mysql.jdbc.driver");
这样的语句也可以加载驱动。
  • 原因

2023010115520162610.png

Jar hell问题以及解决办法

  • 当一个类或者一个资源文件存在多个jar中,就会存在jar hell问题。
  • 可以通过以下代码来诊断问题:
        class JarHell{
          public static void main(String[] args) throws IOException {
              String resourceName = "java/lang/String.class";//重复的类名
              ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
              Enumeration<URL> urls = classLoader.getResources(resourceName);
              while (urls.hasMoreElements()){
                  URL url = urls.nextElement();
                  System.out.println(url);
              }
          }
        }