【前提】
js通过ajax提交数据到后台,一旦数据量大了,就会出现问题,比如:
【分析】
对于这个问题,目前场景中,数据的传输量在5M到20M之间,先通过js拼接成一个大的json串,之后在通过ajax发送post请求到达后端。
局部代码展示:
1.js
function submitForm() { layer.load(1, {shade: [0.3]}); var stringJson=JSON.stringify(paramsBuilder()); //将数据放到json中 var formData = new FormData(); formData.append("params",stringJson); formData.append("type",2); $.ajax({ url: CAR_PATH + "/vincent/test_1.do", type: "POST", data:formData, // 告诉jQuery不要去处理发送的数据 processData : false, // 告诉jQuery不要去设置Content-Type请求头 contentType : false, success: function (result) { layer.closeAll(); if (result && result.success) { closeWin(); window.opener.location.reload(); } else { alert(result.messages[0]); } }, error: function (result) { layer.closeAll(); alert("系统异常,请稍后重试!"); } }); }
2.controller
@RequestMapping(value = "/test_1", method = RequestMethod.POST) @ResponseBody public Message editProductConfig(HttpServletRequest request,String params,Integer type) { LOGGER.error("上传文件限制的大小: "+ multipartResolver.getFileUpload().getSizeMax()); LOGGER.error("入参打印到控制台: " + params); final Object logsId = LogUtils.initLogsId(); final String desc = ""; Message message = new Message(); try { if (StringUtils.isBlank(params)) { message.setSuccess(false); message.getMessages().add("非法调用!"); return message; } ProductConfigForm form = fromJson(params, ProductConfigForm.class); //将入参从json反解析成实体 //and so on ~逻辑 } }
问题可能出现在两个方向:
一、json数据拼接太过耗时间和浏览器性能,导致浏览器直接超时甚至卡死。
验证思路:断点跟前端js代码,看是否浏览器的内存卡死是死在多个for循环的拼接过程。
二、json数据量太大,ajax不能将这么大的数据传输到后台
验证思路:后台controller出打断点,通过Log输出param入参。
解决思路:如果是这个原因导致,可以从如下几个角度来思考解决
1)pako_deflate.js 压缩请求内容(注意点:什么压缩 什么时候不要缩) 2)数据拆分 多次请求 3)拼装数据 后移 4)稀疏矩阵 5)流传输 6)验证是否运维方面的配置导致
【验证】
1. js拼接json导致浏览器卡死
这一步确实有问题,解决方案请参考我的下一篇博客:(后续放入链接)
2. 数据传输大小限制(看到这里,有个前提,js拼接过程出的问题已经解决)
(1)在controller设置断点,发现param的值为null,此时的数据量在1M~2M之间。
很明显,post请求并没有将入参传到controller中,此时需要查看tomcat/conf/server.xml下的配置:
<Connector Executor="tomcatThreadPool" URIEncoding="UTF-8" acceptCount="800" compressableMimeType="text/html,text/xml,text/javascript,text/css,text/plain,application/msexcel" compression="on" compressionMinSize="2048" connectionTimeout="60000" disableUploadTimeout="true" enableLookups="false" maxHttpHeaderSize="8192" maxSpareThreads="75" maxThreads="800" minSpareThreads="25" port="80" protocol="HTTP/1.1" redirectPort="8443" maxPostSize="31457280"/>
将其中的maxPostSize值设置为可能传输到controller的最大数据量,这里我设置了30Mb(31457280)。
查资料,采取了通过FormData()的方式包装数据到controller:https://segmentfault.com/a/1190000012327982 (因为我们将一个大的Form表单在js中拆分了多个Form表单,具体参考我的下一篇博客(后续放入链接))。
之后发现,数据量小于5M的情况下,可以将json数据传输到controller当中。
(2)当数据量继续增加的时候,新的问题出现了,如下:
[ERROR]: 系统异常: org.springframework.web.multipart.MaxUploadSizeExceededException: Maximum upload size of 5242880 bytes exceeded; nested exception is org.apache.commons.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (10423705) exceeds the configured maximum (5242880)
这个错误是SpringMVC报出来的,时间点在进入Controller之前,因为我打的logger查看param入参都没有执行。之后跟了源码,有几个类值得注意:
1.FileUploadBase.java
SpringMVC控制文件传输到controller数据量的set方法,此类为基类,在他下面有一些子类丰富该功能。
public void setSizeMax(long sizeMax) { this.sizeMax = sizeMax; //设置sizeMax的值,Spring允许传输到后台的最大数据量 }
抛出我遇到异常的方法:在这里比较了允许的最大值,和实际的数据量
FileItemIteratorImpl(RequestContext ctx) throws FileUploadException, IOException { if (ctx == null) { throw new NullPointerException("ctx parameter"); } String contentType = ctx.getContentType(); if ((null == contentType) || (!contentType.toLowerCase().startsWith(MULTIPART))) { throw new InvalidContentTypeException( "the request doesn't contain a " + MULTIPART_FORM_DATA + " or " + MULTIPART_MIXED + " stream, content type header is " + contentType); } InputStream input = ctx.getInputStream(); if (sizeMax >= 0) { int requestSize = ctx.getContentLength(); if (requestSize == -1) { input = new LimitedInputStream(input, sizeMax) { protected void raiseError(long pSizeMax, long pCount) throws IOException { FileUploadException ex = new SizeLimitExceededException( "the request was rejected because" + " its size (" + pCount + ") exceeds the configured maximum" + " (" + pSizeMax + ")", pCount, pSizeMax); throw new FileUploadIOException(ex); } }; } else { if (sizeMax >= 0 && requestSize > sizeMax) { throw new SizeLimitExceededException( "the request was rejected because its size (" + requestSize + ") exceeds the configured maximum (" + sizeMax + ")", requestSize, sizeMax); } } } }
2.CommonsFileUploadSupport.java
对第1个类的适配类,参考方法:
public void setMaxUploadSize(long maxUploadSize) { this.fileUpload.setSizeMax(maxUploadSize); }
3.CommonsMultipartResolver.java
元素为多个时候,对servlet进行请求和解析。
protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException { String encoding = determineEncoding(request); FileUpload fileUpload = prepareFileUpload(encoding); try { List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request); return parseFileItems(fileItems, encoding); } catch (FileUploadBase.SizeLimitExceededException ex) { throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex); } catch (FileUploadException ex) { throw new MultipartException("Could not parse multipart servlet request", ex); } }
主要在上述3个方法中设置断点,通过比较发现每次被限制的大小为5242880(5M),之后在web.xml中发现了它被spring初始化过程扫描,于是重新写一个xml,在<servlet>的<param-value>中覆盖之前的值:
classpath*:maxuploadsizeContext.xml
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize" value="52428800"/> <property name="defaultEncoding" value="UTF-8"/> </bean>
设置maxUploadSize值大小为50Mb,再次启动,问题解决。
(3)本地测试没有问题之后,部署预生产环境,发现问题重现,考虑出现原因,client端发送请求到预生产tomcat服务器,期间经过nginx代理服务器,可能在这里被做了拦截,经过排查,发现:
nginx报错:upstream time out(110: Connection timed out) while reading response header from upstream.
很明显,nginx响应超时了,说明nginx代理对json数据也做了处理,另外需要排查nginx是否有限制数据量大小,结果发现而且都做了限制,修改如下图所示:
重置在代理服务器上的响应时间,以及nginx允许的最大传输量之后,该问题解决。
That's all,期待下一篇分享js拼接json数据太慢,占内存的解决方案。