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-bucket3. 配置类
@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/爬虫 实战干货,不让你白来。
