SpringBoot 整合 MinIO 实现文件存储——私有化 OSS 方案

项目里总少不了文件上传下载的功能——用户头像、合同附件、产品图片。用阿里云 OSS 方便但要钱,自己存服务器又麻烦。MinIO 是一个开源的对象存储服务,兼容 S3 协议,可以私有化部署,性能和功能完全不输商业 OSS。

一、MinIO 简介

MinIO vs 其他方案: 阿里云 OSS → 按量付费,省心但长期用成本高 FastDFS → 部署复杂,社区不活跃 MinIO → 开源免费,部署简单,性能强悍(号称读写 183GB/s) 自己存磁盘 → 简单但不支持分布式,备份困难

MinIO 的优势:

  • 兼容 AWS S3 接口,SDK 直接可用
  • 部署简单,一个 Docker 命令启动
  • 支持分布式部署(多台机器做集群)
  • 有 Web 管理界面
  • 开源且社区活跃

二、安装 MinIO

1. Docker 一键部署(推荐)

dockerrun-d\--nameminio\-p9000:9000\-p9001:9001\-eMINIO_ROOT_USER=admin\-eMINIO_ROOT_PASSWORD=admin123456\-vD:\minio\data:/data\quay.io/minio/minio server /data --console-address":9001"

启动后访问:

  • API 端口:http://localhost:9000
  • 管理后台http://localhost:9001(账号 admin / 密码 admin123456)

2. 在管理台创建 Bucket

登录管理后台 → 点击「Create Bucket」→ 输入名称(如my-bucket)→ 确认。

三、SpringBoot 集成 MinIO

1. 引入依赖

<dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.7</version></dependency>

2. 配置

minio:endpoint:http://localhost:9000access-key:adminsecret-key:admin123456bucket:my-bucket

3. 配置类

@ConfigurationpublicclassMinIOConfig{@Value("${minio.endpoint}")privateStringendpoint;@Value("${minio.access-key}")privateStringaccessKey;@Value("${minio.secret-key}")privateStringsecretKey;@BeanpublicMinioClientminioClient(){returnMinioClient.builder().endpoint(endpoint).credentials(accessKey,secretKey).build();}}

四、文件上传下载

1. 文件上传服务

@ServicepublicclassFileService{@AutowiredprivateMinioClientminioClient;@Value("${minio.bucket}")privateStringbucket;/** * 上传文件 * @param file 上传的文件 * @param objectName 存储的文件名(如 avatar/2026/06/abc123.jpg) */publicStringupload(MultipartFilefile,StringobjectName)throwsException{// 检查 bucket 是否存在booleanfound=minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());if(!found){minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());}// 上传minioClient.putObject(PutObjectArgs.builder().bucket(bucket).object(objectName).stream(file.getInputStream(),file.getSize(),-1).contentType(file.getContentType()).build());// 返回可访问的 URLreturnendpoint+"/"+bucket+"/"+objectName;}/** * 上传文件(自动生成文件名) */publicStringupload(MultipartFilefile)throwsException{// 原始文件名StringoriginalFilename=file.getOriginalFilename();// 扩展名Stringext=originalFilename.substring(originalFilename.lastIndexOf("."));// 新文件名:日期 + UUIDStringobjectName=DateUtil.today()+"/"+IdUtil.simpleUUID()+ext;returnupload(file,objectName);}/** * 上传文件(指定目录前缀) */publicStringupload(MultipartFilefile,Stringprefix,LonguserId)throwsException{Stringext=originalFilename.substring(originalFilename.lastIndexOf("."));StringobjectName=prefix+"/"+userId+"/"+IdUtil.simpleUUID()+ext;returnupload(file,objectName);}}

2. Controller

@RestController@RequestMapping("/file")publicclassFileController{@AutowiredprivateFileServicefileService;@PostMapping("/upload")publicResultVO<String>upload(@RequestParam("file")MultipartFilefile){if(file.isEmpty()){returnResultVO.error(400,"请选择文件");}try{// 校验文件大小(10MB)if(file.getSize()>10*1024*1024){returnResultVO.error(400,"文件不能超过10MB");}// 校验文件类型(只允许图片和 PDF)StringcontentType=file.getContentType();if(contentType==null||!contentType.startsWith("image/")&&!contentType.equals("application/pdf")){returnResultVO.error(400,"不支持的文件格式");}Stringurl=fileService.upload(file);returnResultVO.success(url);}catch(Exceptione){returnResultVO.error(500,"上传失败: "+e.getMessage());}}@PostMapping("/upload/avatar")publicResultVO<String>uploadAvatar(@RequestParam("file")MultipartFilefile,@RequestParamLonguserId){try{Stringurl=fileService.upload(file,"avatar",userId);returnResultVO.success(url);}catch(Exceptione){returnResultVO.error(500,"上传失败");}}}

五、文件删除

publicvoiddelete(StringobjectName)throwsException{minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(objectName).build());}publicvoiddeleteByUrl(StringfileUrl){// 从 URL 中提取 objectName// http://localhost:9000/my-bucket/avatar/1/xxx.jpgStringprefix=endpoint+"/"+bucket+"/";StringobjectName=fileUrl.substring(prefix.length());delete(objectName);}

六、获取文件列表

publicList<String>listFiles(Stringprefix){List<String>files=newArrayList<>();Iterable<Result<Item>>results=minioClient.listObjects(ListObjectsArgs.builder().bucket(bucket).prefix(prefix)// 按前缀过滤.recursive(true)// 递归查询.build());for(Result<Item>result:results){files.add(result.get().objectName());}returnfiles;}

七、生成临时访问链接

有些文件不想公开访问,可以生成带有效期的临时链接:

publicStringgetPresignedUrl(StringobjectName,intexpiryMinutes)throwsException{returnminioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().bucket(bucket).object(objectName).method(Method.GET).expiry(expiryMinutes,TimeUnit.MINUTES).build());}

八、前端上传

<formid="uploadForm"enctype="multipart/form-data"><inputtype="file"name="file"id="fileInput"><buttontype="button"onclick="uploadFile()">上传</button></form><script>asyncfunctionuploadFile(){constfileInput=document.getElementById('fileInput');constformData=newFormData();formData.append('file',fileInput.files[0]);constresp=awaitfetch('/file/upload',{method:'POST',body:formData,});constresult=awaitresp.json();if(result.code===200){console.log('文件地址:',result.data);// 回显图片document.getElementById('preview').src=result.data;}}</script>

九、Nginx 代理 MinIO

生产环境中,MinIO 一般不直接暴露端口,而是通过 Nginx 代理:

server { listen 80; server_name file.example.com; location / { proxy_pass http://127.0.0.1:9000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

配置后访问http://file.example.com/my-bucket/xxx.jpg即可查看文件。

十、MinIO vs 阿里云 OSS 怎么选

场景推荐方案
个人/小项目,没有公网服务器阿里云 OSS(省心)
公司项目,服务器在本地机房MinIO(省成本)
高并发、大流量场景阿里云 OSS(CDN 加速)
数据隐私要求高(政务、金融)MinIO 私有化部署
学习/练手项目MinIO(Docker 几分钟搞定)

一句话:不差钱上阿里云 OSS,想省钱且能自己维护服务器的用 MinIO,功能体验几乎一样。


💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。