Codificación pisando el pozo: error java.lang.NoSuchMethodError / problema de carga de clase con el mismo nombre / delegación de padres [colección recomendada]

Este artículo presenta un caso de resolución de problemas de excepciones que realmente se encuentran.

fondo del problema

  • La versión comercial, la función anterior se actualiza y se ha agregado un nuevo atributo al parámetro de entrada de la interfaz de dubbo en un paquete de terceros al que se hizo referencia anteriormente. Este nuevo atributo debe usarse esta vez; por lo tanto, la versión del paquete de segunda parte se ha actualizado en el pom;

  • Pasó la función de prueba en el entorno local;

  • En el entorno de prueba, la compilación y el inicio son normales, y cuando el código del módulo se ejecuta en tiempo de ejecución, se informa un error java.lang.NoSuchMethodError ;

Solución de problemas

1. La especulación preliminar es que el paquete de instantáneas de segunda mano utilizado se reemplazó antes de la implementación del entorno de prueba , y el paquete donde se ubicaron los atributos recién agregados originales se reemplazó por el código de la versión anterior, lo que resultó en NoSuchMethodError;

Al verificar los registros de carga de paquetes en el almacén, se encuentra que el paquete no ha sido reemplazado;

Esta situación es bastante extrema, si se reemplaza antes de la compilación, la compilación debería fallar en teoría ;

2. Se especula que puede haber varias clases con "nombres de clase completamente calificados exactamente iguales" en el proyecto

Ctrl+N busca el nombre de la clase y descubre que, de hecho, ¡es el caso!

Razones históricas

Estas dos clases están realmente relacionadas;

  • Por razones históricas, el proyecto donde se encuentra JarA se ha dividido en microservicios, y la interfaz dubbo donde se encuentra la clase C se ha dividido en un nuevo proyecto de microservicio donde se encuentra JarB;

  • Para reducir el impacto del cambio de microservicios en el lado comercial, cuando el proyecto de microservicio donde se empaqueta JarB, la interfaz dubbo donde se encuentra la clase C y los nombres de clase completamente calificados de las clases relacionadas son consistentes con el proyecto original donde se encuentra JarA. ;

  • Al mismo tiempo, el servicio dubbo del proyecto donde se encuentra el JarA original reenvía internamente la llamada al nuevo servicio;

  • De esta manera, se garantiza que después de que se implemente el nuevo servicio, los nombres de servicio antiguo y nuevo en zk son los mismos, y el lado comercial no tiene percepción;

análisis del problema

  • Hay 2 clases con el mismo nombre de clase completo en el proyecto, que se registra como clase C aquí;

  • Las clases C del proyecto provienen de dos paquetes de terceros en la dependencia de Maven, que se registran como JarA y JarB;

  • La definición de clase C en JarB es básicamente la misma que la definición de clase C en JarA Esta vez, la clase C en el nuevo paquete de JarB agrega un atributo M;

  • Durante el proceso de compilación, el compilador carga JarB primero de acuerdo con el orden en que se introducen las dependencias en el archivo pom de Maven, de modo que se utiliza la clase C de JarB, por lo que se ejecuta el método setter del nuevo atributo M en el código. , y se pasa la compilación;

  • De acuerdo con el modelo de delegación principal de JVM, de forma predeterminada, las clases con el mismo nombre de clase completo solo se cargarán una vez, por lo que cuando JVM carga la clase C, solo seleccionará una de JarA o JarB;

  • Cuando se ejecuta, la JVM carga la clase C. De acuerdo con la selección de [sistema operativo] , el archivo de clase de la clase C de JarA se carga esta vez, y la clase C de JarA no tiene un nuevo atributo M, por lo que el método setter de M se ejecuta y se informa una excepción de tiempo de ejecución Indica que no se puede encontrar el método setter;

Dos clases C con el mismo nombre provienen de diferentes paquetes Jar de terceros. Son iguales. De acuerdo con el mecanismo de carga de clases de JVM, el modelo de delegación de padres, las clases con el mismo nombre de clase completo solo se cargarán una vez de forma predeterminada (a menos que la delegación de los padres se rompe manualmente.Modelo);

Las clases en el paquete Jar se cargan usando AppClassLoader, y hay un concepto de espacio de nombres en el cargador de clases. Bajo el mismo cargador de clases, la clase con el mismo nombre de paquete y nombre de clase solo se cargará una vez. Si ya se ha cargado cargado, utilizado directamente cargado ;

En general, el compilador selecciona la clase C esperada de JarB de acuerdo con el orden del paquete en el pom, y la JVM solo carga la clase C antigua en JarA en tiempo de ejecución; por lo tanto, conduce a: la compilación pasa y se solicita NoSuchMethodError en tiempo de ejecución ;

resumen

1. ¿Cuál es la causa de java.lang.NoSuchMethodError durante esta ejecución?

Hay varias clases con el mismo nombre de clase completo en el paquete de otro fabricante del proyecto y se carga la clase incorrecta en tiempo de ejecución;

2. Dado que se seleccionó la clase incorrecta, ¿por qué no hay error de compilación?

  • La carga de clases de JVM es un modo de carga diferido, que selecciona aleatoriamente archivos .class en el directorio especificado para cargarlos durante el tiempo de ejecución;

  • Para el compilador local, cambie el orden de Jar que prefiere el compilador para seleccionar qué clase (este orden se puede ajustar manualmente en el IDE local);

Por ejemplo, en el ejemplo aquí, debido a que es una dependencia maven, el maestro necesita poner la dependencia de JarA delante de JarB para modificar el orden de carga de clases seleccionado por el compilador.existe, de la siguiente manera:

3. Si hay varias clases con el mismo nombre de clase completo en la dependencia, ¿qué clase cargará la JVM?

Una afirmación más confiable es que el propio sistema operativo controla el orden de carga predeterminado de los paquetes Jar ; es decir, ¡no es claro ni incierto para nosotros !

El orden de carga del paquete Jar está relacionado con el parámetro classpath.Cuando se usa la idea para iniciar el servicio springboot, puede ver el parámetro classpath, cuanto antes sea la ruta del paquete, antes se cargará;

En otras palabras, si se carga la clase en el paquete Jar anterior y hay una clase con el mismo nombre y la misma ruta en el paquete Jar posterior, se ignorará y no se cargará;

4. ¿Cómo solucionar este tipo de problema?

Permítanme hablar sobre la conclusión primero: debido a que el sistema operativo controla el orden de carga, la clase cargada en tiempo de ejecución puede no ser consistente con la clase seleccionada en tiempo de compilación, por lo que esta situación debe evitarse en principio en lugar de resolverse.

En teoría, no debería haber dos nombres de clase completamente calificados. Si los hay, generalmente se debe a que dos paquetes de terceros hacen referencia a una determinada dependencia al mismo tiempo. En este momento, se puede realizar la exclusión manual;

Para este caso, el último paquete de JarA ha eliminado por completo esta "clase C duplicada", por lo que solo es necesario actualizar la versión del paquete de segunda parte de JarA, y no habrá más clases;

Además, JDK proporciona algunas operaciones para destruir específicamente el modelo de delegación parental, lo que permite que las clases con el mismo nombre de clase completo se "carguen varias veces";

5. ¿Cómo darse cuenta de que las clases con el mismo nombre de clase completo se "cargan varias veces"?

La forma más sencilla es establecer el padre del cargador de clases personalizado en nulo y omitir el cargador de clases de la aplicación, de modo que dos cargadores de clases personalizados de este tipo puedan cargar las dos clases respectivamente; los ejemplos son los siguientes:

El orden de las referencias Jar en el IDE determina qué clase se carga:

Ejemplos de código para cargar estas dos clases por separado:

/**
 * @author Akira
 * @description
 * @date 2023/2/10
 */
public class SameClassTestLocal {

    public static void main(String[] args) {

        ClassLoader classloader = Thread.currentThread().getContextClassLoader();
        try {
            // 获取所有SameClassTest的.class路径
            Enumeration<URL> urls = classloader.getResources("pkg/my/SameClassTest.class");
            while (urls.hasMoreElements()) {
                // url有2个:jar:file:/E:/jar/jarB.jar!/pkg/my/SameClassTest.class和jar:file:/E:/jar/jarA.jar!/pkg/my/SameClassTest.class
                URL url = (URL) urls.nextElement();
                String fullPath = url.getPath();
                System.out.println("fullPath: " + fullPath);
                // 截取fullPath 获取jar文件的路径以及文件名,用来读取.class文件
                String[] strs = fullPath.split("!");
                String jarFilePath = strs[0].replace("file:/", "");
                String classFullName = strs[1].substring(1).replace(".class", "").replace("/", ".");
                System.out.println("jarFilePath: " + jarFilePath);
                System.out.println("classFullName: " + classFullName);

                // 关键步骤:用兄弟类加载器分别加载 父加载器置位null
                File file = new File(jarFilePath);
                URLClassLoader loader = new URLClassLoader(new URL[]{file.toURI().toURL()}, null);
                try {
                    // 加载类 .class转Class对象
                    Class<?> clazz = loader.loadClass(classFullName);
                    // 反射创建实体
                    Object obj = clazz.getDeclaredConstructor().newInstance();
                    // 获取全部属性
                    final Field[] fields = clazz.getDeclaredFields();
                    if (fields.length > 0) {
                        // 这里通过反射获取Fields 判断是否有新属性"reqNo"
                        final boolean containsReqNo = Stream.of(fields).map(Field::getName).collect(Collectors.toSet()).contains("reqNo");
                        if (containsReqNo) {
                            // 如果有则执行setter方法
                            Method method = clazz.getMethod("setReqNo", String.class);
                            method.invoke(obj, "seqStr");
                            System.out.println("当前类的包路径:" + jarFilePath + ";当前类具备reqNo属性 " + "json:" + JSON.toJSONString(obj));
                        } else {
                            System.out.println("当前类的包路径:" + jarFilePath + ";当前类具备不具备属性-跳过");
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("-----当前类加载完成-----");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

producción:

fullPath: file:/E:/jar/jarB.jar!/pkg/my/SameClassTest.class
jarFilePath: E:/jar/jarB.jar
classFullName: pkg.my.SameClassTest
当前类的包路径:E:/jar/jarB.jar;当前类具备reqNo属性 json:{"reqNo":"seqStr"}
-----当前类加载完成-----

fullPath: file:/E:/jar/jarA.jar!/pkg/my/SameClassTest.class
jarFilePath: E:/jar/jarA.jar
classFullName: pkg.my.SameClassTest
当前类的包路径:E:/jar/jarA.jar;当前类具备不具备属性-跳过
-----当前类加载完成-----

Conocimientos complementarios: cargador de clases

El papel de los cargadores de clases

类加载器,顾名思义就是一个可以将Java字节码加载为java.lang.Class实例的工具;这个过程包括,读取字节数组、验证、解析、初始化等;

类加载器的特点

  • 懒加载/动态加载:JVM并不是在启动时就把所有的.class文件都加载一遍,而是在程序运行的过程中,用到某个类时动态按需加载;这个动态加载的特点为热部署、热加载做了有力支持;

  • 依赖加载:跟Spring的Bean的依赖注入过程有点像,当一个类加载器加载一个类时,这个类所依赖的、引用的其他所有类,都由这个类加载器加载;除非在程序中显式地指定另外一个类加载器加载;

哪几种类加载器

  • 启动类加载器(Bootstrap ClassLoader):负责加载<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径,并且是虚拟机识别的类库加载到虚拟机内存中;无法被Java程序直接引用;自定义类加载器时,如果想设置Bootstrap ClassLoader为其父加载器,可直接设置parent=null;

  • 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定路径中的所有类库;其父类加载器为启动类加载器;

  • 应用程序类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)上所指定的类库;被启动类加载器加载的,但它的父加载器是扩展类加载器;在一个应用程序中,系统类加载器一般是默认类加载器;

  • 自定义类加载器(User ClassLoader):用户自己定义的类加载器;一般情况下我们不会自定义类加载器,除非特殊情况破坏双亲委派模型,需要实现java.lang.ClassLoader接口;

一个类的唯一性

一个类的唯一性由加载它的类加载器和这个类的本身决定,类唯一标识包括2部分:(1)类的全限定名(2)类加载器的实例ID

比较两个类是否相等(包括Class对象的equals()、isAssignableFrom()、isInstance()以及instanceof关键字等),只有在这两个类是由同一个类加载器加载的前提下才有意义;否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这两个类就必定不相等;

补充知识:JVM双亲委派

什么是双亲委派

实现双亲委派机制,首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载;

  • JVM的类加载器是分层次的,它们有父子关系,而这个关系不是继承维护,而是组合,每个类加载器都持有一个parent字段;

  • 除了启动类加载器外,其他所有类加载器都需要继承抽象类ClassLoader;

ClassLoader类的3个关键方法

  • defineClass方法:调用native方法把Java类的字节码解析成一个Class对象;

  • findClass:找到.class文件并把.class文件读到内存得到字节码数组,然后调用defineClass方法得到Class对象;

  • loadClass实现双亲委派机制;当一个类加载器收到“去加载一个类”的请求时,会先把这个请求“委派”给其父类类加载器;这样无论哪个层的类加载器,加载请求最终都会委派给顶层的启动类加载器,启动类加载器在其目录下尝试加载该类;父加载器找不到该类时,子加载器才会自己尝试加载这个类;

为什么使用双亲委派模型?

双亲委派保证类加载器"自下而上的委派,自上而下的加载",保证每一个类在各个类加载器中都是同一个类,换句话说,就是保证一个类只会加载一次

一个非常明显的目的就是保证java官方的类库<JAVA_HOME>\lib和扩展类库<JAVA_HOME>\lib\ext的加载安全性,不会被开发者覆盖

如果开发者通过自定义类尝试覆盖JDK中的类并加载,JVM一定会优先加载JDK中的类而不再加载用户自己尝试覆盖而定义的类;例如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类;

此外,根据ClassLoader类的源码(java.lang.ClassLoader#preDefineClass),java禁止用户用自定义的类加载器加载java.开头的官方类,也就是说只有启动类加载器BootstrapClassLoader才能加载java.开头的官方类;

类似的,如果开发者自己开发开源框架,也可以自定义类加载器,利用双亲委派模型,保护自己框架需要加载的类不被应用程序覆盖;

破坏双亲委派?

双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式;当开发者有特殊需求时,这个委派和加载顺序完全是可以被破坏的:

  • 如想要自己显示的加载某个指定类;

  • 或者由于一些框架的特殊性,如Tomcat需要加载不同工程路径的类,Tomcat中可以部署多个web项目,为了保证每个web项目互相独立,所以不能都由AppClassLoader加载,所以自定义了类加载器WebappClassLoader;

  • 以及本篇提到的加载全限定类名相同的多个类;

破幻双亲委派模型的方式:

上面介绍了,实现双亲委派的核心就在ClassLoader#loadClass;如果想不遵循双亲委派的类加载顺序,可以自定义类加载器,重写loadClass,不再先委派父亲类加载器而是选择优先自己加载

另一种简单粗暴的方式就是直接将父加载器parent指定位null,这样做主要就是跳过了默认的应用程序类加载器(Application ClassLoader),自己来加载某个指定类

参考:

Java双亲委派模型:为什么要双亲委派?如何打破它?破在哪里?

如何加载两个jar包中含有相同包名和类名的类

JVM jar包加载顺序

Supongo que te gusta

Origin blog.csdn.net/minghao0508/article/details/129047769
Recomendado
Clasificación