RestTemplate 上传文件

在调用第三方Api服务的时候,如果涉及到文件上传,就需要自己通过 Http 客户端发起 Multipart 请求来上传文件。

在 Spring 应用中比较流行的 Http 客户端就是 RestTemplate。本文将会指导你如何用 RestTemplate 发起 Multipart 请求来上传文件。

服务端

在服务端创建一个用于测试的 FileUploadController, 它接受来自客户端的 Multipart 文件请求,并且响应文件的相关信息。

package cn.springdoc.demo.controller;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import jakarta.servlet.http.HttpServletResponse;

@RestController
@RequestMapping("/upload")
public class FileUploadController {

    private static final Logger log = LoggerFactory.getLogger(FileUploadController.class);

    /**
        * 文件上传
        * @param file
        * @param response
        * @return
        * @throws IOException
        */
    @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Map<String, Object> upload (@RequestPart("file") MultipartFile file, HttpServletResponse response) throws IOException {
        
        log.info("文件名称:{}", file.getOriginalFilename());
        log.info("表单名称:{}", file.getName());
        log.info("文件大小:{}", file.getSize());
        log.info("文件类型:{}", file.getContentType());
        
        try(InputStream in = file.getInputStream()){
            // 丢弃上传的文件数据
            int ret = StreamUtils.drain(in);
            log.info("丢弃字节:{}", ret);
        }
        
        Map<String, Object> ret = new HashMap<>();
        ret.put("originalFilename", file.getOriginalFilename());
        ret.put("name", file.getName());
        ret.put("size", file.getSize());
        ret.put("contentType", file.getContentType());
        
        return ret;
    }
}

客户端

package cn.springdoc.test;


import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

public class MultipartTest {

    public static void main(String[] args) {
        
        RestTemplate restTemplate = new RestTemplate();

        // 消息头
        HttpHeaders headers = new HttpHeaders();
        // Content-Type 为 multipart/form-data
        headers.setContentType(MediaType.MULTIPART_FORM_DATA); 

        // body builder
        MultipartBodyBuilder builder = new MultipartBodyBuilder();
        // 从磁盘获取到文件
        Resource logo = new FileSystemResource("C:\\Users\\KevinBlandy\\Desktop\\512.png");
        // 设置表单名称,文件,以及文件类型
        builder.part("file", logo, MediaType.IMAGE_PNG);
        
        // 完整的请求体体
        MultiValueMap<String, HttpEntity<?>> multipartBody = builder.build();

        // 完整的 http 消息
        HttpEntity<MultiValueMap<String, HttpEntity<?>>> httpEntity = new HttpEntity<>(multipartBody, headers);

        // 发起请求,获取响应
        ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:8080/upload", httpEntity, String.class);

        System.out.println(responseEntity.getBody());
    
}

很简单,不到 50 行代码。总结如下:

  1. 构建 HttpHeaders,一定要把 Content-Type 头设置为 MediaType.MULTIPART_FORM_DATA
  2. 构建 MultipartBodyBuilder,通过这个 builder 来设置要上传的文件对象。
  3. 文件对象必须是 Resource 接口的实现,Spring 预制了很多不同的实现可用于不同场景,如下:
    • FileSystemResource 用于指定磁盘文件。
    • ClassPathResource 用于指定 classpath 资源文件。
    • ByteArrayResource 用于指定内存中的字节数据。
    • InputStreamResource 用于指定 InputStream 流。
  4. 通过 header 和 body 构建完整的请求体。
  5. 发起请求。

如果你的 Spring Web 版本比较高的话,可能会在运行时遇到 ClassNotFoundException: org.reactivestreams.Publisher 异常。

Exception in thread "main" java.lang.NoClassDefFoundError: org/reactivestreams/Publisher
   at cn.springdoc.test.MultipartTest.main(MultipartTest.java:26)
Caused by: java.lang.ClassNotFoundException: org.reactivestreams.Publisher
   at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
   at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
   at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
   ... 1 more

此时你需要在客户端添加 reactive-streams 依赖。

<!-- https://mvnrepository.com/artifact/org.reactivestreams/reactive-streams -->
<dependency>
   <groupId>org.reactivestreams</groupId>
   <artifactId>reactive-streams</artifactId>
</dependency>

测试

先启动服务器,再执行客户端的 main 方法进行测试,服务器端日志输出如下:

c.s.d.controller.FileUploadController    : 文件名称:512.png
c.s.d.controller.FileUploadController    : 表单名称:file
c.s.d.controller.FileUploadController    : 文件大小:19825
c.s.d.controller.FileUploadController    : 文件类型:image/png
c.s.d.controller.FileUploadController    : 丢弃字节:19825

客户端获取到的响应如下:

{"size":19825,"name":"file","contentType":"image/png","originalFilename":"512.png"}

上传成功。