如何从带有 Jersey 的多部分表单中读取具有相同名称的多个(文件)输入?

2022-01-21 00:00:00 forms multipart java jax-rs jersey

我已经成功开发了一项服务,在该服务中,我读取了在泽西岛以多部分形式上传的文件.这是我一直在做的一个极其简化的版本:

I have successfully developed a service, in which I read files uploaded in a multipart form in Jersey. Here's an extremely simplified version of what I've been doing:

@POST
@Path("FileCollection")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadFile(@FormDataParam("file") InputStream uploadedInputStream,
        @FormDataParam("file") FormDataContentDisposition fileDetail) throws IOException {
    //handle the file
}

这很好用,但我得到了一个新的要求.除了我要上传的文件之外,我还必须处理任意数量的资源.假设这些是图像文件.

This works just fine but I've been given a new requirement. In addition to the file I'm uploading, I have to handle an arbitrary number of resources. Let's assume these are image files.

我想我只需为客户端提供一个表单,其中包含一个文件输入、一个输入第一个图像和一个允许向表单添加更多输入的按钮(使用 AJAX 或简单的 JavaScript).

I figured I'd just provide the client with a form with one input for the file, one input for the first image and a button to allow adding more inputs to the form (using AJAX or simply plain JavaScript).

<form action="blahblahblah" method="post" enctype="multipart/form-data">
   <input type="file" name="file" />
   <input type="file" name="image" />
   <input type="button" value="add another image" />
   <input type="submit"  />
</form>

因此用户可以在表单中附加更多图像输入,如下所示:

So the user can append the form with more inputs for images, like this:

<form action="blahblahblah" method="post" enctype="multipart/form-data">
   <input type="file" name="file" />
   <input type="file" name="image" />
   <input type="file" name="image" />
   <input type="file" name="image" />
   <input type="button" value="add another image" />
   <input type="submit"  />
</form>

我希望读取与集合同名的字段足够简单.我已经用 MVC .NET 中的文本输入成功地完成了它,我认为在泽西岛不会更难.原来我错了.

I hoped it would be simple enough to read the fields with the same name as a collection. I've done it successfully with text inputs in MVC .NET and I thought it wouldn't be harder in Jersey. It turns out I was wrong.

没有找到关于该主题的教程,我开始尝试.

Having found no tutorials on the subject, I started experimenting.

为了了解如何做,我将问题简化为简单的文本输入.

In order to see how to do it, I dumbed the problem down to simple text inputs.

<form action="blahblabhblah" method="post" enctype="multipart/form-data">
   <fieldset>
       <legend>Multiple inputs with the same name</legend>
       <input type="text" name="test" />
       <input type="text" name="test" />
       <input type="text" name="test" />
       <input type="text" name="test" />
       <input type="submit" value="Upload It" />
   </fieldset>
</form>

显然,我需要某种集合作为我的方法的参数.这是我尝试过的,按集合类型分组.

Obviously, I needed to have some sort of collection as a parameter to my method. Here's what I tried, grouped by collection type.

首先,我检查了 Jersey 是否足够聪明,可以处理一个简单的数组:

At first, I checked whether Jersey was smart enough to handle a simple array:

@POST
@Path("FileCollection")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadFile(@FormDataParam("test") String[] inputs) {
    //handle the request
}

但数组没有按预期注入.

but the array wasn't injected as expected.

在惨败之后,我记得 MultiValuedMap 对象可以开箱即用地处理.

Having failed miserably, I remembered that MultiValuedMap objects could be handled out of the box.

@POST
@Path("FileCollection")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadFile(MultiValuedMap<String, String> formData) {
    //handle the request
}

但它也不起作用.这一次,我遇到了异常

but it doesn't work either. This time, I got an exception

SEVERE: A message body reader for Java class javax.ws.rs.core.MultivaluedMap, 
and Java type javax.ws.rs.core.MultivaluedMap<java.lang.String, java.lang.String>, 
and MIME media type multipart/form-data; 
boundary=----WebKitFormBoundaryxgxeXiWk62fcLALU was not found.

我被告知可以通过包含 mimepull 库来消除此异常,因此我在我的 pom 中添加了以下依赖项:

I was told that this exception could be gotten rid of by including the mimepull library so I added the following dependency to my pom:

    <dependency>
        <groupId>org.jvnet</groupId>
        <artifactId>mimepull</artifactId>
        <version>1.3</version>
    </dependency>

不幸的是,问题仍然存在.这可能是选择正确的正文阅读器并为泛型使用不同参数的问题.我不知道该怎么做.我想同时使用文件和文本输入,以及其他一些输入(主要是 Long 值和自定义参数类).

Unfortunately the problem persists. It's probably a matter of choosing the right body reader and using different parameters for the generic. I'm not sure how to do this. I want to consume both file and text inputs, as well as some others (mostly Long values and custom parameter classes).

经过更多研究,我发现了 FormDataMultiPart 类.我已经成功地使用它从我的表单中提取字符串值

After some more research, I found the FormDataMultiPart class. I've successfully used it to extract the string values from my form

@POST
@Path("upload2")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadMultipart(FormDataMultiPart multiPart){
    List<FormDataBodyPart> fields = multiPart.getFields("test");
    System.out.println("Name	Value");
    for(FormDataBodyPart field : fields){
        System.out.println(field.getName() + "	" + field.getValue());
        //handle the values
    }
    //prepare the response
}

问题是,这是我的问题的简化版本的解决方案.虽然我知道 Jersey 注入的每个参数都是在某个时候通过解析字符串创建的(难怪,毕竟它是 HTTP)并且我有一些编写自己的参数类的经验,但我真的不知道如何将这些字段转换为InputStreamFile 实例用于进一步处理.

The problem is, this is a solution to the simplified version of my problem. While I know that every single parameter injected by Jersey is created by parsing a string at some point (no wonder, it's HTTP after all) and I have some experience writing my own parameter classes, I don't really how to convert these fields to InputStream or File instances for further processing.

因此,在深入研究 Jersey 源代码以了解这些对象是如何创建的之前,我决定在这里询问是否有更简单的方法来读取一组(未知大小的)文件.你知道如何解决这个难题吗?

Therefore, before diving into Jersey source code to see how these objects are created, I decided to ask here whether there is an easier way to read a set (of unknown size) of files. Do you know how to solve this conundrum?

推荐答案

我已经通过FormDataMultipart的例子找到了解决方案.事实证明我非常接近答案.

I have found the solution by following the example with FormDataMultipart. It turns out I was very close to the answer.

FormDataBodyPart 类提供了一种方法,该方法允许其用户将值作为 InputStream 读取(或者理论上,任何其他具有消息正文阅读器的类).

The FormDataBodyPart class provides a method that allows its user to read the value as InputStream (or theoretically, any other class, for which a message body reader is present).

这是最终的解决方案:

表格保持不变.我有几个同名的字段,我可以在其中放置文件.可以同时使用 multiple 表单输入(从目录上传许多文件时需要这些输入)和共享名称的大量输入(从不同位置上传未指定数量的文件的灵活方式).也可以使用 JavaScript 在表单中附加更多输入.

The form remains unchanged. I have a couple of fields with the same name, in which I can place files. It's possible to use both multiple form inputs (you want these when uploading many files from a directory) and numerous inputs that share a name (Flexible way to upload an unspecified number of files from different location). It's also possible to append the form with more inputs using JavaScript.

<form action="/files" method="post" enctype="multipart/form-data">
   <fieldset>
       <legend>Multiple inputs with the same name</legend>
       <input type="file" name="test" multiple="multiple"/>
       <input type="file" name="test" />
       <input type="file" name="test" />
   </fieldset>
   <input type="submit" value="Upload It" />
</form>

服务 - 使用 FormDataMultipart

这是一种从多部分表单中读取文件集合的简化方法.所有相同的输入都分配给一个 List 并且它们的值使用 InputStreamapidocs/1.1.4/contribs/jersey-multipart/com/sun/jersey/multipart/FormDataBodyPart.html#getValueAs%28java.lang.Class%29">getValueAs 方法 FormDataBodyPart.一旦将这些文件作为 InputStream 实例,就可以轻松地用它们做几乎任何事情.

Service - using FormDataMultipart

Here's a simplified method that reads a collection of files from a multipart form. All inputs with the same are assigned to a List and their values are converted to InputStream using the getValueAs method of FormDataBodyPart. Once you have these files as InputStream instances, it's easy to do almost anything with them.

@POST
@Path("files")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadMultipart(FormDataMultiPart multiPart) throws IOException{        
    List<FormDataBodyPart> fields = multiPart.getFields("test");        
    for(FormDataBodyPart field : fields){
        handleInputStream(field.getValueAs(InputStream.class));
    }
    //prepare the response
}

private void handleInputStream(InputStream is){
    //read the stream any way you want
}

相关文章