最好以JSON形式将文件和关联数据发布到RESTful WebService


756

这可能是一个愚蠢的问题,但我有一个夜晚。在一个应用程序中,我正在开发RESTful API,我们希望客户端将数据作为JSON发送。此应用程序的一部分要求客户端上载文件(通常是图像)以及有关该图像的信息。

我很难跟踪单个请求中的情况。是否可以将文件数据Base64转换为JSON字符串?我需要在服务器上执行2个帖子吗?我不应该为此使用JSON吗?

附带说明一下,我们在后端使用Grails,并且本机移动客户端(iPhone,Android等)可以访问这些服务(如果有区别的话)。


1
那么,什么是最好的方法呢?
James111

3
在URL查询字符串(而不是JSON)中发送元数据。
jrc

Answers:


632

我在这里问了类似的问题:

如何使用REST Web服务上载带有元数据的文件?

您基本上有三个选择:

  1. Base64对文件进行编码,但要以将数据大小增加约33%为代价,并在服务器和客户端中增加用于编码/解码的处理开销。
  2. 首先在multipart/form-dataPOST中发送文件,然后将ID返回给客户端。然后,客户端发送带有ID的元数据,然后服务器将文件和元数据重新关联。
  3. 首先发送元数据,然后将ID返回给客户端。然后,客户端发送带有ID的文件,然后服务器将文件和元数据重新关联。

29
如果选择选项1,是否仅将Base64内容包括在JSON字符串中?{file:'234JKFDS#$ @#$ MFDDMS ....',名称:'somename'...}还是还有其他内容吗?
Gregg 2010年

15
就像您说的那样,Gregg只是将其包括为属性,而值将是base64编码的字符串。这可能是最简单的方法,但根据文件大小,可能不切实际。例如,对于我们的应用程序,我们需要发送每个2-3 MB的iPhone图像。增加33%是不可接受的。如果您仅发送20KB的小型图片,那么这种开销可能会更容易接受。
Daniel T.

19
我还应该提到base64编码/解码也将花费一些处理时间。这可能是最容易的事情,但肯定不是最好的。
Daniel T.

8
json与base64?嗯..我正在考虑坚持使用multipart / form
Omnipresent

11
为什么拒绝在一个请求中使用multipart / form-data?
1nstinct

107

您可以使用multipart / form-data 内容类型在一个请求中发送文件和数据:

在许多应用中,有可能向用户呈现表格。用户将填写该表单,包括键入的信息,通过用户输入生成的信息或包含在用户选择的文件中的信息。填写表单后,表单中的数据将从用户发送到接收应用程序。

MultiPart / Form-Data的定义源自这些应用程序之一。

http://www.faqs.org/rfcs/rfc2388.html

“ multipart / form-data”包含一系列部分。每个部分均应包含一个内容处理标头[RFC 2183],其中处理类型为“ form-data”,并且该处理包含一个(附加)参数“ name”,其中该参数的值为原始值表单中的字段名称。例如,零件可能包含标题:

内容处置:表单数据;名称=“用户”

其值与“用户”字段的输入相对应。

您可以在边界之间的每个部分中包括文件信息或字段信息。我已经成功实现了RESTful服务,该服务要求用户提交数据和表单,并且multipart / form-data可以完美地工作。该服务是使用Java / Spring构建的,而客户端使用的是C#,因此,不幸的是,我没有任何Grails示例可以向您提供有关如何设置服务的信息。在这种情况下,您不需要使用JSON,因为每个“表单数据”部分都为您提供了一个位置,用于指定参数的名称及其值。

使用multipart / form-data的好处是,您使用的是HTTP定义的标头,因此您坚持使用现有HTTP工具创建服务的REST理念。


1
谢谢,但是我的问题主要是希望对请求使用JSON,如果可能的话。我已经知道我可以按照您的建议发送。
Gregg 2010年

15
是的,这实质上是我对“我是否应该为此不使用JSON?”的回应。您是否希望客户端使用JSON有特定原因?
McStretch

3
最有可能是业务需求或保持一致性。当然,理想的做法是基于Content-Type HTTP标头接受两种格式(表单数据和JSON响应)。
Daniel T.

2
选择JSON可以在客户端和服务器端生成更精美的代码,从而减少潜在的错误。昨天的表格数据是这样。
superarts.org 2015年

5
对于为.NET开发人员带来伤害的话,我深表歉意。尽管英语不是我的母语,但对我而言,对技术本身说些粗鲁的话不是一个有效的借口。使用表单数据非常棒,如果继续使用它,您也会变得更棒!
superarts.org

53

我知道这个线程已经很老了,但是,我这里缺少一个选项。如果您具有要发送的元数据(任何格式)以及要上传的数据,则可以发出单个multipart/related请求。

Multipart / Related媒体类型适用于由几个相互关联的身体部位组成的复合对象。

您可以查看RFC 2387规范以了解更多详细信息。

基本上,此类请求的每个部分都可以具有不同类型的内容,并且所有部分都以某种方式相关(例如,图像及其元数据)。零件由边界字符串标识,最后的边界字符串后跟两个连字符。

例:

POST /upload HTTP/1.1
Host: www.hostname.com
Content-Type: multipart/related; boundary=xyz
Content-Length: [actual-content-length]

--xyz
Content-Type: application/json; charset=UTF-8

{
    "name": "Sample image",
    "desc": "...",
    ...
}

--xyz
Content-Type: image/jpeg

[image data]
[image data]
[image data]
...
--foo_bar_baz--

到目前为止,我最喜欢您的解决方案。不幸的是,似乎没有办法在浏览器中创建多部分/相关请求。
Petr Baudis

您是否有任何使客户(尤其是JS客户)以这种方式与api通信的经验
pvgoddijn

不幸的是,目前在php(7.2.1)上没有此类数据的阅读器,您将必须构建自己的解析器
dewd 18'July

可悲的是服务器和客户端对此没有很好的支持。
纳德·甘巴里

14

我知道这个问题很旧,但是在最近的几天里,我搜索了整个网络以解决这个问题。我有grails REST Web服务和iPhone Client,它们发送图片,标题和描述。

我不知道我的方法是否是最好的,但如此简单。

我使用UIImagePickerController拍摄图片,然后使用请求的标头标签发送NSData到服务器,以发送图片的数据。

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"myServerAddress"]];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:UIImageJPEGRepresentation(picture, 0.5)];
[request setValue:@"image/jpeg" forHTTPHeaderField:@"Content-Type"];
[request setValue:@"myPhotoTitle" forHTTPHeaderField:@"Photo-Title"];
[request setValue:@"myPhotoDescription" forHTTPHeaderField:@"Photo-Description"];

NSURLResponse *response;

NSError *error;

[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

在服务器端,我使用以下代码接收照片:

InputStream is = request.inputStream

def receivedPhotoFile = (IOUtils.toByteArray(is))

def photo = new Photo()
photo.photoFile = receivedPhotoFile //photoFile is a transient attribute
photo.title = request.getHeader("Photo-Title")
photo.description = request.getHeader("Photo-Description")
photo.imageURL = "temp"    

if (photo.save()) {    

    File saveLocation = grailsAttributes.getApplicationContext().getResource(File.separator + "images").getFile()
    saveLocation.mkdirs()

    File tempFile = File.createTempFile("photo", ".jpg", saveLocation)

    photo.imageURL = saveLocation.getName() + "/" + tempFile.getName()

    tempFile.append(photo.photoFile);

} else {

    println("Error")

}

我不知道将来是否会遇到问题,但现在在生产环境中可以正常工作。


1
我喜欢使用http标头的选项。当元数据和标准http标头之间存在某些对称性时,此方法特别有用,但您显然可以发明自己的。
EJ Campbell

14

这是我的方法API(我使用的示例)-如您所见,您file_id在API中不使用任何(将文件标识符上传到服务器):

  1. photo在服务器上创建对象:

    POST: /projects/{project_id}/photos   
    body: { name: "some_schema.jpg", comment: "blah"}
    response: photo_id
  2. 上载文件(请注意file采用单数形式,因为每张照片只有一个):

    POST: /projects/{project_id}/photos/{photo_id}/file
    body: file to upload
    response: -

然后例如:

  1. 阅读照片列表

    GET: /projects/{project_id}/photos
    response: [ photo, photo, photo, ... ] (array of objects)
  2. 阅读一些照片细节

    GET: /projects/{project_id}/photos/{photo_id}
    response: { id: 666, name: 'some_schema.jpg', comment:'blah'} (photo object)
  3. 读取照片文件

    GET: /projects/{project_id}/photos/{photo_id}/file
    response: file content

因此得出的结论是,首先通过POST创建对象(照片),然后发送带有文件的第二个请求(同样是POST)。


3
这似乎是实现此目标的更“ RESTFUL”方式。
James Webster

新创建资源的POST操作必须以对象的简单版本详细信息返回位置ID
Ivan Proskuryakov

@ivanproskuryakov为什么“必须”?在上面的示例中(第2点中的POST),文件ID无效。第二个参数(对于第2点中的POST)我使用单数形式的'/ file'(不是'/ files'),因此不需要ID,因为路径:/ projects / 2 / photos / 3 / file将完整信息提供给身份照片文件。
KamilKiełczewski17年

来自HTTP协议规范。w3.org/Protocols/rfc2616/rfc2616-sec10.html 10.2.2 201已创建“新创建的资源可以由响应实体中返回的URI引用,其中最具体的URI由位置标头字段。” @KamilKiełczewski(一个)和(两个)可以合并为一个POST操作POST:/ projects / {project_id} / photos将返回您的位置标头,该标头可用于GET单张照片(资源*)操作GET:具有所有详细信息的单张照片CGET:获取照片的所有收藏集
Ivan Proskuryakov

1
如果元数据和上载是单独的操作,则端点具有以下问题:对于文件上载,使用了POST操作-POST不是幂等的。由于要更改资源而不创建新资源,因此必须使用PUT(幂等)。REST与称为资源的对象一起使用。POST:“ ../ photos /” PUT:“ ../ photos / {photo_id}” GET:“ ../ photos /“ GET:” ../ photos / {photo_id}” PS。将上载分隔到单独的端点可能会导致意外行为。restapitutorial.com/lessons/idempotency.html restful-api-design.readthedocs.io/en/latest/resources.html
Ivan Proskuryakov

6

FormData对象:使用Ajax上传文件

XMLHttpRequest级别2增加了对新FormData接口的支持。FormData对象提供了一种轻松构造一组代表表单字段及其值的键/值对的方法,然后可以使用XMLHttpRequest send()方法轻松发送该键/值对。

function AjaxFileUpload() {
    var file = document.getElementById("files");
    //var file = fileInput;
    var fd = new FormData();
    fd.append("imageFileData", file);
    var xhr = new XMLHttpRequest();
    xhr.open("POST", '/ws/fileUpload.do');
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
             alert('success');
        }
        else if (uploadResult == 'success')
             alert('error');
    };
    xhr.send(fd);
}

https://developer.mozilla.org/zh-CN/docs/Web/API/FormData


6

由于唯一缺少的示例是ANDROID示例,因此我将其添加。该技术使用了应在Activity类中声明的自定义AsyncTask。

private class UploadFile extends AsyncTask<Void, Integer, String> {
    @Override
    protected void onPreExecute() {
        // set a status bar or show a dialog to the user here
        super.onPreExecute();
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        // progress[0] is the current status (e.g. 10%)
        // here you can update the user interface with the current status
    }

    @Override
    protected String doInBackground(Void... params) {
        return uploadFile();
    }

    private String uploadFile() {

        String responseString = null;
        HttpClient httpClient = new DefaultHttpClient();
        HttpPost httpPost = new HttpPost("http://example.com/upload-file");

        try {
            AndroidMultiPartEntity ampEntity = new AndroidMultiPartEntity(
                new ProgressListener() {
                    @Override
                        public void transferred(long num) {
                            // this trigger the progressUpdate event
                            publishProgress((int) ((num / (float) totalSize) * 100));
                        }
            });

            File myFile = new File("/my/image/path/example.jpg");

            ampEntity.addPart("fileFieldName", new FileBody(myFile));

            totalSize = ampEntity.getContentLength();
            httpPost.setEntity(ampEntity);

            // Making server call
            HttpResponse httpResponse = httpClient.execute(httpPost);
            HttpEntity httpEntity = httpResponse.getEntity();

            int statusCode = httpResponse.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                responseString = EntityUtils.toString(httpEntity);
            } else {
                responseString = "Error, http status: "
                        + statusCode;
            }

        } catch (Exception e) {
            responseString = e.getMessage();
        }
        return responseString;
    }

    @Override
    protected void onPostExecute(String result) {
        // if you want update the user interface with upload result
        super.onPostExecute(result);
    }

}

因此,当您要上传文件时,只需调用:

new UploadFile().execute();

嗨,什么是AndroidMultiPartEntity,请解释一下...,如果我要上传pdf,word或xls文件,我必须做些什么,请提供一些指导...我对此并不陌生。
阿米

1
@amitpandya我已将代码更改为通用文件上传,因此任何阅读它的人都更加清楚
lifeisfoo

2

我想发送一些字符串到后端服务器。我没有将json与multipart一起使用,而是使用了请求参数。

@RequestMapping(value = "/upload", method = RequestMethod.POST)
public void uploadFile(HttpServletRequest request,
        HttpServletResponse response, @RequestParam("uuid") String uuid,
        @RequestParam("type") DocType type,
        @RequestParam("file") MultipartFile uploadfile)

网址看起来像

http://localhost:8080/file/upload?uuid=46f073d0&type=PASSPORT

我正在传递两个参数(uuid和type)以及文件上传。希望这对没有复杂json数据发送的人有所帮助。


1

您可以尝试使用https://square.github.io/okhttp/库。您可以将请求主体设置为多部分,然后分别添加文件和json对象,如下所示:

MultipartBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("uploadFile", uploadFile.getName(), okhttp3.RequestBody.create(uploadFile, MediaType.parse("image/png")))
                .addFormDataPart("file metadata", json)
                .build();

        Request request = new Request.Builder()
                .url("https://uploadurl.com/uploadFile")
                .post(requestBody)
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

            logger.info(response.body().string());

0
@RequestMapping(value = "/uploadImageJson", method = RequestMethod.POST)
    public @ResponseBody Object jsongStrImage(@RequestParam(value="image") MultipartFile image, @RequestParam String jsonStr) {
-- use  com.fasterxml.jackson.databind.ObjectMapper convert Json String to Object
}

-5

请确保您具有以下导入。当然其他标准进口

import org.springframework.core.io.FileSystemResource


    void uploadzipFiles(String token) {

        RestBuilder rest = new RestBuilder(connectTimeout:10000, readTimeout:20000)

        def zipFile = new File("testdata.zip")
        def Id = "001G00000"
        MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>()
        form.add("id", id)
        form.add('file',new FileSystemResource(zipFile))
        def urld ='''http://URL''';
        def resp = rest.post(urld) {
            header('X-Auth-Token', clientSecret)
            contentType "multipart/form-data"
            body(form)
        }
        println "resp::"+resp
        println "resp::"+resp.text
        println "resp::"+resp.headers
        println "resp::"+resp.body
        println "resp::"+resp.status
    }

1
这个得到java.lang.ClassCastException: org.springframework.core.io.FileSystemResource cannot be cast to java.lang.String
马里亚诺·鲁伊斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.