SpringMVC底層數據傳輸校驗的方案

  • 2019 年 10 月 8 日
  • 筆記

團隊的項目正常運行了很久,但近期偶爾會出現BUG。目前觀察到的有兩種場景:一是大批量提交業務請求,二是生成批量導出文件。出錯後,再執行一次就又正常了。

經過跟蹤日誌,發現是在Server之間進行json格式大數據量傳輸時會丟失部分字元,造成接收方拿到完整字元串後不能正確解析成json,因此報錯。

同其他團隊同事們溝通後發現,不僅僅是我們項目有這個問題,我們不是一個人在戰鬥。

1 問題現象

伺服器之間使用http+json的數據傳輸方案,在傳輸過程中,一些json數據發生錯誤,導致數據接收方解析json報錯,系統功能因此失敗。

下面截取了一小段真實數據錯誤,在傳輸的json中,有一個數據項是departmentIdList,其內容時一個長整型數組。

傳輸之前的數據為:

"departmentIdList": [ 719, 721, 722, 723, 7367, 7369, 7371, 7373, 7375, 7377 ]

接收到的數據為:

"departmentIdlist": [ 719, 721'373, 7375, 7377 ]

可以看到,這個錯誤導致了兩個問題:

1、json解析失敗

2、丟失了一些有效數據

詳細檢查系統日誌之後,這是偶發bug,並且只在傳輸數據較大時發生。

2 可選的解決方案

2.1 請架構組協助解決

這是最直接的解決方案,因為我們項目使用架構組提供的環境,他們需要提供可靠的底層數據傳輸機制。

2.2 壓縮傳輸數據

因為數據量大時容易發生,並且傳輸的都是普通文本,可以考慮對內容進行壓縮後傳輸。普通文件壓縮率也很高,壓縮後內容長度能做到原數據10%以內,極大減少傳輸出錯的幾率。

2.3 對傳輸數據進行MD5校驗

將傳輸數據作為一個完整數據塊,傳輸之前先做一個md5摘要,並將原數據和摘要一併發送;接收方收到數據後,先進行數據校驗工作,校驗成功後再進行後續操作流程,如果不成功可以輔助重傳或直接報錯等機制。

3 方案設計

為了徹底解決這個問題,設計了一個底層方案

3.1 設計原則

1、適用類型:Spring MVC項目,數據發送方使用RestTemplate工具類,使用fastjson作為json工具類。

2、數據校驗,使用MD5加密,當然也可以配合數據壓縮機制,減少傳輸數據量。

3、提供底層解決方案,不需要對系統程式碼做大規模調整。

3.2 核心設計

數據發送方,重載RestTemplate,在數據傳輸之前對數據進行md5摘要,並將原始數據和 md5摘要一併傳輸。

數據接收方,重載AbstractHttpMessageConverter,接收到數據後,對數據進行MD5校驗。

3.3 DigestRestTemplate關鍵程式碼

對原json進行摘要,並同原始數據一起生成一個新的json對象。

private Object digestingJson(JSONObject json) throws Exception { String requestJsonMd5 = JsonDigestUtil.createMD5(json); JSONObject newJson = new JSONObject(); newJson.put("content", json); newJson.put("md5", requestJsonMd5); return newJson;}

重載的postForEntity函數核心部分,如果傳入參數是 JSONObject,則調用方法對數據進行摘要操作,並用新生成的json進行傳輸。

Object newRequest = null;if (request instanceof JSONObject) { JSONObject json = (JSONObject) request; try { newRequest = digestingJson(json); } catch (Exception e) { }}if (newRequest == null) { newRequest = request;}return super.postForEntity(url, newRequest, responseType);

3.4 DigestFastJsonHttpMessageConverter 核心程式碼

首先會判斷是否是經過md5摘要的json,是有摘要的數據進行校驗,否則直接返回對象。

private JSONObject getDigestedJson(JSONObject json) { if (json.size()==2&&json.containsKey("md5")&&json.containsKey("content")) { String md5 = json.getString("md5"); String content = json.getString("content"); logger.info("degested json : {}", json); try { String newMd5 = JsonDigestUtil.createMD5(content); if (newMd5.equals(md5)) { json = JSON.parseObject(content); } else { logger.error("md5 is not same : {} vs {}", md5, newMd5); throw new RuntimeException("content is modified"); } } catch (Exception e) { } } else { logger.info("may not be digested json"); } return json;}

原有的處理數據程式碼增加調用該方法的程式碼

@Overrideprotected Object readInternal(Class<? extends Object> clazz,HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { JSONObject json = null; InputStream in = inputMessage.getBody(); Charset jsonCharset = fastJsonConfig.getCharset(); Feature[] jsonFeatures = fastJsonConfig.getFeatures(); json = JSON.parseObject(in, jsonCharset, clazz, jsonFeatures); json = getDigestedJson(json); return json;}

當前的程式碼,如果數據校驗失敗,簡單拋出異常。後續可以增加更多的機制,比如在RestTemplate處增加校驗,如果發現校驗失敗,則重傳。

3.5 數據發送方項目配置

以Spring Boot項目為例

在Main類中定義 restTemplate

@Bean(name = "restTemplate")public RestTemplate getRestTemplate() { RestTemplate restTemplate = new DigestRestTemplate(); return restTemplate;}

需要調用RestTemplate的程式碼,只需要依賴注入RestTemplate

@AutowiredRestTemplate restTemplate;

3.6 數據接收方項目設置

在SpringBootApplication類中定義

@Beanpublic HttpMessageConverters fastJsonHttpMessageConverters() { DigestFastJsonHttpMessageConverter fastConverter = new DigestFastJsonHttpMessageConverter(); FastJsonConfig fastJsonConfig = new FastJsonConfig(); fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat); fastConverter.setFastJsonConfig(fastJsonConfig); HttpMessageConverter<?> converter = fastConverter; return new HttpMessageConverters(converter);}

4 後記

經過測試,這個方案是可行的。如果為了能夠適應更多的項目及更多的Java技術棧,需要對程式碼進行進一步完善。