What is class isolation technology

As long as you write enough Java code, there will be a situation where the system introduces a new middleware JAR package, which will compile properly and run with an error: Java. Lang. NoSuchMethodError, then HengChi HengChi start Google to find the solution, and finally find in hundreds of dependent package was almost blind eyes, to find the conflict of the jar, put the question to solve after started poking fun at middleware why do so many different versions of the jar, writing code for five minutes, I’ve been queuing for bags all day.

This is a common situation in Java development because different JAR packages depend on different versions of some common JAR packages (such as the logging component). This is not a problem at compile time, but at runtime it will cause an error because the class loaded is not as expected. Here’s an example: A and B rely on the v1 and v2 versions of C respectively, and the error method is added to the Log of V2 version. Now, two JAR packages, A and B, as well as v0.1 and V0.2 versions of C, are introduced into the project. Maven can only select a C version when packaging, assuming v1 is selected. When it comes time to run, by default all classes in a project are loaded with the same classloader, so no matter how many versions of C you rely on, only one version of C will end up being loaded into the JVM. When B access Log. The error, you will find no error at all in the Log method, and then thrown exception. Java lang. NoSuchMethodError. This is a classic case of class conflict.

Class conflicts are easy to solve if the version is backward compatible, just exclude the lower version and be done. But if the version is not backward compatible, it is a “save mom or save girlfriend” dilemma.

In order to avoid the dilemma, some people put forward the class isolation technology to solve the problem of class conflict. The principle of class isolation is simple: each module is loaded using a separate class loader so that the dependencies of different modules do not affect each other. As shown in the figure below, different modules are loaded with different class loaders. Why does this resolve class conflicts? A Java mechanism is used here: classes loaded by different class loaders appear to the JVM to be two different classes, because the only identification of a class in the JVM is the class loader + the class name. This way we can load two different versions of C classes at the same time, even though they have the same class name. Note that class loaders refer to instances of class loaders. It is not necessary to define two different class loaders. For example, PluginClassLoaderA and PluginClassLoaderB can be different instances of the same class loader.

How is class isolation implemented

As mentioned earlier, class isolation means having jar packages for different modules loaded with different class loaders. To do this, we need to enable the JVM to use custom class loaders to load our written classes and their associated classes.

So how do you do that? A simple way to do this would be for the JVM to provide a setup interface for the global class loader so that we could replace the global class loader directly, but that would not solve the problem of having multiple custom class loaders at the same time.

In fact, the JVM provides a very simple and effective way of doing this, which I call the classloading conduction rule: The JVM selects the classloader of the current class to load all referenced classes of that class. For example, if we define two classes, TestA and TestB, and TestA refers to TestB, as long as we use a custom class loader to load TestA, then at run time, when TestA calls TestB, TestB is also loaded by the JVM’s class loader using TestA. By analogy, all jar classes associated with TestA and its reference classes are loaded by the custom class loader. In this way, we simply have the module’s main class loaded with a different class loader, and each module will be loaded with the main class loader, thus allowing multiple modules to use different class loaders. This is the core principle behind OSGi and SofaArk’s ability to achieve class isolation.

Now that we know how class isolation works, we start by overriding the class loader. To implement your own ClassLoader, let the custom ClassLoader inherit java.lang.ClassLoader and override the classloading method. Here we have two options: override findClass(String name). One is to override loadClass(String name). So which one should you choose? What’s the difference between the two?

Let’s try to rewrite these two methods to implement a custom classloader.

Override findClass

TestA will print its own classloader, and then call TestB to print its own classloader. We expect that MyClassLoaderParentFirst, which overrides the findClass method, will be able to load TestA, Let TestB also be automatically loaded by MyClassLoaderParentFirst.

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()); }}Copy the code

Then rewrite the findClass method, which loads the class file according to the file path and then calls defineClass to get the class object.

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 = 0;
            while((bytesNumRead = ins.read(buffer)) ! = -1) { baos.write(buffer, 0, bytesNumRead); }return baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        returnnew byte[] {}; }}Copy the code

Finally, we write a main method that calls the custom classloader to load TestA, and then print the classloader information through reflection.

public class MyTest {

    public static void main(String[] args) throws Exception {
        MyClassLoaderParentFirst myClassLoaderParentFirst = new MyClassLoaderParentFirst();
        Class testAClass = myClassLoaderParentFirst.findClass("com.java.loader.TestA");
        Method mainMethod = testAClass.getDeclaredMethod("main", String[].class);
        mainMethod.invoke(null, new Object[]{args});
    }
Copy the code

The result is as follows:

TestA: com.java.loader.MyClassLoaderParentFirst@1d44bcfa
TestB: sun.misc.Launcher$AppClassLoader@18b4aac2
Copy the code

TestA is loaded by MyClassLoaderParentFirst, but TestB is loaded by AppClassLoader. Why is that?

To answer this question, it’s important to understand a class loading rule: the JVM invokes the classLoader.loadClass method when triggering class loading. This method implements parental delegation:

  1. Delegate the query to the parent loader
  2. If the parent loader cannot find it, the findClass method is called to load it

Once this rule is understood, the reason for the result of execution is found: The JVM does use MyClassLoaderParentFirst to load TestB, but because of parental delegation, TestB is delegated to MyClassLoaderParentFirst’s parent loader, AppClassLoader.

You may also wonder why the parent loader of MyClassLoaderParentFirst is AppClassLoader. Since the main method classes we define are loaded by default by the JDK’s own AppClassLoader, according to class loading rules, MyClassLoaderParentFirst referenced by the main class is also loaded by the AppClassLoader that loads the main class. Since the parent class of MyClassLoaderParentFirst is ClassLoader, the default constructor for ClassLoader automatically sets the value of the parent loader to AppClassLoader.

    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
Copy the code

Override loadClass

Overwriting the findClass method will cause TestB to be loaded by the AppClassLoader, which does not meet the goal of class isolation, so we have to override the loadClass method to break the parent delegation mechanism. The code looks like this:

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 { Class result = null; Result = jdkClassloader.loadClass (name); } catch (Exception e) {// ignore}if(result ! = null) {return result;
        }
        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) { //省略 }

}

Copy the code

Note here that we have overridden the loadClass method, which means that all classes, including those in the java.lang package, will be loaded through MyClassLoaderCustom, but class isolation does not include classes that come with the JDK. Result = jdkClassloader.loadClass (name);

The test code is as follows:

public class MyTest { public static void main(String[] args) throws Exception { // The parent of the AppClassLoader (ExtClassLoader) is used as the jdkClassLoader of MyClassLoaderCustom new MyClassLoaderCustom(Thread.currentThread().getContextClassLoader().getParent()); ClasstestAClass = myClassLoaderCustom.loadClass("com.java.loader.TestA");
        Method mainMethod = testAClass.getDeclaredMethod("main", String[].class); mainMethod.invoke(null, new Object[]{args}); }}Copy the code

The result is as follows:

TestA: com.java.loader.MyClassLoaderCustom@1d44bcfa
TestB: com.java.loader.MyClassLoaderCustom@1d44bcfa
Copy the code

As you can see, by overwriting the loadClass method, we managed to get TestB loaded into the JVM using MyClassLoaderCustom as well.

conclusion

Class isolation technology is born to solve the dependency conflict. It destroys the parent delegation mechanism through the custom class loader, and then realizes the class isolation of different modules by using the class loading conduction rules.

The resources

Delve into Java class loaders