某大厂P9,工作10年,年薪300万+,带上百人团队。他说:“今年业务砍了三轮,团队从200人砍到80人。下一个砍的,可能就是他
45岁,P9,干了10年,年薪三百多万,手底下以前两百号人,听着是不是挺稳?结果业务一砍就是三轮,团队从200砍到80,桌子还没凉呢,人已经少了一大半。
最扎心的是,他不是看不懂局面的人。能做到这个位置,什么风向没见过?但真落到自己头上,照样慌。外面的人还觉得他是人生赢家,房子票子都有,履历也硬。
可公司算账的时候,不看你过去带过多少人,也不看你熬过多少夜。只看这条线还值不值钱,你这个成本还扛不扛得住。
所以他问“我还能撑多久”,其实挺真实的。到这个年纪,位置越高,反而越不好动。往上没坑,往下尴尬,换地方别人还得掂量你贵不贵。HR看完都得沉默两秒。
线上配置一覆盖,老字段全没了。
明明只想把这一段:
{
"db": {
"pool": {
"max": 80
}
}
}
合到原配置里,结果最后只剩 max,min、timeout 全被干掉。
这种 Bug 我一看就不太想查业务代码,八成是合并对象的时候写成了“浅合并”。
比如原对象是这样:
{
"db": {
"host": "127.0.0.1",
"pool": {
"min": 10,
"max": 50,
"timeout": 3000
}
},
"log": {
"level": "INFO"
}
}
新对象只传了:
{
"db": {
"pool": {
"max": 80
}
}
}
如果直接 putAll:
oldMap.putAll(newMap);
db 这一层会被整个替换掉。
也就是说,程序不会关心 db.pool.max 是不是只改了一个字段,它只看到 db 这个 key 被覆盖了。
这个地方坑就坑在,看代码像只改了一行,实际效果是删了一片。
深度合并要做的事也不复杂:两个对象的同一个 key,如果值都是对象,就继续往下合;否则右边覆盖左边。
我一般会把规则写死,不在业务里散着判断。
publicclassDeepMerger{
@SuppressWarnings("unchecked")
publicstatic Map<String, Object> merge(
Map<String, Object> base,
Map<String, Object> patch
){
if (base == null) {
base = new LinkedHashMap<>();
}
if (patch == null || patch.isEmpty()) {
return base;
}
for (Map.Entry<String, Object> item : patch.entrySet()) {
String key = item.getKey();
Object newVal = item.getValue();
Object oldVal = base.get(key);
if (oldVal instanceof Map && newVal instanceof Map) {
Map<String, Object> merged = merge(
new LinkedHashMap<>((Map<String, Object>) oldVal),
(Map<String, Object>) newVal
);
base.put(key, merged);
continue;
}
base.put(key, newVal);
}
return base;
}
}
这里我特意用了 LinkedHashMap,不是为了高级,就是为了排查问题时顺序别乱跳。
现场看日志的时候,字段顺序一变,人容易怀疑错方向。
跑一下:
Map<String, Object> oldCfg = new LinkedHashMap<>();
oldCfg.put("db", new LinkedHashMap<>(Map.of(
"host", "127.0.0.1",
"pool", new LinkedHashMap<>(Map.of(
"min", 10,
"max", 50,
"timeout", 3000
))
)));
Map<String, Object> newCfg = new LinkedHashMap<>();
newCfg.put("db", new LinkedHashMap<>(Map.of(
"pool", new LinkedHashMap<>(Map.of(
"max", 80
))
)));
Map<String, Object> result = DeepMerger.merge(oldCfg, newCfg);
System.out.println(result);
输出大概是:
{
db={
host=127.0.0.1,
pool={
min=10,
max=80,
timeout=3000
}
}
}
这才是“只改 max”。
不过这里还有个细节,null 到底算不算覆盖?
这个不能拍脑袋。配置中心里,我一般让 null 表示“清空字段”;接口参数里,我更倾向于忽略 null,因为前端少传字段太常见了。
如果业务希望 null 不覆盖,判断加一行就行:
if (newVal == null) {
continue;
}
别把这个规则藏在递归里面,后面排查会很难受。
数组也一样,别默认深度合并。数组到底是追加、去重,还是整体替换,业务差异很大。
我的习惯是:数组默认整体替换。
base.put(key, newVal);
简单,但不容易出脏数据。
深度合并这个题,难点不是递归,是边界规则。
同 key 都是对象,往下走。
不是对象,右边覆盖左边。
null 和数组,提前定规矩。
代码反而别写太花,越花越像给后人埋雷。