SpringMVC底層資料轉送校正的方案(修改版),springmvc修改版

來源:互聯網
上載者:User

SpringMVC底層資料轉送校正的方案(修改版),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次;如果仍然不成功則拋出異常由上層代碼處理。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.