javascript
SpringMVC底层数据传输校验的方案(修改版)
團(tuán)隊(duì)的項(xiàng)目正常運(yùn)行了很久,但近期偶爾會(huì)出現(xiàn)BUG。目前觀察到的有兩種場(chǎng)景:一是大批量提交業(yè)務(wù)請(qǐng)求,二是生成批量導(dǎo)出文件。出錯(cuò)后,再執(zhí)行一次就又正常了。
經(jīng)過跟蹤日志,發(fā)現(xiàn)是在Server之間進(jìn)行json格式大數(shù)據(jù)量傳輸時(shí)會(huì)丟失部分字符,造成接收方拿到完整字符串后不能正確解析成json,因此報(bào)錯(cuò)。
同其他團(tuán)隊(duì)同事們溝通后發(fā)現(xiàn),不僅僅是我們項(xiàng)目有這個(gè)問題,我們不是一個(gè)人在戰(zhàn)斗。
1 問題現(xiàn)象
服務(wù)器之間使用http+json的數(shù)據(jù)傳輸方案,在傳輸過程中,一些json數(shù)據(jù)發(fā)生錯(cuò)誤,導(dǎo)致數(shù)據(jù)接收方解析json報(bào)錯(cuò),系統(tǒng)功能因此失敗。
下面截取了一小段真實(shí)數(shù)據(jù)錯(cuò)誤,在傳輸?shù)膉son中,有一個(gè)數(shù)據(jù)項(xiàng)是departmentIdList,其內(nèi)容時(shí)一個(gè)長(zhǎng)整型數(shù)組。
?
傳輸之前的數(shù)據(jù)為:
"departmentIdList" : [ 719, 721, 722, 723, 7367, 7369, 7371, 7373, 7375, 7377 ]
接收到的數(shù)據(jù)為:
"departmentIdlist" : [ 719, 721'373, 7375, 7377 ]
可以看到,這個(gè)錯(cuò)誤導(dǎo)致了兩個(gè)問題:
1、 json解析失敗
2、 丟失了一些有效數(shù)據(jù)
詳細(xì)檢查系統(tǒng)日志之后,這是偶發(fā)bug,并且只在傳輸數(shù)據(jù)較大時(shí)發(fā)生。
2 可選的解決方案
2.1 請(qǐng)架構(gòu)組協(xié)助解決
這是最直接的解決方案,因?yàn)槲覀冺?xiàng)目使用架構(gòu)組提供的環(huán)境,他們需要提供可靠的底層數(shù)據(jù)傳輸機(jī)制。
2.2 壓縮傳輸數(shù)據(jù)
因?yàn)閿?shù)據(jù)量大時(shí)容易發(fā)生,并且傳輸?shù)亩际瞧胀ㄎ谋?#xff0c;可以考慮對(duì)內(nèi)容進(jìn)行壓縮后傳輸。普通文件壓縮率也很高,壓縮后內(nèi)容長(zhǎng)度能做到原數(shù)據(jù)10%以內(nèi),極大減少傳輸出錯(cuò)的幾率。
2.3 對(duì)傳輸數(shù)據(jù)進(jìn)行MD5校驗(yàn)
將傳輸數(shù)據(jù)作為一個(gè)完整數(shù)據(jù)塊,傳輸之前先做一個(gè)md5摘要,并將原數(shù)據(jù)和摘要一并發(fā)送;接收方收到數(shù)據(jù)后,先進(jìn)行數(shù)據(jù)校驗(yàn)工作,校驗(yàn)成功后再進(jìn)行后續(xù)操作流程,如果不成功可以輔助重傳或直接報(bào)錯(cuò)等機(jī)制。
3 方案設(shè)計(jì)
為了徹底解決這個(gè)問題,設(shè)計(jì)了一個(gè)底層方案
3.1 設(shè)計(jì)原則
1、 適用類型:Spring MVC項(xiàng)目,數(shù)據(jù)發(fā)送方使用RestTemplate工具類,使用fastjson作為json工具類。
2、 數(shù)據(jù)校驗(yàn),使用MD5加密,當(dāng)然也可以配合數(shù)據(jù)壓縮機(jī)制,減少傳輸數(shù)據(jù)量。
3、 提供底層解決方案,不需要對(duì)系統(tǒng)代碼做大規(guī)模調(diào)整。
3.2 核心設(shè)計(jì)
?
數(shù)據(jù)發(fā)送方,重載RestTemplate,在數(shù)據(jù)傳輸之前對(duì)數(shù)據(jù)進(jìn)行md5摘要,并將原始數(shù)據(jù)和 md5摘要一并傳輸。
數(shù)據(jù)接收方,重載AbstractHttpMessageConverter,接收到數(shù)據(jù)后,對(duì)數(shù)據(jù)進(jìn)行MD5校驗(yàn)。
3.3 DigestRestTemplate關(guān)鍵代碼
對(duì)原json進(jìn)行摘要,并同原始數(shù)據(jù)一起生成一個(gè)新的json對(duì)象。
| 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函數(shù)核心部分,如果傳入?yún)?shù)是 JSONObject,則調(diào)用方法對(duì)數(shù)據(jù)進(jìn)行摘要操作,并用新生成的json進(jìn)行傳輸。
| 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 核心代碼
首先會(huì)判斷是否是經(jīng)過md5摘要的json,是有摘要的數(shù)據(jù)進(jìn)行校驗(yàn),否則直接返回對(duì)象。
| 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; } |
原有的處理數(shù)據(jù)代碼增加調(diào)用該方法的代碼
| @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; } |
當(dāng)前的代碼,如果數(shù)據(jù)校驗(yàn)失敗,簡(jiǎn)單拋出異常。后續(xù)可以增加更多的機(jī)制,比如在RestTemplate處增加校驗(yàn),如果發(fā)現(xiàn)校驗(yàn)失敗,則重傳。
3.5 數(shù)據(jù)發(fā)送方項(xiàng)目配置
以Spring Boot項(xiàng)目為例
在Main類中定義 restTemplate
| @Bean(name = "restTemplate") public RestTemplate getRestTemplate() { ? RestTemplate restTemplate = new DigestRestTemplate(); ? return restTemplate; } |
需要調(diào)用RestTemplate的代碼,只需要依賴注入RestTemplate
| @Autowired RestTemplate restTemplate; |
3.6 數(shù)據(jù)接收方項(xiàng)目設(shè)置
在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 出錯(cuò)重傳機(jī)制
在數(shù)據(jù)接收端,當(dāng)數(shù)據(jù)校驗(yàn)失敗時(shí),會(huì)拋出一個(gè)RuntimeException異常(如果要做到產(chǎn)品,當(dāng)然應(yīng)該自定義一個(gè)高大上的Exception)。
4.1 服務(wù)器端隨機(jī)模擬傳輸失敗
為了模擬測(cè)試,在接收方的代碼中,增加隨機(jī)失敗的情況。見下面代碼中黑體字部分,大約10%的概率會(huì)失敗。
| 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 發(fā)送方Catch異常重傳
當(dāng)接收端拋異常后,最終會(huì)發(fā)送一個(gè)500錯(cuò)誤到數(shù)據(jù)發(fā)送方。
| org.springframework.web.client.HttpServerErrorException: 500 Internal Server Error |
最簡(jiǎn)單的處理方式,在發(fā)送方校驗(yàn)是否發(fā)生了 500 錯(cuò)誤,如果發(fā)生了就重傳。這個(gè)方案的代碼如下:
| 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; ??? } ? } } |
當(dāng)傳輸錯(cuò)誤后,圖示代碼會(huì)最多嘗試發(fā)送五次。仍然失敗后考慮拋異常,由發(fā)送端上層代碼處理。
但這個(gè)代碼有一個(gè)很明顯的問題,接收端的任何錯(cuò)誤如數(shù)據(jù)保存失敗,都會(huì)導(dǎo)致發(fā)送端重傳數(shù)據(jù)。下面讀一下Spring的代碼,看看是如何處理異常的。
4.3 SpringMVC異常處理
4.3.1 第一層處理
在類AbstractMessageConverterMethodArgumentResolver的readWithMessageConverters()方法中,會(huì)Catch IOException,相關(guān)代碼為
| catch (IOException ex) { ? throw new HttpMessageNotReadableException( ??? "Could not read document: " + ex.getMessage(), ex); } |
HttpMessageNotReadableException是繼承自RuntimeException的一個(gè)異常。
4.3.2 第二層處理
在類InvocableHandlerMethod的getMethodArgumentValues()方法,Catch Exception打印一下日志,然后繼續(xù)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異常,會(huì)將原異常直接處理,如果是Runtime Exception,會(huì)轉(zhuǎn)換成繼承自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,所以會(huì)交由processHandlerException處理。看看它的代碼
| ModelAndView exMv = null; for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) { ? exMv =resolver.resolveException(request, response, handler, ex); ? if (exMv != null) { ??? break; ? } } …(如果exMv不為空,會(huì)單獨(dú)處理) throw ex; |
可以看到,這部分代碼如果沒有處理,會(huì)繼續(xù)拋出異常,回到 processDispatchResult()
| catch (Exception ex) { ?triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } |
呃,太復(fù)雜,先不往下看了。因?yàn)槲覀冃枰獏^(qū)分是數(shù)據(jù)傳輸錯(cuò)誤還是其他錯(cuò)誤,可以考慮數(shù)據(jù)出錯(cuò)時(shí)拋異常,不拋普通的RuntimeException,而是HttpMessageNotReadableException,看看數(shù)據(jù)發(fā)送端會(huì)有什么變化。
4.3.4 數(shù)據(jù)接收方拋新異常
修改了數(shù)據(jù)接收方代碼中拋出異常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"); ??} ? // 調(diào)試用,后續(xù)刪掉 ? if (random.nextInt(15) < 10) { ??? logger.info("random throw exception"); ??? throw new HttpMessageNotReadableException("content be modified"); ? } ? return json; } |
?
4.3.5 數(shù)據(jù)發(fā)送端修改代碼
| 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錯(cuò)誤,發(fā)送方會(huì)嘗試共發(fā)送5次;如果是其他異常或5次都不成功,則拋出異常。
5 后記
經(jīng)過測(cè)試,這個(gè)方案是可行的。如果為了能夠適應(yīng)更多的項(xiàng)目及更多的Java技術(shù)棧,需要對(duì)代碼進(jìn)行進(jìn)一步完善。
補(bǔ)充:第一版發(fā)布后,同學(xué)們很關(guān)心如何重傳的問題。對(duì)這個(gè)也做了一些測(cè)試,補(bǔ)充到文檔中。如果是數(shù)據(jù)傳輸錯(cuò)誤,會(huì)嘗試共傳輸5次;如果仍然不成功則拋出異常由上層代碼處理。
?
轉(zhuǎn)載于:https://www.cnblogs.com/codestory/p/6761800.html
總結(jié)
以上是生活随笔為你收集整理的SpringMVC底层数据传输校验的方案(修改版)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何在惠州市麦地路附件找到能学俄语的地方
- 下一篇: 精通JavaScript--01面向对象