阿里妹导读
本文作者将分享一个使用List.of后掉进的坑以及爬坑的全过程,希望大家能引以为戒同时引起这样的意识:在使用新技术前先搞清楚其实现的原理。
引
随着卓越工程的推进,很多底层技术的升级迭代被正式投入使用,例如 JDK11 的升级。然而,当我们拥抱变化,欣喜地使用一些新特性或者语法糖的同时,也有可能正在无意识的掉入一些陷阱。
本篇文章,我将分享一个使用List.of后掉进的坑以及爬坑的全过程,希望大家能引以为戒同时引起这样的意识:在使用新技术前先搞清楚其实现的原理。
案发现场
一句话总结:在一次后端发布的变更后,前端解析接口返回的格式失败。
前情提要:
过程回顾:
后端发布的变更示意:
// 发布前
public List before(Long id) {
...
if (...) {
return null;
}
...
}
// 发布后
public List 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 Unmodifiable Lists for details.
*
* @param the {@code List}'s element type
* @return an empty {@code List}
*
* @since 9
*/
static List of() {
return ImmutableCollections.emptyList();
}
}
从官方注释中得到3点结论:
这是一个 JDK9 之后的特性;
返回的是一个不可修改的数组;
底层实现使用的 的 方法,而 这个类是一个不可变集合的容器类;
2. 渐入佳境:.
class ImmutableCollections {
static List emptyList() {
return (List) 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 继承了t,而 t 实际又实现了List;
ListN 中的静态变量 会被初始化为一个空的 ListN 的对象;
方法中做了 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 {
private final E[] elements;
ListN(E... input) {
// copy and check manually to avoid TOCTOU
"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实现了自定义的序列化方法 和反序列方法 。直接抛出异常是一个防御性措施初始化list,说明该类直接反序列化会报错,来保证自己的不可变性。而 表示在序列化写入的时候替换成另一个对象,在这里返回的是一个内部的序列化代理对象(关键线索之二)。在实例化这个对象的时候,传递了2个变量:
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 字段初始化list,另外一个字段 array 被 标识所以序列化处理过程中会被忽略,这下我们终于知道tag = 1是怎么来的了。
结案陈词
综上所述,当后端在HSF接口中使用了List.of()做返回,在 node 调用 HSF 序列化获取返回结果时会解析成一个带有tag字段的对象,而不是预期的空数组。这个问题其实想解决很简单,将List.of()替换成我们常用的Lists.()就行,本质上还是对底层实现的不清晰不了解导致了这整个事件。
当然在结尾处,其实还有一个疑点,在 HSF 控制台调试这个接口的时候,我发现它的 json 结构是可以正确解析的:
怀疑可能是序列化类型的问题, 也是用了泛化调用,序列化类型是 ,可能 node 的序列化类型不一样,这个后续研究确定后我再补充一下。
最后的反思与大家共勉:对于新技术(或者新特性)的应用一定要先搞清楚内部的实现细节,不然可能出现使用时的大坑。
欢迎加入【阿里云开发者公众号】读者群
这是一个专门面向“阿里云开发者”公众号的读者交流空间
在这里你可以探讨技术和实践,我们也会定期发布群福利和活动~
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。