将文件和关联数据发布到RESTful WebService,最好是JSON

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

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

顺便说一句,我们在后端使用Grails,这些服务由原生移动客户端(iPhone、Android等)访问,如果其中任何一项有所不同的话。

840656 次浏览

您可以使用多部分/表单数据内容类型在一个请求中发送文件和数据:

在许多应用程序中,用户可以看到一个表单。用户将填写表单,包括以下信息由用户输入键入、生成或包含在用户已选择。填写表单时,来自表单从用户发送到接收应用程序。

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,因为每个“form-data”部分都为您提供了指定参数名称及其值的位置。

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

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

如何使用REST Web服务上传包含元数据的文件?

你基本上有三个选择:

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

我知道这个问题很老,但在过去的几天里,我搜索了整个网络来解决这个问题。我有grails REST网络服务和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 attributephoto.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")
}

我不知道我将来是否会遇到问题,但现在在正式生产环境中工作得很好。

FormData对象:使用Ajax上传文件

XMLHttpRequest Level 2增加了对新的FormData接口的支持。FormData对象提供了一种轻松构建一组表示表单字段及其值的键/值对的方法,然后可以使用XMLHttpRequest end()方法轻松发送这些键/值对。

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

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

由于唯一缺少的示例是android示例,因此我将添加它。此技术使用应在您的活动类中声明的自定义AsyncTeam。

private class UploadFile extends AsyncTask<Void, Integer, String> {@Overrideprotected void onPreExecute() {// set a status bar or show a dialog to the user heresuper.onPreExecute();}
@Overrideprotected void onProgressUpdate(Integer... progress) {// progress[0] is the current status (e.g. 10%)// here you can update the user interface with the current status}
@Overrideprotected 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() {@Overridepublic void transferred(long num) {// this trigger the progressUpdate eventpublishProgress((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 callHttpResponse 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;}
@Overrideprotected void onPostExecute(String result) {// if you want update the user interface with upload resultsuper.onPostExecute(result);}
}

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

new UploadFile().execute();

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

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::"+respprintln "resp::"+resp.textprintln "resp::"+resp.headersprintln "resp::"+resp.bodyprintln "resp::"+resp.status}

我知道这个线程很旧,但是,我在这里缺少一个选项。如果您有元数据(任何格式)要与要上传的数据一起发送,您可以发出一个multipart/related请求。

多部分/相关媒体类型用于由多个相互关联的主体部分组成的复合对象。

您可以查看rfc2387规范以获取更深入的详细信息。

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

示例:

POST /upload HTTP/1.1Host: www.hostname.comContent-Type: multipart/related; boundary=xyzContent-Length: [actual-content-length]
--xyzContent-Type: application/json; charset=UTF-8
{"name": "Sample image","desc": "...",...}
--xyzContent-Type: image/jpeg
[image data][image data][image data]...--foo_bar_baz--

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

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

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

     POST: /projects/{project_id}/photos/{photo_id}/filebody: file to uploadresponse: -

然后,例如:

  1. 读取照片列表

     GET: /projects/{project_id}/photosresponse: [ 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}/fileresponse: file content

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

我想发送一些字符串到后端服务器。我没有在multipart中使用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)

url看起来像

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

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

您可以尝试使用https://square.github.io/okhttp/库。您可以将请求正文设置为multipart,然后像这样分别添加文件和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());