HTTP 请求

Multipart 上传

import com.ajaxjs.util.httpremote.MultipartPost;
import com.ajaxjs.util.httpremote.model.Response;
import java.io.File;
import java.util.LinkedHashMap;
import java.util.Map;

Map<String, Object> parts = new LinkedHashMap<>();
parts.put("name", "Ada");
parts.put("document", new File("document.pdf"));
Response response = MultipartPost.upload("https://example.com/upload", parts,
        connection -> connection.setReadTimeout(30_000));
if (!response.isOk()) {
    throw new IllegalStateException("Upload failed: " + response.getHttpCode(), response.getEx());
}

MultipartPost.uploadFile 接受 (url, fieldName, File, callback)(url, fieldName, fileName, byte[]/InputStream, callback)调用方负责关闭传入的输入流。内部打开的文件流会关闭。上传流程管理并关闭请求输出流,读取并关闭响应输入流,在 finally 中断开连接。init 回调仅配置连接。

每次上传生成 boundary,回调后重新设置匹配的内容类型,使用 8192 字节分块传输并禁止重定向,避免自动重放上传。非 2xx 返回失败的 Response;校验、回调或写入错误可能抛出。若需重定向,请明确制定重放和目标校验策略。

MultipartWriter 仅负责编码:传入 OutputStream 和可选 boundary,然后只调用一次 write(...) 写完整正文。每次调用都输出结束 boundary,多字段/文件须放在一个 Map 中,不能多次追加。HTTP 头使用 getContentType() 的值。它不关闭也不刷新调用方输出流,不关闭调用方输入流。boundary 须匹配 [A-Za-z0-9_-]{1,70}。头部/文本使用 UTF-8;null 值变为空文本。名称中的 CR/LF 被拒绝,引号/反斜杠会转义。字节/流使用专用重载,不要依赖内部 Map 键元数据。I/O 错误包装为 UncheckedIOException

文件下载

import com.ajaxjs.util.httpremote.HttpFileDownload;

new HttpFileDownload("https://example.com/file.pdf", "downloads/report.pdf").download();
new HttpFileDownload(new String[] {
        "https://example.com/a.pdf", "https://example.com/b.pdf"
}, "downloads").downloadAllAsync().join();

注解代理:仅支持已实现的子集

import com.ajaxjs.util.httpremote.call.CallHandler;
import com.ajaxjs.util.httpremote.call.annotation.GET;
import com.ajaxjs.util.httpremote.call.annotation.Url;

public class ApiExample {
    @Url("https://example.com")
    public interface Api {
        @GET("/health")
        String health();
    }

    public static String health() {
        return CallHandler.create(Api.class).health();
    }
}

当前 CallHandler 分派 @GET@POST@PUT@DELETE,返回 String、Map 或 Bean。POST/PUT 选择第一个 Map 参数作为正文;@POST 默认 FORM,@PUT 默认 JSON_BODY。类级 @Url.initConnection 在方法级初始化器之前执行;初始化器会缓存,必须可以安全复用,并提供 public 无参构造函数。

路径替换依赖 Java 参数名(编译时使用 -parameters),直接使用 toString(),不做 URL 编码。URL 拼接只是简单连接,不是 URI 解析;请自行构造并校验编码后的值。

不能仅因源码存在注解/接口就认为已支持:

流、TLS 与日志

二进制响应应在连接前安装 Request.setInputStreamConsumer,在回调内读取,之后流会关闭。Head.gzip(connection, in) 只包装单个、不区分大小写的 gzip 编码 token(允许外围空白);无编码或组合编码会返回原流。构造 GZIPInputStream 时的 IOException 会包装为 UncheckedIOException;之后读取返回的流仍可能抛出受检异常 IOException,包括数据损坏或截断的情况。不会自动解压。

保持正常的 TLS 信任和主机名检查。SkipSSL.init() 修改 JVM 全局默认值,信任所有证书和主机名;setSSL_Ignore 对单个连接关闭两种校验。getSocketFactory 即便配置了客户端 key manager 也仍信任全部服务端,失败时可能返回 null。loadCert 为包级可见,不是应用可调用的公开 API。这些不是生产环境信任库配置工具。

Request.printLog 在 info 级别记录 URL、UTF-8 请求正文和最多 460 字符的响应,并将格式化文本放入 MDC。截断不等于脱敏。防止凭据/个人信息泄漏,不要通过信任所有 TLS 证书来“解决”生产证书错误。

依据:MultipartPostMultipartWriterHttpFileDownloadCallHandlerHeadSkipSSL;本地测试 TestMultipartPostTestMultipartWriterTestHttpIoHelpers。现有下载和代理测试包含网络/环境假设,不构成安全证明。