项目里总少不了文件上传下载的功能------用户头像、合同附件、产品图片。用阿里云 OSS 方便但要钱,自己存服务器又麻烦。MinIO 是一个开源的对象存储服务,兼容 S3 协议,可以私有化部署,性能和功能完全不输商业 OSS。
一、MinIO 简介
MinIO vs 其他方案:
阿里云 OSS → 按量付费,省心但长期用成本高
FastDFS → 部署复杂,社区不活跃
MinIO → 开源免费,部署简单,性能强悍(号称读写 183GB/s)
自己存磁盘 → 简单但不支持分布式,备份困难
MinIO 的优势:
- 兼容 AWS S3 接口,SDK 直接可用
- 部署简单,一个 Docker 命令启动
- 支持分布式部署(多台机器做集群)
- 有 Web 管理界面
- 开源且社区活跃
二、安装 MinIO
1. Docker 一键部署(推荐)
bash
docker run -d \
--name minio \
-p 9000:9000 \
-p 9001:9001 \
-e MINIO_ROOT_USER=admin \
-e MINIO_ROOT_PASSWORD=admin123456 \
-v D:\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. 引入依赖
xml
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version>
</dependency>
2. 配置
yaml
minio:
endpoint: http://localhost:9000
access-key: admin
secret-key: admin123456
bucket: my-bucket
3. 配置类
java
@Configuration
public class MinIOConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.access-key}")
private String accessKey;
@Value("${minio.secret-key}")
private String secretKey;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
四、文件上传下载
1. 文件上传服务
java
@Service
public class FileService {
@Autowired
private MinioClient minioClient;
@Value("${minio.bucket}")
private String bucket;
/**
* 上传文件
* @param file 上传的文件
* @param objectName 存储的文件名(如 avatar/2026/06/abc123.jpg)
*/
public String upload(MultipartFile file, String objectName) throws Exception {
// 检查 bucket 是否存在
boolean found = 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()
);
// 返回可访问的 URL
return endpoint + "/" + bucket + "/" + objectName;
}
/**
* 上传文件(自动生成文件名)
*/
public String upload(MultipartFile file) throws Exception {
// 原始文件名
String originalFilename = file.getOriginalFilename();
// 扩展名
String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
// 新文件名:日期 + UUID
String objectName = DateUtil.today() + "/" + IdUtil.simpleUUID() + ext;
return upload(file, objectName);
}
/**
* 上传文件(指定目录前缀)
*/
public String upload(MultipartFile file, String prefix, Long userId) throws Exception {
String ext = originalFilename.substring(originalFilename.lastIndexOf("."));
String objectName = prefix + "/" + userId + "/" + IdUtil.simpleUUID() + ext;
return upload(file, objectName);
}
}
2. Controller
java
@RestController
@RequestMapping("/file")
public class FileController {
@Autowired
private FileService fileService;
@PostMapping("/upload")
public ResultVO<String> upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return ResultVO.error(400, "请选择文件");
}
try {
// 校验文件大小(10MB)
if (file.getSize() > 10 * 1024 * 1024) {
return ResultVO.error(400, "文件不能超过10MB");
}
// 校验文件类型(只允许图片和 PDF)
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/") && !contentType.equals("application/pdf")) {
return ResultVO.error(400, "不支持的文件格式");
}
String url = fileService.upload(file);
return ResultVO.success(url);
} catch (Exception e) {
return ResultVO.error(500, "上传失败: " + e.getMessage());
}
}
@PostMapping("/upload/avatar")
public ResultVO<String> uploadAvatar(@RequestParam("file") MultipartFile file,
@RequestParam Long userId) {
try {
String url = fileService.upload(file, "avatar", userId);
return ResultVO.success(url);
} catch (Exception e) {
return ResultVO.error(500, "上传失败");
}
}
}
五、文件删除
java
public void delete(String objectName) throws Exception {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.build()
);
}
public void deleteByUrl(String fileUrl) {
// 从 URL 中提取 objectName
// http://localhost:9000/my-bucket/avatar/1/xxx.jpg
String prefix = endpoint + "/" + bucket + "/";
String objectName = fileUrl.substring(prefix.length());
delete(objectName);
}
六、获取文件列表
java
public List<String> listFiles(String prefix) {
List<String> files = new ArrayList<>();
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());
}
return files;
}
七、生成临时访问链接
有些文件不想公开访问,可以生成带有效期的临时链接:
java
public String getPresignedUrl(String objectName, int expiryMinutes) throws Exception {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.bucket(bucket)
.object(objectName)
.method(Method.GET)
.expiry(expiryMinutes, TimeUnit.MINUTES)
.build()
);
}
八、前端上传
html
<form id="uploadForm" enctype="multipart/form-data">
<input type="file" name="file" id="fileInput">
<button type="button" onclick="uploadFile()">上传</button>
</form>
<script>
async function uploadFile() {
const fileInput = document.getElementById('fileInput');
const formData = new FormData();
formData.append('file', fileInput.files[0]);
const resp = await fetch('/file/upload', {
method: 'POST',
body: formData,
});
const result = await resp.json();
if (result.code === 200) {
console.log('文件地址:', result.data);
// 回显图片
document.getElementById('preview').src = result.data;
}
}
</script>
九、Nginx 代理 MinIO
生产环境中,MinIO 一般不直接暴露端口,而是通过 Nginx 代理:
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/爬虫 实战干货,不让你白来。