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

Quarkus直接返回Minio InputStream避OOM却下载空文件的问题

问题:Minio文件流直接返回前端时下载空文件,如何避免OOM并正常流式传输

背景代码

我用Minio获取文件的核心方法如下,GetObjectResponse继承自FilterInputStream:

public GetObjectResponse getFile(String bucket, String object) {
    validateEnableMinio();
    try {
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                .bucket(bucket)
                .object(object)
                .build();
        return minioClient.getObject(getObjectArgs);
    } catch (Exception e) {
        LOGGER.error("minio exception", e);
        throw new RuntimeException("get file error");
    }
}

尝试的方案及问题

1. 返回BufferedInputStream(下载空文件)

为避免全量加载字节到内存引发OOM,尝试直接返回BufferedInputStream,但下载得到的是0字节空文件:

@GET
@Path("/download")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Uni<Response> download1(@RestQuery String fileName) {
    var stat = minioService.statObject(fileName);
    try (GetObjectResponse getObjectResponse =  minioService.getFile(fileName)) {
        BufferedInputStream bufferedInputStream = new BufferedInputStream(getObjectResponse);
        Response.ResponseBuilder builder = Response.ok(bufferedInputStream);
        builder.type(MediaType.APPLICATION_OCTET_STREAM_TYPE);
        builder.header("Content-Disposition", "attachment; filename=" + stat.object());
        return Uni.createFrom().item(builder.build());
    } catch (Exception e) {
        LOGGER.error("error", e);
        return Uni.createFrom().item(Response.status(Response.Status.INTERNAL_SERVER_ERROR).build()) ;
    }
}

2. 使用readAllBytes()(正常但会OOM)

改用readAllBytes()可以正常下载完整文件,但该方法会把所有字节加载到内存,大文件场景会触发OOM:

@GET
@Path("/download")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Uni<Response> download1(@RestQuery String fileName) {
    var stat = minioService.statObject(fileName);
    try (GetObjectResponse getObjectResponse =  minioService.getFile(fileName)) {
        BufferedInputStream bufferedInputStream = new BufferedInputStream(getObjectResponse);
        Response.ResponseBuilder builder = Response.ok(bufferedInputStream.readAllBytes());
        builder.type(MediaType.APPLICATION_OCTET_STREAM_TYPE);
        builder.header("Content-Disposition", "attachment; filename=" + stat.object());
        return Uni.createFrom().item(builder.build());
    } catch (Exception e) {
        LOGGER.error("error", e);
        return Uni.createFrom().item(Response.status(Response.Status.INTERNAL_SERVER_ERROR).build()) ;
    }
}

3. 返回本地文件流(正常工作)

直接返回本地文件的输入流时可以正常下载,说明框架处理本地流逻辑没问题:

@GET
@Path("/download1")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Uni<Response> downloadFile() {
    String filePath = "D:\\xxx\\settings.xml"; // 替换为实际文件路径

    File file = new File(filePath);

    if (!file.exists() || !file.isFile()) {
        return Uni.createFrom().item(Response.status(Response.Status.NOT_FOUND).build());
    }

    try {
        InputStream is = new BufferedInputStream(new FileInputStream(file));

        return Uni.createFrom().item(Response.ok(is)
                .header("Content-Disposition", "attachment; filename=" + file.getName())
                .build());
    } catch (IOException e) {
        return Uni.createFrom().item(Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("error").build());
    }
}

4. 直接返回Minio的InputStream(仍空文件)

去掉额外包装,直接返回Minio的输入流,依旧下载空文件:

@GET
@Path("/download")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response download1(@RestQuery String fileName) {
    var stat = minioService.statObject(fileName);
    GetObjectArgs getObjectArgs = GetObjectArgs.builder()
            .bucket("test1")
            .object(fileName)
            .build();
    try (InputStream inputStream =  minioClient.getObject(getObjectArgs)) {
        Response.ResponseBuilder builder = Response.ok(inputStream);
        builder.type(MediaType.APPLICATION_OCTET_STREAM_TYPE);
        builder.header("Content-Disposition", "attachment; filename=" + stat.object());
        return builder.build();
    } catch (Exception e) {
        LOGGER.error("exception", e);
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
    }
}

核心问题

如何无需本地文件中转,直接将Minio返回的InputStream流式返回给前端,同时避免OOM?

解决方案

问题根源是try-with-resources的自动关闭时机错误:当你在try块内构建Response并返回时,try-with-resources会在方法返回前就关闭InputStream,此时框架还未开始读取流数据,导致返回空文件。

修复方法:不要用try-with-resources包裹Minio的InputStream,改为由JAX-RS框架负责关闭流——JAX-RS规范中,Response中的实体流会在处理完成后自动关闭,无需手动管理。

正确代码(Uni异步版本)

@GET
@Path("/download")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Uni<Response> download1(@RestQuery String fileName) {
    GetObjectResponse getObjectResponse = null;
    try {
        var stat = minioService.statObject(fileName);
        getObjectResponse = minioService.getFile(fileName);
        BufferedInputStream bufferedInputStream = new BufferedInputStream(getObjectResponse);
        
        Response.ResponseBuilder builder = Response.ok(bufferedInputStream);
        builder.type(MediaType.APPLICATION_OCTET_STREAM_TYPE);
        builder.header("Content-Disposition", "attachment; filename=" + stat.object());
        
        return Uni.createFrom().item(builder.build());
    } catch (Exception e) {
        // 获取流失败时手动关闭资源
        if (getObjectResponse != null) {
            try {
                getObjectResponse.close();
            } catch (IOException ex) {
                LOGGER.error("failed to close minio stream", ex);
            }
        }
        LOGGER.error("download error", e);
        return Uni.createFrom().item(Response.status(Response.Status.INTERNAL_SERVER_ERROR).build()) ;
    }
}

正确代码(同步Response版本)

@GET
@Path("/download")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response download1(@RestQuery String fileName) {
    InputStream inputStream = null;
    try {
        var stat = minioService.statObject(fileName);
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                .bucket("test1")
                .object(fileName)
                .build();
        inputStream = minioClient.getObject(getObjectArgs);
        
        Response.ResponseBuilder builder = Response.ok(inputStream);
        builder.type(MediaType.APPLICATION_OCTET_STREAM_TYPE);
        builder.header("Content-Disposition", "attachment; filename=" + stat.object());
        
        return builder.build();
    } catch (Exception e) {
        // 获取流失败时手动关闭资源
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException ex) {
                LOGGER.error("failed to close minio stream", ex);
            }
        }
        LOGGER.error("download exception", e);
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
    }
}

补充说明

  1. 流关闭责任:JAX-RS框架(如Quarkus使用的RESTEasy)会自动处理Response中实体流的关闭,无需手动关闭Minio返回的InputStream,框架会在数据传输完成后安全关闭,避免资源泄漏。
  2. BufferedInputStream作用:添加缓冲流可以减少对Minio服务的IO调用次数,提升传输效率,建议保留。
  3. 异常场景处理:如果在获取流的过程中抛出异常,此时流还未交给框架,需要手动关闭流避免资源泄漏,代码中已添加对应逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 22:34:55