JVM 线程上下文类加载器 ServiceLoader

858 阅读9分钟

先从一段代码说起

前提要导入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());
    }
}
  • 结果

image.png

疑问

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

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

image.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 中放置提供者配置文件来标识服务提供者 image.png image.png
  • 所以ServiceLoader就单单根据Driver.class就找到的了类。

稍稍阅读一下其源码

  • 看一下它的load方法

image.png

  • 看它的构造方法

image.png

用-XX:+TraceClassLoading看看

image.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());
    }
}

image.png

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

小结

  • 当前线程上下文类加载器,是用来弥补(打破)双亲委托模型的,让SPI可以被正常加载和使用
  • 要明确,线程上下文类加载器是在程序运行的时候才有的,并去加载类的。
  • 打破双亲委托的秘诀就线程上下文类加载器,它和一般的类加载器不同,它是动态的,其实有点像反射,让父类加载器加载的SPI接口,在运行期间可以访问SPI的实现类,如果用传统的类加载器去加载SPI的实现类就会导致接口无法访问实现类。非要用也不是不可以,比如指定AppClassLoader去加载SPI接口和其实现类也行,让它们在同一个命名空间,但是可能会导致不同本版的SPI被加载,导致冲突。
  • 线程上下文类加载器是可以跨越不同的类加载器边界的。

JDBC

  • 通过阅读DriverManager源码,它就是ServiceLoader加载的驱动。 image.png
  • 通过阅读其源码发现使用驱动的时候并不需要
    Class.forName("com.mysql.jdbc.driver");
    
    这样的语句也可以加载驱动。
  • 原因

image.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);
          }
      }
    }
    

自问自答

  • SPI接口不是会被父类加载器加载一次,然后又被ServiceLoader加载一次吗,这不会冲突吗?
    • SPI 接口可能会被不同的类加载器加载多次,但是这不会导致冲突,因为每个类加载器都有自己的命名空间,所以每次加载得到的是不同的类对象,虽然它们都有相同的类名和类定义,但它们实际上是不同的类对象。
    • 具体来说,SPI 接口通常是由父类加载器加载的,因为 SPI 接口是应用程序的一部分,通常由应用程序的父类加载器加载。然后,在运行时,ServiceLoader 使用当前线程的上下文类加载器来加载实现类。这样,即使父类加载器已经加载了 SPI 接口,但是实现类是由不同的类加载器加载的,因此不会冲突。
    • 因此,在使用 ServiceLoader 加载 SPI 服务时,SPI 接口可能会被父类加载器加载一次,而实现类则是由线程上下文类加载器加载的。这不会导致冲突,因为实现类是由不同的类加载器加载的,而且 ServiceLoader 会使用线程上下文类加载器加载的接口实例,从而避免了可能的冲突。
  • ServiceLoader会加载SPI接口吗?
    • ServiceLoader 并不会主动加载 SPI 接口,因为 SPI 接口通常是由应用程序的父类加载器加载的一部分。ServiceLoader 的作用是从特定的路径下(META-INF/services/)查找并加载指定的 SPI 实现类。
    • 具体来说,应用程序通常会在自己的代码中使用 SPI 接口,然后通过 ServiceLoader 加载实现类。应用程序的父类加载器会在应用程序启动时加载 SPI 接口,然后在运行时通过 ServiceLoader 加载 SPI 实现类,从而完成 SPI 服务的动态扩展。这种方式避免了应用程序自己管理 SPI 实现类的加载和初始化,使得应用程序更加灵活和可扩展。