如何设置InputStream的内容长度

37

我正在将文件上传到Amazon S3存储桶。文件已经上传,但是我收到了以下警告:

警告:未为流数据指定内容长度。 流内容将在内存中缓冲,可能导致内存不足错误。

因此,我在我的代码中添加了以下行:

metaData.setContentLength(IOUtils.toByteArray(input).length);

但后来我收到了以下消息。我甚至不知道这是一个警告还是什么。

读取的数据长度与期望的长度不同:dataLength=0; expectedLength=111992; includeSkipped=false; in.getClass()=class sun.net.httpserver.FixedLengthInputStream; markedSupported=false; marked=0; resetSinceLastMarked=false; markCount=0; resetCount=0

如何将contentLength设置为InputSteam的metaData?任何帮助都将不胜感激。

3个回答

50

使用 IOUtils.toByteArray 读取数据时,会消耗 InputStream。这可能导致 AWS API 读取到的数据长度为零。

将内容读入字节数组并提供一个包装该数组的 InputStream 给 API:

byte[] bytes = IOUtils.toByteArray(input);
metaData.setContentLength(bytes.length);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, key, byteArrayInputStream, metadata);
client.putObject(putObjectRequest);

您应该考虑使用分段上传的 API,以避免将整个 InputStream 加载到内存中。例如:

byte[] bytes = new byte[BUFFER_SIZE];
String uploadId = client.initiateMultipartUpload(new InitiateMultipartUploadRequest(bucket, key)).getUploadId();

int bytesRead = 0;
int partNumber = 1;
List<UploadPartResult> results = new ArrayList<>();
bytesRead = input.read(bytes);
while (bytesRead >= 0) {
    UploadPartRequest part = new UploadPartRequest()
        .withBucketName(bucket)
        .withKey(key)
        .withUploadId(uploadId)
        .withPartNumber(partNumber)
        .withInputStream(new ByteArrayInputStream(bytes, 0, bytesRead))
        .withPartSize(bytesRead);
    results.add(client.uploadPart(part));
    bytesRead = input.read(bytes);
    partNumber++;
}
CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest()
    .withBucketName(bucket)
    .withKey(key)
    .withUploadId(uploadId)
    .withPartETags(results);
client.completeMultipartUpload(completeRequest);

12
请注意,如果您要将大文件上传到S3,请使用上述方法将数据首先加载到字节数组中。这可能会导致内存不足异常。 - maxTrialfire
@maxTrialfire 如果是这样,当您上传大文件时,如何防止OOM发生? - user482594
2
@user482594 如果是这种情况,您需要进行分块(多部分)上传。 - maxTrialfire
1
你也可以先写入一个文件,以避免OOM。 - Dave Moten
通过将整个流缓冲到内存中,这个解决方案只是手动重复了AWS SDK已经自动完成的工作,只是这次没有任何打印警告!要获得更高效的解决方案,请参见下面的答案。 - Robert Jack Will
1
这仍然会将整个文件加载到内存中。它没有解决问题,只是将问题转移了。 - BrianC

12

请注意,通过使用ByteBuffer,您只是手动执行了AWS SDK已经自动为您完成的操作!它仍将整个流缓冲到内存中,并且与产生SDK警告的原始解决方案一样好。

只有在您拥有另一种方法来知道流的长度时,才能摆脱内存问题,例如,当您从文件创建流时:

void uploadFile(String bucketName, File file) {
    try (final InputStream stream = new FileInputStream(file)) {
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(file.length());
        s3client.putObject(
                new PutObjectRequest(bucketName, file.getName(), stream, metadata)
        );
    }
}

只有在特定情况下,InputStream接口才不能让您精确地知道其长度,除非您读取它。 - kidnan1991
那么,如果使用URL类从互联网下载该文件,就没有办法了吗? - shinzou

0

热点新闻!AWS SDK 2.0 已经内置了上传文件的支持:

        s3client.putObject(
                (builder) -> builder.bucket(myBucket).key(file.getName()),
                RequestBody.fromFile(file)
        );

还有RequestBody方法可以接收字符串或缓冲区,并自动高效地设置Content-Length。仅当您有另一种类型的InputStream时,您仍然需要自己提供长度-不过现在有了所有其他选项,这种情况应该更少见。


1
该方法不会自动关闭输入流,最终您将遇到“打开文件过多”异常。 - Zoraida
但是你仍然需要知道长度,如果你只传递一个输入流是无法做到的。因此,在将文件发送到S3之前,仍需要将整个文件加载到内存中,或者让S3将文件加载到内存中。无论哪种方式,都不能流式传输数据。 - BrianC
如果您需要使用公共URL流上传文件,会不会将整个文件加载到内存/文件中? - shinzou

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接