好久没更新博客啦,跟各位博友说声抱歉~~~今天这篇博文的标题不知道是否有些拗口,如果是 那请容许我解释下为何有这篇博文:
大家都知道我们在处理网络请求的时候一般分为两种:
- 普通表单
- multipart/formdata表单
这两种表单在html上的区别很直接,前者不需要修饰,后者需要enctype="multipart/form-data" 这一个属性来修饰所在的html。
但是如果我们的html中的表单提交被js(jquery)所代劳了,那么jquery内部是很聪明的,即使你没有用multipart/form-data去修饰,只要有文件他在ajax提交的时候会自己帮你转换。
而在我们的ios/android基本上道理相同,如果你使用了较为成熟的框架,他会自动检测你的提交数据中是否有文件流而帮你转换格式,例如我们的ASIHttpRequest就会聪明的判断,使服务器端更容易处理。
===================分割线==========================
想必使用JAX-RS中的jersey实现来处理单文件上传的接口,这个太过于简单了,这里就不仔细说了,相信网上一搜一大片。
这里来个标准的实现:
//创建新专题 @POST @Path("createSubject.do") @Produces("application/json;charset=UTF-8") @Consumes(MediaType.MULTIPART_FORM_DATA) public String createSubject(@FormDataParam("pic") InputStream imageInputStream, @FormDataParam("pic") FormDataContentDisposition imageDetail) { //获取工程根目录 String rootPath=new File("").getAbsolutePath(); rootPath+= File.separator+"webapps"; //拼接文件目录 String imageFileLocation = rootPath + File.separator+"res"+ File.separator + System.currentTimeMillis() + "." + FileUtil.getEndWith(imageDetail.getFileName()); File image=writeToFile(imageInputStream, imageFileLocation); SimpleJSONObject res=new SimpleJSONObject(); res.add("status", 1); res.add("msg", "创建专题成功"); return res.toString(); } public static File writeToFile(InputStream is, String uploadedFileLocation) { // TODO Auto-generated method stub File file = new File(uploadedFileLocation); OutputStream os = null; try { os = new FileOutputStream(file); byte buffer[] = new byte[4 * 1024]; while ((is.read(buffer)) != -1) { os.write(buffer); } os.flush(); } catch (Exception e) { e.printStackTrace(); } finally { try { os.close(); } catch (Exception e) { e.printStackTrace(); } } System.out.println(uploadedFileLocation+"文件大小"+file.length()); if (file.length()<5) { file.delete(); return null; } return file; }
注意这里为什么说他一定是针对单个文件的上传呢,因为前文已经说了我们既然是上传文件,那么文件必定是以I/O流的形式来传输的,对服务器来讲,他铁定是InputStream了,而这里我们将InputStream 以WebService注解的形式作为参数声明到方法中,实际上无论怎样我们都只能获取一个InputStream,即一个文件。即使你上传了多个文件,我们也只会获取一个,如果你企图多次调用这个inputStream对象,那么会抛出该inputStream已经被关闭的异常。
所以本文的目的是在于探讨如果多快好省地实现多个、不定数量、多类型的文件上传策略。
众所周知有一个笨方法很多人都在用,那就是给我们这个接口制定多个文件参数,即:
除了你可以上传文件file之外,你还能上传file1,file2,file3,file4....理论上限是程序员的耐心和服务器的承受能力。如果这样做的话,我们这个方法中的参数需要复制N次,并且以这种加后缀的参数命名方式愚蠢地进行下去。
而我们如果想调用这个接口,我们的html页面还得这样写:
<input type=‘file‘ name=‘file‘><br> <input type=‘file‘ name=‘file1‘><br> <input type=‘file‘ name=‘file2‘><br> ... <input type=‘file‘ name=‘file100‘>
虽然效果是好的,但是我认为既然jersey这么简单易用,那他肯定有别的办法可以避免这么愚蠢的行为,于是在我的研究下得到了一个好的解决办法:
所有的文件提交参数名都保持一致,例如都叫"file",而我们在获取的时候,不再获取具体的某个inputStream,而是获取整个multipart 表单体。
什么是multipart表单体?如果有兴趣的朋友可以打开浏览器的调试工具,找到network这一栏,然后找个带文件的表单提交一下 看看他的报文头,例如我写的这一个表单:
我们提交他,看看报文头:
可以明显地看到 我们刚刚表单中的几个字段都已经以"WebKitFormBoundaryxxxxx"什么的分开了,而每一项正好对应着我们的input项。
大家可以试试不带文件的表单提交,报文头是否是有这个"WebKitFormBoundaryxxxxx",此处略去关于他的废话,关键来了,这里有我们可以看到他的文件名,文件类型,这是上面的text字段所没有的,这就是文件的特殊之处,当然更特殊的地方就在于他的inputStream了,我们在这里是看不到的。
402881e843a0279e0143a027edc70000
------WebKitFormBoundary8xwjqtqZ60aSNAIz
Content-Disposition: form-data; name="file"; filename="20130328010936284_easyicon_net_96.png"
Content-Type: image/png
------WebKitFormBoundary8xwjqtqZ60aSNAIz
Content-Disposition: form-data; name="file"; filename="Price.png"
Content-Type: image/png
不好意思由于截图工具不太好没截取完整,但是我们可以看到实际上2个file的name是一模一样的~而且他们都在报文中了,那么我们的jersey岂有只理会其中一个文件的道理?怎样把多个file的inputStream拿到手,就是此文的终极目标啦。
细心研究后发现,我们的multipartform被jersey划分为多个FormDataBodyPart,并且以一个List对象来存放,所以我们的策略就是直接获取这个multipart:
@POST @Path("addNewTopic.do") @Consumes(MediaType.MULTIPART_FORM_DATA) public String addNewTopic(@FormDataParam("apiKey") String apiKey, @FormDataParam("text") String text, @FormDataParam("subject") String subject, FormDataMultiPart form)就是最后一项了,这里我们没有用@来修饰他。
取到他之后,我们获取里面的每一个part:
List<FormDataBodyPart> l= form.getFields("file");
接下来,按道理 我们传了2个文件,就算是100个name是file的文件,我们都能够获取:
for (FormDataBodyPart p : l) { InputStream is=p.getValueAs(InputStream.class); FormDataContentDisposition detail=p.getFormDataContentDisposition();其实是我们把最简单的方法中用@注解的参数手动获取了,但是他们现在在循环体内了。
接下来,我们除了接受不定数量的文件,还要判断他的类型!
MediaType type=p.getMediaType();拿到它之后,他有个属性,getType()和getSubType分别获取出来是我们刚刚在报文头看到的image 和png,他们拼在一起就是MIME-Type image/png,这个东西我们手机端是可以手动修改了,ASI里面就有 ,大家记得吗?
接下来的操作如出一辙了:
String fileLocation = rootPath + File.separator+"res"+ File.separator + System.currentTimeMillis() + "." + FileUtil.getEndWith(detail.getFileName()); File file=writeToFile(is, fileLocation);
最后我们来写一个简单的html+js来测试这个接口:
html表单:
<form id="classForm" method="post" enctype="multipart/form-data" action="../api/createClass.do"> <fieldset> <legend>请尽量填写完整这些信息。</legend> <input type="text" hidden="hidden" id="apiKey" class="half" value=‘<%=me.getApiKey() %>‘ name="apiKey"/> <p> <label class="required" for="text">文字内容:</label><br/> <input type="text" id="text" class="half" value=‘‘ name="text"/> <small>例如:今天的课太无聊啦!</small> </p> <p> <label class="required" for="subject">专题:</label><br/> <select id="subject" name="subject" class="half"> <option >请选择专题</option> </select> </p> <div id="files"> <p> <label class="required" for="file">图片:</label><br/> <input type="file" id="file" class="half" value=‘‘ name="file"/> </p> </div> <div id="addition"></div> <label class="btn" onclick="addMore();">不够!我还要添加图片</label> <p> <label class="required" for="file">语音:</label><br/> <input type="file" id="file" class="half" value=‘‘ name="file"/> </p> </fieldset> </form>
js(这里用了下jquery,也可以直接用DOM):
function addMore() { var html=$("#files").html(); var html2=$("#addition").html(); html2+=html; $("#addition").html(html2); }
效果:
这里的不管你添加多少张文件,他的name都是"file",并且我们后台都可以通过一个枚举遍历里面的每一个文件~
看看最后发布成功的效果:
两张图片的
不定数量文件的(这里音频文件不好显示出来,所以都用的图片,但是原则上肯定是所有格式都支持的)
原创文章,版权所有,转载请注明转自墨半成霜的博客,谢谢~~