一個(gè)詭異的Json反序列化問(wèn)題
前言
最近我在做知識(shí)星球中的商品秒殺系統(tǒng),昨天遇到了一個(gè)詭異的json反序列化問(wèn)題,感覺挺有意思的,現(xiàn)在拿出來(lái)跟大家一起分享一下,希望對(duì)你會(huì)有所幫助。
案發(fā)現(xiàn)場(chǎng)
我最近在做知識(shí)星球中的商品秒殺系統(tǒng),寫了一個(gè)filter,獲取用戶請(qǐng)求的header中獲取JWT的token信息。
然后根據(jù)token信息,獲取到用戶信息。
在轉(zhuǎn)發(fā)到業(yè)務(wù)接口之前,將用戶信息設(shè)置到用戶上下文當(dāng)中。
這樣接口中的業(yè)務(wù)代碼,就能通過(guò)用戶上下文,獲取到當(dāng)前登錄的用戶信息了。
我們的token和用戶信息,為了性能考慮都保存到了Redis當(dāng)中。
用戶信息是一個(gè)json字符串。
當(dāng)時(shí)在用戶登錄接口中,將用戶實(shí)體,使用fastjson工具,轉(zhuǎn)換成了字符串:
JSON.toJSONString(userDetails);
保存到了Redis當(dāng)中。
然后在filter中,通過(guò)一定的key,獲取Redis中的字符串,反序列化成用戶實(shí)體。
使用的同樣是fastjson工具:
JSON.parseObject(json, UserEntity.class);
但在反序列化的過(guò)程中,filter拋異常了:com.alibaba.fastjson.JSONException: illegal identifier : \pos 1, line 1, column 2{\"accountNonExpired\":true,\"accountNonLocked\":true,\"authorities\":[{\"authority\":\"admin\"}],\"credentialsNonExpired\":true,\"enabled\":true,\"id\":13,\"password\":\"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe\",\"roles\":[\"admin\"],\"username\":\"admin\"}
2 分析問(wèn)題
我剛開始以為是json數(shù)據(jù)格式有問(wèn)題。
將json字符串復(fù)制到在線json工具:https://www.sojson.com,先去掉化之后,再格式數(shù)據(jù),發(fā)現(xiàn)json格式?jīng)]有問(wèn)題:
圖片
然后寫了一個(gè)專門的測(cè)試類,將日志中打印的json字符串復(fù)制到j(luò)son變量那里,使用JSON.parseObject方法,將json字符串轉(zhuǎn)換成Map對(duì)象:
public class Test {
public static void main(String[] args) {
String json = "{\"accountNonExpired\":true,\"accountNonLocked\":true,\"authorities\":[{\"authority\":\"admin\"}],\"credentialsNonExpired\":true,\"enabled\":true,\"id\":13,\"password\":\"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe\",\"roles\":[\"admin\"],\"username\":\"admin\"}";
Map map = JSON.parseObject(json, Map.class);
// 輸出解析后的 JSON 對(duì)象
System.out.println(map);
}
}
執(zhí)行結(jié)果:
{password=$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe, credentialsNnotallow=true, roles=["admin"], accountNnotallow=true, id=13, authorities=[{"authority":"admin"}], enabled=true, accountNnotallow=true, username=admin}
竟然轉(zhuǎn)換成功了。
這就讓我有點(diǎn)懵逼了。。。
為什么相同的json字符串,在Test類中能夠正常解析,而在filter當(dāng)中卻不行?
當(dāng)時(shí)怕搞錯(cuò)了,debug了一下filter,發(fā)現(xiàn)獲取到的json數(shù)據(jù),跟Test類中的一模一樣:
圖片
帶著一臉的疑惑,我做了下面的測(cè)試。
莫非是反序列化工具有bug?
3 改成gson工具
我嘗試了一下將json的反序列化工具改成google的gson,代碼如下:
Map map = new Gson().fromJson(userJson, Map.class);
運(yùn)行之后,報(bào)了一個(gè)新的異常:com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 2 path $
這里提示json字符串中包含了:$。
而$是特殊字符,password是做了加密處理的,里面包含$和.,這兩種特殊字符。
為了快速解決問(wèn)題,我先將這兩個(gè)特字符替換成空字符串:
json = json.replace("$","").replace(".","");
日志中打印出的json中的password,已經(jīng)不包含這兩個(gè)特殊字符了:
2a10o3XfeGr0SHStAwLuJRW6ykE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe
但調(diào)整之后代碼報(bào)了下面的異常:com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Expected name at line 1 column 2 path $.
跟剛剛有點(diǎn)區(qū)別,但還是有問(wèn)題。
4 改成jackson工具
我又嘗試了一下json的反序列化工具,改成Spring自帶的的jackson工具,代碼如下:
ObjectMapper objectMapper = new ObjectMapper();
try {
Map map = objectMapper.readValue(json, Map.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
調(diào)整之后,反序列化還是報(bào)錯(cuò):com.fasterxml.jackson.core.JsonParseException: Unexpected character ('\' (code 92)): was expecting double-quote to start field name
3種反序列化工具都不行,說(shuō)明應(yīng)該不是fastjson的bug導(dǎo)致的當(dāng)前json字符串,反序列化失敗。
到底是什么問(wèn)題呢?
5 轉(zhuǎn)義
之前的數(shù)據(jù),我在仔細(xì)看了看。
里面是對(duì)雙引號(hào),是使用了轉(zhuǎn)義的,具體是這樣做的:\"。
莫非還是這個(gè)轉(zhuǎn)義的問(wèn)題?
其實(shí)我之前已經(jīng)注意到了轉(zhuǎn)義的問(wèn)題,但使用Test類測(cè)試過(guò),沒有問(wèn)題。
當(dāng)時(shí)的代碼是這樣的:
public class Test {
public static void main(String[] args) {
String json = "{\"accountNonExpired\":true,\"accountNonLocked\":true,\"authorities\":[{\"authority\":\"admin\"}],\"credentialsNonExpired\":true,\"enabled\":true,\"id\":13,\"password\":\"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe\",\"roles\":[\"admin\"],\"username\":\"admin\"}";
Map map = JSON.parseObject(json, Map.class);
// 輸出解析后的 JSON 對(duì)象
System.out.println(map);
}
}
里面也包含了一些轉(zhuǎn)義字符。
我?guī)е囈辉嚨男膽B(tài),接下來(lái),打算將轉(zhuǎn)義字符去掉。
看看原始的json字符串,解析有沒有問(wèn)題。
怎么去掉轉(zhuǎn)義字符呢?
手寫工具類,感覺不太好,可能會(huì)寫漏一些特殊字符的場(chǎng)景。
我想到了org.apache.commons包下的StringEscapeUtils類,它里面的unescapeJava方法,可以輕松去掉Java代碼中的轉(zhuǎn)義字符。
于是,我調(diào)整了一下代碼:
json = StringEscapeUtils.unescapeJava(json);
JSON.parseObject(json, UserEntity.class);
這樣處理之后,發(fā)現(xiàn)反序列化成功了。
總結(jié)
這個(gè)問(wèn)題最終發(fā)現(xiàn)還是轉(zhuǎn)義的問(wèn)題。
那么,之前Test類中json字符串,也使用了轉(zhuǎn)義,為什么沒有問(wèn)題?
當(dāng)時(shí)的代碼是這樣的:
public class Test {
public static void main(String[] args) {
String json = "{\"accountNonExpired\":true,\"accountNonLocked\":true,\"authorities\":[{\"authority\":\"admin\"}],\"credentialsNonExpired\":true,\"enabled\":true,\"id\":13,\"password\":\"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe\",\"roles\":[\"admin\"],\"username\":\"admin\"}";
Map map = JSON.parseObject(json, Map.class);
System.out.println(map);
}
}
但在filter中的程序,在讀取到這個(gè)json字符串之后,發(fā)現(xiàn)該字符串中包含了\轉(zhuǎn)義符號(hào),程序自動(dòng)把它變成了\\\。
調(diào)整一下Test類的main方法,改成三個(gè)斜杠的json字符串:
public static void main(String[] args) {
String json = "{\\\"accountNonExpired\\\":true,\\\"accountNonLocked\\\":true,\\\"authorities\\\":[{\\\"authority\\\":\\\"admin\\\"}],\\\"credentialsNonExpired\\\":true,\\\"enabled\\\":true,\\\"id\\\":13,\\\"password\\\":\\\"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe\\\",\\\"roles\\\":[\\\"admin\\\"],\\\"username\\\":\\\"admin\\\"}";
Map map = JSON.parseObject(json, Map.class);
System.out.println(map);
}
執(zhí)行結(jié)果:Exception in thread "main" com.alibaba.fastjson.JSONException: illegal identifier : \pos 1, line 1, column 2{\"accountNonExpired\":true,\"accountNonLocked\":true,\"authorities\":[{\"authority\":\"admin\"}],\"credentialsNonExpired\":true,\"enabled\":true,\"id\":13,\"password\":\"$2a$10$o3XfeGr0SHStAwLuJRW6y.kE0UTerQfv3SXrAcVLuJ6M3hEsC9RKe\",\"roles\":[\"admin\"],\"username\":\"admin\"}拋出了跟文章最開始一樣的異常。
說(shuō)明其實(shí)就是轉(zhuǎn)義的問(wèn)題。
之前,我將項(xiàng)目的日志中的json字符串,復(fù)制到idea的Test的json變量中,當(dāng)時(shí)將最外層的雙引號(hào)一起復(fù)制過(guò)來(lái)了,保存的是1個(gè)斜杠的數(shù)據(jù)。
這個(gè)操作把我誤導(dǎo)了。
而后面從在線的json工具中,把相同的json字符串,復(fù)制到idea的Test的json變量中,在雙引號(hào)當(dāng)中粘貼數(shù)據(jù),保存的卻是3個(gè)斜杠的數(shù)據(jù),它會(huì)自動(dòng)轉(zhuǎn)義。
讓我意識(shí)到了問(wèn)題。
好了,下次如果遇到類似的問(wèn)題,可以直接使用org.apache.commons包下的StringEscapeUtils類,先去掉轉(zhuǎn)義,再反序列化,這樣可以快速解決問(wèn)題。
此外,這次使用了3種不同的反序列化工具,也看到了其中的一些差異。