只重写 findclass 而不重写 loadclass,因为 loadclass 已实现双亲委派逻辑,直接重写会破坏委派导致 classnotfoundexception 或 linkageerror;findclass 是安全钩子,负责定位字节码、读取为 byte[]、调用 defineclass 定义类。

自定义类加载器的核心在于继承 ClassLoader 并重写 findClass(String name) 方法,而不是直接重写 loadClass(String name) —— 因为后者已由父类实现了双亲委派逻辑,破坏它容易引发 ClassNotFoundException 或 LinkageError。
为什么只重写 findClass,不重写 loadClass?
loadClass 是类加载的入口,JDK 默认实现会先委托父加载器尝试加载(双亲委派),仅当父加载器无法加载时,才调用 findClass。若直接重写 loadClass 并跳过委派,会导致核心类(如 java.lang.Object)被重复加载或隔离,引发运行时异常。而 findClass 是留给子类“真正找字节码并定义类”的唯一安全钩子。
重写 findClass 的标准步骤
在 findClass 中需完成三件事:定位字节码、读取为 byte[]、调用 defineClass 转为 Class 对象。示例:
public class MyClassLoader extends ClassLoader {
private String baseDir; // 自定义路径,如 "classes/"
public MyClassLoader(String baseDir) {
this.baseDir = baseDir;
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
byte[] bytes = loadByteCode(name);
if (bytes == null) {
throw new ClassNotFoundException(name);
}
return defineClass(name, bytes, 0, bytes.length);
}
private byte[] loadByteCode(String name) {
String path = baseDir + name.replace('.', '/') + ".class";
try (InputStream is = new FileInputStream(path)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b;
while ((b = is.read()) != -1) {
baos.write(b);
}
return baos.toByteArray();
} catch (IOException e) {
return null;
}
}
}
关键细节与注意事项
- 不要在 findClass 中调用 getClassLoader():当前类的类加载器可能不是你实例化的那个(尤其在反射或动态代理场景),应依赖 this(即当前加载器实例)
- defineClass 返回的 Class 对象,其类加载器自动设为当前实例:无需手动设置,也不可修改
-
资源路径需转义包名为斜杠:
"com.example.Foo"→"com/example/Foo.class",用name.replace('.', '/') - 避免重复加载同一类:ClassLoader 本身不缓存,但可自行用 ConcurrentHashMap 缓存已加载的 Class,提升性能(注意线程安全)
如何使用自定义加载器加载类
创建实例后,显式调用 loadClass(它会触发 findClass):
MyClassLoader loader = new MyClassLoader("./my-classes/");
Class> clazz = loader.loadClass("com.example.Hello");
Object obj = clazz.getDeclaredConstructor().newInstance();
注意:不能用 new MyClassLoader(...).loadClass(...) 后直接强转——因为 JVM 不认识这个类的类型签名,需通过反射调用方法或使用接口解耦。










