巧用jax-rs之jersey实现不确定数量、多类型文件批量一次上传接口

大家都知道我们在处理网络请求的时候一般分为两种:

  1. 普通表单
  2. 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;

  }

  @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'>
<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)

  @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");
<FormDataBodyPart> l= form.getFields("file");

接下来,按道理 我们传了2个文件,就算是100个name是file的文件,我们都能够获取:

for (FormDataBodyPart p : l) {
      InputStream is=p.getValueAs(InputStream.class);
      FormDataContentDisposition detail=p.getFormDataContentDisposition();
 (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);
"res"+ File.separator
          + System.currentTimeMillis() + "."
          + FileUtil.getEndWith(detail.getFileName());
      File file=writeToFile(is, fileLocation);

好了,我们这个无敌万能的文件上传接口就做好了,他不仅可以接受不确定数量的文件,而且文件格式也来者不拒,都能分开处理,只需要对这个type进行判断了,如果是图片/音频则分开存放。

最后我们来写一个简单的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" οnclick="addMore();">不够!我还要添加图片</label>
              <p>
                <label class="required" for="file">语音:</label><br/>
                <input type="file" id="file" class="half" value='' name="file"/>
              </p>
   
            </fieldset>

          </form>


            <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" οnclick="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);
}
 addMore() {
  var html=$("#files").html();
  var html2=$("#addition").html();
  html2+=html;
  $("#addition").html(html2);
}

效果:


这里的不管你添加多少张文件,他的name都是"file",并且我们后台都可以通过一个枚举遍历里面的每一个文件~

看看最后发布成功的效果:

两张图片的

不定数量文件的(这里音频文件不好显示出来,所以都用的图片,但是原则上肯定是所有格式都支持的)

 

发布了100 篇原创文章 · 获赞 300 · 访问量 68万+

猜你喜欢

转载自blog.csdn.net/win7system/article/details/51120871