用 Long 做 Map 的 Key,存的對象花一下午才取出來,坑慘了


大家好,我是一航!

事情是這樣!某天中午午休完,正在開始下午的搬磚任務,突然群里面熱鬧起來,由於忙,也就沒有去看,過了一會兒,突然有伙伴在群里@我,就去爬樓看了一下大家的聊天記錄,結果是發現了一個很有意思的Bug;看似很基礎Map的取值問題,對於基礎不是特別扎實的朋友來說,但如果真的遇到,可能會被坑慘,群里這位老弟就被坑了一下午,在這里分享給大家。

討論的起因是一個老弟問了這樣一個問題:

簡單一句話表述就是:接口回了個Map,key是Long型的,Map中有數據,可取不到值;

由於基礎數據類型的Key在以Json返回的時候,都被轉成了String,有伙伴兒很快提出確認Key是不是被轉成了String,結果都被否認了;但對於這個否認,我是持有懷疑態度的,所以,這里得必須親自驗證一下;

問題梳理

為了搞清楚狀況,需要先簡單的梳理一下;

  • 業務場景是這樣:

    1. A服務提供了一個接口,返回了一個Map<Long , Object>
    2. B服務通過RestTemplate調用A服務對應的接口,入參就就是一個Long
    3. 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!!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM