因为也是第一次写上传文件代码,所在工程也没有现场的代码可以copy,就开始了面向搜索的编程。
网上的教程的写法一般都是,前端form表单,然后把表单取出后放在FormData对象中,ajax的写法如下
1 <script type="text/javascript">
2 $(function () {
3 $("input[type='button']").click(function () {
4 var formData = new FormData($("#upForm")[0]);
5 $.ajax({
6 type: "post",
7 url: "${pageContext.request.contextPath}/upfile/upload",
8 data: formData,
9 cache: false,
10 processData: false,
11 contentType: false,
12 success: function (data) {
13 alert(data);
14 },
15 error: function (response) {
16 console.log(response);
17 alert("上传失败");
18 }
19 });
20 });
21 });
22 </script>
23 <body>
24 <form id="upForm" method="post" enctype="multipart/form-data">
25 用户名:<input type="text" name="userName" id="userName" /><br/>
26 密码:<input type="password" name="pwd" id="pwd" /><br/>
27 <input type="file" name="image"><br/>
28 <input type="button" value="提交" />
29 </form>
30 </body>
31 </html>
SpringMVC的配置文件,也就是把apache的两个Jar文件集成进来
1 <!-- 定义文件上传解析器 -->
2 <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
3 <property name="defaultEncoding">
4 <value>UTF-8</value>
5 </property>
6 <property name="maxUploadSize">
7 <value>32505856</value><!-- 上传文件大小限制为31M,31*1024*1024 -->
8 </property>
9 <property name="maxInMemorySize">
10 <value>4096</value>
11 </property>
12 </bean>
后台的Controller
1 @Controller
2 @RequestMapping("/upfile")
3 public class UpFileController {
4 @RequestMapping("/upload")
5 @ResponseBody
6 public String getMsg(UserTest user,@RequestParam("image") CommonsMultipartFile file){
7 System.out.println(user.getUserName());
8 System.out.println(file.getOriginalFilename());
9 return "接收成功";
10 }
11 }
通过搜索引擎换关键词看了好几篇博文,都类似这种写法,但我运行之后都是抛出500,或者400异常,500异常后台抛出org.springframework.web.multipart.MultipartException: The current request is not a multipart request,400的话因为没有开启DEBUG级别的日志,但知道是参数不匹配的问题,我就把参数限定为只需要file这个文件,之后就一直是抛出500错误。
开始排错之旅,一开始自然是继续搜,但发现网上的都是千篇一律,写法都一样,那没办法了,只能找差异,然后各种尝试,比如改formData封装的写法,改UpFileController的注解、参数,改CommonsMultipartResolver的参数。像一只无头苍蝇。
没办法了,请教第一个高级程序员同事,他一来就让我用PostMan调一下,Body填form-data,填好key和选择文件,成功打到Controller的方法内了,那就确认了后台代码的逻辑是正确的,那我就通过fiddler比对成功和失败两次请求的报文,方法、路径、协议版本号自然是一样的;headers里面的差异是content-type,
失败的也就是通过Ajax调用的,为false,multipart/form-data;boundary--xxx;成功的为form-data/data;boundary--xxx;Body里面,因为是multipart/form-data,消息体里面就是form表单里面的参数,文件流本身的也有个content-type;成功的为如下图所示为text/x-java-source,失败的文件流类型为application/octet-stream。对比出差异了,那现在就想着怎么让Ajax发出的报文和正确的一致了。
一开始又走错了,一直搜Ajax上传文件,给到写法也是一样的,然后又是走了老路,看了FormDate这个API,并没有设置content-type的地方;又尝试了把Ajax的设置中,contentType: false这个属性的注释,改成multipart/form-data,但报文依旧无法和成功的一致。这时候又开始折腾后台的参数了,希望后台可以正常解析这个报文(注,我也没好好看看抛出异常的这行代码,没有看到Spring判断content-type是用的startWith),自然还是失败了。
继续请教同事,前端的同事让我不要老是看别人怎么写的,要看官方的API,但是也看了下我的写法,感觉也没有不对,就走了;继续请求大佬,也没看出有什么问题,因为这个时候刚下班,同事就都回去了。我又接原地打转,又尝试了一个半小时,还是没有解决,就是吃点东西散散步了。
回来坐了回,有个朋友过来看我还在加班,自然他也刚加班完,我就抓住他,把问题复述了一边,发现可以通过postman,设置文件的类型,就把mutilpart/form-data中文件这个值的content-type也设置为了application/octet-stream,但postman的请求还是成功了;继续比对报文,发现是headers里面的content-type还是为false,multipart/form-data;boundary--xxx;这个时候我就去看了眼spring抛出异常的地方,发现spring限制了报文必须为multipart开头。
1 private void assertIsMultipartRequest(HttpServletRequest request) {
2 String contentType = request.getContentType();
3 if (contentType == null || !contentType.toLowerCase().startsWith("multipart/")) {
4 throw new MultipartException("The current request is not a multipart request");
5 }
6 }
这个时候也知道了是Ajax的问题,想起前端同事说的,我开始去jQuery看Ajax的文档,如下
contentType (default: 'application/x-www-form-urlencoded; charset=UTF-8')
Type: Boolean or String
When sending data to the server, use this content type. Default is "application/x-www-form-urlencoded; charset=UTF-8", which is fine for most cases.
If you explicitly pass in a content-type to $.ajax(), then it is always sent to the server (even if no data is sent).
As of jQuery 1.6 you can pass false to tell jQuery to not set any content type header.
Note: The W3C XMLHttpRequest specification dictates that the charset is always UTF-8; specifying another charset will not force the browser to change the encoding.
Note: For cross-domain requests, setting the content type to anything other than application/x-www-form-urlencoded, multipart/form-data, or text/plain will trigger the browser to send a preflight OPTIONS request to the server.
很明显,需要1.6的以上,我看了下工程用的版本是1.4的,复制个其他项目高版本的Jquery过来,报文headers不是false,multipart/form-data;boundary--xxx而是正确的multipart/form-data;boundary--xxx了。
总结如下:
-
尝试不同的调用方式,如果一开始就使用了Postman,就能确定后台代码的正确了
-
确定环境一致,要确定copy别人代码时,确保和别人,使用的浏览器,框架的版本是否大致一致
-
查看框架抛出的错误栈代码;虽然网上也说了是multipart/form-data的问题,但一开始以为只要content-type包含了multipart/form-data即可
-
查看API的相关文档;虽然通过Ajax关键词搜索,网上都是现成的写法,但作为一个API,方法和参数还是有个迭代的过程,网上都说要用false设置Ajax的contenttype属性,却没有说明这个参数的要求的版本
-
系统学习理论只是,如果对HTTP协议理论了解更深入点,就知道,是一个非法的符号(在几次尝试中Spring也抛出了这个异常)
1 /** org.springframework.http.MediaType.checkToken(String)
2 * Checks the given token string for illegal characters, as defined in RFC 2616, section 2.2.
3 * @throws IllegalArgumentException in case of illegal characters
4 * @see <a href="http://tools.ietf.org/html/rfc2616#section-2.2">HTTP 1.1, section 2.2</a>
5 */
6 private void checkToken(String s) {
7 for (int i=0; i < s.length(); i++ ) {
8 char ch = s.charAt(i);
9 if (!TOKEN.get(ch)) {
10 throw new IllegalArgumentException("Invalid token character '" + ch + "' in token \"" + s + "\"");
11 }
12 }
13 }