「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」
大家好,我是一航!
事情是这样!某天中午午休完,正在开始下午的搬砖任务,突然群里面热闹起来,由于忙,也就没有去看,过了一会儿,突然有伙伴在群里@我,就去爬楼看了一下大家的聊天记录,结果是发现了一个很有意思的Bug;看似很基础Map的取值问题,对于基础不是特别扎实的朋友来说,但如果真的遇到,可能会被坑惨,群里这位老弟就被坑了一下午,在这里分享给大家。
讨论的起因是一个老弟问了这样一个问题:
简单一句话表述就是:接口回了个Map,key是Long型的,Map中有数据,可取不到值;
由于基础数据类型的Key在以Json返回的时候,都被转成了String,有伙伴儿很快提出确认Key是不是被转成了String,结果都被否认了;但对于这个否认,我是持有怀疑态度的,所以,这里得必须亲自验证一下;
问题梳理
为了搞清楚状况,需要先简单的梳理一下;
- 业务场景是这样:
- A服务提供了一个接口,返回了一个Map<Long , Object>
- B服务通过RestTemplate调用A服务对应的接口,入参就就是一个Long
- B服务通过得到Map<Long , Object>响应之后,再通过Long值作为Key,去得到Object
- 问题点:至于这种接口设计方式是否合理,文末另说,这位老弟遇到的问题是:B服务能正常接收到Map<Long , Object>对象,也就是
log.info("map:{}",map)
都能正常输出对应的key和Object;但是通过map.get(sourceId)
取Object,有时候正常,有时候取出来的null;这一下子就变的有意思了;程序员遇到Bug,只要是必现或者能百度到的,那都不算bug,轻轻松松拿下;唯独那种时而出现时而正常的bug,是最头疼的,可能让你一度怀疑人生;
复现Bug
为了能把这个问题点说清楚,按他的写法,我模拟了一下他的业务逻辑,写了一段简单代码复现一下正常情况和异常情况:
- 能正常取值
key为Long l = 123456789000L;
,代码如下:
1 | typescript复制代码@Slf4j |
运行结果
- 取值为null
异常情况下唯一的区别是key换成了Long l = 123456789L;
1 | typescript复制代码public class Main { |
运行结果
结果分析
发现没有!两段代码,除了key不一样,逻辑部分没有任何区别,均无报错,且都能正常运行,那为何一段正常一段结果为null呢?
bug场景复现了,一切就别的简单多了,既然 mp2.get(l)
取的值不同,问题点也肯定就出现在这个附近了,debug去分析一下mp2里面到底放了些啥:
好家伙!事出反常必有妖;
一看这两种情况下mp2对应key的类型(上图箭头部分),应该就明白,为什么key是 long l = 123456789l
的时候,mp2取不到值了吧;因为转换后mp2里面存的压根儿就不是Long型的key,而是一个Integer的key?当Key是Long型的时候,就能正常取到值,当为Integer的时候,取出来的就是null
为什么变成了Integer
明明我存的是一个Long作为key,Json文本转mp2的时候我也是通过Map<Long,String>去接收,似乎一切都有理有据,为什么最后mp2的key一会儿是Integer,一会儿是Long呢?
毕竟核心代码只有这么简单的5行,稍作分析就能知道,问题点是出在这行代码
1 | javascript复制代码Map<Long,String> mp2 = JSON.parseObject(s1,Map.class); |
类型转换传递的对象仅仅是一个 Map.class
;并没有指明Map中的key和value的具体类型是什么;因为泛型擦除,导致fastJson在遇到基础数字类型key的时候,无法判断其具体的类型,只能通过长度去匹配一个最合适的数据类型;由于 123456789
可以使用Integer去接收,就将其转换成了Integer;而 123456789000
就只能通过Long型接收,就转换成了Long型;
以下是fastJson源码中关于数字类型判断的一段代码;用来匹配当前的数字需要转换成什么类型逻辑判断:
1 | go复制代码 if (negative) { |
这样也就能明确解释这个bug所出现的原因了;
如何解决呢?
fastJson
如果单纯是通过fastJson将Json文本转对象,其实处理起来就很简单了,只需要指明一下Map中的key和value是什么类型的即可,代码如下
1 | javascript复制代码Map<Long,String> mp2 = JSON.parseObject(s1,new TypeReference<Map<Long,String>>(){}); |
即使当key为 123456789
的时候,依然能够造成获取到值
RestTemplate
本文的起因,是因为通过RestTemplate请求另外一个服务没有指明泛型对象造成的,因此也需要指明一下;
- 示例接口
1 | typescript复制代码@RestController |
- restTemplate请求
1 | dart复制代码@Autowired |
思考
到这里,整个问题算是解决了!
但有另外一个点,也不得不说一下;这位老弟采用的是Map作为报文交互的对象,是非常不建议用的,通过Map,看似提高了灵活性,毕竟啥对象都可以扔进去,实则给代码的可读性、维护性带来了很大的障碍,因为我没有办法一眼看出这个Map中放了些什么数据,也不知道何时放了数据进去;如果我只是作为一个调用方,想去看一下你返回了些什么,仅仅通过接口定义,我是没办法清晰的看出,而是要深入阅读详细的代码,看你在Map中塞了些什么值,分别代表什么意思,才能加以明确。
而这一系列的问题,可能终将自己挖个深坑把自己给埋了
那么为了提高接口的灵活性、可阅读性以及可扩展性,基于泛型的接口报文数据抽象化是一个重要手段;将报文的Json格式分为公共部分和业务数据部分,让整个数据结构变的更加灵活,但又不失整体的规范,通过响应对象,一眼就能明确你要返回的数据;可参考以下简单示例:
1 | json复制代码// 公共部分 |
对应的代码:
1 | kotlin复制代码@Data |
通过泛型,即可灵活表达任意响应
- 用户
1 | sql复制代码@GetMapping("/user") |
- 商品
1 | swift复制代码@GetMapping("/goods") |
….
好了,今天就分享到这里,愿看到此文的朋友,今后,再无Bug!!!
本文转载自: 掘金