前言
版本上線時發現
fastjson
的toString
方法的返回的字符串與與之前版本的toString
方法返回的字符串不相同,這導致依賴toString
進行md5
計算所得到的結果不相同,更進一步導致其他依賴該md5
值的插件發現和之前的md5
值不相等而重啟,導致數據存在丟失情況。
源碼
從項目中抽取出該模塊代碼,並進行了適當修改,但未改變整個處理邏輯,源碼如下。
package main;
import com.alibaba.fastjson.JSONObject;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Main {
public static void main(String[] args) {
JSONObject obj = new JSONObject();
obj.put("the_plugin_id", "the_plugin_id");
obj.put("the_plugin_name", "the_plugin_name");
obj.put("the_plugin_version", "the_plugin_version");
obj.put("the_plugin_md5", "the_plugin_md5");
obj.put("the_extend_info1", "the_extend_info1");
obj.put("the_extend_info2", "the_extend_info2");
obj.put("the_extend_info3", "the_extend_info3");
obj.put("the_extend_info4", "the_extend_info4");
System.out.println(obj.toString());
System.out.println("md5 ==> " + getMD5String(obj.toString()));
}
private static final char hexDigits[] = {'0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z'};
static public String getMD5String(String source) {
String retString = null;
if (source == null) {
return retString;
}
try {
StringBuffer sb = new StringBuffer();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(source.getBytes(), 0, source.length());
byte[] retBytes = md.digest();
for (byte b : retBytes) {
sb.append(hexDigits[(b >> 4) & 0x0f]);
sb.append(hexDigits[b & 0x0f]);
}
retString = sb.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return retString;
}
}
原因猜想
- 首先懷疑是由於
fastjson
版本不一致的問題導致toString
方法返回的字符串不相同,待比對jar
后發現均依賴fastjson1.2.3
版本,排除由於fastjson
版本問題導致。 - 再者懷疑是由於上線時將
JDK
從1.7
替換到1.8
導致,即是由於JDK
升級引起該問題,下面是驗證過程。
分析驗證
為驗證是否是由於
JDK
升級導致該問題,分別使用不同JDK
運行上述程序,得到結果如下。
- JDK1.7運行結果
{"the_extend_info1":"the_extend_info1","the_plugin_version":"the_plugin_version","the_extend_info2":"the_extend_info2","the_extend_info3":"the_extend_info3","the_extend_info4":"the_extend_info4","the_plugin_name":"the_plugin_name","the_plugin_id":"the_plugin_id","the_plugin_md5":"the_plugin_md5"}
md5 ==> 87d74d87982fe1063a325c5aa97a9ef5
格式化JSON
字符串如下
{"the_extend_info1":"the_extend_info1","the_plugin_version":"the_plugin_version","the_extend_info2":"the_extend_info2","the_extend_info3":"the_extend_info3","the_extend_info4":"the_extend_info4","the_plugin_name":"the_plugin_name","the_plugin_id":"the_plugin_id","the_plugin_md5":"the_plugin_md5"}
- JDK1.8運行結果
{"the_plugin_md5":"the_plugin_md5","the_plugin_id":"the_plugin_id","the_plugin_name":"the_plugin_name","the_extend_info1":"the_extend_info1","the_extend_info2":"the_extend_info2","the_extend_info3":"the_extend_info3","the_extend_info4":"the_extend_info4","the_plugin_version":"the_plugin_version"}
md5 ==> fc8f7f526f5f37141f2fea3a03950f52
格式化JSON
字符串如下
{"the_plugin_md5":"the_plugin_md5","the_plugin_id":"the_plugin_id","the_plugin_name":"the_plugin_name","the_extend_info1":"the_extend_info1","the_extend_info2":"the_extend_info2","the_extend_info3":"the_extend_info3","the_extend_info4":"the_extend_info4","the_plugin_version":"the_plugin_version"}
對比
JDK1.7
和JDK1.8
下運行結果可知toString
方法返回的結果並不相同,這也就導致md5
計算的不相同,進一步導致其他依賴性的問題。
更進一步
當使用
JSONObject obj = new JSONObject();
創建JSONObject
時,跟蹤源碼可以看到其會調用JSONObject(int, boolean)
型構造函數,並且會使用HashMap
維護插入的鍵值對,這是關鍵所在。
HashMap
在JDK1.7
和JDK1.8
中底層有不同的邏輯,JDK1.8
的桶中會維護鏈表 + 紅黑樹
結構,該結果是對JDK1.7
的優化,JDK1.7
中維護鏈表
結構,在桶中元素較多而未達到再哈希的條件時查找效率會比較低下,而JDK1.8
當桶中元素個數達到一定數量時會將鏈表轉化為紅黑樹,這樣便能提高查詢效率,有興趣的讀者可查閱JDK1.7
和JDK1.8
的源碼,JDK1.8
源碼分析傳送門。
解決方案
由前面分析可知,直接使用
JSONObject obj = new JSONObject()
的方法生成JSONObject
對象時,其底層會使用HashMap
維護鍵值對,而HashMap
是和JDK
版本相關的,所以最好的解決方案應該是能和JDK
版本解耦的,而在JSONObject
的構造函數中,可以自定義傳入Map
,這樣就由指定Map
維護插入的鍵值對。可使用LinkedHashMap
來維護插入鍵值對,並且還會維護插入的順序。這樣便能保證在不同JDK
版本下使用toString
方法得到的字符串均相同。
方案驗證
使用
JSONObject obj = new JSONObject(new LinkedHashMap<String, Object>());
代替之前的JSONObject obj = new JSONObject();
即可。
- JDK1.7運行結果
{"the_plugin_id":"the_plugin_id","the_plugin_name":"the_plugin_name","the_plugin_version":"the_plugin_version","the_plugin_md5":"the_plugin_md5","the_extend_info1":"the_extend_info1","the_extend_info2":"the_extend_info2","the_extend_info3":"the_extend_info3","the_extend_info4":"the_extend_info4"}
md5 ==> 5c7725cd161d53f1e25a6a5c55b62c1f
格式化JSON
字符串如下
{"the_plugin_id":"the_plugin_id","the_plugin_name":"the_plugin_name","the_plugin_version":"the_plugin_version","the_plugin_md5":"the_plugin_md5","the_extend_info1":"the_extend_info1","the_extend_info2":"the_extend_info2","the_extend_info3":"the_extend_info3","the_extend_info4":"the_extend_info4"}
- JDK1.8運行結果
{"the_plugin_id":"the_plugin_id","the_plugin_name":"the_plugin_name","the_plugin_version":"the_plugin_version","the_plugin_md5":"the_plugin_md5","the_extend_info1":"the_extend_info1","the_extend_info2":"the_extend_info2","the_extend_info3":"the_extend_info3","the_extend_info4":"the_extend_info4"}
md5 ==> 5c7725cd161d53f1e25a6a5c55b62c1f
格式化JSON
字符串如下
{"the_plugin_id":"the_plugin_id","the_plugin_name":"the_plugin_name","the_plugin_version":"the_plugin_version","the_plugin_md5":"the_plugin_md5","the_extend_info1":"the_extend_info1","the_extend_info2":"the_extend_info2","the_extend_info3":"the_extend_info3","the_extend_info4":"the_extend_info4"}
對比在不同
JDK
下運行的結果,可以發現toString
方法獲得的字符串是完全相同的,md5
值也是完全相同的,即驗證了方案的正確性。
總結
在遇到問題時,特別是現網問題時,需要冷靜分析,大膽猜想,小心求證,一點點找到突破口,這次的排坑過程大致如上所記錄。