How to implement Java class isolation loading?

Introduction: In  Java development, if different jar packages rely on different versions of some common jar packages, an error will be reported at runtime because the loaded classes do not meet expectations. How to avoid this situation? This paper analyzes the causes of conflicts in jar packages and the realization principle of class isolation, and shares two methods for implementing custom class loader.

image.png

One what is class isolation technology

As long as you write enough Java code, this situation will definitely occur: a new jar package of middleware is introduced into the system, everything is normal when compiling, and an error is reported as soon as it runs: java.lang.NoSuchMethodError, and then hum I started looking for a solution, and finally I found the conflicting jar after looking for hundreds of dependencies in the package. After solving the problem, I started to complain about the middleware. Why do so many different versions of the jar? Write code five Minutes, the bags lined up all day.

The above situation is a common situation in the Java development process, and the reason is very simple. Different jar packages rely on different versions of some common jar packages (such as log components). There is no problem when compiling, but when it is running, it will be The loaded class does not meet the expectations and an error is reported. For example: A and B depend on the v1 and v2 versions of C respectively. The Log analogy v1 version of the v2 version has a new error method. Now the project has introduced two jar packages, A and B, and C v0.1. , V0.2 version, maven can only choose a C version when packaging, assuming that the v1 version is selected. At runtime, all classes of a project are loaded with the same class loader by default, so no matter how many versions of C you rely on, only one version of C will be loaded into the JVM in the end. When B wants to access Log.error, it will find that Log does not have an error method at all, and then throws the exception java.lang.NoSuchMethodError. This is a typical case of class conflict.

 

image.png

The problem of class conflicts is actually very easy to solve if the version is downward compatible, and it is over if the lower version is excluded. But if the version is not backward compatible, you will be caught in the dilemma of "saving mom or girlfriend".

In order to avoid the dilemma, some people have proposed class isolation technology to solve the problem of class conflicts. The principle of class isolation is also very simple, that is, let each module use an independent class loader to load, so that the dependencies between different modules will not affect each other. As shown in the figure below, different modules are loaded with different class loaders. Why can this resolve class conflicts? A mechanism of Java is used here: the classes loaded by different class loaders appear to be two different classes in the JVM, because the only identifier of a class in the JVM is the class loader + class name. In this way we can load two different versions of C classes at the same time, even if their class names are the same. Note that the class loader here refers to an instance of a class loader, and it is not necessary to define two different class loaders. For example, PluginClassLoaderA and PluginClassLoaderB in the figure can be different instances of the same class loader.

 

image.png

Two how to achieve class isolation

Earlier we mentioned that class isolation is to allow the jar packages of different modules to be loaded with different class loaders. To do this, we need to allow the JVM to use a custom class loader to load the classes we wrote and their associated classes.

So how to achieve it? A very simple method is that the JVM provides a global class loader setting interface, so that we can directly replace the global class loader, but this cannot solve the problem of multiple custom class loaders at the same time.

In fact, the JVM provides a very simple and effective way. I call it the class loading conduction rule: the JVM will select the class loader of the current class to load all the referenced classes of this class. For example, we define TestA and TestB two classes, TestA will reference TestB, as long as we use a custom class loader to load TestA, then at runtime, when TestA is called to TestB, TestB will also be used by the JVM TestA class The loader loads. By analogy, as long as it is TestA and all jar package classes associated with the referenced class, it will be loaded by the custom class loader. In this way, as long as we let the main method class of the module use a different class loader to load, then each module will be loaded using the class loader of the main method class, so that multiple modules can be loaded with different classes. Device. This is also the core principle that OSGi and SofaArk can achieve class isolation.

After understanding the implementation principle of class isolation, we start with the practical operation by rewriting the class loader. To implement your own class loader, first let the custom class loader inherit java.lang.ClassLoader, and then rewrite the method of class loading. Here we have two choices, one is to rewrite findClass(String name), and the other is Override loadClass(String name). So which one should you choose? What is the difference between the two?

Below we try to rewrite these two methods to implement a custom class loader.

1 Rewrite findClass

First, we define two classes. TestA will print its own class loader, and then call TestB to print its class loader. We expect to implement the class loader MyClassLoaderParentFirst that overrides the findClass method. After TestA is loaded, TestB can also 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());
    }
}

Then rewrite the findClass method. This method first loads the class file according to the file path, and then calls defineClass to obtain 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");
    }

    // 重写了 findClass 方法
    @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();
        }
        return new byte[] {};
    }
}

Finally, write a main method to call a custom class loader to load TestA, and then call the main method of TestA through reflection to print the information of the class loader.

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});
    }

The results of the implementation are as follows:

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

The execution result is not as we expected, TestA is indeed loaded by MyClassLoaderParentFirst, but TestB is loaded by AppClassLoader. Why is this?

To answer this question, we must first understand a rule of class loading: the JVM calls the ClassLoader.loadClass method when class loading is triggered. This method realizes the delegation of parents:

  • Delegated to parent loader query
  • If the parent loader cannot find it, call the findClass method to load

After understanding this rule, the reason for the execution result was found: JVM did use MyClassLoaderParentFirst to load TestB, but because of the parent delegation mechanism, TestB was entrusted to MyClassLoaderParentFirst's parent loader AppClassLoader for loading.

You may also be curious, why the parent loader of MyClassLoaderParentFirst is AppClassLoader? Because the main method class we defined is loaded by the AppClassLoader that comes with the JDK by default, according to the class loading transmission rules, the MyClassLoaderParentFirst referenced by the main class is also loaded by the AppClassLoader that has loaded the main class. Since the parent class of MyClassLoaderParentFirst is ClassLoader, the default construction method of ClassLoader will automatically set the value of the parent loader to AppClassLoader.

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}

2 Override loadClass

Since rewriting the findClass method will be affected by the parental delegation mechanism, TestB is loaded by AppClassLoader, which does not meet the goal of class isolation, so we can only override the loadClass method to destroy the parental delegation mechanism. The code is as follows:

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;
        try {
            //这里要使用 JDK 的类加载器加载 java.lang 包里面的类
            result = jdkClassLoader.loadClass(name);
        } catch (Exception e) {
            //忽略
        }
        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) { //省略 }

}

Note here that we have rewritten the loadClass method, which means that all classes including the classes in the java.lang package will be loaded through MyClassLoaderCustom, but the target of class isolation does not include the classes that come with this part of the JDK, so we use ExtClassLoader to Load the JDK class, the relevant code is: result = jdkClassLoader.loadClass(name);

The test code is as follows:

public class MyTest {

    public static void main(String[] args) throws Exception {
        //这里取AppClassLoader的父加载器也就是ExtClassLoader作为MyClassLoaderCustom的jdkClassLoader
        MyClassLoaderCustom myClassLoaderCustom = new MyClassLoaderCustom(Thread.currentThread().getContextClassLoader().getParent());
        Class testAClass = myClassLoaderCustom.loadClass("com.java.loader.TestA");
        Method mainMethod = testAClass.getDeclaredMethod("main", String[].class);
        mainMethod.invoke(null, new Object[]{args});
    }
}

The execution results are as follows:

TestA: com.java.loader.MyClassLoaderCustom@1d44bcfa
TestB: com.java.loader.MyClassLoaderCustom@1d44bcfa

As you can see, by rewriting the loadClass method, we successfully let TestB also use MyClassLoaderCustom to load into the JVM.

Three summary

Class isolation technology was born to resolve dependency conflicts. It destroys the parent delegation mechanism through a custom class loader, and then uses class loading conduction rules to achieve class isolation of different modules.

Reference

Deep dive into Java class loaders

Original link

This article is the original content of Alibaba Cloud and may not be reproduced without permission

Guess you like

Origin blog.csdn.net/xxscj/article/details/113858635