日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

SpringMVC底层数据传输校验的方案(修改版)

發布時間:2025/5/22 javascript 50 豆豆
生活随笔 收集整理的這篇文章主要介紹了 SpringMVC底层数据传输校验的方案(修改版) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

團隊的項目正常運行了很久,但近期偶爾會出現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;

}

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

@Override

protected 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

@Autowired

RestTemplate restTemplate;

3.6 數據接收方項目設置

在SpringBootApplication類中定義

@Bean

public 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 出錯重傳機制

在數據接收端,當數據校驗失敗時,會拋出一個RuntimeException異常(如果要做到產品,當然應該自定義一個高大上的Exception)。

4.1 服務器端隨機模擬傳輸失敗

為了模擬測試,在接收方的代碼中,增加隨機失敗的情況。見下面代碼中黑體字部分,大約10%的概率會失敗。

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");

??}

? if (random.nextInt(100) < 10) {

??? logger.info("random throw exception");

??? throw new RuntimeException("content be modified");

? }

? return json;

}

?

4.2 發送方Catch異常重傳

當接收端拋異常后,最終會發送一個500錯誤到數據發送方。

org.springframework.web.client.HttpServerErrorException: 500 Internal Server Error

最簡單的處理方式,在發送方校驗是否發生了 500 錯誤,如果發生了就重傳。這個方案的代碼如下:

ResponseEntity<T> responseEntity = null;

int times = 0;

while (times < 5) {

? try {

??? responseEntity = super.postForEntity(url,

?????? ? newRequest, responseType, uriVariables);

??? break;

? } catch (Exception e) {

??? if (e instanceof HttpServerErrorException) {

????? times++;

????? logger.error("post for entity", e);

????? logger.error("resend the {}'st times", times);

??? } else {

????? break;

??? }

? }

}

當傳輸錯誤后,圖示代碼會最多嘗試發送五次。仍然失敗后考慮拋異常,由發送端上層代碼處理。

但這個代碼有一個很明顯的問題,接收端的任何錯誤如數據保存失敗,都會導致發送端重傳數據。下面讀一下Spring的代碼,看看是如何處理異常的。

4.3 SpringMVC異常處理

4.3.1 第一層處理

在類AbstractMessageConverterMethodArgumentResolver的readWithMessageConverters()方法中,會Catch IOException,相關代碼為

catch (IOException ex) {

? throw new HttpMessageNotReadableException(

??? "Could not read document: " + ex.getMessage(), ex);

}

HttpMessageNotReadableException是繼承自RuntimeException的一個異常。

4.3.2 第二層處理

在類InvocableHandlerMethod的getMethodArgumentValues()方法,Catch Exception打印一下日志,然后繼續throw。

try {

? args[i] = this.argumentResolvers.resolveArgument(

??? parameter, mavContainer, request, this.dataBinderFactory);

? continue;

}

catch (Exception ex) {

? if (logger.isDebugEnabled()) {

??? logger.debug(getArgumentResolutionErrorMessage("Failed to resolve", i)

?????? ? , ex);

? }

? throw ex;

}

?

4.3.3 第三層處理

在類org.springframework.web.servlet.DispatcherServlet.doDispatch()分別捕獲了兩種異常,代碼如下

catch (Exception ex) {

? dispatchException = ex;

}

catch (Throwable err) {

? dispatchException = new NestedServletException(

"Handler dispatch failed", err);

}

processDispatchResult(processedRequest, response,

? mappedHandler, mv, dispatchException);

可以看到,如果拋出的Exception異常,會將原異常直接處理,如果是Runtime Exception,會轉換成繼承自ServletException的異常NestedServletException。

4.3.4 處理異常

在 processDispatchResult() 方法中,異常處理核心代碼

if (exception instanceof ModelAndViewDefiningException) {

? logger.debug("ModelAndViewDefiningException encountered", exception);

? mv = ((ModelAndViewDefiningException) exception).getModelAndView();

}

else {

? Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);

? mv = processHandlerException(request, response, handler, exception);

? errorView = (mv != null);

}

我們拋出的異常,明顯不是 ModelAndViewDefiningException,所以會交由processHandlerException處理。看看它的代碼

ModelAndView exMv = null;

for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {

? exMv =resolver.resolveException(request, response, handler, ex);

? if (exMv != null) {

??? break;

? }

}

…(如果exMv不為空,會單獨處理)

throw ex;

可以看到,這部分代碼如果沒有處理,會繼續拋出異常,回到 processDispatchResult()

catch (Exception ex) {

?triggerAfterCompletion(processedRequest, response, mappedHandler, ex);

}

呃,太復雜,先不往下看了。因為我們需要區分是數據傳輸錯誤還是其他錯誤,可以考慮數據出錯時拋異常,不拋普通的RuntimeException,而是HttpMessageNotReadableException,看看數據發送端會有什么變化。

4.3.4 數據接收方拋新異常

修改了數據接收方代碼中拋出異常HttpMessageNotReadableException

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 HttpMessageNotReadableException("content is modified");

????? }

??? } catch (Exception e) {

??? }

? } else {

??? logger.info("may not be digested json");

??}

? // 調試用,后續刪掉

? if (random.nextInt(15) < 10) {

??? logger.info("random throw exception");

??? throw new HttpMessageNotReadableException("content be modified");

? }

? return json;

}

?

4.3.5 數據發送端修改代碼

RestClientException transferException = null;

ResponseEntity<T> responseEntity = null;

int times = 0;

while (times < 5) {

? try {

??? responseEntity = super.postForEntity(url,

?????? ? newRequest, responseType, uriVariables);

??? transferException = null;

??? break;

? } catch (RestClientException e) {

??? transferException = e;

??? boolean transferError = false;

??? if (e instanceof HttpClientErrorException) {

????? HttpClientErrorException clientError =

?????? ??? (HttpClientErrorException) e;

????? transferError = clientError.getRawStatusCode() == 400;

??? }

??? if (transferError) {

????? times++;

????? logger.error("post for entity", e);

????? logger.error("resend the {}'st times", times);

??? } else {

????? break;

??? }

? }

}

if(transferException != null){

? throw transferException;

}

return responseEntity;

如果返回的是400錯誤,發送方會嘗試共發送5次;如果是其他異常或5次都不成功,則拋出異常。

5 后記

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

補充:第一版發布后,同學們很關心如何重傳的問題。對這個也做了一些測試,補充到文檔中。如果是數據傳輸錯誤,會嘗試共傳輸5次;如果仍然不成功則拋出異常由上層代碼處理。

?

轉載于:https://www.cnblogs.com/codestory/p/6761800.html

總結

以上是生活随笔為你收集整理的SpringMVC底层数据传输校验的方案(修改版)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。