杰瑞科技汇

java classloader获取

什么是 ClassLoader?

ClassLoader 是 Java 虚拟机(JVM)的一部分,它的主要职责是“加载”,当 JVM 需要使用一个类时,ClassLoader 会负责查找类的字节码,并将其转换为 Class 对象,然后将其放入内存中。

java classloader获取-图1
(图片来源网络,侵删)

Java 的类加载机制采用“双亲委派模型”(Parent Delegation Model),这有助于安全性:

  1. 当一个 ClassLoader 收到类加载请求时,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器。
  2. 只有当父类加载器反馈自己无法完成这个加载请求(在它的搜索范围内没有找到所需的类)时,子加载器才会尝试自己去加载。

如何获取 ClassLoader?

主要有以下三种方式,它们获取的 ClassLoader 实例不同,用途也不同。

获取当前类的 ClassLoader

这是最常见的方式,用于加载与当前类在同一位置的资源(如配置文件、属性文件等)。

// 在 MyClass.java 这个类中
public class MyClass {
    public void loadResource() {
        // 获取加载当前类 (MyClass.class) 的 ClassLoader
        ClassLoader classLoader = MyClass.class.getClassLoader();
        // 使用这个 ClassLoader 加载资源
        // 资源路径相对于 "classpath" (即 /src/main/resources 目录)
        InputStream inputStream = classLoader.getResourceAsStream("config.properties");
        if (inputStream != null) {
            System.out.println("成功找到 config.properties 文件!");
            // ... 处理输入流 ...
        } else {
            System.out.println("未找到 config.properties 文件。");
        }
    }
}

特点:

java classloader获取-图2
(图片来源网络,侵删)
  • 获取谁? 加载当前类的那个 ClassLoader
  • 通常是谁? 对于在 classpath 下的应用代码,通常是 AppClassLoader(也叫 System ClassLoader)。
  • 适用场景: 加载与当前类同属一个模块或项目的资源文件。

获取当前线程的 Context ClassLoader

这种方式在多线程、特别是涉及框架(如 Spring、Tomcat)和 Java EE 应用时非常重要。

public class MyThreadTask implements Runnable {
    @Override
    public void run() {
        // 获取执行当前代码的线程的 Context ClassLoader
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        System.out.println("当前线程的 Context ClassLoader: " + contextClassLoader);
        // 使用这个 ClassLoader 加载资源
        // 这对于在多线程环境中加载特定模块的资源非常关键
        InputStream inputStream = contextClassLoader.getResourceAsStream("module-config.xml");
        if (inputStream != null) {
            System.out.println("成功通过 ContextClassLoader 找到 module-config.xml 文件!");
        } else {
            System.out.println("未找到 module-config.xml 文件。");
        }
    }
}

特点:

  • 获取谁? 执行当前代码的线程所关联的 ClassLoader
  • 通常是谁? 默认情况下,新线程会继承其创建者线程的 Context ClassLoader,在 Web 服务器(如 Tomcat)中,每个 Web 应用都有自己的 ClassLoader,Tomcat 会将请求线程的 Context ClassLoader 设置为对应 Web 应用的 ClassLoader
  • 适用场景:
    • 框架开发: Spring 等框架在执行用户代码时,会使用 Context ClassLoader 来加载 Bean 定义和配置,确保能加载到用户项目中的类。
    • 多线程环境: 当你的线程需要加载的资源不属于默认的 classpath,而是属于某个特定模块或 Web 应用时。
    • SPI 机制: Java 的 ServiceLoader 使用 Context ClassLoader 来查找服务实现类,这使得不同模块可以提供自己的服务实现,而无需依赖具体的类加载器。

获取系统 ClassLoader (AppClassLoader)

这是获取应用程序自身类加载器的方式。

public class SystemClassLoaderExample {
    public static void main(String[] args) {
        // 获取系统 ClassLoader
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("系统 ClassLoader: " + systemClassLoader);
        // 它通常等同于 MyClass.class.getClassLoader()
        ClassLoader classLoader = SystemClassLoaderExample.class.getClassLoader();
        System.out.println("当前类的 ClassLoader: " + classLoader);
        // 判断它们是否是同一个实例
        System.out.println("它们是同一个实例吗? " + (systemClassLoader == classLoader));
    }
}

特点:

java classloader获取-图3
(图片来源网络,侵删)
  • 获取谁? JVM 启动时由 java.class.path 系统属性指定的类路径下的类加载器,即应用程序类加载器
  • 作用: 它负责加载应用程序自身目录(如 jar 包或 classes 目录)中的类。
  • 适用场景: 当你需要明确地使用应用程序的类加载器,而不是依赖当前类或线程的上下文时。

ClassLoader 层次结构与 getResource vs getResourceAsStream

理解 ClassLoader 的层次结构对于排查问题至关重要。

层次结构

JVM 中的类加载器通常有三层(或更多,如 Tomcat 会增加 WebAppClassLoader):

  1. Bootstrap ClassLoader (启动类加载器)

    • 最顶层,由 C++ 实现,是 JVM 自身的一部分。
    • 负责加载 Java 核心库 (JAVA_HOME/jre/lib/rt.jar, resources.jar 等)。
    • 没有父加载器,在 Java 代码中获取它返回 null
      System.out.println(BootstrapClassLoader.class.getClassLoader()); // 输出 null
  2. Extension ClassLoader (扩展类加载器)

    • 负责加载 Java 的扩展库 (JAVA_HOME/jre/lib/ext/*.jar)。
    • 它的父加载器是 Bootstrap ClassLoader
  3. Application ClassLoader (应用程序类加载器 / System ClassLoader)

    • 负责加载用户类路径(classpath)上所指定的类库。
    • 它的父加载器是 Extension ClassLoader
    • 我们通常通过 ClassLoader.getSystemClassLoader() 获取的就是它。

getResource vs getResourceAsStream

这两个方法都用于从 ClassLoader 中查找资源,但返回类型不同。

  • public URL getResource(String name)

    • 返回: 一个 java.net.URL 对象,代表资源的定位符。
    • 如果找不到资源: 返回 null
    • 用途: 当你需要知道资源的完整路径(file:/...jar:file:/...)时。
  • public InputStream getResourceAsStream(String name)

    • 返回: 一个 java.io.InputStream 流,可以直接读取资源内容。
    • 如果找不到资源: 返回 null
    • 用途: 当你只需要读取资源的内容(如读取配置文件、图片数据等)时,更常用这个方法,因为它更简洁,无需手动关闭 URL 连接。

重要提示: getResource 方法会遵循双亲委派模型,它会先让父加载器去查找,如果父加载器找不到,自己才会去查找。


总结与最佳实践

获取方式 代码示例 获取目标 适用场景
当前类的 ClassLoader MyClass.class.getClassLoader() 加载当前类的 ClassLoader 加载与当前类同属项目的资源文件。最常用
当前线程的 Context ClassLoader Thread.currentThread().getContextClassLoader() 执行当前代码的线程的 ClassLoader 多线程环境、框架(Spring)、SPI机制。处理模块化资源的关键
系统 ClassLoader ClassLoader.getSystemClassLoader() 应用程序类加载器 需要明确使用应用主加载器的场景。

最佳实践:

  1. 加载应用资源: 优先使用 MyClass.class.getClassLoader().getResourceAsStream("..."),这是最标准、最不容易出错的方式。
  2. 在多线程或框架中加载资源: 如果你的代码可能被运行在非默认类加载器环境(在 Tomcat 的一个 Web 应用中),并且需要加载该 Web 应用特有的资源,务必使用 Thread.currentThread().getContextClassLoader(),这是避免 ClassNotFoundExceptionNoClassDefFoundError 的关键。
  3. 避免直接使用 Bootstrap ClassLoader: 普通开发者很少需要直接操作它,当你看到 null 作为 ClassLoader 时,通常就代表它。
  4. 注意资源路径: getResource 的路径是相对于 classpath 根目录的,**不要以 `/
分享:
扫描分享到社交APP
上一篇
下一篇