编码踩坑——运行时报错java.lang.NoSuchMethodError / 同名类加载问题 / 双亲委派【建议收藏】

news2024/11/17 1:26:47

本篇介绍一个实际遇到的排查异常的case,涉及的知识点包括:类加载机制、jar包中的类加载顺序、JVM双亲委派模型、破坏双亲委派模型及自定义类加载器的代码示例;

问题背景

  • 业务版本,旧功能升级,原先引用的一个二方包中的dubbo接口入参新增了属性,本次需要用到这个新属性;因此在pom中升级了该二方包的version;

  • 在本地环境测试功能通过;

  • 到test环境时,编译启动都正常,当运行时执行到该模块代码时报错java.lang.NoSuchMethodError

问题排查

1. 初步推测是使用的snapshot二方包在部署test环境前被替换,原先的新增加的属性所在的包被旧版本代码替换,导致NoSuchMethodError;

通过查看仓库中包上传的记录,发现包并没有被替换;

这种情况比较极端,如果是编译前被替换,理论上应该编译不通过才对

2. 推测可能是工程中存在"全限定类名完全一致"的多个类

Ctrl+N搜索类名,发现确实如此!

历史原因

这两个类实际上是有关系的;

  • 由于历史原因,对JarA所在的工程做了微服务拆分,将C类所在的dubbo接口拆到了JarB所在的新的微服务工程;

  • 为了减小切换微服务对业务方的影响,JarB所在的微服务工程打包时,C类所在的dubbo接口及相关类的全限定类名与原JarA所在的工程保持一致;

  • 同时,原JarA所在的工程的dubbo服务,内部转发调用到新服务;

  • 这样,就保证了新服务部署后,zk中新老服务名相同,业务方无感知;

问题分析

  • 项目中存在2个全限定类名相同的类,这里记为C类;

  • 工程中的C类分别来自Maven依赖中的2个二方包,分别记为JarA和JarB;

  • JarB中的C类的定义与JarA中的C类的定义基本一致,本次JarB新包中C类新增了一个属性M;

  • 在编译过程中,编译器根据Maven的pom文件引入依赖的顺序,先加载了JarB,从而使用JarB的C类,因此代码中执行新属性M的setter方法,编译是通过的;

  • 根据JVM的双亲委派模型,默认情况下相同全限定类名的类只会加载一次,因此JVM加载C类时只会从JarA或JarB选一个;

  • 运行时,JVM加载类C,根据【操作系统】的选择,本次加载了JarA的C类的class文件,而JarA的C类没有新属性属性M,因此执行M的setter方法,报运行时异常提示找不到setter方法;

同名的两个C类来自不同的二方Jar包,他们是平级的,根据JVM的类加载机制——双亲委派模型,相同全限定类名的类默认只会加载一次(除非手动破坏双亲委派模型);

Jar包中的类是使用AppClassLoader加载的,而类加载器中有一个命名空间的概念,同一个类加载器下,相同包名和类名的class只会被加载一次,如果已经加载过了,直接使用加载过的

总的来说,编译器根据pom中的引包顺序选择了我们预期的JarB的C类,而运行时JVM仅加载了JarA中的旧的C类;因此导致——编译通过,运行时提示NoSuchMethodError;

小结

1.本次运行时java.lang.NoSuchMethodError产生的原因?

项目二方包中存在多个全限定类名相同的类,运行时加载错了类;

2.既然选错了类,为什么没有编译错误?

  • JVM类加载是一种懒加载模式,运行时在指定目录随机选择.class文件加载;

  • 本地的编译器,改变编译器优先选择的Jar顺序从而选择哪个类(这个顺序在本地IDE中是可以手动调整的);

例如这里的例子中,由于是maven依赖,因此主需要把JarA的依赖放在JarB前面即可修改编译器选择的类加载顺序,修改后则此处直接编译不通过,提示新属性的setter方法不存在,如下:

3.如果依赖中有多个全限定类名相同的类,那JVM会加载哪一个类呢?

比较靠谱的说法是,操作系统本身,控制了Jar包的默认加载顺序;也就是说,对于我们来说是不明确不确定的!

而Jar包的加载顺序,是跟classpath这个参数有关,当使用idea启动springboot的服务时,可以看到classpath参数的;包路径越靠前,越先被加载;

换句话说,如果靠前的Jar包里的类被加载了,后面Jar包里有同名同路径的类,就会被忽略掉,不会被加载;

4.如何解决这类问题?

先说结论:因为操作系统控制加载顺序,运行时加载的类可能跟编译时选择的类不一致,因此这种情况原则上需要避免而不是解决!

理论上不应该出现两个全限定类名,如果有一般是因为2个二方包同时引用了某个依赖,此时做手动排除即可;

对于本次情况,JarA的最新包已经全部去除了这个"重复的类C",因此只需要更新JarA的二方包version即可,就不会有多个类了;

此外,JDK提供了一些骚操作来专门破坏双亲委派模型,可以让全限定类名相同的类被"加载多次";

5.如何实现全限定类名相同的类被"加载多次"?

这里使用最简单的方式,将自定义的类加载器的parent置位null,跳过应用程序类加载器,这样2个这样的自定义类加载器就可以分别加载这2个类;示例如下:

IDE中Jar引用顺序决定哪个类被加载:

分别加载这2个类的代码示例:

/**
 * @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();
        }

    }

}

输出:

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;当前类具备不具备属性-跳过
-----当前类加载完成-----

补充知识:类加载器

类加载器的作用

类加载器,顾名思义就是一个可以将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包加载顺序

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/350313.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HoloLens 2 丨打包丨MRTK丨Unity丨新手教学

HoloLens 2打包流程制作前言开发工具介绍Visual Studio 2019MRTK插件或示例程序下载打包流程介绍Unity操作修改Visual Studio修改Hololens 修改Hololens 密码忘记总结前言 提示&#xff1a;今日功能介绍 使用 MRTK制作hololens 2的打包流程制作的新手教学。 开发工具介绍 这…

SpringBoot09:Swagger

什么是Swagger&#xff1f; ①是一个API框架 ②可以在线自动生成 RestFul 风格的API文档&#xff0c;实现API文档和API定义同步更新 ③可以直接运行、在线测试 API 接口 ④支持多种语言&#xff08;Java、PHP等&#xff09; 官网&#xff1a;API Documentation & Desi…

《数字经济全景白皮书》金融篇:五十弦翻塞外声,金融热点领域如何实现增长?

易观分析&#xff1a;《数字经济全景白皮书》浓缩了易观分析对于数字经济各行业经验和数据的积累&#xff0c;并结合数字时代企业的实际业务和未来面临的挑战&#xff0c;以及数字技术的创新突破等因素&#xff0c;最终从数字经济发展大势以及各领域案例入手&#xff0c;帮助企…

iOS创建Universal Link

iOS 9之前&#xff0c;一直使用的是URL Schemes技术来从外部对App进行跳转&#xff0c;但是iOS系统中进行URL Schemes跳转的时候如果没有安装App&#xff0c;会提示无法打开页面的提示。 iOS 9之后起可以使用Universal Links技术进行跳转页面&#xff0c;这是一种体验更加完美的…

【Linux详解】——进程控制(创建、终止、等待、替换)

&#x1f4d6; 前言&#xff1a;本期介绍进程控制&#xff08;创建、终止、等待、替换&#xff09;。 目录&#x1f552; 1. 进程创建&#x1f558; 1.1 fork函数初识&#x1f558; 1.2 fork的返回值问题&#x1f558; 1.3 写时拷贝&#x1f558; 1.4 创建多个进程&#x1f552…

【C++】二叉搜索树的实现(递归和非递归实现)

文章目录1、二叉搜索树1.1 构建二叉搜索树1.2 二叉搜索树的插入1.3 二叉搜索树的删除1.4 二叉搜索树插入和删除的递归实现为了学习map和set的底层实现&#xff0c;需要知道红黑树&#xff0c;知道红黑树之前需要知道AVL树。 红黑树和AVL树都用到了二叉搜索树结构&#xff0c;所…

机器人操作规划——Deep Visual Foresight for Planning Robot Motion(2017 ICRA)

1 简介 model-based RL方法&#xff0c;预测Action对图像的变化&#xff0c;以push任务进行研究。 采用完全自监督的学习方式&#xff0c;不需要相机标定、3D模型、深度图像和物理仿真。 2 数据集 采用几百个物体、10个7dof机械臂采集了包括5万个push attempts的数据集。 每…

【软件测试】测试工程师的等级划分(初/中/高/专家),你的晋升之路......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 新手&#xff08;测…

linux下实现Nginx + consul + upsync 完成动态负载均衡

一、yum安装consul #安装yum-utils yum install -y yum-utils#配置consul的下载仓库 yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo#必须上面步骤&#xff0c;不然会找不到仓库 yum -y install consul#查看版本 consul -v 二、启动…

计算机SCI论文课题设计需要注意什么? - 易智编译EaseEditing

课题设计就要本着严谨性和可行性来进行。实验设计的类型要选择准确&#xff0c;统计学的方法要运用合理&#xff0c;研究对象和观察指标的选择也要符合研究目的的要求&#xff0c;技术路线要清晰明了。 关于课题的设计的可行性也要综合考虑&#xff0c;比如前期的相关工作基础…

一文详解Redis持久化的两种方案

一文详解Redis持久化的两种方案1.RDB持久化2.RDB持久化原理3.AOF持久化4.RDB VS AOF1.RDB持久化 RDB全称Redis Database Backup file(Redis数据备份文件)&#xff0c;也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后&#xff0c…

分享107个HTML电子商务模板,总有一款适合您

分享107个HTML电子商务模板&#xff0c;总有一款适合您 107个HTML电子商务模板下载链接&#xff1a;https://pan.baidu.com/s/1VW67Wjso1BRpH7O3IlbZwg?pwd0d4s 提取码&#xff1a;0d4s Python采集代码下载链接&#xff1a;采集代码.zip - 蓝奏云 Aplustemplates 购物模板…

Redis之集群搭建

redis的集群模式简介&#xff1a; redis的集群模式中可以实现多个节点同时提供写操作&#xff0c;redis集群模式采用无中心结构&#xff0c;每个节点都保存数据&#xff0c;节点之间互相连接从而知道整个集群状态。 集群搭建步骤如下 (一台服务器模拟多台服务器) 1.创建6个配置…

关于使用CMT2300A FIFO缓存区间设置为64Byte的问题

首先请看&#xff0c;CMT2300A 是什么产品&#xff0c;或者说是 模组吗&#xff1f; 请看介绍&#xff1a; https://blog.csdn.net/sishuihuahua/article/details/105095994 以及RFPDK 的使用: 这博客&#xff0c;记录了 RFPDK 的使用,以及遇到的一些问题 我说一下&#…

Windows瘦身方法

一、快速删除系统盘临时文件方法, 1、winr打开运行对话框&#xff0c;输入%temp%命令&#xff0c;如图1 图1 2、打开temp文件夹&#xff0c;如图2&#xff0c;选择所有文件&#xff0c;鼠标右键删除或按Del键删除。 图2 二、磁盘清理 1、winr&#xff0c;输入cleanmgr&#x…

重生之我是赏金猎人-SRC漏洞挖掘(十二)-记一次对抗飞塔流量检测的文件上传

0x00 前言 https://github.com/J0o1ey/BountyHunterInChina 欢迎亲们点个star 0x01 起因 某项目靶标&#xff0c;是一个人员管理系统&#xff0c;通过webpack暴露的接口 我们成功找到了一个未鉴权的密码修改接口&#xff0c;通过fuzz 我们获取到了该接口的参数username与…

干了1年“点点点”,自己辞职了,下一步是继续干测试还是转开发?

最后后台有个粉丝向我吐槽&#xff0c;不知道怎么选择了....下面就他的情况说说怎么选择&#xff1f; 目前已经提桶跑路&#xff0c;在大工厂里混了半年初级低级功能测试经验&#xff0c;并没有什么用。测试培训班来的。从破山村贫困户贫困专项出去的&#xff0c;学校上海的。…

基于RK3588的嵌入式linux系统开发(二)——uboot源码移植及编译

由于官方的SDK占用空间较大&#xff08;大约20GB左右&#xff09;&#xff0c;需要联系相关供应商提供&#xff0c;且官方的SDK通过各种脚本文件进行集成编译&#xff0c;难以理解系统开发的详细过程。本章介绍直接从官方Github网站下载源码进行移植&#xff0c;进行uboot移植及…

动态规划【Day02】

动态规划初探坐标型动态规划115 不同的路径 II序列型动态规划515 房屋染色划分型动态规划题目坐标型动态规划 115 不同的路径 II 题目链接 题目描述&#xff1a; “不同的路径” 的跟进问题&#xff1a; 有一个机器人位于一个 mn 网格左上角。 机器人每一时刻只能向下或…

分布式入门

目录 一、RPC 1.1 RPC调用流程 二、SOA 三、微服务 3.1.什么是微服务 3.2.微服务与微服务架构 1 &#xff09;微服务架构 2&#xff09; 微服务 3.3.微服务的优缺点 3.1 微服务的优点 3.2 微服务的缺点 四.微服务的技术栈有哪些 一、RPC RPC&#xff08;Remote Pro…