对象存储MinIO
MinIO
一、对象存储是什么?
对象存储(Object Storage)是一种现代化的数据管理方式,将每个文件视为“对象”,并配有唯一的 key 和元数据。相较传统的文件系统,它更适合非结构化数据,如:
- 图片、音视频
- 日志、备份
- AI 模型与训练数据
- 归档文档等
其核心特征是:
- 无需目录结构,使用 Key 组织数据
- 可水平扩展
- 元数据可自定义
- 通过 REST API 访问
二、S3 协议简介
S3(Simple Storage Service)最初由 AWS 推出,是云对象存储的行业标杆。S3 协议是一套通过 HTTP 实现的 API,支持:
- 创建与管理 Bucket(容器)
- 上传、下载、删除 Object(文件)
- 权限控制与访问策略
- 生成临时下载链接(Presigned URL)
如今已成为事实标准,几乎所有对象存储系统(包括阿里云 OSS、腾讯云 COS)都在兼容它。
三、MinIO:开源的 S3 对象存储方案
MinIO 是一款高性能、企业级的开源对象存储系统,100% 兼容 S3 协议,支持本地部署,适用于私有云、边缘计算与混合云等场景。
✅ 主要特性:
- 🚀 极致性能(Go 语言原生并发)
- 🧩 S3 协议兼容(SDK 无需改动)
- 🧱 分布式、可水平扩展(支持 Erasure Coding)
- 🔐 安全:TLS、KMS、IAM、ACL、Bucket Policy
- 📦 Docker/K8s 原生支持
- 🛠️ CLI 工具:
mc(MinIO Client)
四、MinIO 与 S3 的对比
| 特性 | Amazon S3 | MinIO |
|---|---|---|
| 部署方式 | 云端(AWS 托管) | 本地 / 私有云 / 混合云 |
| 协议 | S3(原版) | 完全兼容 S3 API |
| 性能 | 稳定但略有延迟 | 高性能,低延迟,适合局域网 |
| 成本 | 按量计费 | 开源免费(企业功能需付费) |
| 扩展能力 | 依赖 AWS 基础设施 | 水平扩展,可集群化 |
| 管控能力 | AWS 控制 | 自主控制,数据主权可控 |
五、MinIO 本地部署
windows版本minIO服务端安装(也可以直接官网下载minio.exe文件),注意不要下载成企业版(企业版需要证书否则无法启动),直接下载社区版
Invoke-WebRequest -Uri "https://dl.min.io/aistor/minio/release/windows-amd64/minio.exe" -OutFile "minio.exe"
minio.exe --version

docker版本部署
https://quay.io/repository/minio/minio?tab=tags&tag=RELEASE.2025-04-22T22-12-26Z
# 拉镜像
docker pull quay.io/minio/minio@sha256:a1ea29fa28355559ef137d71fc570e508a214ec84ff8083e39bc5428980b015e
#运行容器
docker run -p 9000:9000 -p 9090:9090 --name minio -d \
-e "MINIO_ACCESS_KEY=minio2025" -e "MINIO_SECRET_KEY=20250801" \
-v E:\Docker\minio\data:/data \
-v E:\Docker\minio\config:/root/.minio \
quay.io/minio/minio server /data --console-address ":9090" -address ":9000"
#设置访问权限
#mc客户端中配置一个别名为 myminio 的连接目标(需要URL 用户名 密码)
mc alias set myminio http://127.0.0.1:9000 minio2025 20250801
#设置 miniodata 的桶可以匿名访问下载内容(无需认证)
mc anonymous set download myminio/miniodata
http://localhost:9090/ 输入minio2025和20250801进入后创建桶和上传图片

启动
# E:\MinIO\Data存储目录;--console-address 是 UI 界面的端口; --no-license表示以社区版方式启动
./minio.exe server E:\MinIO\Data --console-address ":9001"
# 查看版本
./minio.exe --version

浏览器访问 http://127.0.0.1:9001 地址,访问 MinIO 内置的 UI 界面。输入启动时提示的账号 minioadmin,密码 minioadmin 进行登录。

创建存储桶

上传图片
点击 [Upload] 按钮,点击 [Upload File] 选项,选择一个图片上传。

访问文件
文件的访问地址的格式为 http://127.0.0.1:9000/{bucket}/{name},注意是 9000 端口。
默认配置下,访问存储桶是需要请求授权的。

需要用mc命令去设置读写权限

设置后访问成功

Java代码
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.4.3</version>
</dependency>

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.business</groupId>
<artifactId>miniotest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>miniotest</name>
<description>miniotest</description>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<spring-boot.version>2.6.13</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
</dependencies>
</project>
application.yaml
server:
port: 8080
minio:
endpoint: http://192.168.10.195:9000 #Minio服务所在地址
bucketName: miniodata #存储桶名称
accessKey: minio2025 #访问的key
secretKey: 20250801 #访问的秘钥
region: us-east-1 #区域
MinioConfig.java
package com.minio.config;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucketName;
private String region;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.region(region)
.build();
}
}
MinioUtils.java
package com.minio.util;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import sun.misc.BASE64Decoder;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;
/**
* MinIO Utils
*
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MinioUtils {
private final MinioClient minioClient;
/****************************** Operate Bucket Start ******************************/
/**
* init Bucket when start SpringBoot container
* create bucket if the bucket is not exists
*
* @param bucketName
*/
@SneakyThrows(Exception.class)
private void createBucket(String bucketName) {
if (!bucketExists(bucketName)) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
/**
* verify Bucket is exist?,true:false
*
* @param bucketName
* @return
*/
@SneakyThrows(Exception.class)
public boolean bucketExists(String bucketName) {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
/**
* get Bucket strategy
*
* @param bucketName
* @return
*/
@SneakyThrows(Exception.class)
public String getBucketPolicy(String bucketName) {
return minioClient.getBucketPolicy(GetBucketPolicyArgs
.builder()
.bucket(bucketName)
.build());
}
/**
* get all Bucket list
*
* @return
*/
@SneakyThrows(Exception.class)
public List<Bucket> getAllBuckets() {
return minioClient.listBuckets();
}
/**
* Get related information based on bucketName
*
* @param bucketName
* @return
*/
@SneakyThrows(Exception.class)
public Optional<Bucket> getBucket(String bucketName) {
return getAllBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
}
/**
* Delete Bucket based on bucketName, true: deletion successful; false: deletion failed, file may no longer exist
*
* @param bucketName
* @throws Exception
*/
@SneakyThrows(Exception.class)
public void removeBucket(String bucketName) {
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}
/****************************** Operate Bucket End ******************************/
/****************************** Operate Files Start ******************************/
/**
* check file is exist
*
* @param bucketName
* @param objectName
* @return
*/
public boolean isObjectExist(String bucketName, String objectName) {
boolean exist = true;
try {
minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
} catch (Exception e) {
log.error("[MinioUtils]>>>>check file exist, Exception:", e.getMessage());
exist = false;
}
return exist;
}
/**
* check directory exist?
*
* @param bucketName
* @param objectName
* @return
*/
public boolean isFolderExist(String bucketName, String objectName) {
boolean exist = false;
try {
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(objectName).recursive(false).build());
for (Result<Item> result : results) {
Item item = result.get();
if (item.isDir() && objectName.equals(item.objectName())) {
exist = true;
}
}
} catch (Exception e) {
log.error("[MinioUtils]>>>>check file exist, Exception:", e);
exist = false;
}
return exist;
}
/**
* Query files based on file prefix
*
* @param bucketName
* @param prefix
* @param recursive
* @return MinioItem
*/
@SneakyThrows(Exception.class)
public List<Item> getAllObjectsByPrefix(String bucketName,
String prefix,
boolean recursive) {
List<Item> list = new ArrayList<>();
Iterable<Result<Item>> objectsIterator = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build());
if (objectsIterator != null) {
for (Result<Item> o : objectsIterator) {
Item item = o.get();
list.add(item);
}
}
return list;
}
/**
* get file InputStream
*
* @param bucketName
* @param objectName
* @return
*/
@SneakyThrows(Exception.class)
public InputStream getObject(String bucketName, String objectName) {
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
}
/**
* Breakpoint download
*
* @param bucketName
* @param objectName
* @param offset
* @param length
* @return
*/
@SneakyThrows(Exception.class)
public InputStream getObject(String bucketName, String objectName, long offset, long length) {
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.offset(offset)
.length(length)
.build());
}
/**
* Get the list of files under the path
*
* @param bucketName
* @param prefix
* @param recursive
* @return
*/
public Iterable<Result<Item>> listObjects(String bucketName, String prefix, boolean recursive) {
return minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(bucketName)
.prefix(prefix)
.recursive(recursive)
.build());
}
/**
* use MultipartFile to upload files
*
* @param bucketName
* @param file
* @param objectName
* @param contentType
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse uploadFile(String bucketName, MultipartFile file, String objectName, String contentType) {
InputStream inputStream = file.getInputStream();
return minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.contentType(contentType)
.stream(inputStream, inputStream.available(), -1)
.build());
}
/**
* picture upload
* @param bucketName
* @param imageBase64
* @param imageName
* @return
*/
public ObjectWriteResponse uploadImage(String bucketName, String imageBase64, String imageName) {
if (!StringUtils.isEmpty(imageBase64)) {
InputStream in = base64ToInputStream(imageBase64);
String newName = System.currentTimeMillis() + "_" + imageName + ".jpg";
String year = String.valueOf(new Date().getYear());
String month = String.valueOf(new Date().getMonth());
return uploadFile(bucketName, year + "/" + month + "/" + newName, in);
}
return null;
}
public static InputStream base64ToInputStream(String base64) {
ByteArrayInputStream stream = null;
try {
byte[] bytes = new BASE64Decoder().decodeBuffer(base64.trim());
stream = new ByteArrayInputStream(bytes);
} catch (Exception e) {
e.printStackTrace();
}
return stream;
}
/**
* upload local files
*
* @param bucketName
* @param objectName
* @param fileName
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse uploadFile(String bucketName, String objectName, String fileName) {
return minioClient.uploadObject(
UploadObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.filename(fileName)
.build());
}
/**
* upload files based on stream
*
* @param bucketName
* @param objectName
* @param inputStream
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) {
return minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(inputStream, inputStream.available(), -1)
.build());
}
/**
* create file or direatory
*
* @param bucketName
* @param objectName
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse createDir(String bucketName, String objectName) {
return minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(new ByteArrayInputStream(new byte[]{}), 0, -1)
.build());
}
/**
* get file info
*
* @param bucketName
* @param objectName
* @return
*/
@SneakyThrows(Exception.class)
public String getFileStatusInfo(String bucketName, String objectName) {
String result = minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build()).toString();
return result;
}
/**
* copy file
*
* @param bucketName
* @param objectName
* @param srcBucketName
* @param srcObjectName
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse copyFile(String bucketName, String objectName, String srcBucketName, String srcObjectName) {
return minioClient.copyObject(
CopyObjectArgs.builder()
.source(CopySource.builder().bucket(bucketName).object(objectName).build())
.bucket(srcBucketName)
.object(srcObjectName)
.build());
}
/**
* delete file
*
* @param bucketName
* @param objectName
*/
@SneakyThrows(Exception.class)
public void removeFile(String bucketName, String objectName) {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
}
/**
* batch delete file
*
* @param bucketName
* @param keys
* @return
*/
public void removeFiles(String bucketName, List<String> keys) {
List<DeleteObject> objects = new LinkedList<>();
keys.forEach(s -> {
objects.add(new DeleteObject(s));
try {
removeFile(bucketName, s);
} catch (Exception e) {
log.error("[MinioUtil]>>>>batch delete file,Exception:", e);
}
});
}
/**
* get file url
*
* @param bucketName
* @param objectName
* @param expires
* @return url
*/
@SneakyThrows(Exception.class)
public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) {
GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder().expiry(expires).bucket(bucketName).object(objectName).build();
return minioClient.getPresignedObjectUrl(args);
}
/**
* get file url
*
* @param bucketName
* @param objectName
* @return url
*/
@SneakyThrows(Exception.class)
public String getPresignedObjectUrl(String bucketName, String objectName) {
GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
.bucket(bucketName)
.object(objectName)
.method(Method.GET).build();
return minioClient.getPresignedObjectUrl(args);
}
/**
* change URLDecoder to UTF8
*
* @param str
* @return
* @throws UnsupportedEncodingException
*/
public String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException {
String url = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25");
return URLDecoder.decode(url, "UTF-8");
}
}
MinIOController.java
package com.minio.controller;
import com.minio.config.MinioConfig;
import com.minio.util.MinioUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
@Slf4j
@RestController
@RequestMapping("/minio")
public class MinIOController {
@Autowired
private MinioUtils minioUtils;
@Autowired
private MinioConfig minioConfig;
/**
* file upload
*
* @param file
*/
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
try {
//file name
String fileName = file.getOriginalFilename();
String newFileName = System.currentTimeMillis() + "." + StringUtils.substringAfterLast(fileName, ".");
//type
String contentType = file.getContentType();
minioUtils.uploadFile(minioConfig.getBucketName(), file, newFileName, contentType);
return "upload success";
} catch (Exception e) {
e.printStackTrace();
log.error("upload fail");
return "upload fail";
}
}
/**
* delete
*
* @param fileName
*/
@DeleteMapping("/")
public void delete(@RequestParam("fileName") String fileName) {
minioUtils.removeFile(minioConfig.getBucketName(), fileName);
}
/**
* get file info
*
* @param fileName
* @return
*/
@GetMapping("/info")
public String getFileStatusInfo(@RequestParam("fileName") String fileName) {
return minioUtils.getFileStatusInfo(minioConfig.getBucketName(), fileName);
}
/**
* get file url
*
* @param fileName
* @return
*/
@GetMapping("/url")
public String getPresignedObjectUrl(@RequestParam("fileName") String fileName) {
return minioUtils.getPresignedObjectUrl(minioConfig.getBucketName(), fileName);
}
/**
* file download
*
* @param fileName
* @param response
*/
@GetMapping("/download")
public void download(@RequestParam("fileName") String fileName, HttpServletResponse response) {
try {
InputStream fileInputStream = minioUtils.getObject(minioConfig.getBucketName(), fileName);
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
response.setContentType("application/force-download");
response.setCharacterEncoding("UTF-8");
IOUtils.copy(fileInputStream, response.getOutputStream());
} catch (Exception e) {
log.error("download fail");
}
}
}
测试

查看文件详情



浙公网安备 33010602011771号