如何使用 restTemplate Spring-mvc 发送多部分表单数据
How to send Multipart form data with restTemplate Spring-mvc
我正在尝试使用 Jetty 将带有 RestTemplate 的文件上传到 Raspberry Pi。在 Pi 上有一个 servlet 运行:
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
PrintWriter outp = resp.getWriter();
StringBuffer buff = new StringBuffer();
File file1 = (File) req.getAttribute("userfile1");
String p = req.getParameter("path");
boolean success = false;
if (file1 == null || !file1.exists()) {
buff.append("File does not exist\n");
} else if (file1.isDirectory()) {
buff.append("File is a directory\n");
} else {
File outputFile = new File(req.getParameter("userfile1"));
if(isValidPath(p)){
p = DRIVE_ROOT + p;
final File finalDest = new File(p
+ outputFile.getName());
success = false;
try {
copyFileUsingFileChannels(file1, finalDest);
finalDest.setWritable(true);
success = true;
} catch (Exception e) {
e.printStackTrace();
}
if (success){
buff.append("File successfully uploaded.\n");
}
else{
buff.append("Failed to save file.");
}
}
else{
buff.append("Invalid path.\n");
}
}
outp.write(buff.toString());
}
我可以用 curl 成功做到这一点
curl --form userfile1=@/home/pi/src/CreateNewFolderServlet.java --form press=OK localhost:2222/pi/GetFileServlet?path="/media/"
这是应该在 webapp 上具有相同功能的方法。
@ResponseBody
@RequestMapping(value="/upload/",method=RequestMethod.POST ,produces = "text/plain")
public String uploadFile(MultipartHttpServletRequest request2, HttpServletResponse response2){
Iterator<String> itr = request2.getFileNames();
MultipartFile file = request2.getFile(itr.next());
System.out.println(file.getOriginalFilename() +" uploaded!");
System.out.println(file.toString());
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
parts.add("userfile1",file);
//reqEntity.addPart("userfile1", file);
String path="/public/";
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
System.out.println("1");
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<MultiValueMap<String, Object>>(parts, headers);
String url = url2+"/pi/GetFileServlet?path="+path;
System.out.println("2");
/* restTemplate.getMessageConverters().add(new FormHttpMessageConverter());
restTemplate.getMessageConverters().add(
new MappingJackson2HttpMessageConverter());*/
System.out.println("3");
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, request,String.class);
System.out.println("4");
System.out.println("response : " +response);
if(response==null||response.getBody().trim()==""){
return "error";
}
return response.getBody();
}
这是我得到的输出:
ui-elements.html 已上传!
org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@47e7673e
1
2
3
如您所见,没有打印数字 4
控制台也不例外。
调试时发现异常:
org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: org.springframework.web.multipart.support.StandardMultipartFile["inputStream"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: org.springframework.web.multipart.support.StandardMultipartFile["inputStream"])
您遇到异常是因为 RestTemplate 的默认 MessageConverters none 知道如何序列化 MultipartFile 文件包含的 InputStream。当通过 RestTemplate 发送对象时,大多数情况下您希望发送 POJO。您可以通过将 MultipartFile 的字节而不是 MultipartFile 本身添加到 MultiValueMap 来解决此问题。
我认为您的 servlet 部分也有问题。例如
File file1 = (File) req.getAttribute("userfile1");
应该总是 return null,因为 ServletRequest 的 getAttribute 方法没有 return request/form 参数而是 attributes set by the servlet context。您确定它确实适用于您的 curl 示例吗?
这是一个 Spring MVC 方法将文件转发到 servlet 的示例:
Servlet(尽管我在 Spring MVC 容器中对其进行了测试 运行),改编自 here:
@RequestMapping("/pi")
private void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
final String path = request.getParameter("destination");
final Part filePart = request.getPart("file");
final String fileName = request.getParameter("filename");
OutputStream out = null;
InputStream fileContent = null;
final PrintWriter writer = response.getWriter();
try {
out = new FileOutputStream(new File(path + File.separator
+ fileName));
fileContent = filePart.getInputStream();
int read = 0;
final byte[] bytes = new byte[1024];
while ((read = fileContent.read(bytes)) != -1) {
out.write(bytes, 0, read);
}
writer.println("New file " + fileName + " created at " + path);
} catch (FileNotFoundException fne) {
writer.println("You either did not specify a file to upload or are "
+ "trying to upload a file to a protected or nonexistent "
+ "location.");
writer.println("<br/> ERROR: " + fne.getMessage());
} finally {
if (out != null) {
out.close();
}
if (fileContent != null) {
fileContent.close();
}
if (writer != null) {
writer.close();
}
}
}
Spring MVC 方法:
@ResponseBody
@RequestMapping(value="/upload/", method=RequestMethod.POST,
produces = "text/plain")
public String uploadFile(MultipartHttpServletRequest request)
throws IOException {
Iterator<String> itr = request.getFileNames();
MultipartFile file = request.getFile(itr.next());
MultiValueMap<String, Object> parts =
new LinkedMultiValueMap<String, Object>();
parts.add("file", new ByteArrayResource(file.getBytes()));
parts.add("filename", file.getOriginalFilename());
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> requestEntity =
new HttpEntity<MultiValueMap<String, Object>>(parts, headers);
// file upload path on destination server
parts.add("destination", "./");
ResponseEntity<String> response =
restTemplate.exchange("http://localhost:8080/pi",
HttpMethod.POST, requestEntity, String.class);
if (response != null && !response.getBody().trim().equals("")) {
return response.getBody();
}
return "error";
}
使用这些,我可以通过以下 curl 通过 MVC 方法成功将文件上传到 servlet:
curl --form file=@test.dat localhost:8080/upload/
在 ByteArrayResource
中读取整个文件可能是大文件的内存消耗问题。
您可以使用 InputStreamResource
:
在 spring mvc 控制器中代理文件上传
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public ResponseEntity<?> uploadImages(@RequestPart("images") final MultipartFile[] files) throws IOException {
LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
String response;
HttpStatus httpStatus = HttpStatus.CREATED;
try {
for (MultipartFile file : files) {
if (!file.isEmpty()) {
map.add("images", new MultipartInputStreamFileResource(file.getInputStream(), file.getOriginalFilename()));
}
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
String url = "http://example.com/upload";
HttpEntity<LinkedMultiValueMap<String, Object>> requestEntity = new HttpEntity<>(map, headers);
response = restTemplate.postForObject(url, requestEntity, String.class);
} catch (HttpStatusCodeException e) {
httpStatus = HttpStatus.valueOf(e.getStatusCode().value());
response = e.getResponseBodyAsString();
} catch (Exception e) {
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
response = e.getMessage();
}
return new ResponseEntity<>(response, httpStatus);
}
class MultipartInputStreamFileResource extends InputStreamResource {
private final String filename;
MultipartInputStreamFileResource(InputStream inputStream, String filename) {
super(inputStream);
this.filename = filename;
}
@Override
public String getFilename() {
return this.filename;
}
@Override
public long contentLength() throws IOException {
return -1; // we do not want to generally read the whole stream into memory ...
}
}
自版本 5.1 起,Spring 框架附带了自己的 Resource
MultipartFile
实现。因此,您可以通过删除 MultipartInputStreamFileResource
class 并按如下方式填充地图来简化 :
[...]
for (MultipartFile file : files) {
if (!file.isEmpty()) {
map.add("images", file.getResource());
}
}
[...]
我正在尝试使用 Jetty 将带有 RestTemplate 的文件上传到 Raspberry Pi。在 Pi 上有一个 servlet 运行:
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
PrintWriter outp = resp.getWriter();
StringBuffer buff = new StringBuffer();
File file1 = (File) req.getAttribute("userfile1");
String p = req.getParameter("path");
boolean success = false;
if (file1 == null || !file1.exists()) {
buff.append("File does not exist\n");
} else if (file1.isDirectory()) {
buff.append("File is a directory\n");
} else {
File outputFile = new File(req.getParameter("userfile1"));
if(isValidPath(p)){
p = DRIVE_ROOT + p;
final File finalDest = new File(p
+ outputFile.getName());
success = false;
try {
copyFileUsingFileChannels(file1, finalDest);
finalDest.setWritable(true);
success = true;
} catch (Exception e) {
e.printStackTrace();
}
if (success){
buff.append("File successfully uploaded.\n");
}
else{
buff.append("Failed to save file.");
}
}
else{
buff.append("Invalid path.\n");
}
}
outp.write(buff.toString());
}
我可以用 curl 成功做到这一点
curl --form userfile1=@/home/pi/src/CreateNewFolderServlet.java --form press=OK localhost:2222/pi/GetFileServlet?path="/media/"
这是应该在 webapp 上具有相同功能的方法。
@ResponseBody
@RequestMapping(value="/upload/",method=RequestMethod.POST ,produces = "text/plain")
public String uploadFile(MultipartHttpServletRequest request2, HttpServletResponse response2){
Iterator<String> itr = request2.getFileNames();
MultipartFile file = request2.getFile(itr.next());
System.out.println(file.getOriginalFilename() +" uploaded!");
System.out.println(file.toString());
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
parts.add("userfile1",file);
//reqEntity.addPart("userfile1", file);
String path="/public/";
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
System.out.println("1");
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<MultiValueMap<String, Object>>(parts, headers);
String url = url2+"/pi/GetFileServlet?path="+path;
System.out.println("2");
/* restTemplate.getMessageConverters().add(new FormHttpMessageConverter());
restTemplate.getMessageConverters().add(
new MappingJackson2HttpMessageConverter());*/
System.out.println("3");
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, request,String.class);
System.out.println("4");
System.out.println("response : " +response);
if(response==null||response.getBody().trim()==""){
return "error";
}
return response.getBody();
}
这是我得到的输出:
ui-elements.html 已上传!
org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@47e7673e
1
2
3
如您所见,没有打印数字 4 控制台也不例外。 调试时发现异常:
org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: org.springframework.web.multipart.support.StandardMultipartFile["inputStream"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: No serializer found for class java.io.ByteArrayInputStream and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: org.springframework.web.multipart.support.StandardMultipartFile["inputStream"])
您遇到异常是因为 RestTemplate 的默认 MessageConverters none 知道如何序列化 MultipartFile 文件包含的 InputStream。当通过 RestTemplate 发送对象时,大多数情况下您希望发送 POJO。您可以通过将 MultipartFile 的字节而不是 MultipartFile 本身添加到 MultiValueMap 来解决此问题。
我认为您的 servlet 部分也有问题。例如
File file1 = (File) req.getAttribute("userfile1");
应该总是 return null,因为 ServletRequest 的 getAttribute 方法没有 return request/form 参数而是 attributes set by the servlet context。您确定它确实适用于您的 curl 示例吗?
这是一个 Spring MVC 方法将文件转发到 servlet 的示例:
Servlet(尽管我在 Spring MVC 容器中对其进行了测试 运行),改编自 here:
@RequestMapping("/pi")
private void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
final String path = request.getParameter("destination");
final Part filePart = request.getPart("file");
final String fileName = request.getParameter("filename");
OutputStream out = null;
InputStream fileContent = null;
final PrintWriter writer = response.getWriter();
try {
out = new FileOutputStream(new File(path + File.separator
+ fileName));
fileContent = filePart.getInputStream();
int read = 0;
final byte[] bytes = new byte[1024];
while ((read = fileContent.read(bytes)) != -1) {
out.write(bytes, 0, read);
}
writer.println("New file " + fileName + " created at " + path);
} catch (FileNotFoundException fne) {
writer.println("You either did not specify a file to upload or are "
+ "trying to upload a file to a protected or nonexistent "
+ "location.");
writer.println("<br/> ERROR: " + fne.getMessage());
} finally {
if (out != null) {
out.close();
}
if (fileContent != null) {
fileContent.close();
}
if (writer != null) {
writer.close();
}
}
}
Spring MVC 方法:
@ResponseBody
@RequestMapping(value="/upload/", method=RequestMethod.POST,
produces = "text/plain")
public String uploadFile(MultipartHttpServletRequest request)
throws IOException {
Iterator<String> itr = request.getFileNames();
MultipartFile file = request.getFile(itr.next());
MultiValueMap<String, Object> parts =
new LinkedMultiValueMap<String, Object>();
parts.add("file", new ByteArrayResource(file.getBytes()));
parts.add("filename", file.getOriginalFilename());
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> requestEntity =
new HttpEntity<MultiValueMap<String, Object>>(parts, headers);
// file upload path on destination server
parts.add("destination", "./");
ResponseEntity<String> response =
restTemplate.exchange("http://localhost:8080/pi",
HttpMethod.POST, requestEntity, String.class);
if (response != null && !response.getBody().trim().equals("")) {
return response.getBody();
}
return "error";
}
使用这些,我可以通过以下 curl 通过 MVC 方法成功将文件上传到 servlet:
curl --form file=@test.dat localhost:8080/upload/
在 ByteArrayResource
中读取整个文件可能是大文件的内存消耗问题。
您可以使用 InputStreamResource
:
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public ResponseEntity<?> uploadImages(@RequestPart("images") final MultipartFile[] files) throws IOException {
LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
String response;
HttpStatus httpStatus = HttpStatus.CREATED;
try {
for (MultipartFile file : files) {
if (!file.isEmpty()) {
map.add("images", new MultipartInputStreamFileResource(file.getInputStream(), file.getOriginalFilename()));
}
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
String url = "http://example.com/upload";
HttpEntity<LinkedMultiValueMap<String, Object>> requestEntity = new HttpEntity<>(map, headers);
response = restTemplate.postForObject(url, requestEntity, String.class);
} catch (HttpStatusCodeException e) {
httpStatus = HttpStatus.valueOf(e.getStatusCode().value());
response = e.getResponseBodyAsString();
} catch (Exception e) {
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
response = e.getMessage();
}
return new ResponseEntity<>(response, httpStatus);
}
class MultipartInputStreamFileResource extends InputStreamResource {
private final String filename;
MultipartInputStreamFileResource(InputStream inputStream, String filename) {
super(inputStream);
this.filename = filename;
}
@Override
public String getFilename() {
return this.filename;
}
@Override
public long contentLength() throws IOException {
return -1; // we do not want to generally read the whole stream into memory ...
}
}
自版本 5.1 起,Spring 框架附带了自己的 Resource
MultipartFile
实现。因此,您可以通过删除 MultipartInputStreamFileResource
class 并按如下方式填充地图来简化
[...]
for (MultipartFile file : files) {
if (!file.isEmpty()) {
map.add("images", file.getResource());
}
}
[...]