ChatGPT解决这个技术问题 Extra ChatGPT

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

在我正在开发 RESTful API 的应用程序中,我们希望客户端以 JSON 格式发送数据。此应用程序的一部分要求客户端上传文件(通常是图像)以及有关图像的信息。

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

附带说明一下,我们在后端使用 Grails,并且这些服务由本地移动客户端(iPhone、Android 等)访问,如果其中有任何影响的话。

那么,最好的方法是什么?
在 URL 查询字符串中发送元数据,而不是 JSON。

S
Sergio B

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

How do I upload a file with metadata using a REST web service?

你基本上有三个选择:

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


如果我选择选项 1,我是否只在 JSON 字符串中包含 Base64 内容? {file:'234JKFDS#$@#$MFDDMS....', name:'somename'...} 或者还有其他内容吗?
Gregg,正如您所说,您只需将其作为属性包含,其值将是 base64 编码的字符串。这可能是最简单的方法,但根据文件大小可能不实用。例如,对于我们的应用程序,我们需要发送每个 2-3 MB 的 iPhone 图像。增加 33% 是不可接受的。如果您只发送 20KB 的小图像,那么这种开销可能更容易接受。
我还应该提到,base64 编码/解码也需要一些处理时间。这可能是最容易做的事情,但肯定不是最好的。
带有base64的json?嗯..我正在考虑坚持多部分/形式
为什么拒绝在一个请求中使用 multipart/form-data?
C
Community

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

在许多应用程序中,可能会向用户呈现表单。用户将填写表单,包括键入的信息、由用户输入生成的信息或从用户选择的文件中包含的信息。填写表格后,表格中的数据会从用户发送到接收应用程序。 MultiPart/Form-Data 的定义源自这些应用程序之一......

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

“multipart/form-data”包含一系列部分。每个部分都应包含一个内容处置标头 [RFC 2183],其中处置类型为“表单数据”,处置包含“名称”的(附加)参数,其中该参数的值是原始的表单中的字段名称。例如,一个部分可能包含一个标题: Content-Disposition: form-data; name="user",其值对应于“user”字段的条目。

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

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


谢谢,但我的问题集中在想要使用 JSON 作为请求,如果可能的话。我已经知道我可以按照您建议的方式发送它。
是的,这基本上是我对“我不应该为此使用 JSON 吗?”的回应。您希望客户端使用 JSON 是否有特定原因?
很可能是业务需求或保持一致性。当然,理想的做法是根据 Content-Type HTTP 标头同时接受(表单数据和 JSON 响应)。
如果我说的话伤害了一些 .Net 开发人员的感觉,我深表歉意。尽管英语不是我的母语,但这并不是我对技术本身说一些粗鲁的话的正当借口。使用表单数据非常棒,如果你继续使用它,你也会更加棒!
但是在这种情况下,如何在客户端获取文本数据和图像,因为它们都有一个端点?
C
Community

我知道这个线程已经很老了,但是,我在这里缺少一个选项。如果您有要与要上传的数据一起发送的元数据(任何格式),您可以发出一个 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--

到目前为止,我最喜欢您的解决方案。不幸的是,似乎没有办法在浏览器中创建多部分/相关请求。
你有让客户(尤其是 JS 客户)以这种方式与 api 通信的经验吗
不幸的是,目前在 php (7.2.1) 上没有此类数据的阅读器,您必须构建自己的解析器
很遗憾,服务器和客户端对此没有很好的支持。
该解决方案有两个问题:一个是它需要由用于实现的客户端/服务器 Web 框架支持,第二个是,如果 json 部分的验证失败(例如,元数据之一是电子邮件地址),它应该返回错误并导致客户端重新上传文件,这很昂贵
K
Kamil Kiełczewski

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

在服务器上创建照片对象: POST: /projects/{project_id}/photos body: { name: "some_schema.jpg", comment: "blah"} response: photo_id 上传文件(注意文件是单数形式,因为它只是每张照片一张):POST:/projects/{project_id}/photos/{photo_id}/file body:要上传的文件响应:-

然后例如:

读取照片列表 GET:/projects/{project_id}/photos 响应:[ photo, photo, photo, ... ](对象数组) 读取一些照片详细信息 GET:/projects/{project_id}/photos/{photo_id} 响应: { id: 666, name: 'some_schema.jpg', comment:'blah'} (照片对象) 读取照片文件 GET: /projects/{project_id}/photos/{photo_id}/file 响应:文件内容

所以结论是,首先你通过 POST 创建一个对象(照片),然后你发送带有文件的第二个请求(再次 POST)。为了在这种方法中没有 CACHE 问题,我们假设我们只能删除旧照片并添加新照片 - 不能更新二进制照片文件(因为新的二进制文件实际上是......新照片)。但是,如果您需要能够更新二进制文件并缓存它们,那么在点 4 中还返回 fileId 并将 5 更改为 GET:/projects/{project_id}/photos/{photo_id}/files/{文件ID}。


这似乎是实现这一目标的更“RESTFUL”的方式。
新创建资源的 POST 操作,必须以对象的简单版本详细信息返回位置 ID
@ivanproskuryakov 为什么“必须”?在上面的示例中(第 2 点中的 POST),文件 ID 是无用的。第二个参数(对于第 2 点中的 POST)我使用单数形式“/file”(不是“/files”),因此不需要 ID,因为路径:/projects/2/photos/3/file 将完整信息提供给身份照片文件。
来自 HTTP 协议规范。 w3.org/Protocols/rfc2616/rfc2616-sec10.html 10.2.2 201 Created “新创建的资源可以被响应实体中返回的 URI 引用,该资源的最具体 URI 由 Location 标头字段给出。” @KamilKiełczewski(一)和(二)可以组合成一个 POST 操作 POST: /projects/{project_id}/photos 将返回您的位置标头,可用于获取单张照片(资源*)操作 GET:获取包含所有详细信息的单张照片 CGET:获取所有照片集合
如果元数据和上传是单独的操作,那么端点有以下问题: 对于文件上传使用 POST 操作 - POST 不是幂等的。必须使用 PUT(idempotent),因为您正在更改资源而不创建新资源。 REST 使用名为 resources 的对象。 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
N
Nikhil Agrawal

我知道这个问题很老,但在过去的几天里,我搜索了整个网络来解决同样的问题。我有发送图片、标题和描述的 grails REST web 服务和 iPhone 客户端。

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

我使用 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")

}

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


我喜欢这种使用 http 标头的选项。当元数据和标准 http 标头之间存在某种对称性时,这尤其有效,但您显然可以自己发明。
I
Ikar Pohorský

FormData 对象:使用 Ajax 上传文件

XMLHttpRequest Level 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/en-US/docs/Web/API/FormData


l
lifeisfoo

由于唯一缺少的示例是 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 文件我必须做什么,请提供一些指导...我是新手。
@amitpandya 我已将代码更改为通用文件上传,因此阅读它的任何人都更清楚
A
Aslam a

我想向后端服务器发送一些字符串。我没有使用多部分的 json,我使用了请求参数。

@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 和类型)以及文件上传。希望这会帮助那些没有复杂的 json 数据要发送的人。


O
OneXer

您可以尝试使用 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());

s
sunleo
@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
}

M
Mak Kul

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

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
    }

这得到 java.lang.ClassCastException: org.springframework.core.io.FileSystemResource cannot be cast to java.lang.String