How to Implement Class Isolation in Java with Custom ClassLoaders
This article explains why jar version conflicts cause runtime errors in Java, introduces class isolation as a solution, and provides step‑by‑step implementations of custom ClassLoaders using both findClass and loadClass overrides, complete with code examples and execution results.
1 What Is Class Isolation?
When different JARs depend on different versions of a common library, compilation succeeds but the JVM may throw errors such as java.lang.NoSuchMethodError at runtime because the loaded class does not match the expected version.
For example, if module A depends on C v1 and module B depends on C v2, and Maven selects v1 during packaging, B will fail when it tries to call a method that only exists in v2.
Class isolation solves this by assigning each module its own ClassLoader, allowing multiple versions of the same class to coexist because the JVM treats classes loaded by different ClassLoaders as distinct (identified by ClassLoader + class name ).
If the conflicting versions are backward compatible, simply excluding the older version works; otherwise, class isolation is required.
2 How to Implement Class Isolation
To load modules with separate ClassLoaders, we need a custom ClassLoader that can load our classes and their dependencies.
One simple approach is to replace the global ClassLoader, but that does not allow multiple custom loaders simultaneously.
The JVM follows a "class loading propagation rule": when a class is loaded, the same ClassLoader is used to load all classes it references. By loading a module's main class with a custom loader, all its dependent classes are also loaded by that loader. This principle underlies OSGi and SofaArk.
1 Rewrite findClass
Define two test classes, TestA and TestB, where TestA prints its ClassLoader and then invokes TestB:
public class TestA {
public static void main(String[] args) {
TestA testA = new TestA();
testA.hello();
}
public void hello() {
System.out.println("TestA: " + this.getClass().getClassLoader());
TestB testB = new TestB();
testB.hello();
}
}
public class TestB {
public void hello() {
System.out.println("TestB: " + this.getClass().getClassLoader());
}
}Custom ClassLoader that overrides findClass:
public class MyClassLoaderParentFirst extends ClassLoader {
private Map<String, String> classPathMap = new HashMap<>();
public MyClassLoaderParentFirst() {
classPathMap.put("com.java.loader.TestA", "/Users/hansong/IdeaProjects/OhMyJava/CodeRepository/target/classes/com/java/loader/TestA.class");
classPathMap.put("com.java.loader.TestB", "/Users/hansong/IdeaProjects/OhMyJava/CodeRepository/target/classes/com/java/loader/TestB.class");
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
String classPath = classPathMap.get(name);
File file = new File(classPath);
if (!file.exists()) {
throw new ClassNotFoundException();
}
byte[] classBytes = getClassData(file);
if (classBytes == null || classBytes.length == 0) {
throw new ClassNotFoundException();
}
return defineClass(classBytes, 0, classBytes.length);
}
private byte[] getClassData(File file) {
try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesNumRead;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return new byte[]{};
}
}Test driver:
public class MyTest {
public static void main(String[] args) throws Exception {
MyClassLoaderParentFirst loader = new MyClassLoaderParentFirst();
Class<?> testAClass = loader.findClass("com.java.loader.TestA");
Method mainMethod = testAClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[]{args});
}
}Result:
TestA: com.java.loader.MyClassLoaderParentFirst@1d44bcfa
TestB: sun.misc.Launcher$AppClassLoader@18b4aac2Because findClass is still subject to the parent‑delegation model, TestB is loaded by the AppClassLoader.
2 Rewrite loadClass
To break the delegation, override loadClass and delegate only JDK classes to the standard loader:
public class MyClassLoaderCustom extends ClassLoader {
private ClassLoader jdkClassLoader;
private Map<String, String> classPathMap = new HashMap<>();
public MyClassLoaderCustom(ClassLoader jdkClassLoader) {
this.jdkClassLoader = jdkClassLoader;
classPathMap.put("com.java.loader.TestA", "/Users/hansong/IdeaProjects/OhMyJava/CodeRepository/target/classes/com/java/loader/TestA.class");
classPathMap.put("com.java.loader.TestB", "/Users/hansong/IdeaProjects/OhMyJava/CodeRepository/target/classes/com/java/loader/TestB.class");
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// Load JDK classes normally
try {
return jdkClassLoader.loadClass(name);
} catch (Exception e) {
// ignore and try custom loading
}
String classPath = classPathMap.get(name);
File file = new File(classPath);
if (!file.exists()) {
throw new ClassNotFoundException();
}
byte[] classBytes = getClassData(file);
if (classBytes == null || classBytes.length == 0) {
throw new ClassNotFoundException();
}
return defineClass(classBytes, 0, classBytes.length);
}
private byte[] getClassData(File file) { /* same as above */ }
}Driver for the custom loader:
public class MyTest {
public static void main(String[] args) throws Exception {
// Use ExtClassLoader (parent of AppClassLoader) for JDK classes
ClassLoader ext = Thread.currentThread().getContextClassLoader().getParent();
MyClassLoaderCustom loader = new MyClassLoaderCustom(ext);
Class<?> testAClass = loader.loadClass("com.java.loader.TestA");
Method mainMethod = testAClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[]{args});
}
}Result:
TestA: com.java.loader.MyClassLoaderCustom@1d44bcfa
TestB: com.java.loader.MyClassLoaderCustom@1d44bcfaBoth classes are now loaded by the custom loader, achieving true class isolation.
3 Summary
Class isolation addresses dependency conflicts by breaking the parent‑delegation mechanism with custom ClassLoaders and leveraging the JVM's class loading propagation rule, allowing each module to load its own version of a library without interference.
Reference: 深入探讨 Java 类加载器 (https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html)
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Alibaba Cloud Developer
Alibaba's official tech channel, featuring all of its technology innovations.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
