Redian新闻
>
一个 List.of 引发的“血案”

一个 List.of 引发的“血案”

科技

阿里妹导读


本文作者将分享一个使用List.of后掉进的坑以及爬坑的全过程,希望大家能引以为戒同时引起这样的意识:在使用新技术前先搞清楚其实现的原理。

随着卓越工程的推进,很多底层技术的升级迭代被正式投入使用,例如 JDK11 的升级。然而,当我们拥抱变化,欣喜地使用一些新特性或者语法糖的同时,也有可能正在无意识的掉入一些陷阱。
本篇文章,我将分享一个使用List.of后掉进的坑以及爬坑的全过程,希望大家能引以为戒同时引起这样的意识:在使用新技术前先搞清楚其实现的原理。

案发现场

一句话总结:在一次后端发布的变更后,前端解析接口返回的格式失败。

前情提要:

  • 后端 JAVA 应用 JDK 版本11,提供 HSF 服务端接口。

  • 前端通过陆游平台(一个 Node 可视化逻辑编排的平台)配置接口,内部通过 node 泛化调用后端的 HSF 接口,平台解析返回接口结果。

过程回顾:
  1. 后端发布的变更示意:
// 发布前public List<String> before(Long id) {    ...    if (...) {        return null;    }    ...}
// 发布后public List<String> after(Long id) { ... if (...) { return List.of(); } ...}

这里的核心变化点就是将默认的返回从 null 改成了 List.of() 。

为什么可以这么改?已知前端对null和空数组[]做了同样的兼容逻辑。
  1. 前端获取到接口的格式变化:
// 发布前{  "test": null}// 发布后{  "test": {        "tag": 1    }}

这个结构的变更直接导致了前端后续的字段结构解析失败,因为理论上 test 字段需要提供一个数组的格式(也可以是null),但是实际变成了一个对象。
所以整个环节中最离奇的是:为什么我的List.of在前端调用返回的接口中变成了一个带有tag字段的对象,它到底经历了怎么样的转换过程?

案情推理

List.of 触发的离奇现象让我不得不重新审视它,一步步看下它的源码实现。


1. 初窥门径:List.of

public interface List<E> extends Collection<E> {    /**     * Returns an unmodifiable list containing zero elements.     *     * See <a href="#unmodifiable">Unmodifiable Lists</a> for details.     *     * @param <E> the {@code List}'s element type     * @return an empty {@code List}     *     * @since 9     */    static <E> List<E> of() {        return ImmutableCollections.emptyList();    }}

从官方注释中得到3点结论:

  1. 这是一个 JDK9 之后的特性;
  2. 返回的是一个不可修改的数组;
  3. 底层实现使用的 ImmutableCollections 的 emptyList 方法,而 ImmutableCollections 这个类是一个不可变集合的容器类;

2. 渐入佳境:ImmutableCollections.emptyList

class ImmutableCollections {        static <E> List<E> emptyList() {        return (List<E>) ListN.EMPTY_LIST;    }
static final class ListN<E> extends AbstractImmutableList<E> implements Serializable {
// EMPTY_LIST may be initialized from the CDS archive. static @Stable List<?> EMPTY_LIST;
static { VM.initializeFromArchive(ListN.class); if (EMPTY_LIST == null) { EMPTY_LIST = new ListN<>(); } } ... }
static abstract class AbstractImmutableList<E> extends AbstractImmutableCollection<E> implements List<E>, RandomAccess { ... }}

到这一步,案件的主人公终于登场了:一个新的类 ListN。但是在这段代码中,还有很多隐藏的细节线索:

  1. ListN 是 List 的实现类:ListN 继承了AbstractImmutableList,而 AbstractImmutableList 实际又实现了List;
  2. ListN 中的静态变量 EMPTY_LIST 会被初始化为一个空的 ListN 的对象;
  3. emptyList 方法中做了 List 类型的强转,但是由于JAVA的类型转换原则,实际仍然返回的是一个ListN对象(这是关键线索之一),通过排查过程中发现的阿尔萨斯监控也可以确认这一点:

3. 直击要害:node的 HSF 解析

陆游平台调取HSF接口走的是node的泛化调用,默认情况下node只能解析一些基础的java类型,例如List和Map。

一个完整的类型映射表可以查看:java-对象与-node-的对应关系以及调用方法

而遇到这次返回的 ListN,可以确定是这种特殊类型在序列化/反序列化的过程中出现了不同的逻辑导致。

4. 真相大白:ListN的序列化

static final class ListN<E> extends AbstractImmutableList<E>            implements Serializable {    @Stable    private final E[] elements;
@SafeVarargs ListN(E... input) { // copy and check manually to avoid TOCTOU @SuppressWarnings("unchecked") E[] tmp = (E[])new Object[input.length]; // implicit nullcheck of input for (int i = 0; i < input.length; i++) { tmp[i] = Objects.requireNonNull(input[i]); } elements = tmp; }
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { throw new InvalidObjectException("not serial proxy"); } private Object writeReplace() { return new CollSer(CollSer.IMM_LIST, elements); }}
ListN实现了自定义的序列化方法 writeReplace 和反序列方法 readObject。readObject直接抛出异常是一个防御性措施,说明该类直接反序列化会报错,来保证自己的不可变性。而 writeReplace 表示在序列化写入的时候替换成另一个对象,在这里返回的是一个内部的序列化代理对象CollSer(关键线索之二)。在实例化这个CollSer对象的时候,传递了2个变量:
  • CollSer.IMM_LIST 静态值 = 1

  • elements 一个空的对象数组 = new Object[0]

final class CollSer implements Serializable {    private static final long serialVersionUID = 6309168927139932177L;
static final int IMM_LIST = 1; static final int IMM_SET = 2; static final int IMM_MAP = 3; private final int tag;
/** * @serial * @since 9 */ private transient Object[] array;
CollSer(int t, Object... a) { tag = t; array = a; }}
注意这里见到了我们眼熟的 tag 字段,另外一个字段 array 被 transient 标识所以序列化处理过程中会被忽略,这下我们终于知道 tag = 1 是怎么来的了。

结案陈词

综上所述,当后端在HSF接口中使用了 List.of() 做返回,在 node 调用 HSF 序列化获取返回结果时会解析成一个带有tag字段的对象,而不是预期的空数组。这个问题其实想解决很简单,将 List.of() 替换成我们常用的 Lists.newArrayList() 就行,本质上还是对底层实现的不清晰不了解导致了这整个事件。

当然在结尾处,其实还有一个疑点,在 HSF 控制台调试这个接口的时候,我发现它的 json 结构是可以正确解析的:

怀疑可能是序列化类型的问题,hsfops 也是用了泛化调用,序列化类型是 hessian,可能 node 的序列化类型不一样,这个后续研究确定后我再补充一下。

最后的反思与大家共勉:对于新技术(或者新特性)的应用一定要先搞清楚内部的实现细节,不然可能出现使用时的大坑。

欢迎加入【阿里云开发者公众号】读者群


这是一个专门面向“阿里云开发者”公众号的读者交流空间
💡 在这里你可以探讨技术和实践,我们也会定期发布群福利和活动~
欢迎扫码或者添加微信:argentinaliu 加入我们👇




微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
猎鹰火箭在大气层中打出越来越多的“血色空洞” | 科技趣评17岁儿子被酒驾警察撞死,政府向母亲收“血迹清洗费”!最近一部电影引发的大论战,意义十分重大!狮子林手术机器人引发的外科手术的革命,与改善人类生活的湾区硅谷科技创新“花西子引发的商战有多离谱?”哈哈一方有难,八方刁难...什么是三民主义(第四章摘要)4分钟偷走$200万!墨尔本Chadstone发生“惊天盗窃案”,警方:怀疑有内鬼能调动“所有公务员献血”?!27岁的“血槽姐”,有点猛北大鹅腿阿姨引发的商战:全军覆没两个萝卜章引发的惊天骗局四川一起因早恋引发的惨案,给家长一个沉重警示!震惊德州的“三角恋情杀案”,女嫌犯保外就医拔腿就跑,10分钟后再被捉回大家对于“血槽姐”,其实只想追问一个问题“一碗豆浆引发的婆媳矛盾”上热搜:我把你当妈妈,你把我当敌人?一方手帕引发的悲剧马加爵被讥讽后“血洗宿舍”,唯独放过了室友林峰,因为这件小事,林峰逃过一劫……昨晚:探访北欧维京人古航道在夕阳中之美飓风引发的入侵物种?传播危害南卡罗来纳州水域生态与经济不规范的枚举类代码引发的一场事故11月28日:一只小黑猫离世引发的思考:宠物生意如何做?贵州女子遭侵犯,见对方帅气想忍下,结果丈夫撞见引发血案一个由 “ YYYY-MM-dd ” 引发的惨案 !小说:兰欣与乌茶 31重大线上事故!三元表达式引发的空指针问题…LLM-first IDE:Code Agents 超级入口,软件开发的“Excel 时刻”另一个角度!朱令案:一场豪门恩怨引发的血案?!糖尿病溃疡引发的伤口,这家企业产品可加速1.5倍愈合中欧多资产团队:希望为大众理财提供稳定、清晰、透明的“解决方案”最新进展!震惊全球的“澳洲毒蘑菇案”!女子坚称:毒蘑菇是亚超买来的!一个沉默寡言的澳洲家庭主妇,被指控三次谋杀前夫...hiking gear list【健康】一场由缺觉引发的炎症风暴,5种慢病趁虚而入日本偶像鼻祖“杰尼斯”解体:一场性侵案引发的娱乐海啸游沧浪亭阿里云崩盘,三层涟漪所引发的波纹效应小鲸探展记| 从“朋克养生”到“轻养生” 这3家企业如何跟上年轻人的“血脉觉醒”?
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。