web-dev-qa-db-ja.com

Java 9 HttpClientがmultipart / form-dataリクエストを送信します

以下はフォームです:

<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--

「リクエストペイロード」に注意してください。フォームには、ユーザー名とimg(form-data; name = "img"; filename = "out.txt")という2つのパラメータがあり、詳細名はファイルシステム内の実際のファイル名(またはパス)、バックエンド(Spring Controllerなど)で名前(ファイル名ではない)によってファイルを受け取ります。
リクエストをシミュレートするために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());

さて、どうすればパラメータの「名前」を設定できますか?

11
vicfan

私は、Apacheクライアントをプルすることなくプロジェクトでこれを実行したかったので、MultiPartBodyPublisher(Java 11、fyi)を作成しました。

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<InputStream>およびInputStreamだけではありません。

8
ittupelo

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);
    

OR

  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());
2
Naman

このページを見て読んだ後も、しばらくの間この問題に苦労しました。しかし、このページの回答を使用して正しい方向を示し、マルチパートフォームと境界についてさらに読み、いじくり回して、実用的なソリューションを作成することができました。

ソリューションの要点は、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;
}
2
liltitus27

multipart/form-dataまたはその他のコンテンツタイプを使用することは可能ですが、本体を正しい形式で自分でエンコードする必要があります。クライアント自体は、コンテンツタイプに基づくエンコーディングを行いません。

つまり、他のHTTPクライアントを使用する Apache HttpComponents クライアントを使用するか、 @ nullpointer の回答の例のように別のライブラリのエンコーダのみを使用するのが最善の方法です。


自分で本文をエンコードする場合、POSTのようなメソッドを複数回呼び出せないことに注意してください。 POSTは単にBodyProcessorを設定し、それを再度呼び出すだけで、以前に設定されたプロセッサがオーバーライドされます。全体を正しい形式で生成する1つのプロセッサを実装する必要があります。

multipart/form-dataの場合:

  1. boundaryヘッダーを適切な値に設定します
  2. 各パラメーターをエンコードして、例のようにします。基本的に、テキスト入力では次のようになります。

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

    ここでは、名前はHTMLフォームのname属性を参照しています。質問のファイル入力の場合、これはimgになり、値はエンコードされたファイルのコンテンツになります。

2
kapex

私は最近 library をリリースしましたJava 11's HttpClientに便利な拡張機能を提供します。ライブラリにはMultipartBodyPublisherが含まれており、便利で簡単に使用する MultipartBodyPublisher.Builder。これを使用した例を次に示します(JDK11以降が必要です)。

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

必要に応じて、BodyPublisher(またはHttpHeaders)を追加することもできます。詳細は wiki を参照してください。

0