|
@@ -0,0 +1,184 @@
|
|
|
|
|
+package space.anyi.httpClient;
|
|
|
|
|
+
|
|
|
|
|
+import com.fasterxml.jackson.core.type.TypeReference;
|
|
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
+import space.anyi.httpClient.model.FileVO;
|
|
|
|
|
+import space.anyi.httpClient.model.Result;
|
|
|
|
|
+import space.anyi.httpClient.model.UserRequest;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+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.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.nio.file.Files;
|
|
|
|
|
+import java.nio.file.Path;
|
|
|
|
|
+import java.util.UUID;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 请求体示例:BodyPublisher 的多种用法
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>请求方法(POST/PUT 等)通过 {@code .POST(BodyPublisher)} 设置请求体,
|
|
|
|
|
+ * BodyPublisher 是请求体的抽象,JDK 内置了多种实现:</p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>{@link BodyPublishers#ofString(String)}:字符串请求体(最常用)</li>
|
|
|
|
|
+ * <li>{@link BodyPublishers#ofByteArray(byte[])}:字节数组请求体</li>
|
|
|
|
|
+ * <li>{@link BodyPublishers#ofInputStream}:输入流请求体</li>
|
|
|
|
|
+ * <li>{@link BodyPublishers#noBody()}:空请求体(GET/DELETE 默认)</li>
|
|
|
|
|
+ * <li>{@link BodyPublishers#ofFile(Path)}:文件请求体</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>同时演示手工构造 <b>multipart/form-data</b> 文件上传请求体
|
|
|
|
|
+ * (JDK 未内置 multipart 支持,按 RFC 2046 规范手工拼接)。</p>
|
|
|
|
|
+ */
|
|
|
|
|
+public class BodyExample {
|
|
|
|
|
+
|
|
|
|
|
+ /** 服务基地址常量 */
|
|
|
|
|
+ private static final String BASE_URL = "http://localhost:8080";
|
|
|
|
|
+
|
|
|
|
|
+ /** Jackson 对象映射器 */
|
|
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 方式一:ofString — 用字符串作为 JSON 请求体创建用户。
|
|
|
|
|
+ */
|
|
|
|
|
+ public HttpResponse<String> createUserWithJsonString(UserRequest user)
|
|
|
|
|
+ throws IOException, InterruptedException {
|
|
|
|
|
+ String json = objectMapper.writeValueAsString(user);
|
|
|
|
|
+
|
|
|
|
|
+ HttpRequest request = HttpRequest.newBuilder()
|
|
|
|
|
+ .uri(URI.create(BASE_URL + "/api/users"))
|
|
|
|
|
+ .header("Content-Type", "application/json")
|
|
|
|
|
+ .POST(BodyPublishers.ofString(json))
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ return HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 方式二:ofInputStream — 以输入流作为请求体。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>适用场景:请求体较大、不想一次性载入内存时(流式传输)。
|
|
|
|
|
+ * 这里用 ByteArrayInputStream 包一层作演示。</p>
|
|
|
|
|
+ */
|
|
|
|
|
+ public HttpResponse<String> createUserViaInputStream(UserRequest user)
|
|
|
|
|
+ throws IOException, InterruptedException {
|
|
|
|
|
+ String json = objectMapper.writeValueAsString(user);
|
|
|
|
|
+
|
|
|
|
|
+ // 用缓冲输入流包装 JSON 字符串,交给 ofInputStream 发送
|
|
|
|
|
+ ByteArrayInputStream stream =
|
|
|
|
|
+ new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+
|
|
|
|
|
+ HttpRequest request = HttpRequest.newBuilder()
|
|
|
|
|
+ .uri(URI.create(BASE_URL + "/api/users"))
|
|
|
|
|
+ .header("Content-Type", "application/json")
|
|
|
|
|
+ // Supplier 会延迟到真正发送时才获取输入流
|
|
|
|
|
+ .POST(BodyPublishers.ofInputStream(() -> stream))
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ return HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 方式三:ofByteArray + 手工构建 multipart/form-data 请求体 — 文件上传。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>multipart 请求体格式(RFC 2046),每个字段用 boundary 分隔:</p>
|
|
|
|
|
+ * <pre>
|
|
|
|
|
+ * --boundary\r\n
|
|
|
|
|
+ * Content-Disposition: form-data; name="file"; filename="report.txt"\r\n
|
|
|
|
|
+ * Content-Type: text/plain\r\n
|
|
|
|
|
+ * \r\n
|
|
|
|
|
+ * (文件的二进制内容)\r\n
|
|
|
|
|
+ * --boundary--\r\n
|
|
|
|
|
+ * </pre>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param file 待上传的文件(含文件名与内容)
|
|
|
|
|
+ * @return 上传响应
|
|
|
|
|
+ */
|
|
|
|
|
+ public HttpResponse<String> uploadFile(Path file) throws IOException, InterruptedException {
|
|
|
|
|
+ byte[] fileBytes = Files.readAllBytes(file);
|
|
|
|
|
+
|
|
|
|
|
+ // 生成唯一 boundary,客户端与请求头中必须一致
|
|
|
|
|
+ String boundary = "----WebKitFormBoundary" + UUID.randomUUID();
|
|
|
|
|
+
|
|
|
|
|
+ // 组装 multipart 请求体字节数组
|
|
|
|
|
+ byte[] body = buildMultipartBody(file.getFileName().toString(), fileBytes, boundary);
|
|
|
|
|
+
|
|
|
|
|
+ HttpRequest request = HttpRequest.newBuilder()
|
|
|
|
|
+ .uri(URI.create(BASE_URL + "/api/files/upload"))
|
|
|
|
|
+ // Content-Type 必须带 boundary,服务器据此切分各个字段
|
|
|
|
|
+ .header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
|
|
|
|
+ .POST(BodyPublishers.ofByteArray(body))
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ return HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 组装 multipart/form-data 请求体(参考 RFC 2046 规范)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param fileName 上传字段的原始文件名
|
|
|
|
|
+ * @param fileBytes 文件内容字节
|
|
|
|
|
+ * @param boundary 分隔标记
|
|
|
|
|
+ * @return 完整的请求体字节数组
|
|
|
|
|
+ */
|
|
|
|
|
+ private byte[] buildMultipartBody(String fileName, byte[] fileBytes, String boundary)
|
|
|
|
|
+ throws IOException {
|
|
|
|
|
+ // 使用可变字节流拼接:不需要一次性构造大字符串,避免大文件内存翻倍
|
|
|
|
|
+ java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
|
|
|
|
|
+
|
|
|
|
|
+ // 第一个 part 的分隔线
|
|
|
|
|
+ out.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ // 声明该 part 是文件字段(name 对应服务端 @RequestParam("file"))
|
|
|
|
|
+ out.write(("Content-Disposition: form-data; name=\"file\"; filename=\""
|
|
|
|
|
+ + fileName + "\"\r\n").getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ // 声明文件内容类型
|
|
|
|
|
+ out.write(("Content-Type: " + guessContentType(fileName) + "\r\n").getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ // 空行分隔请求头与内容
|
|
|
|
|
+ out.write("\r\n".getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ // 文件二进制内容
|
|
|
|
|
+ out.write(fileBytes);
|
|
|
|
|
+ // 结尾分隔线(--boundary-- 表示结束)
|
|
|
|
|
+ out.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+
|
|
|
|
|
+ return out.toByteArray();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 根据文件扩展名猜测 MIME 类型,JDK 内置工具 Files.probeContentType。
|
|
|
|
|
+ */
|
|
|
|
|
+ private String guessContentType(String fileName) throws IOException {
|
|
|
|
|
+ String type = Files.probeContentType(Path.of(fileName));
|
|
|
|
|
+ return type == null ? "application/octet-stream" : type;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析上传响应,返回文件元信息(Result<FileVO> 结构)。
|
|
|
|
|
+ */
|
|
|
|
|
+ public FileVO uploadFileAndGetFileVO(Path file) throws IOException, InterruptedException {
|
|
|
|
|
+ HttpResponse<String> response = uploadFile(file);
|
|
|
|
|
+
|
|
|
|
|
+ Result<FileVO> result = objectMapper.readValue(
|
|
|
|
|
+ response.body(), new TypeReference<Result<FileVO>>() {
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (result.getCode() != 200) {
|
|
|
|
|
+ throw new IllegalStateException("上传失败: " + result.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ return result.getData();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 演示 noBody():构建一个不含请求体的请求(此处用于展示 GET 写法)。
|
|
|
|
|
+ */
|
|
|
|
|
+ public HttpRequest buildNoBodyGetRequest() {
|
|
|
|
|
+ return HttpRequest.newBuilder()
|
|
|
|
|
+ .uri(URI.create(BASE_URL + "/api/users"))
|
|
|
|
|
+ // GET 不需要请求体,显式声明 noBody 使意图更清晰
|
|
|
|
|
+ .GET()
|
|
|
|
|
+ .build();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|