Skip to content

ClassLoader 常用方法

ClassLoader API 速查

java.lang.ClassLoader 是所有类加载器的父类。理解它的 API,是熟练使用类加载器的基础。

核心方法一览

方法说明
loadClass(String)加载指定的类,包含双亲委派逻辑
findClass(String)在自定义位置查找类,需要重写
defineClass(byte[], ...)将字节数组转换为 Class 对象
findLoadedClass(String)查询某个类是否已被加载
getParent()获取父类加载器
getSystemClassLoader()获取系统类加载器
getResource(String)查找资源文件
getResources(String)查找所有同名资源

loadClass:类加载的入口

loadClass() 是 ClassLoader 的核心方法,定义了双亲委派逻辑。

java
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 第一步:检查是否已加载(findLoadedClass)
    Class<?> result = findLoadedClass(name);
    if (result != null) {
        return result;
    }

    // 第二步:尝试让父加载器加载
    try {
        ClassLoader parent = getParent();
        if (parent != null) {
            result = parent.loadClass(name, false);
        } else {
            result = findBootstrapClassOrNull(name);
        }
    } catch (ClassNotFoundException e) {
        // 父加载器找不到,不报错
    }

    // 第三步:自己加载
    if (result == null) {
        result = findClass(name);
    }

    // 第四步:是否解析
    if (resolve) {
        resolveClass(result);
    }
    return result;
}

不要重写 loadClass(),除非你有特殊原因需要打破双亲委派。正确的做法是重写 findClass()

findClass:子类实现的核心

findClass() 是子类应该重写的方法,负责在特定位置查找字节码。

java
public class DiskClassLoader extends ClassLoader {
    private String basePath;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = name.replace('.', '/') + ".class";
        byte[] data = loadFromDisk(fileName);
        if (data == null) {
            throw new ClassNotFoundException(name);
        }
        return defineClass(name, data, 0, data.length);
    }

    private byte[] loadFromDisk(String fileName) {
        // 从磁盘读取 .class 文件
        // ...
        return null;
    }
}

defineClass:将字节数组变成 Class 对象

defineClass() 是把字节数组转换成 JVM 认识的 Class 对象的关键方法。

java
// 基础用法
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    throws ClassFormatError

// 带 protection domain(安全域)
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
    ProtectionDomain protectionDomain) throws ClassFormatError

常见错误

java
public class DefineClassMistakes {
    public static void main(String[] args) {
        // 错误:defineClass 是 final 方法,不能被子类重写
        // 正确做法是通过重写 findClass 间接调用

        // 错误:在子类中直接调用 defineClass 而不重写 findClass
        // 因为 loadClass 会先调用 findClass

        // 正确模式:
        // 1. 重写 findClass(),读取字节数组
        // 2. 调用 defineClass() 转换成 Class 对象
    }
}

加密/解密场景

java
public class SecureLoader extends ClassLoader {
    private byte[] decrypt(byte[] data, byte key) {
        byte[] result = new byte[data.length];
        for (int i = 0; i < data.length; i++) {
            result[i] = (byte) (data[i] ^ key);
        }
        return result;
    }

    @Override
    protected Class&lt;?&gt; findClass(String name) throws ClassNotFoundException {
        byte[] encrypted = loadEncryptedData(name);
        byte[] decrypted = decrypt(encrypted, SECRET_KEY);
        return defineClass(name, decrypted, 0, decrypted.length);
    }
}

findLoadedClass:检查类是否已加载

java
public class CheckLoaded {
    public static void main(String[] args) {
        ClassLoader loader = CheckLoaded.class.getClassLoader();

        // 检查类是否已被当前加载器加载
        Class&lt;?&gt; clazz = loader.findLoadedClass("java.lang.String");
        System.out.println("String loaded? " + (clazz != null));
        // String 由 Bootstrap 加载,这里返回 null

        // 在加载新类之前先检查
        Class&lt;?&gt; existing = loader.findLoadedClass("com.example.MyClass");
        if (existing == null) {
            loader.loadClass("com.example.MyClass");
        }
    }
}

getParent:获取父加载器

java
public class GetParentDemo {
    public static void main(String[] args) {
        ClassLoader app = GetParentDemo.class.getClassLoader();
        ClassLoader ext = app.getParent();
        ClassLoader boot = ext.getParent();

        System.out.println("App: " + app);        // sun.misc.Launcher$AppClassLoader
        System.out.println("Ext: " + ext);        // sun.misc.Launcher$ExtClassLoader
        System.out.println("Boot: " + boot);      // null
    }
}

getSystemClassLoader:获取系统类加载器

java
public class SystemLoader {
    public static void main(String[] args) {
        // 获取系统类加载器(就是 AppClassLoader)
        ClassLoader system = ClassLoader.getSystemClassLoader();
        System.out.println(system);

        // 系统类加载器的 parent 是扩展类加载器
        System.out.println(system.getParent());
    }
}

getResource / getResources:加载资源文件

ClassLoader 不仅能加载类,还能加载资源文件(如配置、图标):

java
public class ResourceDemo {
    public static void main(String[] args) {
        ClassLoader loader = ResourceDemo.class.getClassLoader();

        // 查找单个资源
        URL url = loader.getResource("application.properties");
        if (url != null) {
            System.out.println("Found: " + url);
        }

        // 查找所有同名资源
        try {
            Enumeration&lt;URL&gt; urls = loader.getResources("META-INF/MANIFEST.MF");
            while (urls.hasMoreElements()) {
                System.out.println("Found: " + urls.nextElement());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 也可以用 Class 的便捷方法(使用 Class 自己的类加载器)
        InputStream is = ResourceDemo.class.getResourceAsStream("/config.xml");
        // 注意:Class 的 getResourceAsStream 中 "/" 代表 classpath 根目录
    }
}

resolveClass:触发类的解析

java
protected void resolveClass(Class&lt;?&gt; c)

调用 loadClass(name, true) 时,第二个参数 resolve=true 会自动调用 resolveClass。它执行类加载的最后一步——解析,把符号引用变成直接引用。

java
public class ResolveDemo {
    public static void main(String[] args) throws Exception {
        ClassLoader loader = ResolveDemo.class.getClassLoader();

        // 不解析的加载(false)
        Class&lt;?&gt; c1 = loader.loadClass("com.example.Foo", false);

        // 解析的加载(true)
        Class&lt;?&gt; c2 = loader.loadClass("com.example.Bar", true);
    }
}

通常不需要手动调用,因为 loadClass(true) 会自动处理。

完整使用示例

java
public class MyClassLoader extends ClassLoader {

    private final String baseDir;

    public MyClassLoader(String baseDir, ClassLoader parent) {
        super(parent);  // 指定父类加载器
        this.baseDir = baseDir;
    }

    // 重写 findClass,不重写 loadClass(保留双亲委派)
    @Override
    protected Class&lt;?&gt; findClass(String name) throws ClassNotFoundException {
        // 1. 构造文件路径
        String fileName = name.replace('.', File.separatorChar) + ".class";
        File file = new File(baseDir, fileName);

        if (!file.exists()) {
            throw new ClassNotFoundException(name);
        }

        // 2. 读取字节数组
        byte[] classData;
        try (FileInputStream fis = new FileInputStream(file);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesRead);
            }
            classData = baos.toByteArray();
        }

        // 3. 调用 defineClass
        return defineClass(name, classData, 0, classData.length);
    }

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

        Class&lt;?&gt; clazz = loader.loadClass("com.example.TestClass");
        System.out.println("Class: " + clazz.getName());
        System.out.println("Loader: " + clazz.getClassLoader());

        Object instance = clazz.getDeclaredConstructor().newInstance();
        System.out.println("Instance: " + instance);
    }
}

本节小结

ClassLoader 的 API 设计遵循了模板方法模式

  • loadClass() 定义了加载的流程(双亲委派)
  • findClass() 是子类实现细节(在哪里找)
  • defineClass() 是最终转换步骤(字节数组 → Class 对象)

理解了这个设计,就能正确地使用和扩展 ClassLoader。

下一节,我们来看 双亲委派机制(原理/优势/沙箱安全),深入理解双亲委派的底层原理。

基于 VitePress 构建