devtools引发的一场关于类加载问题的探究
本文适合有一定 Java、JVM 基础、了解一些 thrift RPC 序列化知识点;同时本文不会介绍类加载的基础知识,如双新委托、findClass|defineClass 等。
通过本文你可以了解下面知识:
- ClassCastException 异常的解决思路;
- Devtools 是如何使用类加载器来工作的;
- 强制类型转化与类加载器是如何关联上的;
- 美团 mtthrift 注解式的序列化与类加载器片面分析;
- 同名不同类加载器造成的另一个异常(IllegalArgumentException)。
- 问题现象
- 工程结构说明
- 解决问题思考
- 快速解决问题
- RestartClassLoader 从何而来?
- 这两个类分别对应的类加载器是什么?
- 第二种解决方式
- 第三种解决方式
- 扩展
问题现象
Caused by: org.apache.thrift.TApplicationException:java.lang.ClassCastException:xx.enums.DriverBiz cannot be cast to xx.enums.DriverBiz
DriverBiz 是一个枚举类型的类(为了符合公司规范、这里擦除包名)
工程结构说明
- rpc 使用的是 thrift(具体对外提供的 client 的定义采用 java 注解式)
- 开发框架采用 springboot,依赖了
spring-boot-devtools
client 中请求参数结构大概情况
@ThriftStructpublic class XXXRequest { private List<Long> ids; // 转化出错的类 private xx.enums.DriverBiz driverBiz;
解决问题思考
快速解决问题
根据多年从事 JAVA 编程的经验如果出现了ClassCastException
,并且包名、类名完全一样,只有一种可能性那就是这两类转换的类来自不同类加载器;(如果是类名不一样,那就是你使用错了^..^)
既然我们有一个大致的方向了,就看看这两个同类名的类的类加载器分别是什么?
怎么看呢? 来一个好用的大杀器 阿里的 arthas;使用文档 ;
查询类的详情情况,这里我们只关注我们类加载器情况
[arthas@67444]$ sc xx.enums.DriverBiz -d ....... class-loader +-sun.misc.Launcher$AppClassLoader@18b4aac2 +-sun.misc.Launcher$ExtClassLoader@1534f01b ....... class-loader +-org.springframework.boot.devtools.restart.classloader.RestartClassLoader@3ad59b01 +-sun.misc.Launcher$AppClassLoader@18b4aac2 +-sun.misc.Launcher$ExtClassLoader@1534f01b
可以看出这个类被这两个类同时加载了,而且 RestartClassLoader@3ad59b01 的“父”加载与上一个类加载器还是同一个, 看到这个问题的解决办法就出了,我们把 devtool 这个依赖去掉即可,我们去掉 devtools 依赖后,再来看看这个类的加载情况;
class-loader +-sun.misc.Launcher$AppClassLoader@18b4aac2 +-sun.misc.Launcher$ExtClassLoader@46d56d67
重新访问一下接口,正常返回,确实问题解决,符合预期; 看到这里相信大多数人就 fix bug, 下一个 bug continue fix; 没有对这个问题进行深一步的思考,这两个类加载器从何而来? 为什么 DriverBiz 同时被这两个加载器加载? 为什么项目启动时没有报错, 请求访问时才出现呢? 抱着这一系列的疑问开始我们下面的分析之路;
RestartClassLoader 从何而来?
很简单,在 idea 中全局地搜,找到了相关的代码 RestartClassLoader.java 在构造方法中直接断点;
经历了下面这些方法,这里不再赘述 spring 自动配置机制
org.springframework.boot.devtools.restart.Restarter.relaunch()org.springframework.boot.devtools.restart.Restarter.doStart()org.springframework.boot.devtools.restart.Restarter.start()org.springframework.boot.devtools.restart.Restarter.restart()
最终会调用 RestartLauncher 的 run 方法来重新启动整个工程(在这之前会调用 Restarter.this.stop();来关闭上一次的 context)
@Overridepublic void run() { try { Class<?> mainClass = getContextClassLoader().loadClass(this.mainClassName); Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); mainMethod.invoke(null, new Object[] { this.args }); } catch (Throwable ex) { this.error = ex; getUncaughtExceptionHandler().uncaughtException(this, ex); }}
看到这里可知 devtools 开启了一个线程来监听文件的变化,如果有变化则关闭之前的 context,然后新建了一个 RestartClassLoader,用这个类加载来加载运行启动的入口类(Main 方法所在的类,这里挺有意思的一层层向上推),然后启动入口类调用 main 方法,为什么要这么做呢?只是要保证这个工程的能依赖的类全部由 RestartClassLoader 来加载(具体看 RestartClassLoader 的加载范围);
注意:这里涉及到一个类加载器原则,如果一个类在加载过程,遇到一个没有加载的类则用当前类加载器(实际上可以理解成触发者的类加载器)先尝试去加载,当然也得看看这个类加载器是否遵守双亲委托原则, 想要更多了解可以想想 mysql 驱动器的加载?
既然 RestartClassLoader 能加载 DriverBiz,但另一个类加载器又与 RestartClassLoader 的“父”加载器是同一个,这显然不合理呀;可以从 RestartClassLoader 看出这个类加载能原则能自己加载就自己加载不能的话则交给“父”加载器, 显然这里 RestartClassLoader 能加载,所以其父加载器 ApplicationClassLoader 没有机会加载了,所以可以猜测可能是其它的类加载器(其父加载器也是 ApplicationClassLoader)也会加载 DriverBiz,但是自己不能加载,委托其“父”加载器来加载,才能造成目前这样的一个现象;
出现 ClassCastException 是在请求时,这个还得从 thrifth 的 RPC 框架入手,梳理 RPC 请求参数序列过程代码如下:
###ThriftMethodProcessor.java###private Object[] readArguments(TProtocol in) throws Exception{ try { int numArgs = method.getParameterTypes().length; Object[] args = new Object[numArgs]; TProtocolReader reader = new TProtocolReader(in); reader.readStructBegin(); while (reader.nextField()) { short fieldId = reader.getFieldId(); // 1.parameterCodecs 一个存放解码器的 map // 2.codec 负责把字节流转化成类实例 ThriftCodec<?> codec = parameterCodecs.get(fieldId); if (codec == null) { reader.skipFieldData(); } else { // 3.这里会委托 TprotocolReader.read 去完成 args[thriftParameterIdToJavaArgumentListPositionMap.get(fieldId)] = reader.readField(codec); } } reader.readStructEnd();###TprotocolReader.java###public Object readField(ThriftCodec<?> codec) throws Exception{ if (!checkReadState(codec.getType().getProtocolType().getType())) { return null; } currentField = null; // 4.这里才是真正通过解码器去反序列化 Object fieldValue = codec.read(protocol); protocol.readFieldEnd(); return fieldValue;}
问题的关键点来了,想要了解 fieldValue 类实例(反列化出来的类对象)的类加载器,就要先去知道 解码器 codec 的类加载器是什么? codec 类对象是在启动时完成,根据参数来生成一个对应的解码器 codec,这个解码器的类加载器居然是一个 DynamicClassLoader 新的类加器,其“父”类加载器是 ThriftCodecManager.class 的类加载器,ThriftCodecManager 是由 ApplicationClassLoader 来加载的(RestartClassLoader 加载不了);
ThriftCodecManager.java
@Injectpublic ThriftCodecManager(final ThriftCodecFactory factory, final ThriftCatalog catalog, @InternalThriftCodec Set<ThriftCodec<?>> codecs){ this.catalog = catalog; typeCodecs = CacheBuilder.newBuilder().build(new CacheLoader<ThriftType, ThriftCodec<?>>() { public ThriftCodec<?> load(ThriftType type) throws Exception { switch (type.getProtocolType()) { case STRUCT: { // 如果 thrift struct 类型则需要产生类加载器,原始类型如 int 则可以直接使用 thrift 自己定义的; return factory.generateThriftTypeCodec(ThriftCodecManager.this, type.getStructMetadata()); }
ThriftCodecByteCodeGenerator.java
public ThriftCodecByteCodeGenerator(){ ... // 直接使用 definClass,也就是这些类加载自身的类类加载器均是 DynamicClassLoader Class<?> codecClass = classLoader.defineClass(codecType.getClassName().replace('/', '.'), byteCode); try { Class<?>[] types = parameters.getTypes(); Constructor<?> constructor = codecClass.getConstructor(types); thriftCodec = (ThriftCodec<T>) constructor.newInstance(parameters.getValues()); } .....}@Overridepublic ThriftCodec<?> generateThriftTypeCodec(ThriftCodecManager codecManager, ThriftStructMetadata metadata){ ThriftCodecByteCodeGenerator<?> generator = new ThriftCodecByteCodeGenerator<>( codecManager, metadata, classLoader, debug ); // 其实在这之前已经产生了,这里只是返回结果 return generator.getThriftCodec();}
DynamicClassLoader.java
public class DynamicClassLoader extends ClassLoader{ // 只有一个 defineClass 方法,这里面也就说 DynamicClassLoader 满足双新委托原则,但是这个类的使用却是直接使用这个方法,没有使用 loadClass, 或 findClass public Class<?> defineClass(String name, byte[] byteCode) throws ClassFormatError { return defineClass(name, byteCode, 0, byteCode.length); }}
上面分析解码器 codec 的类加载器是什么,下面看这个 codec 的 read(反序化)生成的具体代码(也用 arthas 直接可以导出,自己去研究吧^^)
由于这个 codec 类需要依赖 DriverBiz, 所以会导致 DynamicClassLoader 去加载,而 DynamicClassLoader 满足双亲委托模式,直接让 ApplicationClassLoader 来加载,故才有了上面的现象 RestartClassLoader 、ApplicationClassLoader 均加载了 DriverBiz;
ClassCastException 异常点出现在如上图位置,是强制类型转化类型出错; 先别急看一下最简单的示例;
public void test(){ Object obj = new Object(); DriverBiz driverBiz = (DriverBiz)obj;}
对应字节码
0 new #2 <java/lang/Object> 3 dup 4 invokespecial #1 <java/lang/Object.<init>> 7 astore_1 8 aload_1 9 checkcast #3 <xx/enums/DriverBiz>12 astore_213 return
通过字节码更加清楚地看出异常实例点在(DriverBiz)obj;这个语句,换成字节码是一条 checkcast 指令;
那这两个类分别对应的类加载器是什么呢, 是类加载 ApplicationClassLoader 的类转化成 RestartClassLoader 类加载器的类呢,还是相反呢;为了清晰的表 我们定义一下:反序列化出来类实例的类 是类 A, 类 A 的类加载器是 ALoader, 而字节码 checkcast 后的类 是类 B(这里有一些类加载器的基础知识,链接阶段把符号引用变成直接引用,不懂的自行去了解 ),类 B 的类加载器是 BLoader;
这两个类分别对应的类加载器是什么?
通过前面的分析类 A 是由于解码器 codec 引入过来了,故在 checkcast 后的直接引用对应的类是由 ApplicationClassloader 来加载的(DynamicClassLoader 委托); 由结果推导原因类 B 的类加载器是 RestartClassLoader,通过 DEBUG 也验证了这一点,如图下:
到这里可能又有大部分人停止了思考? 为会反序列化出来的类对象 ALoader 是 RestartClassLoader 呢?
按理 EnumThriftCodec.java 是由 ApplicationClassloader 来加载其引用依赖也应该是 ApplicationClassloader 呀,但这个注意一下,EnumThriftCodec 使用的是泛形 T,而泛形有生命周期仅在编译期,之后就被擦除了;所以这个 T 的类加载器是什么不得而知?
private final Map<Integer, T> byEnumValue;
仔细看一下 enumMetadata 居然是一个 Map,这块逻辑也仅是根据一个 int 来获取类实例,那找一下这个类实例是什么时候放进这个 map 的即可!!!
public ThriftEnumMetadata( String enumName, Class<T> enumClass) throws RuntimeException{ // 实例 enumConstant 是由 enumClass 类获取其自己所有的实例,重点是这些实现必然与 enumClass 的类加载是一致的 for (T enumConstant : enumClass.getEnumConstants()) { Integer value; try { value = (Integer) enumValueMethod.invoke(enumConstant); }... // 把实例 enumConstant 放进 map byEnumValue.put(value, enumConstant);
在 new ThriftEnumMetadata 对象进传入的 enumClass 是由 serviceImpl(自定义的类由 RestartClassLoader 加载的)依赖引入的,具体过程如下(可以不看 哈哈):
# serviceImpl 的类加载器为 RestartClassLoaderThriftServiceMetadata serviceMetadata = new ThriftServiceMetadata(serviceImpl.getClass(), codecManager.getCatalog());# 获取 serviceImpl 的 method, 类加载器当然也是同一个呀Method method : findAnnotatedMethods(serviceClass, ThriftMethod.class)ThriftMethodMetadata methodMetadata = new ThriftMethodMetadata(name, method, catalog);# 获取 method 的参数,类加载器当然也是同一个呀 Type[] parameterTypes = method.getGenericParameterTypes();# 把参数一级一级传ThriftType thriftType = catalog.getThriftType(parameterType);thriftType = getThriftTypeUncached(javaType);ThriftEnumMetadata<? extends Enum<?>> thriftEnumMetadata = getThriftEnumMetadata(rawType);enumMetadata = new ThriftEnumMetadataBuilder<>((Class<T>) enumClass).build();# 最终到这里了,所以这个 enumClass 是自定义类某一个函数的参数引出来的一个类对象return new ThriftEnumMetadata<>(enumName, enumClass);
从上分析 enumClass 获取的枚举类实例当然类加载器为 RestartClassLoader,是在 RestartClassLoader 加载 serviceImpl 时就触发了 enumClass 的加载;
第二种解决方式
通过上面的分析 知道 Aloader 是 AppClassloader, Bloader 为 RestartClassLoader; 那么在不去掉 devtools 情况让,我们缩小 devtools 类加载器的范围,让其不加载 DriverBiz 即可,这样 RestartClassLoader 加载不了,会委托其“父”加载器来加载,还是上在的分析由与两个 AppClassloader 其实是同一个对象,所以问题就不存在了;
devtools 为我们提供排除目录文件的配置为
spring.devtools.restart.additional-exclude
第三种解决方式
第二种解决是改变 Bloader,那么我们有没有办法改变 Aloader 呢, 通过上面的分析我们知道是由于 DynamicClassLoader 产生解码器类 codec 才导致的,而采用定义 idl 文件时再编译成 java 类对象,就不需要动态地产生解码器 codec, 所以 他们的类加载器均会是 RestartClassLoader;
扩展
一个与本文同样本源的异常
java.lang.IllegalArgumentException: argument type mismatch
这个异常出现的时机点是 method.invoke()时参数不匹配, 不匹配的原因很多,上面同名不同类加载器也其中的一种,所以下次你看到这类问题,如果排除了其它情况外如果还没有解决的话,看看或许也是同名不同类加载器的原因。
本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。
阅读全文: http://gitbook.cn/gitchat/activity/5db41918e5d2ef314182ae20
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App , GitChat 专享技术内容哦。
devtools引发的一场关于类加载问题的探究相关推荐
- 物联网的通天塔困境:试图平息标准之争反而引发另一场大战
各位看官,请先跟iot101君一起到文学的海洋里遨游一番: 据犹太人的<圣经>记载:大洪水劫后,天上出现了第一道彩虹,这是上帝借彩虹与地上的人们定下的约定,不再用大洪水毁灭大地.此后,天下 ...
- 当有人试图将区块链和数字货币分开的时候,便引发了一场口诛笔伐
曾经,人们对于区块链的认识是单一的,局限的.谈及区块链,必然会谈及数字货币,几乎成为了一个真理.真理是不容挑战的,于是,当有人试图将区块链和数字货币分开的时候,便引发了一场口诛笔伐.后来的发展,告诉我 ...
- 警惕AI,我搭建了一个“枪枪爆头”的视觉AI自瞄程序,却引发了一场“山雨欲来”
前言 前段时间在网上看到<警惕AI外挂!我写了一个枪枪爆头的视觉AI,又亲手"杀死"了它> 这个视频,引起了我极大的兴趣. 视频中提到,在国外有人给使命召唤做了个AI程 ...
- LINE上市后引发的一场社交软件宫斗戏
大家天天都在用聊天工具八卦,其实人家心里也有好多话想说,特别是LINE上市了,正所谓人红是非多,一场社交软件的宫斗戏也就来了. 窃窃私语状 BBM:听说那个韩国.还是日本来的LINE这两天正特别风光, ...
- Vue 3.0 公开代码后,引发国外一场撕逼大战!
点击上方"蓝色字体",选择"星标" 08:51 在看 真爱 前言 在2019年10月5日,尤小右公开了 Vue 3.0 的源代码.开发者技术前线第一时间报道: ...
- 由JS函数返回值引发的一场”血案
---恢复内容开始--- 啊... 本来昨天晚上想写来着,结果陪老婆看电视剧就忘了... 呢滴神啊,原谅我吧. 背景:昨天在项目中做一个小功能的时候,出现了个小问题,而且一开始找了半天也没找到原因. ...
- 警惕由于使用YYYY-MM-dd引发的一场生产问题
文章目录 一.问题复现 一.问题原因 写在前面: 我是「境里婆娑」.我还是从前那个少年,没有一丝丝改变,时间只不过是考验,种在心中信念丝毫未减,眼前这个少年,还是最初那张脸,面前再多艰险不退却. 写博 ...
- MySQL GROUP_CONCAT长度限制引发的一场灾难
GROUP_CONCAT函数是对查处的分组数据对于分组列相同的数据合并成一列用逗号隔开的函数. 但是该函数的长度有个默认限制,默认是1024个字符,超过就会截断,从而导致用count统计GROUP_C ...
- 面试从int数据类型引发的一场血案,请问这真的只是基础吗?
天有不测风云,没想到今天给遇到了,还没起床就听见窗外刷刷刷的雨声,然后噼里啪啦收拾出了门. 面试官:你好,我这边是负责面试的,那咱们就开始吧 小编:好的好的( 点点头,跳过自我介绍 ) -- 面试官: ...
最新文章
- jenkins+sonarqube流水线脚本模板
- 大众点评数据平台架构变迁
- python操作redis--------------数据库增删改查
- .Net 3.5 Remoting编程入门三
- html标签转换含义,html标签含义
- pip升级python版本_GEE学习笔记 六十八:【GEE之Python版教程二】配置Python开发环境...
- android开发中EditText自动获取焦点时隐藏hint的代码
- 零序电流计算软件_每天5分钟跟我一起学电气之电力系统中的零序
- linux tcp传输变慢,linux下建立tcp连接(connect)非常慢的问题的排查
- 升级ubuntu后EMACS 无法使用
- JZOJ5371 组合数问题
- mysql 断开的管道_java.net.SocketException: 断开的管道 (Write failed) 错误,数据库隔一段时间就断开的问题...
- 要做飞思卡尔智能车要学哪些知识?
- Linux环境下向github上传代码(生成token、生成本地密钥)
- 智能陈桥五笔输入法 for linux,最好用的五笔字型--智能五笔,智能陈桥,陈桥五笔,陈桥拼音,GB18030五笔,GB18030输入法...
- 企业应该怎么运营微信公众号?
- python创建数据库字数不限制_KindEditor设置字数限制
- S3C6410板子移植 Android2.2
- 响应式织梦模板测量试验机类网站
- 北京的十大尾货批发市场【接近生活】
热门文章
- 2021物联网国赛通用库开发——D卷
- One-Error多标签分类_使用Folx自动标签功能,自动分类文件
- 横竖屏不同的默认壁纸
- TOP100summit分享实录 | 数字化三支柱:企业数字化转型的众妙之门
- Java金融借贷系统官网网站(含源码+论文+答辩PPT等)
- 【三】3D匹配Matching之可变形曲面匹配Deformable Surface——serialize_deformable_surface_model()算子
- 本地上传文件到Linux服务器
- 玩转华为ENSP模拟器系列 | 配置单段动态VPWS示例 - 使用LSP隧道
- 数学建模 # 论文撰写技巧
- 关于fluent中亚松弛因子under-ralexation factors的思考