团队的项目正常运行了很久,但近期偶尔会出现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次;如果仍然不成功则抛出异常由上层代码处理。

SpringMVC底层数据传输校验的方案(修改版)的更多相关文章

  1. SpringMVC底层数据传输校验的方案

    团队的项目正常运行了很久,但近期偶尔会出现BUG.目前观察到的有两种场景:一是大批量提交业务请求,二是生成批量导出文件.出错后,再执行一次就又正常了. 经过跟踪日志,发现是在Server之间进行jso ...

  2. Indy 10.5.8 for Delphi and Lazarus 修改版(2011)

    Indy 10.5.8 for Delphi and Lazarus 修改版(2011)    Internet Direct(Indy)是一组开放源代码的Internet组件,涵盖了几乎所有流行的I ...

  3. springmvc的数据校验

       springmvc的数据校验 在Web应用程序中,为了防止客户端传来的数据引发程序异常,常常需要对数据进行验证,输入验证分为客户端验证与服务器端验证. 客户端验证主要通过javaScript脚本 ...

  4. Medoo个人修改版

    Medoo是一款轻量级的php数据库操作类,下面不会介绍Medoo的使用方法,想学习Medoo请前往官网自学:http://medoo.in/ 在接触Medoo之前,一直是用自己写的php数据库操作类 ...

  5. Android 仿美团网,大众点评购买框悬浮效果之修改版

    转帖请注明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/17761431),请尊重他人的辛勤劳动成果,谢谢! 我之前写 ...

  6. 黄聪:WordPress图片插件:Auto Highslide修改版(转)

    一直以来很多人都很喜欢我博客使用的图片插件,因为我用的跟原版是有些不同的,效果比原版的要好,他有白色遮罩层,可以直观的知道上下翻图片和幻灯片放映模式.很多人使用原版之后发现我用的更加帅一些,于是很多人 ...

  7. sqm(sqlmapGUI) pcat修改版

    sqlmap是一款开源的注入工具,支持几乎所有的数据库,支持get/post/cookie注入,支持错误回显注入/盲注,还有其他多种注入方法. 支持代理,指纹识别技术判断数据库 .而sqm(sqlma ...

  8. 转载:Eclipse+Spket插件+ExtJs4修改版提供代码提示功能[图]

    转载:Eclipse+Spket插件+ExtJs4修改版提供代码提示功能[图] ExtJs是一种主要用于创建前端用户界面,是一个基本与后台技术无关的前端ajax框架.功能丰富,无人能出其右.无论是界面 ...

  9. springmvc(四) springmvc的数据校验的实现

    so easy~ --WH 一.什么是数据校验? 这个比较好理解,就是用来验证客户输入的数据是否合法,比如客户登录时,用户名不能为空,或者不能超出指定长度等要求,这就叫做数据校验. 数据校验分为客户端 ...

随机推荐

  1. 阿里云Https部署网站

    0.开始之前 文章图片很多,注意流量 首先你得准备好一个已经备案成功的域名,并且有一个在阿里云的服务器部署了的网站. 然后就是你迫切的希望升级网站为HTTPS部署. 那么我们开始吧! 1.申请CA证书 ...

  2. 【微服务】之二:从零开始,轻松搞定SpringCloud微服务系列--注册中心(一)

    微服务体系,有效解决项目庞大.互相依赖的问题.目前SpringCloud体系有强大的一整套针对微服务的解决方案.本文中,重点对微服务体系中的服务发现注册中心进行详细说明.本篇中的注册中心,采用Netf ...

  3. 分布式监控系统Zabbix3.2给异常添加邮件报警

    在前一篇 分布式监控系统Zabbix3.2跳坑指南 中已安装好服务端和客户端,此处客户端是被监控的服务器,可能有上百台服务器.监控的目的一个是可以查看历史状态,可以对比零晨和工作区间数据的对比,以便后 ...

  4. shell,bash,zsh,console,terminal到底是什么意思,它们之间又是什么关系?

    原文链接 终端(terminal,或者叫物理终端):是一种设备,不是一个程序,一般说的就是能提供命令行用户界面的设备,典型的是屏幕和键盘,或其他的一些物理终端.虚拟终端:屏幕和键盘只是一个终端,可能不 ...

  5. Git命令汇总(补充篇)

    上一篇<Git命令汇总基础篇>总结了使用Git的基本命令,这一篇作为补充主要给大家讲一些平时使用中的技巧和总结 . 学会了这些命令,已经基本解决了使用Git中大部分问题. 1.gitign ...

  6. Linux 网络编程中的read和write函数正确的使用方式

    字节流套接字上的read和write函数所表现的行为不同于通常的文件IO,字节流套接字上调用read和write输入或输出的可能比请求的数量少,然而这不是出错的状态,例如某个中端使read和write ...

  7. 基于 Vue.js 的移动端组件库mint-ui实现无限滚动加载更多

    通过多次爬坑,发现了这些监听滚动来加载更多的组件的共同点, 因为这些加载更多的方法是绑定在需要加载更多的内容的元素上的, 所以是进入页面则直接触发一次,当监听到滚动事件之后,继续加载更多, 所以对于无 ...

  8. JPA学习笔记(8)——映射双向一对多关联关系

    双向一对多关联关系 前面的博客讲的都是单向的,而本问讲的是双向的(双向一对多 = 双向多对一) 什么是双向? 我们来对照一下单向和双向 单向/双向 User实体类中是否有List< Order& ...

  9. #include、#import与@class的使用与头文件循环引用问题

    #include #include <>:一般是对系统库文件的引用,编译器会去系统文件文件夹下查找. #include "xxx.h":一般是对自己定义文件的引用,编译 ...

  10. 002Java概述

    1Sun(Stanford University Network )公司1995年推出的高级编程语言 2.面向Internet的编程语言 3.已经成为web应用程序的首选开发语言 4.完全面向对象简单 ...