使用 RestClient 上传文件、JSON 和表单数据

前几天 Spring Boot 3.2 正式 发布 了,本次发布带来了一个新的 HTTP 客户端:RestClient,它提供了 Fluent 风格的 API,比起 RestTemplate 来说更加优雅。

关于 RestClient 的更多信息,你可以参考如下文章:

本文将会带你了解如何使用 RestClient 同时上传文件、JSON 和表单数据。

关于如何使用 RestTemplate 进行文件上传,你可以参考 “RestTemplate 上传文件” 和 “在 Spring Boot 应用中同时上传文件、JSON和表单数据”。

本文使用的 Spring Boot 版本为 3.2.0

创建 Controller

首先在 Spring Boot 应用中,创建一个处理 multipart/form-data 文件上传请求的 Controller。

package cn.springdoc.demo.web.controller;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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.HttpServletRequest;


@RestController
@RequestMapping("/demo")
public class DemoController {

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

    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<String> upload (HttpServletRequest request,
                                        @RequestPart("url") String url,
                                        @RequestPart("meta") Map<String, Object> meta,
                                        @RequestPart("logo") MultipartFile logo) throws IOException {
        log.info("url = {}", url);
        log.info("meta = {}", meta);
        
        // 文件名称
        log.info("logo filename = {}", logo.getOriginalFilename());
        // 文件大小
        log.info("logo size = {}", logo.getSize());
        // 表单名称
        log.info("logo name = {}", logo.getName());
        // 文件类型
        log.info("logo contentType = {}", logo.getContentType());
        
        try(InputStream in = logo.getInputStream()){
            long count = in.transferTo(OutputStream.nullOutputStream());
            log.info("logo writeCount = {}", count);
        }
        
        return ResponseEntity.ok("Ok"); 
    }
    }

如上,这个 /upload 端点,接受三个参数。

  • url:这是一个最简单的表单参数,key=value 形式。
  • meta:这是一个 JSON 类型的参数,使用 Map 进行封装。
  • logo:这是一个文件 MultipartFile 类型的文件参数。

这里使用 @RequestPart 对参数进行注解,MVC 框架会根据不同 Part 的类型进行正确的封装。

RestClient 客户端

在测试类中,创建 RestClient 客户端,执行上传请求。

用于上传文件的 multipart/form-data 请求体比较特殊,它可以包含多个部分(Part),每部分都可以有自己的 Header 和 Body。

为了方便,可以考虑使用 apache 的 httpmime 库来构建 multipart/form-data 请求体。

pom.xml 中添加 httpmime 依赖:

<!--https://mvnrepository.com/artifact/org.apache.httpcomponents/httpmime -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpmime</artifactId>
    <version>4.5.14</version>
</dependency>

创建客户端:

package cn.springdoc.demo.test;

import java.io.File;

import org.apache.http.HttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.StringBody;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestClient;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class DemoApplicationTests {

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

    @Test
    public void test() throws Exception {

        RestClient restClient = RestClient.builder().build();
        
        // 构建 Multipart 请求体
        HttpEntity httpEntity = MultipartEntityBuilder.create()
                // 表单数据
                .addPart("url", new StringBody("https://springdoc.cn", ContentType.APPLICATION_FORM_URLENCODED))
                // JSON数据,
                .addPart("meta", new StringBody("""
                        {
                            "title": "Spring 中文网",
                            "description": "spring中文网为开发者提供 spring、spring-boot、spring-data、spring-security、spring-cloud 等框架的官方中文文档以及前沿新闻资讯和优质的技术教程",
                            "keywords": ["spring", "spring boot", "spring security"]
                        }
                        """, ContentType.APPLICATION_JSON))
                // 文件数据,指定表单名称,文件对象,文件类型,文件名称
                .addBinaryBody("logo", new File("C:\\Users\\KevinBlandy\\Desktop\\512.png"), ContentType.IMAGE_PNG, "logo-512.png")
                .build();
        
        // 执行请求,返回结果封装为 String
        ResponseEntity<String> responseEntity = restClient
                    .post()
                    .uri("http://localhost:8080/demo/upload")
                    // 设置 Content Type
                    .contentType(MediaType.parseMediaType(httpEntity.getContentType().getValue()))
                    .body(httpEntity::writeTo) // 把编码后的请求体写入到服务器
                    .retrieve()
                    .toEntity(String.class)
                    ;
        
        log.info("status = {}", responseEntity.getStatusCode());
        log.info("response = {}", responseEntity.getBody());
    }
}

首先,通过 RestClient Builder 构建 RestClient 对象。

然后通过,MultipartEntityBuilder 构建 Multipart 请求体,请求体的每个 Part 都需要设置 表单名称请求体请求体类型 参数。如果是文件,还需要额外设置一个 “文件名称” 参数。

最后,使用 RestClient 发起 POST 请求,指定 URL 以及 Content Type(这里的 Content Type 值必须由 HttpEntity 提供),然后由 httpEntity 把编码后的 multipart/form-data 数据写入到服务器,并把响应封装为 ResponseEntity<String>

测试

先启动服务器,然后执行客户端测试。

服务器输出的日志如下:

c.s.demo.web.controller.DemoController   : url = https://springdoc.cn
c.s.demo.web.controller.DemoController   : meta = {title=Spring 中文网, description=spring中文网为开发者提供 spring、spring-boot、spring-data、spring-security、spring-cloud 等框架的官方中文文档以及前沿新闻资讯和优质的技术教程, keywords=[spring, spring boot, spring security]}
c.s.demo.web.controller.DemoController   : logo filename = logo-512.png
c.s.demo.web.controller.DemoController   : logo size = 19825
c.s.demo.web.controller.DemoController   : logo name = logo
c.s.demo.web.controller.DemoController   : logo contentType = image/png
c.s.demo.web.controller.DemoController   : logo writeCount = 19825

成功地解析到了客户端上传的表单、JSON 和 文件参数。

最后,客户端输出的日志如下:

c.s.demo.test.DemoApplicationTests       : status = 200 OK
c.s.demo.test.DemoApplicationTests       : response = Ok