Skip to content

自定义类加载器(实现与场景)

为什么需要自定义类加载器

三层类加载器已经能覆盖大多数场景了。但有些情况下,你需要自定义类加载器:

  • 热部署:不重启 JVM 的情况下加载新版本的类
  • 模块化隔离:同一个 JVM 中运行多个版本相同的类
  • 加密保护:对字节码进行加密,自定义加载器负责解密
  • 动态生成:运行时生成字节码并加载(ASM、CGlib 的基础)
  • 非标准来源:从数据库、网络、配置中心等非标准位置加载类

自定义类加载器的实现

自定义类加载器只需要做一件事:继承 ClassLoader 类,重写 findClass() 方法

最小实现

java
public class MyClassLoader extends ClassLoader {

    private String classPath;  // 类文件所在的目录

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException(name);
        }
        // defineClass:将字节数组转换为 Class 对象
        return defineClass(name, classData, 0, classData.length);
    }

    private byte[] loadClassData(String className) {
        // 将类的全限定名转为文件路径
        String fileName = className.replace('.', '/') + ".class";
        String fullPath = classPath + "/" + fileName;

        try (InputStream is = new FileInputStream(fullPath);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            return null;
        }
    }
}

使用自定义类加载器

java
public class UseMyLoader {
    public static void main(String[] args) throws Exception {
        MyClassLoader loader = new MyClassLoader("/path/to/classes");

        // 加载一个类
        Class<?> clazz = loader.loadClass("com.example.MyClass");

        // 创建实例
        Object instance = clazz.getDeclaredConstructor().newInstance();

        System.out.println("Class loaded by: " + clazz.getClassLoader());
    }
}

关键:findClass() vs loadClass()

ClassLoader 中有两个关键方法:

java
// 1. loadClass():处理双亲委派逻辑
// 不建议重写,破坏了双亲委派
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 包含双亲委派逻辑的实现
    // ...
}

// 2. findClass():真正查找类的地方
// 推荐重写,遵循双亲委派
protected Class<?> findClass(String name) throws ClassNotFoundException {
    // 在自定义位置查找类
    // ...
}

最佳实践:重写 findClass(),而不是 loadClass()。这样既能自定义类的查找逻辑,又能保留双亲委派的安全保障。

典型使用场景

场景一:热部署

这是热部署的核心原理:每个模块用独立的类加载器加载。当需要更新模块时,创建一个新的类加载器重新加载,就能实现不重启 JVM 的「热更新」。

java
public class HotDeployDemo {
    private ClassLoader currentLoader;

    public void loadNewVersion(String jarPath) throws Exception {
        // 创建新的类加载器(不继承旧加载器的命名空间)
        URL[] urls = { new URL("file:" + jarPath) };
        URLClassLoader newLoader = new URLClassLoader(urls, currentLoader);

        // 加载新版本的类
        Class<?> newClass = newLoader.loadClass("com.example.Plugin");

        // 替换旧实例
        Object newInstance = newClass.getDeclaredConstructor().newInstance();

        // 更新引用
        replacePlugin(newInstance);

        // 放弃旧加载器引用,触发旧类卸载
        currentLoader = newLoader;
    }
}

Tomcat 的热部署就是基于这个原理:每个 Webapp 有自己的类加载器,更新时销毁旧加载器、创建新加载器。

场景二:类库版本隔离

大型系统可能依赖同一个库的多个版本。自定义类加载器可以解决这个问题:

┌─────────────────────────────────────────┐
│  AppClassLoader                        │
│  ├─ 项目自带的 commons-lang 3.0        │
└───────────────┬───────────────────────┘

    ┌───────────┴───────────┐
    ▼                       ▼
┌───────────────┐     ┌───────────────┐
│  LoaderA      │     │  LoaderB      │
│  加载 2.x 版本 │     │  加载 3.x 版本 │
│  com.foo.Bar  │     │  com.foo.Bar  │
│  (2.0)        │     │  (3.0)        │
└───────────────┘     └───────────────┘

同一个 JVM 中可以同时存在两个 com.foo.Bar 类——只要它们由不同的类加载器加载。

场景三:加密保护

对字节码进行加密,自定义类加载器负责解密:

java
public class DecryptClassLoader extends ClassLoader {

    private static final byte DECRYPT_KEY = (byte) 0x55;

    public DecryptClassLoader(ClassLoader parent) {
        super(parent);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = name.replace('.', '/') + ".class.encrypted";
        byte[] encrypted = loadFile(fileName);
        if (encrypted == null) {
            throw new ClassNotFoundException(name);
        }
        // 解密
        byte[] decrypted = new byte[encrypted.length];
        for (int i = 0; i < encrypted.length; i++) {
            decrypted[i] = (byte) (encrypted[i] ^ DECRYPT_KEY);
        }
        return defineClass(name, decrypted, 0, decrypted.length);
    }

    private byte[] loadFile(String path) {
        // 读取加密后的文件
        // ...
        return null;
    }
}

场景四:数据库 / 网络加载

java
public class NetworkClassLoader extends ClassLoader {
    private String serverUrl;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = downloadClass(name);
        return defineClass(name, classData, 0, classData.length);
    }

    private byte[] downloadClass(String name) {
        // 从 HTTP 服务器下载字节码
        String url = serverUrl + "/" + name.replace('.', '/') + ".class";
        try (InputStream is = new URL(url).openStream()) {
            return is.readAllBytes();
        } catch (IOException e) {
            return null;
        }
    }
}

双亲委派与自定义类加载器的关系

自定义类加载器默认也遵循双亲委派。关键在于理解如何打破它

打破双亲委派的场景

某些 SPI(Service Provider Interface)场景下,必须打破双亲委派。例如 JDBC:

java
// 这是 JDBC 4.0 之前的使用方式
Class.forName("com.mysql.jdbc.Driver");  // 手动加载驱动

// JDBC 4.0+ 使用 SPI 机制
// ServiceLoader 需要使用线程上下文类加载器
// 才能加载 classpath 中的 Driver 实现
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);

SPI 的实现方式决定了必须用线程上下文类加载器(而非应用类加载器)才能正确找到驱动类——因为驱动的实现类是在 java.sql.DriverManager(Bootstrap 加载)内部被加载的。

java.lang.Thread

线程上下文类加载器允许在父子类加载器之间「借道」:

java
public class ContextLoaderDemo {
    public static void main(String[] args) {
        // 获取当前线程的上下文类加载器
        ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();

        // 设置当前线程的上下文类加载器
        Thread.currentThread().setContextClassLoader(myCustomLoader);

        // 在 SPI 框架中,调用链深处会用这个上下文类加载器加载类
    }
}

本节小结

自定义类加载器的核心要点:

  1. 继承 ClassLoader,重写 findClass() —— 遵循双亲委派
  2. defineClass() —— 将字节数组转换为 Class 对象
  3. 常见场景:热部署、版本隔离、加密保护、动态加载、SPI
  4. 打破双亲委派:通过线程上下文类加载器(SPI 机制依赖)

下一节,我们来看 ClassLoader 常用方法,掌握 ClassLoader API 的具体用法。

基于 VitePress 构建