¿Cómo implementar la carga de aislamiento de clases de Java?

Introducción: En el  desarrollo de Java, si diferentes paquetes jar dependen de diferentes versiones de algunos paquetes jar comunes, se informará un error en tiempo de ejecución porque las clases cargadas no cumplen con las expectativas. ¿Cómo evitar esta situación? Este documento analiza las causas de los conflictos en los paquetes jar y el principio de realización del aislamiento de clases, y comparte dos métodos para implementar el cargador de clases personalizado.

image.png

Uno que es la tecnología de aislamiento de clase

Siempre que escriba suficiente código Java, esta situación definitivamente ocurrirá: se introduce un nuevo paquete jar de middleware en el sistema, todo es normal al compilar y se informa de un error tan pronto como se ejecuta: java.lang.NoSuchMethodError, y luego tararear comencé a buscar una solución, y finalmente encontré el jar conflictivo después de buscar cientos de dependencias en el paquete. Después de resolver el problema, comencé a quejarme del middleware. ¿Por qué hay tantas versiones diferentes del jar? Escriba el código cinco minutos, las bolsas en fila todo el día.

La situación anterior es una situación común en el proceso de desarrollo de Java, y la razón es muy simple. Los diferentes paquetes jar dependen de diferentes versiones de algunos paquetes jar comunes (como los componentes de registro). No hay ningún problema al compilar, pero cuando es en ejecución, será La clase cargada no cumple con las expectativas y se informa de un error. Por ejemplo: A y B dependen de las versiones v1 y v2 de C respectivamente. La versión Log analogy v1 de la versión v2 tiene un nuevo método de error. Ahora el proyecto ha introducido dos paquetes jar, A y B, y C v0.1 ., Versión V0.2, maven solo puede elegir una versión C al empaquetar, asumiendo que se selecciona la versión v1. En tiempo de ejecución, todas las clases de un proyecto se cargan con el mismo cargador de clases de forma predeterminada, por lo que no importa en cuántas versiones de C confíe, al final solo se cargará una versión de C en la JVM. Cuando B quiere acceder a Log.error, encontrará que Log no tiene ningún método de error y luego lanza la excepción java.lang.NoSuchMethodError. Este es un caso típico de conflicto de clases.

 

image.png

El problema de los conflictos de clases es realmente muy fácil de resolver si la versión es compatible con versiones anteriores, y se termina si se excluye la versión inferior. Pero si la versión no es compatible con versiones anteriores, se verá atrapado en el dilema de "salvar a mamá o novia".

Para evitar el dilema, algunas personas han propuesto la tecnología de aislamiento de clases para resolver el problema de los conflictos de clases. El principio de aislamiento de clases también es muy simple, es decir, dejar que cada módulo use un cargador de clases independiente para cargar, de modo que las dependencias entre diferentes módulos no se afecten entre sí. Como se muestra en la figura siguiente, los diferentes módulos se cargan con diferentes cargadores de clases. ¿Por qué esto puede resolver los conflictos de clases? Aquí se utiliza un mecanismo de Java: las clases cargadas por diferentes cargadores de clases parecen ser dos clases diferentes en la JVM, porque el único identificador de una clase en la JVM es el cargador de clases + el nombre de la clase. De esta manera podemos cargar dos versiones diferentes de clases C al mismo tiempo, incluso si sus nombres de clase son los mismos. Tenga en cuenta que el cargador de clases aquí se refiere a una instancia de un cargador de clases y no es necesario definir dos cargadores de clases diferentes. Por ejemplo, PluginClassLoaderA y PluginClassLoaderB en la figura pueden ser instancias diferentes del mismo cargador de clases.

 

image.png

Dos cómo lograr el aislamiento de clase

Anteriormente mencionamos que el aislamiento de clases es permitir que los paquetes jar de diferentes módulos se carguen con diferentes cargadores de clases. Para hacer esto, necesitamos permitir que la JVM use un cargador de clases personalizado para cargar las clases que escribimos y sus clases asociadas.

Entonces, ¿cómo lograrlo? Un método muy simple es que la JVM proporciona una interfaz de configuración del cargador de clases global, de modo que podemos reemplazar directamente el cargador de clases global, pero esto no puede resolver el problema de varios cargadores de clases personalizados al mismo tiempo.

De hecho, la JVM proporciona una forma muy simple y efectiva, yo la llamo regla de conducción de carga de clases: la JVM seleccionará el cargador de clases de la clase actual para cargar todas las clases referenciadas de esta clase. Por ejemplo, definimos TestA y TestB dos clases, TestA hará referencia a TestB, siempre que usemos un cargador de clases personalizado para cargar TestA, luego en tiempo de ejecución, cuando TestA se llame a TestB, TestB también será usado por la clase JVM TestA El cargador carga. Por analogía, siempre que sea TestA y todas las clases de paquetes jar asociadas con la clase referenciada, el cargador de clases personalizado lo cargará. De esta manera, siempre y cuando dejemos que la clase del método principal del módulo use un cargador de clases diferente para cargar, entonces cada módulo se cargará usando el cargador de clases de la clase del método principal, de modo que se puedan cargar varios módulos con clases diferentes. .Dispositivo. Este es también el principio fundamental de que OSGi y SofaArk pueden lograr el aislamiento de clase.

Después de comprender el principio de implementación del aislamiento de clases, comenzamos con la operación práctica reescribiendo el cargador de clases. Para implementar su propio cargador de clases, primero deje que el cargador de clases personalizado herede java.lang.ClassLoader, y luego reescriba el método de carga de clases. Aquí tenemos dos opciones, una es reescribir findClass (String name) y la otra es Override loadClass (nombre de la cadena). Entonces, ¿cuál deberías elegir? ¿Cuál es la diferencia entre los dos?

A continuación, intentamos reescribir estos dos métodos para implementar un cargador de clases personalizado.

1 Reescribir findClass

Primero, definimos dos clases. TestA imprimirá su propio cargador de clases y luego llamará a TestB para imprimir su cargador de clases. Esperamos implementar el cargador de clases MyClassLoaderParentFirst que anula el método findClass. Después de cargar TestA, TestB también puede cargarse automáticamente por 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());
    }
}

Luego reescribe el método findClass. Este método primero carga el archivo de clase de acuerdo con la ruta del archivo, y luego llama a defineClass para obtener el objeto Class.

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[] {};
    }
}

Finalmente, escriba un método principal para llamar a un cargador de clases personalizado para cargar TestA, y luego llame al método principal de TestA a través de la reflexión para imprimir la información del cargador de clases.

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

Los resultados de la implementación son los siguientes:

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

El resultado de la ejecución no es el esperado, TestA es efectivamente cargado por MyClassLoaderParentFirst, pero TestB es cargado por AppClassLoader. ¿Por qué es esto?

Para responder a esta pregunta, primero debemos entender una regla de carga de clases: la JVM llama al método ClassLoader.loadClass cuando se activa la carga de clases. Este método realiza la delegación de padres:

  • Delegado a la consulta del cargador principal
  • Si el cargador principal no puede encontrarlo, llame al método findClass para cargar

Después de comprender esta regla, se encontró el motivo del resultado de la ejecución: JVM usó MyClassLoaderParentFirst para cargar TestB, pero debido al mecanismo de delegación principal, TestB se confió al cargador principal AppClassLoader de MyClassLoaderParentFirst para la carga.

También puede tener curiosidad, ¿por qué el cargador principal de MyClassLoaderParentFirst es AppClassLoader? Debido a que la clase de método principal que definimos es cargada por el AppClassLoader que viene con el JDK por defecto, de acuerdo con las reglas de transmisión de carga de clases, MyClassLoaderParentFirst referenciado por la clase principal también es cargado por el AppClassLoader que ha cargado la clase principal. Dado que la clase principal de MyClassLoaderParentFirst es ClassLoader, el método de construcción predeterminado de ClassLoader establecerá automáticamente el valor del cargador principal en AppClassLoader.

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

2 Anular loadClass

Dado que la reescritura del método findClass se verá afectada por el mecanismo de delegación parental, AppClassLoader carga TestB, que no cumple con el objetivo de aislamiento de clases, por lo que solo podemos anular el método loadClass para destruir el mecanismo de delegación parental. El código es el siguiente:

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) { //省略 }

}

Tenga en cuenta aquí que hemos reescrito el método loadClass, lo que significa que todas las clases, incluidas las clases en el paquete java.lang, se cargarán a través de MyClassLoaderCustom, pero el objetivo del aislamiento de clases no incluye las clases que vienen con esta parte del JDK, entonces usamos ExtClassLoader para cargar la clase JDK, el código relevante es: resultado = jdkClassLoader.loadClass (nombre);

El código de prueba es el siguiente:

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

Los resultados de la ejecución son los siguientes:

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

Como puede ver, al reescribir el método loadClass, permitimos que TestB también use MyClassLoaderCustom para cargar en la JVM.

Tres resumen

La tecnología de aislamiento de clases nació para resolver conflictos de dependencia. Destruye el mecanismo de delegación principal a través de un cargador de clases personalizado y luego usa reglas de conducción de carga de clases para lograr el aislamiento de clases de diferentes módulos.

Referencia

Profundice en los cargadores de clases de Java

Enlace original

Este artículo es el contenido original de Alibaba Cloud y no se puede reproducir sin permiso.

Supongo que te gusta

Origin blog.csdn.net/xxscj/article/details/113858635
Recomendado
Clasificación