一、安装Minio
1.1 拉取镜像
docker pull minio/minio
docker images
1.2创建挂载目录
1.2.1 创建数据目录
mkdir -p /docker-minio/data
1.2.2 创建配置文件目录
mkdir -p /docker-minio/config
1.2.3 设置权限
chmod -R 777 /docker-minio/data /docker-minio/config
1.3启动MinIO容器
docker run -d --name minio \
-p 9000:9000 -p 9090:9090 \
-e "MINIO_ROOT_USER=minioAdmin" \
-e "MINIO_ROOT_PASSWORD=minioAdmin" \
-v /docker-minio/data:/data \
-v /docker-minio/config:/root/.minio \
minio/minio server /data \
--console-address ":9090" \
--address ":9000"
注意:MinIO 规定 Access key 长度至少为 3,Secret key 长度至少为 8。
这个 Docker 命令用于启动一个 MinIO 对象存储服务器容器。MinIO 是一个高性能、与 Amazon S3 API 兼容的开源对象存储解决方案。下面是详细解释:
1.
docker run
作用: Docker 命令的核心部分,用于创建并启动一个新的容器。
2.
-d
作用:
detach
的缩写。以后台(守护进程)模式运行容器。容器启动后,控制台会立即返回,你可以继续使用终端,而容器在后台运行。要查看容器日志,可以使用docker logs minio
。3.
--name minio
作用: 为容器指定一个名称
minio
。这使得后续管理容器(如启动、停止、查看日志)更加方便,无需使用复杂的容器 ID。例如:
docker stop minio
docker start minio
docker logs minio
4.
-p 9000:9000 -p 9090:9090
作用: 将容器内部的端口映射到宿主机的端口。
-p 9000:9000
: 将容器内部的 9000 端口映射到宿主机的 9000 端口。这个端口是 MinIO API 服务端口,用于客户端(SDK、命令行工具mc
、应用程序)与 MinIO 服务进行交互(上传、下载、管理对象等),兼容 S3 API。
-p 9090:9090
: 将容器内部的 9090 端口映射到宿主机的 9090 端口。这个端口是 MinIO 控制台(Web UI)端口,用于通过浏览器访问图形化管理界面来管理存储桶、用户、策略等。5.
-e "MINIO_ROOT_USER=minioAdmin"
作用: 设置容器内的环境变量
MINIO_ROOT_USER
。这个变量定义了 MinIO 的 根用户(管理员)用户名。这里设置为minioAdmin
。非常重要!6.
-e "MINIO_ROOT_PASSWORD=minioAdmin"
作用: 设置容器内的环境变量
MINIO_ROOT_PASSWORD
。这个变量定义了 MinIO 的 根用户(管理员)密码。这里设置为minioAdmin
。非常重要!强烈建议在生产环境中使用强密码替代这个示例密码。7.
-v /docker-minio/data:/data
作用: 创建一个数据卷,将宿主机上的目录
/docker-minio/data
挂载到容器内部的目录/data
。
/docker-minio/data
: 宿主机上的目录路径,这个目录将持久化存储 MinIO 对象(文件)本身。
/data
: 容器内部的路径。MinIO 服务器会将所有上传的对象存储在这个目录下。意义: 这个挂载确保了 MinIO 存储的实际文件数据不会丢失。即使容器被删除或重启,宿主机
/docker-minio/data
下的数据仍然存在。下次启动新容器并挂载同一目录,数据即可恢复。8.
-v /docker-minio/config:/root/.minio
作用: 创建另一个数据卷,将宿主机上的目录
/docker-minio/config
挂载到容器内部的目录/root/.minio
。
/docker-minio/config
: 宿主机上的目录路径,这个目录将持久化存储 MinIO 的服务器配置、用户信息、策略等元数据。
/root/.minio
: 容器内部 MinIO 服务默认存储其配置文件和状态的路径。意义: 这个挂载确保了 MinIO 的配置、用户账号、访问策略等关键元数据不会丢失。删除或重建容器后,只要挂载回这个目录,所有配置和用户信息都会保留。
9.
minio/minio
作用: 指定要运行的 Docker 镜像。这里是官方 MinIO 镜像,从 Docker Hub 的
minio/minio
仓库拉取。如果本地没有,Docker 会自动从 Hub 下载。10.
server /data
作用: 这是传递给
minio/minio
镜像的 启动命令。
server
: 告诉 MinIO 可执行文件以服务器模式运行。
/data
: 指定 MinIO 服务器在容器内部存储对象数据的路径。这必须与第 7 条 (-v ...:/data
) 中挂载到容器内部的路径一致。MinIO 会使用这个目录来保存上传的文件。11.
--console-address ":9090"
作用: MinIO 服务器启动参数。显式指定 MinIO 控制台(Web UI)监听的地址和端口。
:9090
: 表示绑定到容器内的所有网络接口 (0.0.0.0
) 的 9090 端口。这直接对应了第 4 条 (-p 9090:9090
) 中映射的端口。访问http://<宿主机IP>:9090
即可打开管理控制台。12.
--address ":9000"
作用: MinIO 服务器启动参数。显式指定 MinIO API 服务(S3 兼容接口)监听的地址和端口。
:9000
: 表示绑定到容器内的所有网络接口 (0.0.0.0
) 的 9000 端口。这直接对应了第 4 条 (-p 9000:9000
) 中映射的端口。客户端(如mc
,AWS SDK, 应用程序)通过http://<宿主机IP>:9000
来访问存储服务。
1.4 访问Minio控制台
启动容器后,可以通过浏览器访问 MinIO 控制台:
http://<宿主机IP>:9001
使用之前设置的用户名和密码(minioAdmin
和 minioAdmin
)登录。
二、SpringBoot集成minio
2.1 导入maven做标
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.0</version>
</dependency>
2.2 自定义配置
在yml添加如下配置:
minio:
url: http://121.40.159.231:9000
#minio账户
accessKey: minioAdmin
#minio密码
secretKey: minioAdmin
bucketName: nbsjfx
然后创建对应的配置类:
@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProp {
/**
* Minio存储服务服务的地址
*/
private String url;
/**
* Minio的访问账号
*/
private String accessKey;
/**
* Minio的访问密码
*/
private String secretKey;
/**
* Minio的存储桶名称
*/
private String bucketName;
}
2.3 创建MinioClient(向 Spring 容器中注册一个 MinioClient
类型的 Bean),并创建Bucket
@Configuration
@RequiredArgsConstructor
public class MinioConfig {
private final MinioProp minioProp;
/**
* 创建MinioClient实例
*/
@Bean
public MinioClient minioClient() {
MinioClient minioClient = MinioClient.builder()
.endpoint(minioProp.getUrl())
.credentials(minioProp.getAccessKey(), minioProp.getSecretKey())
.build();
return minioClient;
}
/**
* 创建Bucket
*/
@SneakyThrows
@PostConstruct
public void createBucket() {
MinioClient minioClient = minioClient();
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder()
.bucket(minioProp.getBucketName())
.build());
if (!bucketExists) {
minioClient.makeBucket(MakeBucketArgs.builder()
.bucket(minioProp.getBucketName())
.build());
}
}
}
2.4 自定义工具类
自定义的MinioUtil代码如下:
@Component
@RequiredArgsConstructor
public class MinioUtil {
private final MinioClient minioClient;
private final MinioProp minioProp;
private final HttpServletResponse httpServletResponse;
/**
* 检查bucket是否存在,不存在则创建
*
* @param bucketName bucket名称
* @return 是否创建成功
*/
@SneakyThrows
public Boolean createBucket(String bucketName) {
boolean exists = minioClient.bucketExists(BucketExistsArgs.builder()
.bucket(bucketName)
.build());
if (!exists) {
minioClient.makeBucket(MakeBucketArgs.builder()
.bucket(bucketName)
.build());
}
return Boolean.TRUE;
}
/**
* 获取所有bucket列表
*
* @return bucket列表
*/
@SneakyThrows
public List<Bucket> getAllBuckets() {
List<Bucket> bucketList = minioClient.listBuckets();
return bucketList;
}
/**
* 删除bucket (桶为空时,才能删除)
*
* @param bucketName bucket名称
* @return 是否删除成功
*/
@SneakyThrows
public Boolean deleteBucket(String bucketName) {
minioClient.removeBucket(RemoveBucketArgs.builder()
.bucket(bucketName)
.build());
return Boolean.TRUE;
}
/**
* 获取bucket中的所有文件列表
*
* @param bucketName bucket名称
* @return 文件列表
*/
@SneakyThrows
public List<Item> getAllFilesByBucket(String bucketName) {
Iterable<Result<Item>> iterable = minioClient.listObjects(ListObjectsArgs.builder()
.bucket(bucketName)
.build());
List<Item> itemList = new ArrayList<>();
for (Result<Item> result : iterable) {
Item item = result.get();
itemList.add(item);
}
return itemList;
}
/**
* 上传文件
*
* @param inputStream 文件输入流
* @param bucketName bucket名称
* @param fileName 文件名称
* @return 是否上传成功
*/
@SneakyThrows
public Boolean uploadFile(InputStream inputStream, String bucketName, String fileName) {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(inputStream, -1, 5242889L)
.build());
return Boolean.TRUE;
}
/**
* 上传文件
*
* @param multipartFile 文件对象
* @return 是否上传成功
*/
@SneakyThrows
public Boolean uploadFile(MultipartFile multipartFile) {
return uploadFile(multipartFile.getInputStream(), minioProp.getBucketName(), multipartFile.getOriginalFilename());
}
/**
* 删除文件
*
* @param bucketName bucket名称
* @param fileName 文件名称
* @return 是否删除成功
*/
@SneakyThrows
public Boolean deleteFile(String bucketName, String fileName) {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.build()
);
return Boolean.TRUE;
}
/**
* 删除文件
*
* @param fileName 文件名称
* @return 是否删除成功
*/
public Boolean deleteFile(String fileName) {
return deleteFile(minioProp.getBucketName(), fileName);
}
/**
* 下载文件
*
* @param bucketName bucket名称
* @param fileName 文件名称
*/
@SneakyThrows
public void downloadFile(String bucketName, String fileName) {
GetObjectResponse getObjectResponse = minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.build());
byte[] buf = new byte[1024];
int len;
try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()) {
while ((len = getObjectResponse.read(buf)) != -1) {
os.write(buf, 0, len);
}
os.flush();
byte[] bytes = os.toByteArray();
httpServletResponse.setCharacterEncoding("utf-8");
// 设置强制下载不打开
//httpServletResponse.setContentType("application/force-download");
httpServletResponse.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
httpServletResponse.addHeader("Content-Disposition", "attachment;filename=".concat(URLEncoder.encode(fileName, "UTF-8")));
try (ServletOutputStream stream = httpServletResponse.getOutputStream()) {
stream.write(bytes);
stream.flush();
}
}
}
/**
* 下载文件
*
* @param fileName 文件名称
*/
public void downloadFile(String fileName) {
downloadFile(minioProp.getBucketName(), fileName);
}
/**
* 获取预览文件url (预览链接默认7天后过期)
*
* @param bucketName bucket名称
* @param fileName 文件名称
* @return 文件url
*/
@SneakyThrows
public String getPreviewFileUrl(String bucketName, String fileName) {
GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(fileName)
.build();
return minioClient.getPresignedObjectUrl(args);
}
/**
* 获取预览文件url (预览链接默认7天后过期)
*
* @param fileName 文件名称
* @return 文件url
*/
public String getPreviewFileUrl(String fileName) {
return getPreviewFileUrl(minioProp.getBucketName(), fileName);
}
}
三、nginx代理minio的文件链接
3.1 前置说明
当我们创建桶后,Minio会在数据挂载目录下,创建一个和桶同名的文件夹。
上面的过程中,我们创建了一个为nbsjfx的桶,对应的文件夹如下:
目前桶里有一个文件,为1.jpg:
3.2 反向代理
server {
listen 9999;
server_name 127.0.0.1;
location / {
root /docker-minio/data;
}
测试结果:
四、适配器模式(实战教程)
百度百科对于适配器的介绍:适配器模式_百度百科
在计算机编程中,适配器模式(有时候也称包装样式或者包装)将一个类的接口适配成用户所期待的。一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。
适配器模式(Adapter Pattern)是一种结构型设计模式,它允许接口不兼容的类能够一起工作。适配器模式通过将一个类的接口转换成客户期望的另一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作。
在这个场景中,我们需要实现一个统一的OSS(对象存储服务)接口,然后为不同的云存储服务(如minio、阿里云OSS、京东云OSS)提供适配器。这样,客户端就可以通过统一的接口来操作不同的云存储服务,而无需关心底层不同服务的具体实现细节。
4.1 创建OSS适配器接口
/**
* OSS适配器接口
*/
public interface OssAdapter {
/**
* 上传文件
* @param multipartFile 上传的文件
* @return 上传成功返回true,否则返回false
*/
Boolean uploadFile(MultipartFile multipartFile);
}
我们这里通过上传文件方法作为案例,希望不管什么厂商的Oss都通过这个接口的方式进行文件上传
4.2 创建对应的适配器实现类
4.2.1 创建minio适配器实现类
/**
* Minio Oss 适配器
*/
@Slf4j
public class MinioOssAdapter implements OssAdapter {
@Autowired
private MinioUtil minioUtil;
@Override
public Boolean uploadFile(MultipartFile multipartFile) {
log.info("Minio适配器实现文件上传");
return minioUtil.uploadFile(multipartFile);
}
}
注意到,此处我们没有把该类注册为spring的bean,所以@Autowired注解在提升错误。
因为我们的系统,一般都只使用一种文件存储服务,使用我们这里预期是到时候通过配置类的形式交由spring容器进行管理。
4.2.2 创建aliyun适配器实现类
/**
* 阿里云 OSS 适配器
*/
@Slf4j
public class AliyunOssAdapter implements OssAdapter {
//也是注入阿里云Oss相关的工具类和实现等
@Override
public Boolean uploadFile(MultipartFile multipartFile) {
log.info("阿里云适配器实现文件上传");
// TODO 阿里云 OSS 上传文件
return null;
}
}
4.2.3 创建配置类,按需注入
首先在yml文件中,自定义类型:
oss:
oss-type: minio
然后创建配置类如下:(根据类型完成注入)
@Slf4j
@Configuration
public class OssConfig {
@Value("${oss.oss-type}")
private String ossType;
@Bean
public OssAdapter ossAdapter(){
log.info("文件存储ossType: {}", ossType);
if ("minio".equals(ossType)){
return new MinioOssAdapter();
}else if ("aliyun".equals(ossType)){
return new AliyunOssAdapter();
}
throw new IllegalArgumentException("未找到对应的Oss文件存储适配器");
}
}
4.3 抽取文件服务类(根据自己的系统自定义)
@Service
public class FileService {
@Autowired
private OssAdapter ossAdapter;
public Boolean uploadFile(MultipartFile multipartFile) {
return ossAdapter.uploadFile(multipartFile);
}
}
4.4 编写控制层进行测试
@RestController
@RequestMapping("/file")
public class FileController {
@Autowired
private FileService fileService;
@RequestMapping("/uploadFile")
public IResult uploadFile(MultipartFile multipartFile) {
return new ResultBean<>(fileService.uploadFile(multipartFile));
}
}
当配置文件配置的类型为minio时,测试结果:
同时minio中也确实上传了文件:
当我们把配置文件的类型改为aliyun时,测试结果:
4.5 总结
至此,我们实现将不同 OSS 提供商的接口抽象成一个统一的接口,从而实现解耦和灵活切换。
优势:
统一接口:客户端使用一致的API操作不同云存储
解耦:业务代码与具体云服务实现解耦
可扩展:新增云服务只需添加新适配器
易维护:各云服务实现相互隔离,修改互不影响