【问题标题】:Java 9 HttpClient send a multipart/form-data requestJava 9 HttpClient 发送多部分/表单数据请求
【发布时间】:2018-03-05 15:40:24
【问题描述】:

下面是一个表格:

<form action="/example/html5/demo_form.asp" method="post" 
enctype=”multipart/form-data”>
   <input type="file" name="img" />
   <input type="text" name=username" value="foo"/>
   <input type="submit" />
</form>

何时提交此表单,请求将如下所示:

POST /example/html5/demo_form.asp HTTP/1.1
Host: 10.143.47.59:9093
Connection: keep-alive
Content-Length: 326
Accept: application/json, text/javascript, */*; q=0.01
Origin: http://10.143.47.59:9093
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryEDKBhMZFowP9Leno
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4

Request Payload
------WebKitFormBoundaryEDKBhMZFowP9Leno
Content-Disposition: form-data; name="username"

foo
------WebKitFormBoundaryEDKBhMZFowP9Leno
Content-Disposition: form-data; name="img"; filename="out.txt"
Content-Type: text/plain


------WebKitFormBoundaryEDKBhMZFowP9Leno--

请注意“Request Payload”,可以看到表单中的两个参数,用户名和img(form-data; name="img"; filename="out.txt"),以及Finename 是文件系统中的真实文件名(或路径),您将在后端(例如 spring 控制器)中按名称(而不是文件名)接收文件。
如果我们使用 Apache Httpclient 来模拟请求,我们会写这样的代码:

MultipartEntity mutiEntity = newMultipartEntity();
File file = new File("/path/to/your/file");
mutiEntity.addPart("username",new StringBody("foo", Charset.forName("utf-8")));
mutiEntity.addPart("img", newFileBody(file)); //img is name, file is path

但是在java 9中,我们可以写这样的代码:

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.
        newBuilder(new URI("http:///example/html5/demo_form.asp"))
       .method("post",HttpRequest.BodyProcessor.fromString("foo"))
       .method("post", HttpRequest.BodyProcessor.fromFile(Paths.get("/path/to/your/file")))
       .build();
HttpResponse response = client.send(request, HttpResponse.BodyHandler.asString());
System.out.println(response.body());

现在你明白了,我该如何设置参数的“名称”?

【问题讨论】:

  • 您能否分享一个在单击按钮时进行的示例 API 调用。您可以使用浏览器检查部分中的网络设置进行监控。
  • 您好,我知道如何监控网络请求,也知道如何使用 HttpClient Httpclient 发送此类请求。让我感到困惑的是如何使用 Java 9 中的 Httpclient 来做到这一点。
  • 我的意思是我知道如何使用“Apache”Httpclient发送这样的请求。
  • 已更新答案。此处使用的 util 仅用于将文件输入转换为字节数组,也可以是自定义实现。
  • 非常感谢您的帮助。

标签: java http multipartform-data http2 java-9


【解决方案1】:

您可以进行 multiform-data 调用的方向如下:

BodyProcessor 可以与它​​们的默认实现一起使用,或者也可以使用自定义实现。使用它们的几种方法是:

  1. 通过字符串读取处理器:

    HttpRequest.BodyProcessor dataProcessor = HttpRequest.BodyProcessor.fromString("{\"username\":\"foo\"}")
    
  2. 使用路径从文件创建处理器

    Path path = Paths.get("/path/to/your/file"); // in your case path to 'img'
    HttpRequest.BodyProcessor fileProcessor = HttpRequest.BodyProcessor.fromFile(path);
    

  1. 您可以使用apache.commons.lang(或您可以想出的自定义方法)将文件输入转换为字节数组,以添加一个小工具,例如:

    org.apache.commons.fileupload.FileItem file;
    
    org.apache.http.HttpEntity multipartEntity = org.apache.http.entity.mime.MultipartEntityBuilder.create()
           .addPart("username",new StringBody("foo", Charset.forName("utf-8")))
           .addPart("img", newFileBody(file))
           .build();
    multipartEntity.writeTo(byteArrayOutputStream);
    byte[] bytes = byteArrayOutputStream.toByteArray();
    

    然后 byte[] 可以与BodyProcessor 一起使用:

    HttpRequest.BodyProcessor byteProcessor = HttpRequest.BodyProcessor.fromByteArray();
    

此外,您可以将 request 创建为:

HttpRequest request = HttpRequest.newBuilder()
            .uri(new URI("http:///example/html5/demo_form.asp"))
            .headers("Content-Type","multipart/form-data","boundary","boundaryValue") // appropriate boundary values
            .POST(dataProcessor)
            .POST(fileProcessor)
            .POST(byteProcessor) //self-sufficient
            .build();

可以将相同的响应作为文件处理,并使用新的HttpClient 使用

HttpResponse.BodyHandler bodyHandler = HttpResponse.BodyHandler.asFile(Paths.get("/path"));

HttpClient client = HttpClient.newBuilder().build();

作为:

HttpResponse response = client.send(request, bodyHandler);
System.out.println(response.body());

【讨论】:

  • 我非常感谢您的回答。但是你能告诉文件的名称吗?就像 "Content-Disposition: form-data; name="myfile"; filename="/path/to/your/file" "
  • 是的,它的形式是'img',但是你在java代码中的哪里设置呢?
  • "Content-Disposition: form-data; name="myfile"; filename="/path/to/your/file" ---我指的是名称,而不是文件名
  • 不幸的是,多次调用POST 不起作用。您只能设置单个处理器。 (至少在最新版本的客户端中)
  • @Kapep 可能是,我自己也没有尝试过。这些只是一个可以利用的方向。 byteProcessor 不过,我认为应该是自给自足的。
【解决方案2】:

可以使用multipart/form-data 或任何其他内容类型 - 但您必须自己以正确的格式对正文进行编码。客户端本身不会根据内容类型进行任何编码。

这意味着您最好的选择是使用另一个 HTTP 客户端,例如 Apache HttpComponents 客户端,或者只使用另一个库的编码器,例如 @nullpointer 的答案。


如果您自己对正文进行编码,请注意您不能多次调用 POST 之类的方法。 POST 只需设置 BodyProcessor 并再次调用它将覆盖任何先前设置的处理器。您必须实现一个能够以正确格式生成整个主体的处理器。

对于multipart/form-data,这意味着:

  1. boundary 标头设置为适当的值
  2. 对每个参数进行编码,使其看起来像您的示例。文本输入基本上是这样的:

    boundary + "\nContent-Disposition: form-data; name=\"" + name + "\"\n\n" + value + "\n"
    

    这里,名称是指 HTML 表单中的 name 属性。对于问题中的文件输入,这将是img,值将是编码的文件内容。

【讨论】:

    【解决方案3】:

    即使在看到并阅读了此页面之后,我也曾为这个问题苦苦挣扎过一段时间。但是,使用此页面上的答案为我指明了正确的方向,阅读了有关多部分表单和边界的更多信息,并进行了修补,我能够创建一个可行的解决方案。

    解决方案的要点是使用 Apache 的 MultipartEntityBuilder 来创建实体及其边界(HttpExceptionBuilder 是一个本土类):

    import java.io.BufferedInputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.Optional;
    import java.util.function.Supplier;
    
    import org.apache.commons.lang3.Validate;
    import org.apache.http.HttpEntity;
    import org.apache.http.entity.BufferedHttpEntity;
    import org.apache.http.entity.ContentType;
    import org.apache.http.entity.mime.MultipartEntityBuilder;
    
    /**
     * Class containing static helper methods pertaining to HTTP interactions.
     */
    public class HttpUtils {
        public static final String MULTIPART_FORM_DATA_BOUNDARY = "ThisIsMyBoundaryThereAreManyLikeItButThisOneIsMine";
    
        /**
         * Creates an {@link HttpEntity} from a {@link File}, loading it into a {@link BufferedHttpEntity}.
         *
         * @param file     the {@link File} from which to create an {@link HttpEntity}
         * @param partName an {@link Optional} denoting the name of the form data; defaults to {@code data}
         * @return an {@link HttpEntity} containing the contents of the provided {@code file}
         * @throws NullPointerException  if {@code file} or {@code partName} is null
         * @throws IllegalStateException if {@code file} does not exist
         * @throws HttpException         if file cannot be found or {@link FileInputStream} cannot be created
         */
        public static HttpEntity getFileAsBufferedMultipartEntity(final File file, final Optional<String> partName) {
            Validate.notNull(file, "file cannot be null");
            Validate.validState(file.exists(), "file must exist");
            Validate.notNull(partName, "partName cannot be null");
    
            final HttpEntity entity;
            final BufferedHttpEntity bufferedHttpEntity;
    
            try (final FileInputStream fis = new FileInputStream(file);
                    final BufferedInputStream bis = new BufferedInputStream(fis)) {
                entity = MultipartEntityBuilder.create().setBoundary(MULTIPART_FORM_DATA_BOUNDARY)
                        .addBinaryBody(partName.orElse("data"), bis, ContentType.APPLICATION_OCTET_STREAM, file.getName())
                        .setContentType(ContentType.MULTIPART_FORM_DATA).build();
    
                try {
                    bufferedHttpEntity = new BufferedHttpEntity(entity);
                } catch (final IOException e) {
                    throw HttpExceptionBuilder.create().withMessage("Unable to create BufferedHttpEntity").withThrowable(e)
                            .build();
                }
            } catch (final FileNotFoundException e) {
                throw HttpExceptionBuilder.create()
                        .withMessage("File does not exist or is not readable: %s", file.getAbsolutePath()).withThrowable(e)
                        .build();
            } catch (final IOException e) {
                throw HttpExceptionBuilder.create()
                        .withMessage("Unable to create multipart entity from file: %s", file.getAbsolutePath())
                        .withThrowable(e).build();
            }
    
            return bufferedHttpEntity;
        }
    
        /**
         * Returns a {@link Supplier} of {@link InputStream} containing the content of the provided {@link HttpEntity}. This
         * method closes the {@code InputStream}.
         *
         * @param entity the {@link HttpEntity} from which to get an {@link InputStream}
         * @return an {@link InputStream} containing the {@link HttpEntity#getContent() content}
         * @throws NullPointerException if {@code entity} is null
         * @throws HttpException        if something goes wrong
         */
        public static Supplier<? extends InputStream> getInputStreamFromHttpEntity(final HttpEntity entity) {
            Validate.notNull(entity, "entity cannot be null");
    
            return () -> {
                try (final InputStream is = entity.getContent()) {
                    return is;
                } catch (final UnsupportedOperationException | IOException e) {
                    throw HttpExceptionBuilder.create().withMessage("Unable to get InputStream from HttpEntity")
                            .withThrowable(e).build();
                }
            };
        }
    }
    

    然后是使用这些辅助方法的方法:

    private String doUpload(final File uploadFile, final String filePostUrl) {
        assert uploadFile != null : "uploadFile cannot be null";
        assert uploadFile.exists() : "uploadFile must exist";
        assert StringUtils.notBlank(filePostUrl, "filePostUrl cannot be blank");
    
        final URI uri = URI.create(filePostUrl);
        final HttpEntity entity = HttpUtils.getFileAsBufferedMultipartEntity(uploadFile, Optional.of("partName"));
        final String response;
    
        try {
            final Builder requestBuilder = HttpRequest.newBuilder(uri)
                    .POST(BodyPublisher.fromInputStream(HttpUtils.getInputStreamFromHttpEntity(entity)))
                    .header("Content-Type", "multipart/form-data; boundary=" + HttpUtils.MULTIPART_FORM_DATA_BOUNDARY);
    
            response = this.httpClient.send(requestBuilder.build(), BodyHandler.asString());
        } catch (InterruptedException | ExecutionException e) {
            throw HttpExceptionBuilder.create().withMessage("Unable to get InputStream from HttpEntity")
                        .withThrowable(e).build();
        }
    
        LOGGER.info("Http Response: {}", response);
        return response;
    }
    

    【讨论】:

    • ThisIsMyBoundaryThereAreManyLikeItButThisOneIsMine 成就了我的一天
    【解决方案4】:

    我想为一个项目执行此操作,而不必引入 Apache 客户端,所以我写了一个 MultiPartBodyPublisher(Java 11,仅供参考):

    import java.io.IOException;
    import java.io.InputStream;
    import java.io.UncheckedIOException;
    import java.net.http.HttpRequest;
    import java.nio.charset.StandardCharsets;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.util.*;
    import java.util.function.Supplier;
    
    public class MultiPartBodyPublisher {
        private List<PartsSpecification> partsSpecificationList = new ArrayList<>();
        private String boundary = UUID.randomUUID().toString();
    
        public HttpRequest.BodyPublisher build() {
            if (partsSpecificationList.size() == 0) {
                throw new IllegalStateException("Must have at least one part to build multipart message.");
            }
            addFinalBoundaryPart();
            return HttpRequest.BodyPublishers.ofByteArrays(PartsIterator::new);
        }
    
        public String getBoundary() {
            return boundary;
        }
    
        public MultiPartBodyPublisher addPart(String name, String value) {
            PartsSpecification newPart = new PartsSpecification();
            newPart.type = PartsSpecification.TYPE.STRING;
            newPart.name = name;
            newPart.value = value;
            partsSpecificationList.add(newPart);
            return this;
        }
    
        public MultiPartBodyPublisher addPart(String name, Path value) {
            PartsSpecification newPart = new PartsSpecification();
            newPart.type = PartsSpecification.TYPE.FILE;
            newPart.name = name;
            newPart.path = value;
            partsSpecificationList.add(newPart);
            return this;
        }
    
        public MultiPartBodyPublisher addPart(String name, Supplier<InputStream> value, String filename, String contentType) {
            PartsSpecification newPart = new PartsSpecification();
            newPart.type = PartsSpecification.TYPE.STREAM;
            newPart.name = name;
            newPart.stream = value;
            newPart.filename = filename;
            newPart.contentType = contentType;
            partsSpecificationList.add(newPart);
            return this;
        }
    
        private void addFinalBoundaryPart() {
            PartsSpecification newPart = new PartsSpecification();
            newPart.type = PartsSpecification.TYPE.FINAL_BOUNDARY;
            newPart.value = "--" + boundary + "--";
            partsSpecificationList.add(newPart);
        }
    
        static class PartsSpecification {
    
            public enum TYPE {
                STRING, FILE, STREAM, FINAL_BOUNDARY
            }
    
            PartsSpecification.TYPE type;
            String name;
            String value;
            Path path;
            Supplier<InputStream> stream;
            String filename;
            String contentType;
    
        }
    
        class PartsIterator implements Iterator<byte[]> {
    
            private Iterator<PartsSpecification> iter;
            private InputStream currentFileInput;
    
            private boolean done;
            private byte[] next;
    
            PartsIterator() {
                iter = partsSpecificationList.iterator();
            }
    
            @Override
            public boolean hasNext() {
                if (done) return false;
                if (next != null) return true;
                try {
                    next = computeNext();
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
                if (next == null) {
                    done = true;
                    return false;
                }
                return true;
            }
    
            @Override
            public byte[] next() {
                if (!hasNext()) throw new NoSuchElementException();
                byte[] res = next;
                next = null;
                return res;
            }
    
            private byte[] computeNext() throws IOException {
                if (currentFileInput == null) {
                    if (!iter.hasNext()) return null;
                    PartsSpecification nextPart = iter.next();
                    if (PartsSpecification.TYPE.STRING.equals(nextPart.type)) {
                        String part =
                                "--" + boundary + "\r\n" +
                                "Content-Disposition: form-data; name=" + nextPart.name + "\r\n" +
                                "Content-Type: text/plain; charset=UTF-8\r\n\r\n" +
                                nextPart.value + "\r\n";
                        return part.getBytes(StandardCharsets.UTF_8);
                    }
                    if (PartsSpecification.TYPE.FINAL_BOUNDARY.equals(nextPart.type)) {
                        return nextPart.value.getBytes(StandardCharsets.UTF_8);
                    }
                    String filename;
                    String contentType;
                    if (PartsSpecification.TYPE.FILE.equals(nextPart.type)) {
                        Path path = nextPart.path;
                        filename = path.getFileName().toString();
                        contentType = Files.probeContentType(path);
                        if (contentType == null) contentType = "application/octet-stream";
                        currentFileInput = Files.newInputStream(path);
                    } else {
                        filename = nextPart.filename;
                        contentType = nextPart.contentType;
                        if (contentType == null) contentType = "application/octet-stream";
                        currentFileInput = nextPart.stream.get();
                    }
                    String partHeader =
                            "--" + boundary + "\r\n" +
                            "Content-Disposition: form-data; name=" + nextPart.name + "; filename=" + filename + "\r\n" +
                            "Content-Type: " + contentType + "\r\n\r\n";
                    return partHeader.getBytes(StandardCharsets.UTF_8);
                } else {
                    byte[] buf = new byte[8192];
                    int r = currentFileInput.read(buf);
                    if (r > 0) {
                        byte[] actualBytes = new byte[r];
                        System.arraycopy(buf, 0, actualBytes, 0, r);
                        return actualBytes;
                    } else {
                        currentFileInput.close();
                        currentFileInput = null;
                        return "\r\n".getBytes(StandardCharsets.UTF_8);
                    }
                }
            }
        }
    }
    

    你可以大致这样使用它:

    MultiPartBodyPublisher publisher = new MultiPartBodyPublisher()
           .addPart("someString", "foo")
           .addPart("someInputStream", () -> this.getClass().getResourceAsStream("test.txt"), "test.txt", "text/plain")
           .addPart("someFile", pathObject);
    HttpRequest request = HttpRequest.newBuilder()
           .uri(URI.create("https://www.example.com/dosomething"))
           .header("Content-Type", "multipart/form-data; boundary=" + publisher.getBoundary())
           .timeout(Duration.ofMinutes(1))
           .POST(publisher.build())
           .build();
    

    请注意,输入流的addPart 实际上采用Supplier&lt;InputStream&gt; 而不仅仅是InputStream

    【讨论】:

    • 考虑把这个放到github上。
    • @ittupelo 你值得喝啤酒!这绝对是美丽的
    • @ittupelo 现在我已经更仔细地查看并使用了它 - 它可以大大简化,我希望有时间将它发布在 github 上。在我看来,当要上传 2 个文件时,您似乎也有问题,因为我已经更改了很多代码,无法确定。
    • 我确实使用它在一个请求中上传多个文件,所以应该可以。很想看到你的变化……我最近没想太多。
    • 这确实很棒!我必须修改最后一位以使文件字节流工作。 byte[] bytes = currentFileInput.readAllBytes(); currentFileInput.close(); currentFileInput = null; byte[] actual = new byte[bytes.length + 2]; byte[] newline = "\r\n".getBytes(StandardCharsets.UTF_8); System.arraycopy(bytes, 0, actual, 0, bytes.length); System.arraycopy(newline, 0, actual, bytes.length, newline.length); return actual; 很抱歉格式不好。
    【解决方案5】:

    您可以使用Methanol。它包含一个MultipartBodyPublisher 和一个方便易用的MultipartBodyPublisher.Builder。下面是一个使用它的例子(需要JDK11或更高版本):

    var multipartBody = MultipartBodyPublisher.newBuilder()
        .textPart("foo", "foo_text")
        .filePart("bar", Path.of("path/to/file.txt"))
        .formPart("baz", BodyPublishers.ofInputStream(() -> ...))
        .build();
    var request = HttpRequest.newBuilder()
        .uri(URI.create("https://example.com/"))
        .POST(multipartBody)
        .build();
    

    请注意,您可以添加任何您想要的BodyPublisherHttpHeaders。查看docs 了解更多信息。

    【讨论】:

      【解决方案6】:

      虽然正确的答案是成熟的实现并且可能是正确的,但它对我不起作用。

      我的解决方案灵感来自here。我刚刚清理了我的用例不需要的部分。我个人,使用多部分形式仅上传图片或 zip 文件(单数)。代码:

          public static HttpRequest buildMultiformRequest(byte[] body) {
              String boundary = "-------------" + UUID.randomUUID().toString();
              Map<String, byte[]> data = Map.of("formFile", body);
      
              return HttpRequest.newBuilder()
                      .uri(URI.create(<URL>))
                      .POST(HttpRequest.BodyPublishers.ofByteArrays(buildMultipartData(data, boundary, "filename.jpeg", MediaType.IMAGE_JPEG_VALUE)))
                      .header("Content-Type", "multipart/form-data; boundary=" + boundary)
                      .header("Accept", MediaType.APPLICATION_JSON_VALUE)
                      .timeout(Duration.of(5, ChronoUnit.SECONDS))
                      .build();
          }
      
          public static ArrayList<byte[]> buildMultipartData(Map<String, byte[]> data, String boundary, String filename, String mediaType) {
              var byteArrays = new ArrayList<byte[]>();
              var separator = ("--" + boundary + "\r\nContent-Disposition: form-data; name=").getBytes(StandardCharsets.UTF_8);
      
              for (var entry : data.entrySet()) {
                  byteArrays.add(separator);
                  byteArrays.add(("\"" + entry.getKey() + "\"; filename=\"" + filename + "\"\r\nContent-Type:" + mediaType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8));
                  byteArrays.add(entry.getValue());
                  byteArrays.add("\r\n".getBytes(StandardCharsets.UTF_8));
              }
      
              byteArrays.add(("--" + boundary + "--").getBytes(StandardCharsets.UTF_8));
              return byteArrays;
          }
      

      【讨论】:

        【解决方案7】:

        以下内容对我有用,即在内存中创建一个原始 HTTP 正文作为字符串,然后使用标准 BodyPublisher.ofString

        以下链接显示了正文的外观:https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST

        String data = "--boundary\nContent-Disposition: form-data; name=\"type\"\r\n\r\nserverless";
        byte[] fileContents = Files.readAllBytes(f.toPath());
        data += "\r\n--boundary\nContent-Disposition: form-data; name=\"filename\"; filename=\""
                + f.getName() + "\"\r\n\r\n" + new String(fileContents, StandardCharsets.ISO_8859_1); // iso-8859-1 is http default
        data += "\r\n--boundary--"; // end boundary
        
        HttpRequest.BodyPublisher bodyPublisher = HttpRequest.BodyPublishers.ofString(data, StandardCharsets.ISO_8859_1);
        
        HttpRequest request = HttpRequest.newBuilder()
                            .uri(uri)
                            .setHeader("Content-Type", "multipart/form-data;boundary=\"boundary\"")
                            .POST(bodyPublisher).build();
        HttpResponse<String> response = getClient().send(request, HttpResponse.BodyHandlers.ofString());
        

        注意\r\n 而不是只说\n - 我使用 Apache Commons File Upload 测试了这两者,可能是因为这是 RFC 所期望的。

        还要注意使用 ISO-8859-1 而不是 UTF-8。我使用它是因为它是标准的 - 我没有使用 UTF-8 对其进行测试 - 如果服务器也是这样配置的,它可能会起作用。

        getClient 大致是这样做的:

        HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_1_1)
            .connectTimeout(Duration.ofSeconds(20))
            .build()
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2020-06-18
          • 2016-11-11
          • 1970-01-01
          • 2020-10-12
          • 2018-09-15
          • 2022-12-05
          • 2023-04-08
          • 2020-01-18
          相关资源
          最近更新 更多