[英]Java 9 HttpClient send a multipart/form-data request
下面是一個表格:
<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”,在form中可以看到兩個參數,username和img(form-data; name="img"; filename="out.txt"),finename就是文件系統中的真實文件名(或路徑),您將在后端(例如彈簧控制器)中按名稱(而不是文件名)接收文件。
如果我們使用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());
現在您明白了,我如何設置參數的“名稱”?
我想在不需要引入 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<InputStream>
而不僅僅是InputStream
。
您可以實現多形式數據調用的方向如下:
BodyProcessor
可以與它們的默認實現一起使用,或者也可以使用自定義實現。 使用它們的方法很少:
通過字符串讀取處理器為:
HttpRequest.BodyProcessor dataProcessor = HttpRequest.BodyProcessor.fromString("{\\"username\\":\\"foo\\"}")
使用文件路徑從文件創建處理器
Path path = Paths.get("/path/to/your/file"); // in your case path to 'img' HttpRequest.BodyProcessor fileProcessor = HttpRequest.BodyProcessor.fromFile(path);
或
您可以使用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();
此外,您可以將請求創建為:
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());
您可以使用甲醇。 它包含一個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();
請注意,您可以添加任何您想要的BodyPublisher
或HttpHeaders
。 查看文檔以獲取更多信息。
可以使用multipart/form-data
或任何其他內容類型 - 但您必須自己以正確的格式對正文進行編碼。 客戶端本身不會根據內容類型進行任何編碼。
這意味着您最好的選擇是使用另一個 HTTP 客戶端,例如Apache HttpComponents客戶端,或者只使用另一個庫的編碼器,例如@nullpointer的答案示例。
如果您自己對主體進行編碼,請注意您不能多次調用POST
等方法。 POST
只是設置BodyProcessor
並再次調用它只會覆蓋任何先前設置的處理器。 您必須實現一個以正確格式生成整個身體的處理器。
對於multipart/form-data
,這意味着:
boundary
標頭設置為適當的值編碼每個參數,使其在您的示例中看起來像。 對於文本輸入,基本上是這樣的:
boundary + "\\nContent-Disposition: form-data; name=\\"" + name + "\\"\\n\\n" + value + "\\n"
這里,名稱指的是 HTML 表單中的name
屬性。 對於問題中的文件輸入,這將是img
,值將是編碼的文件內容。
我在這個問題上掙扎了一段時間,即使在看到和閱讀了這個頁面之后。 但是,使用此頁面上的答案為我指明了正確的方向,閱讀了有關多部分表單和邊界的更多信息,並進行了修補,我能夠創建一個可行的解決方案。
解決方案的要點是使用 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;
}
雖然正確答案是全面實施並且可能是正確的,但它對我不起作用。
我的解決方案從這里獲得靈感。 我剛剛清理了我的用例不需要的部分。 我個人使用多部分表單僅上傳圖片或 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;
}
以下對我BodyPublisher.ofString
,即在內存中創建一個原始 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()
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.