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.

Alibaba Cloud Developer
Alibaba Cloud Developer
Alibaba Cloud Developer
How to Implement Class Isolation in Java with Custom ClassLoaders

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@18b4aac2

Because 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@1d44bcfa

Both 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)
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaJVMclassloaderCustom ClassLoaderClass IsolationDependency Conflict
Alibaba Cloud Developer
Written by

Alibaba Cloud Developer

Alibaba's official tech channel, featuring all of its technology innovations.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.