You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Quarkus中用Uni和AsyncFile实现MinIO大文件预览?

在Quarkus中用Uni和Reactive流实现MinIO大视频文件的流式预览

要实现不落地临时文件的MinIO大视频预览,核心是利用Quarkus的Reactive特性,直接将MinIO的输入流转为响应流,同时支持HTTP范围请求(视频拖动需要)。以下是完整实现方案:

1. 添加必要依赖

确保pom.xml中包含Quarkus MinIO、Reactive REST和Vertx扩展:

<dependencies>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-minio</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy-reactive</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-vertx</artifactId>
    </dependency>
</dependencies>

2. 配置MinIO连接

在application.properties中填入你的MinIO服务信息:

quarkus.minio.endpoint=http://your-minio-host:9000
quarkus.minio.access-key=your-access-key
quarkus.minio.secret-key=your-secret-key
quarkus.minio.bucket=your-target-bucket
quarkus.minio.ssl=false # 如果用HTTPS则设为true

3. 实现流式预览接口

这个接口会处理Range请求,从MinIO读取指定字节范围的数据流,直接返回给客户端,完全不需要本地存储:

import io.minio.MinioClient;
import io.minio.GetObjectArgs;
import io.minio.StatObjectArgs;
import io.minio.StatObjectResponse;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;

@Path("/preview")
public class MinioVideoPreviewResource {

    @Inject
    MinioClient minioClient;

    @Inject
    io.vertx.mutiny.core.Vertx vertx;

    // 可从配置中读取,此处为示例写死
    private static final String TARGET_BUCKET = "your-target-bucket";

    @GET
    @Path("/video/{objectName}")
    public Uni<Response> streamVideo(@Context HttpHeaders headers, @PathParam("objectName") String objectName) {
        // 先获取文件元数据(大小、类型)
        return Uni.createFrom().completionStage(() -> 
                minioClient.statObject(StatObjectArgs.builder()
                        .bucket(TARGET_BUCKET)
                        .object(objectName)
                        .build()))
                .onItem().transformToUni(stat -> {
                    long totalSize = stat.size();
                    String contentType = stat.contentType() != null ? stat.contentType() : "video/mp4";

                    // 解析Range请求头,支持视频进度拖动
                    String rangeHeader = headers.getHeaderString(HttpHeaders.RANGE);
                    long startByte = 0;
                    long endByte = totalSize - 1;

                    if (rangeHeader != null) {
                        String[] rangeParts = rangeHeader.replace("bytes=", "").split("-");
                        startByte = Long.parseLong(rangeParts[0]);
                        if (rangeParts.length > 1 && !rangeParts[1].isEmpty()) {
                            endByte = Long.parseLong(rangeParts[1]);
                        }
                        endByte = Math.min(endByte, totalSize - 1);
                    }

                    // 从MinIO获取指定范围的输入流
                    return Uni.createFrom().completionStage(() -> 
                            minioClient.getObject(GetObjectArgs.builder()
                                    .bucket(TARGET_BUCKET)
                                    .object(objectName)
                                    .offset(startByte)
                                    .length(endByte - startByte + 1)
                                    .build()))
                            .onItem().transform(inputStream -> {
                                // 将输入流转为Reactive Multi,实现流式传输
                                ReadableByteChannel channel = Channels.newChannel(inputStream);
                                Multi<ByteBuffer> dataStream = Multi.createFrom().generator(
                                        () -> channel,
                                        (ch, emitter) -> {
                                            ByteBuffer buffer = ByteBuffer.allocate(8192); // 8KB缓冲区,可按需调整
                                            try {
                                                int readBytes = ch.read(buffer);
                                                if (readBytes == -1) {
                                                    emitter.complete();
                                                    return null;
                                                }
                                                buffer.flip();
                                                emitter.emit(buffer);
                                                buffer.clear();
                                                return ch;
                                            } catch (Exception e) {
                                                emitter.fail(e);
                                                return null;
                                            }
                                        }
                                );

                                // 构建响应
                                Response.ResponseBuilder responseBuilder;
                                if (rangeHeader != null) {
                                    // 部分内容响应,符合HTTP Range规范
                                    responseBuilder = Response.status(Response.Status.PARTIAL_CONTENT)
                                            .header(HttpHeaders.CONTENT_RANGE, 
                                                    String.format("bytes %d-%d/%d", startByte, endByte, totalSize));
                                } else {
                                    responseBuilder = Response.ok();
                                }

                                return responseBuilder
                                        .header(HttpHeaders.ACCEPT_RANGES, "bytes")
                                        .header(HttpHeaders.CONTENT_LENGTH, endByte - startByte + 1)
                                        .header(HttpHeaders.CONTENT_TYPE, contentType)
                                        .entity(dataStream) // 直接传入Multi,Reactive REST自动处理流式输出
                                        .build();
                            });
                })
                // 异常处理:文件不存在返回404
                .onFailure().recoverWithItem(e -> 
                        Response.status(Response.Status.NOT_FOUND).entity("视频文件不存在").build());
    }
}

关键说明

  • 无本地存储:直接从MinIO获取输入流,转成Reactive流后返回,全程不落地文件。
  • Range请求支持:解析HTTP Range头,返回指定字节范围的内容,让浏览器可以支持视频进度拖动。
  • 低内存占用:用8KB缓冲区分批读取数据,不会把整个大文件加载到内存。
  • 异步非阻塞:基于Uni和Multi实现全异步处理,符合Quarkus的Reactive架构。

内容的提问来源于stack exchange,提问作者Kambei

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 19:53:19