使用RestTemplate请求发送多部分文件作为POST参数

47

我正在使用Spring 3和RestTemplate进行工作。基本上,我有两个应用程序,其中一个必须通过rest template将值发布到另一个应用程序。

当要发布的值是字符串时,它可以完美运行,但是当我需要发布混合和复杂的参数(例如MultipartFiles)时,我会遇到转换器异常。

例如,我有以下代码:

App1 - PostController:

@RequestMapping(method = RequestMethod.POST)
public String processSubmit(@ModelAttribute UploadDTO pUploadDTO, 
        BindingResult pResult) throws URISyntaxException, IOException {
    URI uri = new URI("http://localhost:8080/app2/file/receiver");

    MultiValueMap<String, Object> mvm = new LinkedMultiValueMap<String, Object>();
    mvm.add("param1", "TestParameter");
    mvm.add("file", pUploadDTO.getFile()); // MultipartFile

    Map result = restTemplate.postForObject(uri, mvm, Map.class);
    return "redirect:postupload";
}

在另一方面...我有另一个Web应用程序(App2),它从App1接收参数。

App2 - ReceiverController

@RequestMapping(value = "/receiver", method = { RequestMethod.POST })
public String processUploadFile(
        @RequestParam(value = "param1") String param1,
        @RequestParam(value = "file") MultipartFile file) {

    if (file == null) {
        System.out.println("Shit!... is null");
    } else {
        System.out.println("Yes!... work done!");
    }
    return "redirect:postupload";
}

我的application-context.xml文件:

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <property name="messageConverters">
        <list>
            <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter" />
            <bean class="org.springframework.http.converter.FormHttpMessageConverter" />
            <bean class="org.springframework.http.converter.StringHttpMessageConverter" />
            <bean class="org.springframework.http.converter.xml.SourceHttpMessageConverter" />
        </list>
    </property>
</bean>

<bean id="multipartResolver"  
    class="org.springframework.web.multipart.commons.CommonsMultipartResolver">  
    <property name="maxUploadSize">  
        <value>104857600</value>  
    </property>  
    <property name="maxInMemorySize">  
        <value>4096</value>  
    </property>      
</bean>  

当我使用RestTemplate的postForObject方法时,此处显示了异常的堆栈信息...

org.springframework.http.converter.HttpMessageNotWritableException: Could not write request: no suitable HttpMessageConverter found for request type [org.springframework.web.multipart.commons.CommonsMultipartFile]
at org.springframework.http.converter.FormHttpMessageConverter.writePart(FormHttpMessageConverter.java:292)
at org.springframework.http.converter.FormHttpMessageConverter.writeParts(FormHttpMessageConverter.java:252)
at org.springframework.http.converter.FormHttpMessageConverter.writeMultipart(FormHttpMessageConverter.java:242)
at org.springframework.http.converter.FormHttpMessageConverter.write(FormHttpMessageConverter.java:194)
at org.springframework.http.converter.FormHttpMessageConverter.write(FormHttpMessageConverter.java:1)
at org.springframework.web.client.RestTemplate$HttpEntityRequestCallback.doWithRequest(RestTemplate.java:588)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:436)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:415)
at org.springframework.web.client.RestTemplate.postForObject(RestTemplate.java:294)
at com.yoostar.admintool.web.UploadTestController.create(UploadTestController.java:86)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.springframework.web.bind.annotation.support.HandlerMethodInvoker.invokeHandlerMethod(HandlerMethodInvoker.java:175)
at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.invokeHandlerMethod(AnnotationMethodHandlerAdapter.java:421)
at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.handle(AnnotationMethodHandlerAdapter.java:409)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:774)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:719)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:644)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:560)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:637)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:76)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:127)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:298)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:857)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:588)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:489)
at java.lang.Thread.run(Thread.java:619)

所以我的问题是:

  1. 是否可以使用POST方法通过RestTemplate发送MultipartFile?
  2. 是否有一些特定的转换器需要使用来发送这种类型的对象?我的意思是在我的配置中是否有一些可以使用?

https://dev59.com/O-o6XIcBkEYKwwoYIAgL#63650486 - ehsan maddahi
10个回答

66

不需要使用需要磁盘上的文件的FileSystemResource来解决这个问题,可以使用ByteArrayResource,这样你可以在你的post中发送一个字节数组(此代码适用于Spring 3.2.3):

MultiValueMap<String, Object> map = new LinkedMultiValueMap<String, Object>();
final String filename="somefile.txt";
map.add("name", filename);
map.add("filename", filename);
ByteArrayResource contentsAsResource = new ByteArrayResource(content.getBytes("UTF-8")){
            @Override
            public String getFilename(){
                return filename;
            }
        };
map.add("file", contentsAsResource);
String result = restTemplate.postForObject(urlForFacade, map, String.class);

我重写了ByteArrayResource的getFilename方法,因为如果不这样做,我会得到一个空指针异常(显然这取决于Java激活 .jar是否在类路径上,如果是,则使用文件名尝试确定内容类型)


10
“content”的数据类型是什么? - user754657
@Luxspes,你如何覆盖ByteArrayResource类的final方法?这是不可能的。 - xyz
1
@xyz,getFilename方法不是final的:http://docs.spring.io/autorepo/docs/spring/3.2.3.RELEASE/javadoc-api/org/springframework/core/io/AbstractResource.html#getFilename() - Luxspes
这应该被接受,我遇到了同样的问题,这个解决方案完美地解决了。 - yelliver
1
将整个文件加载到内存中可能会成为大型文件的问题。 - Lorenzo Polidori
显示剩余2条评论

15

我前几天也遇到了同样的问题。通过Google搜索,我来到了这里和其他一些地方,但是没有给出解决此问题的方法。最终,我将上传的文件(MultiPartFile)保存为临时文件,然后使用FileSystemResource通过RestTemplate上传。这是我使用的代码:

String tempFileName = "/tmp/" + multiFile.getOriginalFileName();
FileOutputStream fo = new FileOutputStream(tempFileName);

fo.write(asset.getBytes());    
fo.close();   

parts.add("file", new FileSystemResource(tempFileName));    
String response = restTemplate.postForObject(uploadUrl, parts, String.class, authToken, path);   


//clean-up    
File f = new File(tempFileName);    
f.delete();

我仍在寻找更优雅的解决方案来解决这个问题。


我不清楚为什么您要接收上传的文件然后再次上传,但是假设您有一个有效的理由这样做,您可以使用MultipartFile.transferTo()方法(http://bit.ly/Pm6sPN)而不是FileOutputStream.write()调用,这比您目前的方式更加简洁。否则,这里的真正技巧是使用FileSystemResource。这由ResourceHttpMessageConverter处理。否则(我很惊讶这不存在),您需要编写一个专门用于多部分文件处理的转换器。 - Spanky Quigman
FileSystemResource 只有在我们知道文件的确切路径时才能正常工作。但由于浏览器安全问题,这是不可实现的。 - user754657

9

我最近为这个问题苦苦挣扎了3天。客户端发送请求的方式可能不是问题的原因,服务器可能没有配置处理多部分请求。以下是我必须要做的才能使其正常工作:

pom.xml - 添加commons-fileupload依赖项(如果您没有使用诸如maven之类的依赖管理,请下载并将jar添加到您的项目中)

<dependency>
  <groupId>commons-fileupload</groupId>
  <artifactId>commons-fileupload</artifactId>
  <version>${commons-version}</version>
</dependency>

web.xml - 添加多部分过滤器和映射

<filter>
  <filter-name>multipartFilter</filter-name>
  <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>multipartFilter</filter-name>
  <url-pattern>/springrest/*</url-pattern>
</filter-mapping>

app-context.xml - 添加多部分解析器

<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <beans:property name="maxUploadSize">
        <beans:value>10000000</beans:value>
    </beans:property>
</beans:bean>

您的控制器

@RequestMapping(value=Constants.REQUEST_MAPPING_ADD_IMAGE, method = RequestMethod.POST, produces = { "application/json"})
public @ResponseBody boolean saveStationImage(
        @RequestParam(value = Constants.MONGO_STATION_PROFILE_IMAGE_FILE) MultipartFile file,
        @RequestParam(value = Constants.MONGO_STATION_PROFILE_IMAGE_URI) String imageUri, 
        @RequestParam(value = Constants.MONGO_STATION_PROFILE_IMAGE_TYPE) String imageType, 
        @RequestParam(value = Constants.MONGO_FIELD_STATION_ID) String stationId) {
    // Do something with file
    // Return results
}

您的客户

public static Boolean updateStationImage(StationImage stationImage) {
    if(stationImage == null) {
        Log.w(TAG + ":updateStationImage", "Station Image object is null, returning.");
        return null;
    }

    Log.d(TAG, "Uploading: " + stationImage.getImageUri());
    try {
        RestTemplate restTemplate = new RestTemplate();
        FormHttpMessageConverter formConverter = new FormHttpMessageConverter();
        formConverter.setCharset(Charset.forName("UTF8"));
        restTemplate.getMessageConverters().add(formConverter);
        restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());

        restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setAccept(Collections.singletonList(MediaType.parseMediaType("application/json")));

        MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();

        parts.add(Constants.STATION_PROFILE_IMAGE_FILE, new FileSystemResource(stationImage.getImageFile()));
        parts.add(Constants.STATION_PROFILE_IMAGE_URI, stationImage.getImageUri());
        parts.add(Constants.STATION_PROFILE_IMAGE_TYPE, stationImage.getImageType());
        parts.add(Constants.FIELD_STATION_ID, stationImage.getStationId());

        return restTemplate.postForObject(Constants.REST_CLIENT_URL_ADD_IMAGE, parts, Boolean.class);
    } catch (Exception e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));

        Log.e(TAG + ":addStationImage", sw.toString());
    }

    return false;
}

应该可以解决问题了。我尽可能添加了更多信息,因为我花了几天时间拼凑出完整的问题,希望这能有所帮助。


我添加了依赖项“commons-fileupload”,并使用“MultiValueMap”设置了我的参数,它起作用了。谢谢@TrueCoke - greenhorn

7
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
parts.add("name 1", "value 1");
parts.add("name 2", "value 2+1");
parts.add("name 2", "value 2+2");
Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg");
parts.add("logo", logo);
Source xml = new StreamSource(new StringReader("<root><child/></root>"));
parts.add("xml", xml);

template.postForLocation("http://example.com/multipart", parts);

1
这个答案的逻辑难道不正是与提问者所做的完全相同吗?如果我将此响应复制并粘贴到我的代码中以替换自己的请求,我会得到与他相同的错误。我认为这里真正的问题是,为什么对于某些用户使用LinkedMultiValueMap时会出现“没有找到适当的HttpMessageConverter”,而对于其他用户则没有?是否需要链接某个库? - deepwinter

5

我们中的一个人使用filesystemresource进行类似操作。 尝试一下。

mvm.add("file", new FileSystemResource(pUploadDTO.getFile())); 

假设您的.getFile输出是一个Java文件对象,那应该与我们的一样工作,只是有一个文件参数。

1
OP 正试图重新上传 MultipartFile,而不是 File。因此,他必须将其保存在磁盘上以获取 File。这并不像一开始看起来那么简单。 - Tristan

5
你可以简单地使用MultipartHttpServletRequest
例如:
 @RequestMapping(value={"/upload"}, method = RequestMethod.POST,produces = "text/html; charset=utf-8")
 @ResponseBody
 public String upload(MultipartHttpServletRequest request /*@RequestBody MultipartFile file*/){
    String responseMessage = "OK";
    MultipartFile file = request.getFile("file");
    String param = request.getParameter("param");
    try {
        System.out.println(file.getOriginalFilename());
        System.out.println("some param = "+param);
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8));
        // read file
    }
    catch(Exception ex){
        ex.printStackTrace();
        responseMessage = "fail";
    }
     return responseMessage;
}

在使用request.getParameter()时,参数名称必须与前端名称相同。

请注意,通过getFile()提取的文件与其他附加参数通过getParameter()提取。


您不会重新上传在此代码中接收到的文件。 - Tristan
谢谢,这正是我所需要的……只需将 MultipartFile 更改为 MultipartHttpServletRequest 并分别获取文件和参数。 - Renan Ribeiro

2

我必须做与@Luxspes上面所做的相同的事情..而且我正在使用Spring 4.2.6。花了很长时间弄清楚为什么ByteArrayResource从客户端传输到服务器,但服务器没有识别它。

ByteArrayResource contentsAsResource = new ByteArrayResource(byteArr){
            @Override
            public String getFilename(){
                return filename;
            }
        };

1
如果您需要发送一个由多部分文件组成的文件,其中包括需要使用特定HttpMessageConverter转换的对象,并且无论尝试什么都会出现“没有合适的HttpMessageConverter”错误,您可能想尝试使用以下方法:
RestTemplate restTemplate = new RestTemplate();
FormHttpMessageConverter converter = new FormHttpMessageConverter();

converter.addPartConverter(new TheRequiredHttpMessageConverter());
//for example, in my case it was "new MappingJackson2HttpMessageConverter()"

restTemplate.getMessageConverters().add(converter);

这对我解决了一个问题,我的自定义对象与文件(在我的情况下是FileSystemResource的实例)一起成为我需要发送的多部分文件的一部分。我尝试了TrueGuidance等众多网络上找到的解决方案,但均无效,随后我查看了FormHttpMessageConverter的源代码并尝试了这个方法。

0
我曾经遇到过这个问题,找到了一个比使用ByteArrayResource更简单的解决方案。
只需要做以下操作:
public void loadInvoices(MultipartFile invoices, String channel) throws IOException {

    init();

    Resource invoicesResource = invoices.getResource();

    LinkedMultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
    parts.add("file", invoicesResource);

    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
    httpHeaders.set("channel", channel);

    HttpEntity<LinkedMultiValueMap<String, Object>> httpEntity = new HttpEntity<>(parts, httpHeaders);

    String url = String.format("%s/rest/inbound/invoices/upload", baseUrl);

    restTemplate.postForEntity(url, httpEntity, JobData.class);
}

它有效,而且不需要处理文件系统或字节数组。


0

你需要将FormHttpMessageConverter添加到你的applicationContext.xml中,才能够发布多部分文件。

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <property name="messageConverters">
        <list>
            <bean class="org.springframework.http.converter.StringHttpMessageConverter" />
            <bean class="org.springframework.http.converter.FormHttpMessageConverter" />
        </list>
    </property>
</bean>

请参考http://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/converter/FormHttpMessageConverter.html获取示例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接