Spring boot REST API 使用 WebClient 和 DataBufferUtils 从服务器流式传输大文件下载

问题描述 投票:0回答:1

我正在从 Rest 控制器编写一个 API,以从服务器下载大文件,而不会使内存过载。为此,我正在尝试下载流式传输文件。

目前,我已经使用 WebClient 发送 GET 请求,然后将响应写入 HttpServletResponse

这是我的API代码

@GetMapping(value = "/download/streaming/{fileId}", produces = { "application/pdf", "application/json" })
public void downloadDocumentStreaming(@Parameter(name = "fileId") @PathVariable("fileId") String fileId,
        HttpServletResponse response) {
    try {
        HttpHeaders httpHeaders = //Some http header
        httpHeaders.setAccept(List.of(MediaType.APPLICATION_OCTET_STREAM));

        Flux<DataBuffer> flux = WebClient.create().get()
                .uri(builder -> builder.host(serverHostUrl).port(serverPort).scheme("https")
                        .path("/dowload/file/{fileId}").build(fileId))
                .headers(headers -> headers.addAll(httpHeaders)).retrieve().bodyToFlux(DataBuffer.class);

        DataBufferUtils.write(flux, response.getOutputStream()).share().blockLast();

    } catch (IOException e) {
        e.printStackTrace();
    }
}

为了测试我的 API 是否没有使内存过载,我使用以下 VM 参数运行应用程序:-Xmx64m

下载适用于小于 64 MB 的文件 但对于最大的文件(例如:74 MB 文件),我在 Postman 测试时收到以下错误。

Caused by: java.lang.OutOfMemoryError: Cannot reserve 4194304 bytes of direct buffer memory (allocated: 63010883, limit: 67108864)
at java.base/java.nio.Bits.reserveMemory(Bits.java:178) ~[na:na]
at java.base/java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:121) ~[na:na]
at java.base/java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:332) ~[na:na]
at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:701) ~[netty-buffer-4.1.87.Final.jar:4.1.87.Final]
at io.netty.buffer.PoolArena$DirectArena.newChunk(PoolArena.java:676) ~[netty-buffer-4.1.87.Final.jar:4.1.87.Final]
at io.netty.buffer.PoolArena.allocateNormal(PoolArena.java:215) ~[netty-buffer-4.1.87.Final.jar:4.1.87.Final]
at io.netty.buffer.PoolArena.tcacheAllocateNormal(PoolArena.java:197) ~[netty-buffer-4.1.87.Final.jar:4.1.87.Final]
at io.netty.buffer.PoolArena.allocate(PoolArena.java:139) ~[netty-buffer-4.1.87.Final.jar:4.1.87.Final]
at io.netty.buffer.PoolArena.allocate(PoolArena.java:129) ~[netty-buffer-4.1.87.Final.jar:4.1.87.Final]
at io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:396) ~[netty-buffer-4.1.87.Final.jar:4.1.87.Final]
at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:188) ~[netty-buffer-4.1.87.Final.jar:4.1.87.Final]
at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:179) ~[netty-buffer-4.1.87.Final.jar:4.1.87.Final]
at io.netty.buffer.AbstractByteBufAllocator.ioBuffer(AbstractByteBufAllocator.java:140) ~[netty-buffer-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator$MaxMessageHandle.allocate(DefaultMaxMessagesRecvByteBufAllocator.java:120) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:150) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) ~[netty-transport-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.87.Final.jar:4.1.87.Final]
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

由于内存超载,流媒体似乎并没有真正工作。 另一方面,我对 swagger Ui 还有另一个问题。即使文件大小很小,swagger Ui 也无法渲染响应并抛出错误 无法识别的响应类型;将内容显示为文本

如果有人能找到解决方法来解决这个问题,我会非常高兴。

非常感谢。

spring-boot rest streaming swagger-ui spring-webclient
1个回答
0
投票

我假设您从非阻塞服务器下载了大文件。然后我们可以使用

StreamingResponseBody
直接写入 OutputStream,然后使用 ResponseEntity 将写入的信息传递回客户端。你的方法看起来像这样

@GetMapping(value = "/download/streaming/{fileId}", produces = { "application/pdf", "application/json" })
public ResponseEntity<StreamingResponseBody> downloadDocumentStreaming(@Parameter(name = "fileId") @PathVariable("fileId") String fileId) {

    HttpHeaders httpHeaders = //Some http header
        httpHeaders.setAccept(List.of(MediaType.APPLICATION_OCTET_STREAM));

    Flux<DataBuffer> flux = WebClient.create().get()
            .uri(builder -> builder.host(serverHostUrl).port(serverPort).scheme("https")
                    .path("/dowload/file/{fileId}").build(fileId))
            .headers(headers -> headers.addAll(httpHeaders)).retrieve().bodyToFlux(DataBuffer.class);

    StreamingResponseBody stream = outputStream -> Mono.create(sink ->
            DataBufferUtils.write(flux, outputStream).subscribe(DataBufferUtils::release,
                sink::error,
                sink::success))
        .block();

    return new ResponseEntity(stream, HttpStatus.OK);
}

请注意,如果您使用 Postman 进行测试,那么您还需要在 Postman 中设置 Max 响应。

© www.soinside.com 2019 - 2024. All rights reserved.