HttpClient使用管道流提交Multipart请求

HttpClient 是JDK11提供的一个全新HTTP客户端Api,超级好用。
Multipart 请求 HttpClient 并没有提供 Multipart 请求体的构建Api。但是可以使用apache的开源httpmime库来进行构建。

org.apache.httpcomponents httpmime 4.5.13

构建一个 MultipartBody
// 构建Multipart请求 HttpEntity httpEntity = MultipartEntityBuilder.create() // 表单数据 .addPart("name", new StringBody(UriUtils.encode("SpringBoot中文社区", StandardCharsets.UTF_8), ContentType.APPLICATION_FORM_URLENCODED)) // JSON数据 .addPart("info", new StringBody("{\"site\": \"https://springboot.io\", \"now\": 2021}", ContentType.APPLICATION_JSON)) // 文件数据 .addBinaryBody("file", file, ContentType.APPLICATION_OCTET_STREAM, "eclipse-jee-2019-12-R-win32-x86_64.zip") .build(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream((int) httpEntity.getContentLength()); // 把body写入到内存 httpEntity.writeTo(byteArrayOutputStream);

Multipart 请求可以一次性post多个子body,通常用来上传本地磁盘上的文件。所以这种请求体可能会异常庞大。甚至内存不能完整的存入整个请求体。那么这个时候有2种办法可以解决。
  1. 先把构建好的Body数据写入到磁盘,再通过IO磁盘数据,提交给服务器
  2. 使用管道流,在读取磁盘数据进行body构建的时候,直接通过管道提交到远程服务器
管道流 管道流,顾名思义,可以往一边写,从另一边读。
// 创建读取流 PipedInputStream pipedInputStream = new PipedInputStream(); // 创建写入流 PipedOutputStream pipedOutputStream = new PipedOutputStream(); // 连接写入和读取流 pipedInputStream.connect(pipedOutputStream);

通过往pipedOutputStream写入数据,就可以在pipedInputStream 读取。
不能使用单线程既读又写,因为读写都是阻塞方法。任何一方阻塞住了了,另一方就会一直处于等待状态,导致死锁
完整Demo 客户端
package io.springcloud.test; import java.io.IOException; import java.io.InputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; 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.springframework.web.util.UriUtils; public class MainTest {public static void main(String[] args) throws Exception {// 管道流 PipedInputStream pipedInputStream = new PipedInputStream(); PipedOutputStream pipedOutputStream = new PipedOutputStream(); pipedInputStream.connect(pipedOutputStream); // 本地文件 InputStream file =Files.newInputStream(Paths.get("D:\\eclipse-jee-2019-12-R-win32-x86_64.zip")); // 构建Multipart请求 HttpEntity httpEntity = MultipartEntityBuilder.create() // 表单数据 .addPart("name", new StringBody(UriUtils.encode("SpringBoot中文社区", StandardCharsets.UTF_8), ContentType.APPLICATION_FORM_URLENCODED)) // JSON数据 .addPart("info", new StringBody("{\"site\": \"https://springboot.io\", \"now\": 2021}", ContentType.APPLICATION_JSON)) // 文件数据 .addBinaryBody("file", file, ContentType.APPLICATION_OCTET_STREAM, "eclipse-jee-2019-12-R-win32-x86_64.zip") .build(); // 异步写入数据到管道流 new Thread(() -> { try (file; pipedOutputStream){ httpEntity.writeTo(pipedOutputStream); } catch (IOException e) { e.printStackTrace(); } }).start(); HttpClient httpClient = HttpClient.newHttpClient(); try (pipedInputStream){ // 创建请求和请求体 HttpRequest request = HttpRequest .newBuilder(new URI("http://localhost/upload")) // 设置ContentType .header("Content-Type", httpEntity.getContentType().getValue()) .header("Accept", "text/plain") // 从管道流读取数据,提交给服务器 .POST(BodyPublishers.ofInputStream(() -> pipedInputStream)) .build(); // 执行请求,获取响应 HttpResponse responseBody = httpClient.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8)); System.out.println(responseBody.body()); } } }

服务端
package io.springcloud.web.controller; import java.nio.charset.StandardCharsets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 org.springframework.web.util.UriUtils; import com.google.gson.JsonObject; @RestController @RequestMapping("/upload") public class UploadController {private static final Logger LOGGER = LoggerFactory.getLogger(UploadController.class); @PostMapping public Object upload (@RequestPart("file") MultipartFile file, @RequestPart("info") JsonObject info, @RequestPart("name") String name) {LOGGER.info("file: name={}, size={}", file.getOriginalFilename(), file.getSize()); LOGGER.info("info: {}", info.toString()); LOGGER.info("name: {}", UriUtils.decode(name, StandardCharsets.UTF_8)); return ResponseEntity.ok("success"); } }

启动服务端后,执行客户端请求,服务端日志输出
2021-09-24 13:38:15.067INFO 2660 --- [XNIO-1 task-1] i.s.web.controller.UploadController: file: name=eclipse-jee-2019-12-R-win32-x86_64.zip, size=369653147 2021-09-24 13:38:15.067INFO 2660 --- [XNIO-1 task-1] i.s.web.controller.UploadController: info: {"site":"https://springboot.io","now":2021} 2021-09-24 13:38:15.067INFO 2660 --- [XNIO-1 task-1] i.s.web.controller.UploadController: name: SpringBoot中文社区

【HttpClient使用管道流提交Multipart请求】首发:https://springboot.io/t/topic...

    推荐阅读