javascript
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底层数据传输校验的方案(修改版)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何在惠州市麦地路附件找到能学俄语的地方
- 下一篇: 整理前端css/js/jq常见问题及解决