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(); } }
补充说明
- 流关闭责任:JAX-RS框架(如Quarkus使用的RESTEasy)会自动处理Response中实体流的关闭,无需手动关闭Minio返回的InputStream,框架会在数据传输完成后安全关闭,避免资源泄漏。
- BufferedInputStream作用:添加缓冲流可以减少对Minio服务的IO调用次数,提升传输效率,建议保留。
- 异常场景处理:如果在获取流的过程中抛出异常,此时流还未交给框架,需要手动关闭流避免资源泄漏,代码中已添加对应逻辑。
内容的提问来源于stack exchange,提问作者commonBoy
相关产品推荐
相关产品推荐

