登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Java HttpRequest 怎样设置请求体发布器

来源:17golang原创

时间:2026-10-26 23:44:10 386浏览 收藏

Java HttpRequest 的请求体不是直接挂一个字符串,而是交给 HttpRequest.BodyPublisher 持续提供要发送的字节。最常用的写法是先根据数据来源选择 BodyPublishers 工厂,再把返回值传给 POST、PUT 或 method:

import java.net.URI;
import java.net.http.HttpRequest;
import java.nio.charset.StandardCharsets;

// 字符串请求体按 UTF-8 转成 BodyPublisher,再绑定到 POST
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.example.test/users"))
        .header("Content-Type", "application/json; charset=UTF-8")
        .POST(HttpRequest.BodyPublishers.ofString(
                "{\"name\":\"Ada\"}", StandardCharsets.UTF_8))
        .build();

官方 API 地址:https://docs.oracle.com/en/java/javase/25/docs/api/java.net.http/java/net/http/HttpRequest.BodyPublishers.html

选择请求体发布器时,先判断数据是文本、字节、文件还是流,再检查字符集、长度和是否可能重复发布;不要把所有输入都先拼成一个大字符串。

把请求体来源抽象成 BodyPublisher

HttpRequest.Builder 负责 URI、请求头和方法等请求定义,BodyPublisher 负责把高层数据转成适合发送的字节流,HttpClient 才负责实际发送。这个分工很像一个发布器策略:请求构建代码只依赖发布器接口,数据来源的差异交给 BodyPublishers 工厂封装。

HttpRequest Builder、BodyPublisher 工厂、HTTP 方法和 HttpClient 发送之间的静态职责结构图
图1:请求体发布器职责结构图,展示请求定义、发布器和发送接口的静态边界;这是说明图,不是运行截图。

因此,设置请求体的核心不是寻找一个“请求体字段”,而是先得到一个 BodyPublisher。POST、PUT 和通用的 method 方法都接受它;请求头中的 Content-Type 仍然需要由调用方按接口契约设置,两者解决的是不同问题。

按数据来源选择 BodyPublishers 工厂

常见工厂可以按输入形态分组。文本优先考虑 ofString,已经有完整字节时使用 ofByteArray,文件内容使用 ofFile。如果数据来自响应式发布器或需要拼接多个片段,再考虑 fromPublisher 与 concat。

数据来源工厂关键边界
Java 字符串ofString(body) 或指定 Charset不指定时按 UTF-8 转换
完整字节数组ofByteArray(bytes)可用 offset 和 length 选取子范围
本地文件ofFile(path)路径不存在时会抛出文件未找到异常
输入流供应器ofInputStream(supplier)重复发送时供应器需要能重新提供流
Flow 发布器fromPublisher(publisher)不传长度时长度未知
多个发布器concat(publishers)只有组成部分都已知长度时总长度才已知
import java.net.http.HttpRequest;
import java.nio.file.Path;

// 文件内容由发布器按路径提供,不需要先把整个文件读进字符串
HttpRequest.BodyPublisher fileBody = HttpRequest.BodyPublishers.ofFile(
        Path.of("payload.json"));

// 二进制协议已经准备好字节时,直接发布指定数组
HttpRequest.BodyPublisher binaryBody = HttpRequest.BodyPublishers.ofByteArray(
        new byte[] {0x01, 0x02, 0x03});

// 没有请求体时使用空发布器,适合需要显式传入 BodyPublisher 的自定义方法
HttpRequest.BodyPublisher emptyBody = HttpRequest.BodyPublishers.noBody();

这些工厂不会替你决定业务数据格式。JSON 是否有效、字节数组是否采用正确编码、文件内容是否匹配接口,都属于调用方自己的契约;发布器只负责把已经确定的数据来源接到请求构建器上。

字符串、字节数组、文件、输入流和 Flow.Publisher 适配到 BodyPublisher 的静态关系图
图2:数据来源与长度契约结构图,展示不同 BodyPublishers 工厂的输入边界和复用约束;这是说明图,不是运行截图。

把发布器绑定到 POST、PUT 和自定义方法

请求方法决定发布器在请求定义中的位置。普通表单或 JSON 提交可以用 POST(publisher),资源整体替换常用 PUT(publisher);需要 PATCH 等其他方法时使用 method(name, publisher)。方法名和请求体内容是两个独立维度,不要通过修改 Content-Type 来代替选择 HTTP 方法。

import java.net.URI;
import java.net.http.HttpRequest;

String json = "{\"enabled\":true}";
HttpRequest.BodyPublisher body = HttpRequest.BodyPublishers.ofString(json);

// POST 使用发布器提交一份资源或命令数据
HttpRequest post = HttpRequest.newBuilder(URI.create("https://api.example.test/jobs"))
        .header("Content-Type", "application/json")
        .POST(body)
        .build();

// method 允许把同一个发布器交给接口约定的其他 HTTP 方法
HttpRequest patch = HttpRequest.newBuilder(URI.create("https://api.example.test/jobs/7"))
        .header("Content-Type", "application/json")
        .method("PATCH", body)
        .build();

同一个发布器是否适合被多个请求复用,要结合它的来源判断。内存中的字符串和字节数组通常更容易重复提供;文件发布器由路径描述内容;输入流发布器则需要特别关注每次发送是否都能拿到新的流。不要因为变量可以复用,就推断底层数据一定可以无限次读取。

处理长度契约与输入流复用

fromPublisher(publisher) 会把长度视为未知;带第二个参数的版本则把长度声明为一个正数,而且这个数必须等于发布器实际发送的字节数。长度不是估算值,少发或多发都会破坏请求体契约。

import java.net.http.HttpRequest;
import java.nio.ByteBuffer;
import java.util.concurrent.Flow;

// publisher 的每次发布必须恰好提供 12 个字节
Flow.Publisher publisher = createPublisher();
HttpRequest.BodyPublisher fixedLength = HttpRequest.BodyPublishers
        .fromPublisher(publisher, 12L);

// 未知长度版本不声明总字节数,不能把它当成“长度大约为 12”
HttpRequest.BodyPublisher unknownLength = HttpRequest.BodyPublishers
        .fromPublisher(publisher);

上面的 createPublisher() 只是示意一个已有的 Flow.Publisher,本文没有运行这段代码。若长度已知,声明值必须是正数且与实际字节数完全一致;若长度未知,就使用不带长度的适配器,而不是猜一个数字。

ofInputStream 接收的是 Supplier,而不是已经打开的一次性流。官方 API 特别强调请求需要重复发送时会再次调用供应器,因此供应器应按调用重新打开资源;把同一个已经读到末尾的流反复返回,会让重试或重复发布失去正确的数据来源。

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.http.HttpRequest;
import java.nio.file.Files;
import java.nio.file.Path;

Path payload = Path.of("payload.bin");
HttpRequest.BodyPublisher streamBody = HttpRequest.BodyPublishers.ofInputStream(() -> {
    try {
        // 每次调用都创建新的流,避免把一次性流跨请求复用
        return Files.newInputStream(payload);
    } catch (IOException error) {
        // Supplier 不能直接抛 checked exception,保留原始原因再转为 unchecked
        throw new UncheckedIOException("无法打开请求体文件", error);
    }
});

用反例和判断清单检查实现

最常见的错误不是不会调用工厂,而是把不同层面的契约混在一起。下面几项可以作为代码评审时的快速检查:

  • 文本是否明确指定了接口要求的字符集;不指定时,ofString 按 UTF-8 转换。
  • 已知长度是否以字节为单位,而不是 Java 字符数、代码点数量或字符串长度。
  • fromPublisher 的声明长度是否和实际发布字节数相等;不确定时不要虚构长度。
  • 输入流供应器是否每次都能提供新的流,并在错误路径保留底层异常原因。
  • 请求方法是否与接口契约一致;Content-Type 只描述媒体类型,不能代替 POST、PUT 或 PATCH。
  • 确实需要无体请求时,是否使用了 noBody() 或对应的无体方法,而不是传入一个内容为空但语义不清的字符串。

一个典型反例是把大文件先读成字符串,再调用 ofString。它把文件的字节语义改成了字符语义,增加了内存占用,也可能在编码不匹配时改变原始内容。文件本身就是二进制载荷时,优先使用 ofFile;只有接口明确要求文本转换时,才在入口决定字符集。

另一个反例是把 fromPublisher(publisher, 1024) 当成“最多 1024 字节”。这个重载表达的是精确长度,不是上限;如果只能知道上限,应改用未知长度版本,或者让上游发布器建立真实的长度契约。

HttpRequest 怎样发送 JSON 请求体?

先把 JSON 文本交给 BodyPublishers.ofString,再在 Builder 上设置 Content-Type: application/json,最后传给 POST 或接口要求的其他方法。

ofString 默认使用什么字符集?

无 Charset 参数的 ofString 使用 UTF-8。若服务端要求其他字符集,应显式传入 Charset,并让 Content-Type 的声明与实际编码保持一致。

fromPublisher 的 contentLength 可以填写 0 吗?

不能。带长度的重载要求正数,并且该长度必须等于发布器实际发送的字节数;需要无体语义时应使用 noBody()。

为什么 ofInputStream 要传 Supplier?

请求可能需要重复发送,Supplier 可以在每次需要时重新取得输入流。返回同一个已消费流,不能保证后续发布仍有完整请求体。

归纳起来,Java HttpRequest 设置请求体的稳定模式是:先按数据来源选择 BodyPublishers,再把得到的 BodyPublisher 绑定到 HTTP 方法;最后核对字符集、字节长度和流的可重复性。这样请求构建代码表达的是协议意图,数据读取代码则保留自己的资源边界。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>