当前位置: 首页 > news >正文

05_MinIO+Java SpringBoot 实现透传代理下载

(一)背景

在当前的文件交付场景中,系统部署了 MinIO 作为成品文件的存储与分发平台。通常情况下,我们采用 MinIO 官方提供的命令行工具 mc(MinIO Client)进行对象文件的拷贝(下载),主要操作流程如下:

  1. 配置服务别名
    使用以下命令设置 MinIO 服务的别名,以便后续访问:

mc alias set <别名> <MinIO服务地址> <用户名> <密码>

  1. 执行文件拷贝(下载)
    通过以下命令将指定文件下载到本地:

mc cp <别名>/<桶名>/<文件路径> ~/download/

然而,在实际的文件交付过程中,部分用户更倾向于通过点击链接直接下载文件,而不希望依赖命令行工具。

(二)MINIO下载链接生成方式

1.公有桶访问

操作路径:将目标桶设置为 public 模式 → 用户可直接通过以下 URL 访问文件:

http://<MinIO服务地址>/<桶名>/<对象路径>

此方式不支持设置有效期,且存在文件被公开访问的风险,安全性较低。

2.预签名下载链接

MinIO 提供生成临时有效的预签名 URL 的方式,具体包括:

  • 方式一:通过 MinIO Web 控制台生成
    路径:登录 MinIO 控制台 → 选择目标文件 → 点击 “Share” → 获取分享链接

⚠️ 该链接有效期最长为 12 小时,不支持更长时间设置。

  • 方式二:通过 mc 命令行工具生成
    示例命令如下:
mc alias set <别名> <MinIO服务地址> <用户名> <密码>
mc share download --expire <时间字符串> <别名>/<桶名>/<文件路径>

⚠️ 该方式依赖 mc 工具,且生成链接的最长有效期为 7 天


(三)流式透传代理实现

针对这一需求,我们可以基于 Spring Boot 封装一个下载 API,通过接口形式提供文件下载服务。该 API 可作为一层代理,将 MinIO 中的文件以流的形式透传至客户端,实现点击链接即可下载的功能。

1.完整实现代码

先看一下完整代码

POM文件

<!-- MinIO Java SDK --><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.9</version></dependency>

Controller层

@RestController
@RequestMapping("/minio")
@RequiredArgsConstructor
public class OpMinioController {@GetMapping("/download")public void downloadFile(@RequestParam(required = false) String endpoint,@RequestParam String accessKey,@RequestParam String secretKey,@RequestParam(required = false) String bucket,@RequestParam String filePath,HttpServletResponse response) {try {endpoint = (endpoint == null || endpoint.isEmpty()) ? "http://127.0.0.1:9000" : endpoint;bucket = (bucket == null || bucket.isEmpty()) ? "public-bucket" : bucket;// 构建 MinIO 客户端MinioClient minioClient = MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();// 获取对象元信息(获取文件名和 content-type)StatObjectResponse stat = minioClient.statObject(StatObjectArgs.builder().bucket(bucket).object(filePath).build());// 提取文件名(用于下载提示)String fileName = Paths.get(filePath).getFileName().toString();// 设置响应头,不缓存,并作为下载response.setContentType(stat.contentType());response.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(fileName, StandardCharsets.UTF_8) + "\"");response.setHeader("Content-Length", String.valueOf(stat.size()));response.setHeader("Cache-Control", "no-cache");// 获取对象流并写入响应流(流式透传)try (InputStream in = minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(filePath).build());ServletOutputStream out = response.getOutputStream()) {byte[] buffer = new byte[8192];int bytesRead;while ((bytesRead = in.read(buffer)) != -1) {out.write(buffer, 0, bytesRead);}out.flush();}} catch (ErrorResponseException e) {// MinIO 返回的业务错误(例如:找不到文件、访问权限错误)response.setStatus(getHttpStatusFromMinioError(e));writeJsonError(response, e.errorResponse().message());} catch (Exception e) {// 其他不可预期的异常response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);writeJsonError(response, "服务内部错误: " + e.getMessage());}}/*** 输出 JSON 错误响应。** @param response HttpServletResponse* @param message  错误消息*/private void writeJsonError(HttpServletResponse response, String message) {try {response.setContentType("application/json;charset=UTF-8");response.getWriter().write("{\"error\":\"" + message + "\"}");} catch (IOException ex) {// 忽略}}/*** 将 MinIO 的错误码映射为 HTTP 状态码** @param e MinIO 错误响应异常* @return HTTP 状态码*/private int getHttpStatusFromMinioError(ErrorResponseException e) {String code = e.errorResponse().code();switch (code) {case "NoSuchKey":case "NoSuchObject":return HttpServletResponse.SC_NOT_FOUND; // 404 文件不存在case "AccessDenied":case "InvalidAccessKeyId":case "SignatureDoesNotMatch":return HttpServletResponse.SC_UNAUTHORIZED; // 401 认证失败case "NoSuchBucket":return HttpServletResponse.SC_NOT_FOUND; // 404 桶不存在default:return HttpServletResponse.SC_BAD_GATEWAY; // 502 其他错误}}
}

此接口将各项参数作为路径参数并暴露对外,便于调试与改造,可以通过浏览器或代码请求这个接口完成下载,例如:

GET http://localhost:8080/minio/download?endpoint=http://localhost:9000&accessKey=minioadmin&secretKey=minioadmin&bucket=test-bucket&filePath=example.pdf

2.传统下载和透传代理的区别

传统下载: 后端“先下载再转发”,占用内存、延迟高
透传代理: 后端“边下载边输出”,低延迟、低资源占用,表现得像“中间转发服务器”。

对比项传统下载方式透传代理(流式转发)
实现方式后端先将对象完整下载到服务器本地临时文件或内存,再输出给客户端后端建立输入流和输出流的桥接,边下载边输出,无需完整缓存
文件处理等待文件全部下载完毕才开始响应文件边读边写,立即响应客户端
内存/磁盘使用高,占用较多内存或临时文件空间低,仅使用缓冲区空间(如8KB)
延迟表现高延迟,受文件大小影响明显低延迟,客户端可更快开始下载
失败场景失败可能发生在下载或写出任一阶段,前者浪费资源更容易感知异常,失败快速反馈给客户端

具体代码实现对比

// 下载并写入响应输出流
try (InputStream in = minioClient.getObject(byte[] fileBytes;try (InputStream inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(filePath).build())) {fileBytes = inputStream.readAllBytes();  // 一次性读取全部内容}response.setContentType(stat.contentType());response.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(Paths.get(filePath).getFileName().toString(), StandardCharsets.UTF_8) + "\"");response.setContentLength(fileBytes.length);  // 需要提前知道长度response.getOutputStream().write(fileBytes);
} catch (Exception e) {throw new RuntimeException("文件下载失败: " + e.getMessage(), e);
}
// 设置响应头,不缓存,并作为下载
response.setContentType(stat.contentType());
response.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(fileName, StandardCharsets.UTF_8) + "\"");
response.setHeader("Content-Length", String.valueOf(stat.size()));
response.setHeader("Cache-Control", "no-cache");// 获取对象流并写入响应流(流式透传)
try (InputStream in = minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(filePath).build());ServletOutputStream out = response.getOutputStream()) {byte[] buffer = new byte[8192];int bytesRead;while ((bytesRead = in.read(buffer)) != -1) {out.write(buffer, 0, bytesRead);}out.flush();}
} catch (Exception e) {throw new RuntimeException("文件下载失败: " + e.getMessage(), e);
}
http://www.lryc.cn/news/571169.html

相关文章:

  • 如何确定驱动480x320分辨率的显示屏所需的MCU主频
  • 为何前馈3DGS的边界总是“一碰就碎”?PM-Loss用“3D几何先验”来解
  • Mac 安装JD-GUI
  • 低轨导航 | 低轨卫星导航PNT模型,原理,公式,matlab代码
  • 软件工程:流程图如何画?
  • Python 爬虫入门 Day 5 - 使用 XPath 进行网页解析(lxml + XPath)
  • springboot使用kafka
  • Jmeter的三种参数化方式详解
  • web前端开发核心基础:Html结构分析,head,body,不同标签的作用
  • Java内存模型与线程
  • Anaconda 使用
  • 力扣经典算法篇-17-反转字符串中的单词(逆序遍历,数组分割,正则表达式)
  • 4_STM32F103ZET6芯片系统架构和寄存器
  • 通过自适应训练样本选择弥合基于锚点和无锚点检测之间的差距之ATSS论文阅读
  • 【论文阅读】BACKDOOR FEDERATED LEARNING BY POISONING BACKDOOR-CRITICAL LAYERS
  • Matlab自学笔记五十九:符号变量的代入和替代subs精讲
  • Windows10安装WSL Ubuntu
  • 设计模式:单例模式多种方式的不同实现
  • vue中diff算法的原理
  • 把springboot打包为maven可引入的jar
  • Maven 的 settings.xml详解
  • 深度学习中常见的激活函数分析
  • Android Studio Jetpack Compose毛玻璃特效按钮
  • 【数据结构】栈和队列详解
  • 线性放大器设计方案:248-双极性任意波1M带宽400Vpp高压线性放大器
  • 欧拉安装vboxlinuxadditions时,出错
  • 第九章——8天Python从入门到精通【itheima】-95~96-Python的异常模块与包(自定义模块并导入+自定义Python包)
  • MySQL 命令行的核心操作命令详解
  • 资深Java工程师的面试题目(一)微服务
  • 如何高效分享WordPress博客文章