大家好,我是一航!
事情是這樣!某天中午午休完,正在開始下午的搬磚任務,突然群里面熱鬧起來,由於忙,也就沒有去看,過了一會兒,突然有伙伴在群里@我,就去爬樓看了一下大家的聊天記錄,結果是發現了一個很有意思的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;
,代碼如下:@Slf4j public class Main { public static void main(String[] args) throws Exception { //A服務的數據 Map<Long,String> mp = new HashMap<>(); Long l = 123456789000L; mp.put(l,"123"); log.info("key:{}",l); // B服務通過網絡請求得到A服務的響應文本 String s1 = JSON.toJSONString(mp); log.info("json文本:{}",s1); // 將文本轉換成Map對象 Map<Long,String> mp2 = JSON.parseObject(s1,Map.class); log.info("json文本轉換的Map對象:{}",mp2); // 通過key取值 log.info("通過key:{}得到的值:{}",l,mp2.get(l)); } }
運行結果
-
取值為null
異常情況下唯一的區別是key換成了
Long l = 123456789L;
public class Main { public static void main(String[] args) throws Exception { //A服務的數據 Map<Long,String> mp = new HashMap<>(); Long l = 123456789L; mp.put(l,"123"); // B服務通過網絡請求得到A服務的響應文本 String s1 = JSON.toJSONString(mp); log.info("json文本:{}",s1); // 將文本轉換成Map對象 Map<Long,String> mp2 = JSON.parseObject(s1,Map.class); log.info("json文本轉換的Map對象:{}",mp2); // 通過key取值 log.info("通過key:{}得到的值:{}",l,mp2.get(l)); } }
運行結果
結果分析
發現沒有!兩段代碼,除了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行,稍作分析就能知道,問題點是出在這行代碼
Map<Long,String> mp2 = JSON.parseObject(s1,Map.class);
類型轉換傳遞的對象僅僅是一個Map.class
;並沒有指明Map中的key和value的具體類型是什么;因為泛型擦除,導致fastJson在遇到基礎數字類型key的時候,無法判斷其具體的類型,只能通過長度去匹配一個最合適的數據類型;由於123456789
可以使用Integer去接收,就將其轉換成了Integer;而123456789000
就只能通過Long型接收,就轉換成了Long型;
以下是fastJson源碼中關於數字類型判斷的一段代碼;用來匹配當前的數字需要轉換成什么類型邏輯判斷:
if (negative) {
if (i > this.np + 1) {
if (result >= -2147483648L && type != 76) {
if (type == 83) {
return (short)((int)result);
} else if (type == 66) {
return (byte)((int)result);
} else {
return (int)result;
}
} else {
return result;
}
} else {
throw new NumberFormatException(this.numberString());
}
} else {
result = -result;
if (result <= 2147483647L && type != 76) {
if (type == 83) {
return (short)((int)result);
} else if (type == 66) {
return (byte)((int)result);
} else {
return (int)result;
}
} else {
return result;
}
}
這樣也就能明確解釋這個bug所出現的原因了;
如何解決呢?
fastJson
如果單純是通過fastJson將Json文本轉對象,其實處理起來就很簡單了,只需要指明一下Map中的key和value是什么類型的即可,代碼如下
Map<Long,String> mp2 = JSON.parseObject(s1,new TypeReference<Map<Long,String>>(){});
即使當key為123456789
的時候,依然能夠造成獲取到值
RestTemplate
本文的起因,是因為通過RestTemplate請求另外一個服務沒有指明泛型對象造成的,因此也需要指明一下;
-
示例接口
@RestController @RequestMapping("/a") public class TestController { @GetMapping("/b") public Map<Long, String> b() { Map<Long, String> mp = new HashMap<>(); mp.put(1L,"123"); mp.put(123456789L,"456"); mp.put(123456789000L,"789"); return mp; } }
-
restTemplate請求
@Autowired RestTemplate restTemplate; @Test public void restTemplate() throws Exception { ParameterizedTypeReference<Map<Long, String>> typeRef = new ParameterizedTypeReference<Map<Long, String>>() {}; Map<Long, String> mp = restTemplate.exchange("http://127.0.0.1:8080/a/b", HttpMethod.GET, new HttpEntity<>(null), typeRef).getBody(); log.info("mp:{}", mp); log.info("獲取key為:{} 的值:{}",1L,mp.get(1L)); log.info("獲取key為:{} 的值:{}",123456789L,mp.get(123456789L)); log.info("獲取key為:{} 的值:{}",123456789000L,mp.get(123456789000L)); }
思考
到這里,整個問題算是解決了!
但有另外一個點,也不得不說一下;這位老弟采用的是Map作為報文交互的對象,是非常不建議用的,通過Map,看似提高了靈活性,畢竟啥對象都可以扔進去,實則給代碼的可讀性、維護性帶來了很大的障礙,因為我沒有辦法一眼看出這個Map中放了些什么數據,也不知道何時放了數據進去;如果我只是作為一個調用方,想去看一下你返回了些什么,僅僅通過接口定義,我是沒辦法清晰的看出,而是要深入閱讀詳細的代碼,看你在Map中塞了些什么值,分別代表什么意思,才能加以明確。
而這一系列的問題,可能終將自己挖個深坑把自己給埋了
那么為了提高接口的靈活性、可閱讀性以及可擴展性,基於泛型的接口報文數據抽象化是一個重要手段;將報文的Json格式分為公共部分和業務數據部分,讓整個數據結構變的更加靈活,但又不失整體的規范,通過響應對象,一眼就能明確你要返回的數據;可參考以下簡單示例:
// 公共部分
{
"code":0,
"msg":"成功",
"data":{
// 業務數據
}
}
對應的代碼:
@Data
public class BaseBean<T> {
private Integer code;
private String msg;
private T data;
}
通過泛型,即可靈活表達任意響應
-
用戶
@GetMapping("/user") public BaseBean<User> user() { // 這里去獲取User BaseBean<User> user = new BaseBean<>(); return user; }
-
商品
@GetMapping("/goods") public BaseBean<Goods> goods() { // 這里去獲取商品 BaseBean<Goods> goods = new BaseBean<>(); return user; }
....
好了,今天就分享到這里,願看到此文的朋友,今后,再無Bug!!!