您现在的位置是:亿华云 > 知识
一个 List.of 引发的“血案”
亿华云2025-10-04 04:07:35【知识】1人已围观
简介来源:阿里云开发者阿里妹导读本文作者将分享一个使用List.of后掉进的坑以及爬坑的全过程,希望大家能引以为戒同时引起这样的意识:在使用新技术前先搞清楚其实现的原理。引随着卓越工程的推进,很多底层技术
来源:阿里云开发者
阿里妹导读
本文作者将分享一个使用List.of后掉进的坑以及爬坑的全过程,希望大家能引以为戒同时引起这样的血案意识:在使用新技术前先搞清楚其实现的原理。
引
随着卓越工程的血案推进,很多底层技术的血案升级迭代被正式投入使用,例如 JDK11 的血案升级。然而,血案当我们拥抱变化,血案欣喜地使用一些新特性或者语法糖的血案同时,也有可能正在无意识的血案掉入一些陷阱。本篇文章,血案我将分享一个使用List.of后掉进的血案坑以及爬坑的全过程,希望大家能引以为戒同时引起这样的血案意识:在使用新技术前先搞清楚其实现的原理。案发现场
一句话总结:在一次后端发布的血案变更后,前端解析接口返回的血案格式失败。
前情提要:
后端 JAVA 应用 JDK 版本11,血案提供 HSF 服务端接口。
前端通过陆游平台(一个 Node 可视化逻辑编排的平台)配置接口,内部通过 node 泛化调用后端的网站模板 HSF 接口,平台解析返回接口结果。
过程回顾:后端发布的变更示意:// 发布前public List<String> before(Long id) { ... if (...) { return null; } ...}// 发布后public List<String> after(Long id) { ... if (...) { return List.of(); } ...}这里的核心变化点就是将默认的返回从 null 改成了 List.of() 。
为什么可以这么改?已知前端对null和空数组[]做了同样的兼容逻辑。前端获取到接口的格式变化:// 发布前{ "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点结论:
这是一个 JDK9 之后的特性;返回的是源码下载一个不可修改的数组;底层实现使用的 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。但是在这段代码中,还有很多隐藏的细节线索:
ListN 是 List 的实现类:ListN 继承了AbstractImmutableList,而 AbstractImmutableList 实际又实现了List;ListN 中的静态变量 EMPTY_LIST 会被初始化为一个空的 ListN 的对象;emptyList 方法中做了 List 类型的强转,但是由于JAVA的类型转换原则,实际仍然返回的是一个ListN对象(这是关键线索之一),通过排查过程中发现的阿尔萨斯监控也可以确认这一点:
3. 直击要害:node的 HSF 解析
陆游平台调取HSF接口走的是node的香港云服务器泛化调用,默认情况下node只能解析一些基础的java类型,例如List和Map。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 的序列化类型不一样,这个后续研究确定后我再补充一下。
最后的反思与大家共勉:对于新技术(或者新特性)的应用一定要先搞清楚内部的实现细节,不然可能出现使用时的大坑。很赞哦!(4)
相关文章
- .com域名是国际最广泛流行的通用域名,目前全球注册量第一的域名,公司企业注册域名的首选。国际化公司通常会注册该类域名。
- 评估域名涉及的行业规模与发展状况成正比。
- 小白注册网站域名该怎么办?有什么步骤?
- 为什么大家都选优质域名?到底存在着什么好处?
- 4、说起来容易
- 4、说起来容易
- 新手可以注册cc域名吗?cc域名有什么特点?
- 4、域名传输时,取决于域名原始用户的邮箱是否有效,以及他是否将密码发送到此邮箱。
- 为什么大家都选优质域名?到底存在着什么好处?
- 众所周知,com域名拥有最大的流通市场和流通历史。最好选择com域名,特别是在购买域名时处理域名。其次可以是cn域名、net域名、org域名等主流域名,现在比较流行的王域名和顶级域名,都是值得注册和投资的。
热门文章
站长推荐
6、提示添加成功,点击确认进行最后的确定操作。一般10分钟就解析生效,可以用域名进行访问了。
审核通过的域名将显示在域名竞拍页面,并进入正式拍卖期,买家可以在拍卖周期内出价,加价幅度与拍卖保证金说明,点此查看。
2、定期提交和投标域名注册。例如,益华网络点击“立即预订”后,平台会抢先为客户注册域名。当然,一个域名可能会被多个客户预订,所以出价最高的人中标。
(4) 使用何种形式的域名后缀对网页搜索影响不大,但域名后缀也需要考虑方便用户记忆
4、企业无形资产:通用网站已成为企业网络知识产权的重要组成部分,属于企业的无形资产,也有助于提升企业的品牌形象和技术领先形象。它是企业品牌资产不可或缺的一部分。
记住那句话,域名向来不属于任何人,谁先买就归谁,购买期过后,域名又不再属于任何人。
4、选择一个安全的域名注册商进行域名注册