012.SpringBoot集成MinIO

1,662 阅读11分钟

MinIO介绍部分

MinIO简介

MinIO 是一款高性能、分布式的对象存储系统,说白了就是一个文件服务器,替代以前的FastDFS。MinIO提供高性能、S3兼容的对象存储。Minio 是一个基于Go语言的对象存储服务。它实现了大部分亚马逊S3云存储服务接口,可以看做是是S3的开源版本,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。

在中国:阿里巴巴、腾讯、百度、中国联通、华为、中国移动等等多家企业也都在使用MinIO产品。

官网:min.io

中文:www.minio.org.cn/docs.minio.org.cn/docs/

MinIO各个版本对比

image-20240426163243660

这个我从网上查询到的功能价格对比图,不保证一定准确。

开源版本是基于GNU AGPL v3协议开源的。这里需要了解下AGPL开源许可,在企业环境中使用 Minio,需要特别关注 AGPL 的要求,以避免因许可证合规性问题引发法律风险。为了避免自己开发的代码受到这个AGPL的“传染性”风险。需要注意:

  • 不要修改其源码,最好就是直接使用其官方发行版本。
  • 将 AGPL 软件部署为一个独立的服务,与私有代码保持隔离(通过网络通信交互,而不是在同一进程或项目中运行)。
  • 避免提供基于 AGPL 软件的(SaaS)网络服务

MinIO特点

  • 数据保护——分布式Minio采用 纠删码来防范多个节点宕机和位衰减bit rot。分布式Minio至少需要4个硬盘,使用分布式Minio自动引入了纠删码功能。
  • 高可用——单机Minio服务存在单点故障,相反,如果是一个有N块硬盘的分布式Minio,只要有N/2硬盘在线,你的数据就是安全的。不过你需要至少有N/2+1个硬盘来创建新的对象。

例如,一个16节点的Minio集群,每个节点16块硬盘,就算8台服務器宕机,这个集群仍然是可读的,不过你需要9台服務器才能写数据。

MinIO 基础概念

  • S3——Simple Storage Service,简单存储服务,这个概念是Amazon在2006年推出的,对象存储就是从那个时候诞生的。S3提供了一个简单Web服务接口,可用于随时在Web上的任何位置存储和检索任何数量的数据。

  • Object——存储到 Minio 的基本对象,如文件、字节流,Anything...

  • Bucket——用来存储 Object 的逻辑空间。每个 Bucket 之间的数据是相互隔离的。

  • Drive——部署 Minio 时设置的磁盘,Minio 中所有的对象数据都会存储在 Drive 里。

  • Set——一组 Drive 的集合,分布式部署根据集群规模自动划分一个或多个 Set ,每个 Set 中的 Drive 分布在不同位置。

    • 一个对象存储在一个Set上
    • 一个集群划分为多个Set
    • 一个Set包含的Drive数量是固定的,默认由系统根据集群规模自动计算得出
    • 一个SET中的Drive尽可能分布在不同的节点上

MinIO在Centos上的安装步骤

1. 创建单独的目录放置MinIO

# 单独放置MinIO的所有内容
mkdir minio

2. 下载MinIO

# 进入minio目录
cd minio
# 下载MinIO
wget https://dl.min.io/server/minio/release/linux-amd64/minio

3.创建脚本需要的目录和文件

目录名作用说明
data存实际文件的目录
logminio日志目录
start.sh启动脚本
stop.sh停止脚本

4.启动脚本

#!/bin/bash
​
MINIO_DIR='/root/minio'
DATA_DIR=$MINIO_DIR'/data'
LOG_DIR=$MINIO_DIR'/log/minio.log'
MINIO_ACCESS_KEY='admin'
MINIO_SECRET_KEY='admin123'
SERVER_PORT='9000'
CONSOLE_PORT='9001'
PID_FILE=$MINIO_DIR'/minio.pid'# 检查Minio目录是否存在
if [ ! -d "$MINIO_DIR" ]; then
  echo "Minio directory does not exist: $MINIO_DIR"
  exit 1
fi# 检查日志目录是否存在
LOG_DIR_PATH=$(dirname "$LOG_DIR")
if [ ! -d "$LOG_DIR_PATH" ]; then
  mkdir -p "$LOG_DIR_PATH"
fi# 检查Minio是否已经在运行
if [ -f "$PID_FILE" ]; then
  echo "Minio is already running with PID $(cat $PID_FILE)"
  exit 1
fiexport MINIO_ACCESS_KEY=$MINIO_ACCESS_KEY
export MINIO_SECRET_KEY=$MINIO_SECRET_KEY# 启动Minio
nohup $MINIO_DIR/minio server --address 0.0.0.0:$SERVER_PORT --console-address 0.0.0.0:$CONSOLE_PORT $DATA_DIR > $LOG_DIR 2>&1 &
echo $! > "$PID_FILE"echo "Start Success!"

5.停止脚本

#!/bin/bash
​
MINIO_DIR='/root/minio'
PID_FILE=$MINIO_DIR'/minio.pid'# 检查PID文件是否存在
if [ ! -f "$PID_FILE" ]; then
  echo "Minio is not running or PID file not found."
  exit 1
fi# 获取PID并终止进程
PID=$(cat "$PID_FILE")
if [ -z "$PID" ]; then
  echo "PID file is empty, unable to stop Minio."
  exit 1
fi# 确保进程存在
if ps -p $PID > /dev/null; then
  kill $PID
  if [ $? -eq 0 ]; then
    echo "Minio stopped successfully."
    rm -f "$PID_FILE"  # 删除PID文件
  else
    echo "Failed to stop Minio."
  fi
else
  echo "No Minio process found with PID $PID."
  rm -f "$PID_FILE"  # 删除无效的PID文件
  exit 1
fi

Springboot集成部分

Minio SDK方式

MinIO 官方提供了一个 Java SDK,可以方便地在 Spring Boot 应用中集成 MinIO。

添加依赖

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

该版本是截止发文时的最新版本。

minio相关配置

# Minio配置
minio:
  url: http://localhost:9000  # Minio 服务的 URL
  accessKey: admin            # Minio 的 Access Key
  secretKey: admin123         # Minio 的 Secret Key
  bucketName: my-bucket       # Minio 的存储桶名称

在application.yml文件中加入的内容

bucket的名称,其实就是区分项目的标识。针对一个项目来说就是对应一个bucket,可以理解成该项目对应的所有资源文件的存储目录。所以在项目的全局文件中进行配置。

属性类MinioProperties

@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
    private String url;
    private String accessKey;
    private String secretKey;
    private String bucketName;
}

作用就是承载minio的相关配置。这里需要注意的是需要在springboot的启动类上加上@EnableConfigurationProperties注解,启用对配置属性的支持

配置类MinioConfig

@Slf4j
@Configuration
@RequiredArgsConstructor
public class MinioConfig {
​
    private final MinioProperties minioProperties;
​
​
    @Bean
    public MinioClient minioClient() {
        MinioClient minioClient = MinioClient.builder()
                .endpoint(minioProperties.getUrl())
                .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
                .build();
​
        // 检查存储桶是否存在,如果不存在则创建
        try {
            boolean isExist = minioClient.bucketExists(BucketExistsArgs.builder()
                    .bucket(minioProperties.getBucketName()).build());
            if (!isExist) {
                minioClient.makeBucket(MakeBucketArgs.builder()
                        .bucket(minioProperties.getBucketName()).build());
            }
        } catch (Exception e) {
            log.error("初始化 minio 失败", e);
        }
        return minioClient;
    }
}

该配置类的主要作用就是在springboot启动时,容器中就有MinioClient这个我们可以操作minio的客户端工具类了。

测试Controller类

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/minio")
public class MinioController {

    private final MinioClient minioClient;
    private final MinioProperties minioProperties;

    /**
     * 上传文件
     */
    @RequestMapping("/upload")
    public String upload(@RequestParam("file") MultipartFile file) {
        try {
            PutObjectArgs objectArgs = PutObjectArgs.builder()
                    .stream(file.getInputStream(), file.getSize(), -1)
                    .bucket(minioProperties.getBucketName())
                    .object("ccc/" + file.getOriginalFilename())
                    .contentType(file.getContentType())
                    .build();
            minioClient.putObject(objectArgs);
            return "上传成功";
        }catch (Exception e) {
            log.error("上传文件失败", e);
            return "上传失败";
        }
    }

     /**
     * 下载文件
     */
    @RequestMapping("/download")
    public void download(@RequestParam("fileName") String fileName, HttpServletResponse response) {
        try (GetObjectResponse objectResponse = minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(minioProperties.getBucketName())
                        .object(fileName)
                        .build())) {
            // 设置响应头
            response.setContentType(objectResponse.headers().get("Content-Type"));
            response.setHeader("Content-Disposition",
                    "attachment; filename=" + java.net.URLEncoder.encode(fileName, "UTF-8"));
            // 写入响应流
            IoUtil.copy(objectResponse, response.getOutputStream());
            response.getOutputStream().flush();
        } catch (Exception e) {
            log.error("下载文件失败", e);
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * 获取文件访问的完整URL
     */
    @RequestMapping("/fileUrl")
    public String getFileUrl(@RequestParam("fileName") String fileName) {
        try {
            String url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .bucket(minioProperties.getBucketName())
                    .object(fileName)
                    .method(Method.GET)
                    .build());
            log.info("文件访问URL: {}", url);
            return url;
        } catch (Exception e) {
            log.error("获取文件URL失败", e);
            return "获取文件URL失败";
        }
    }

    /**
     * 删除文件
     */
    @RequestMapping("/delete")
    public String deleteFile(@RequestParam("fileName") String fileName) {
        try {
            minioClient.removeObject(RemoveObjectArgs.builder()
                    .bucket(minioProperties.getBucketName())
                    .object(fileName)
                    .build());
            log.info("文件 {} 删除成功", fileName);
            return "删除成功";
        } catch (Exception e) {
            log.error("删除文件失败", e);
            return "删除失败";
        }
    }

}

说明:

  • 上传方法中的文件名尽量用全局唯一的名字。比如:uuid,雪花算法等,我例子中是用原文件名,仅用于演示。这么名字相同,后面的文件会覆盖之前上传的。

  • 上传方法中的.object可以设置在bucket中的路径。其实就是子目录的作用,层级可以非常深。

  • 上传方法中的.contentType尽量设置,否则默认都是'application/octet-stream'。无法进行预览。

  • 上传的文件就会保存在minio启动脚本中指定的DATA_DIR路径中。

  • 获取文件访问url方法,会返回一个有时效的链接。比如:http://localhost:9000/my-bucket/%E9%AD%94%E6%96%B9%E5%9B%BE.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20241224%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20241224T060814Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=1662ac35e91d65ddacc4c01cf751c9d99aaa5257d36606ac1183e425c2537725。解释下这个url:

    • http://localhost:9000/my-bucket/%E9%AD%94%E6%96%B9%E5%9B%BE.jpg:

      • http://localhost:9000: 这是MinIO服务器的地址和端口。
      • /my-bucket/: 这是存储对象的桶(bucket)的名称。
      • %E9%AD%94%E6%96%B9%E5%9B%BE.jpg: 这是存储在桶中的对象名称,它已经进行了URL编码,解码后为“魔方图.jpg”【我最近没事就对对7阶的魔方】。
    • X-Amz-Algorithm=AWS4-HMAC-SHA256:

      • 这是用于生成签名的算法,这里使用的是AWS版本4的HMAC-SHA256算法。
    • X-Amz-Credential=admin%2F20241224%2Fus-east-1%2Fs3%2Faws4_request:

      • 这是用于签名的凭证信息,包括用户名、日期、区域和请求类型。这里的admin是用户名,20241224是日期,us-east-1是假设的区域(MinIO服务不验证区域),s3/aws4_request指示这是一个S3请求。
      • %2F 是URL编码中的斜杠字符/
    • X-Amz-Date=20241224T060814Z:

      • 这是请求的时间戳,采用ISO 8601格式,并且是UTC时间。
    • X-Amz-Expires=604800:

      • 这是链接的有效期限,单位为秒。在这个例子中,604800秒等于7天。
    • X-Amz-SignedHeaders=host:

      • 这是请求中已经签名的HTTP头部。在这个例子中,只有host头部被签名。
    • X-Amz-Signature=1662ac35e91d65ddacc4c01cf751c9d99aaa5257d36606ac1183e425c2537725:

      • 这是根据上述参数生成的签名值,用于验证请求的合法性。
  • 如果bucket是公有属性的话,其实可以通过**http://localhost:9000/my-bucket/%E9%AD%94%E6%96%B9%E5%9B%BE.jpg**进行直接访问。

    image-20241224143239010

AWS SDK 方式

Amazon S3 的 API 规范是由 亚马逊公司(Amazon.com)制定的。具体来说,它是由亚马逊的云计算部门 Amazon Web Services(AWS)设计和发布的,AWS 提供了一整套云计算服务,其中包括存储服务 S3(Simple Storage Service)。之所以用该方式实现,主要是出于对未来对象存储服务可能不用MinIO的考虑。由于S3是一套API规范,目前好多云服务商的OSS服务都是支持S3规范的,比如:阿里云、七牛云、腾讯云、百度云等等。

添加依赖

<dependency>
    <groupId>software.amazon.awssdk</groupId>
    <artifactId>s3</artifactId>
    <version>2.29.39</version>
</dependency>

该版本是截止发文时的最新版本。

minio相关配置

# Minio配置
minio:
  url: http://localhost:9000  # Minio 服务的 URL
  accessKey: admin            # Minio 的 Access Key
  secretKey: admin123         # Minio 的 Secret Key
  bucketName: my-bucket       # Minio 的存储桶名称

同上面的Minio SDK方式中的配置内容

属性配置类S3Properties

@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class S3Properties {
    private String url;
    private String accessKey;
    private String secretKey;
    private String bucketName;
}

同上面的Minio SDK方式中的类似

配置类S3Config

@Configuration
@RequiredArgsConstructor
public class S3Config {

    private final S3Properties s3Properties;


    @Bean
    public S3Client s3Client() {
        // 使用AWS SDK来创建S3客户端,用于连接到 Minio
        return S3Client.builder()
                // 设置 Minio 的端点
                .endpointOverride(URI.create(s3Properties.getUrl()))
                // Minio 本身不关心区域,可以选择任意区域
                .region(Region.US_EAST_1)
                .credentialsProvider(() ->
                        AwsBasicCredentials.create(s3Properties.getAccessKey()
                                , s3Properties.getSecretKey()))
                .build();
    }

    @Bean
    public S3Presigner s3Presigner() {
        return S3Presigner.builder()
                .endpointOverride(URI.create(s3Properties.getUrl()))
                .region(Region.US_EAST_1) // 区域
                .credentialsProvider(() ->
                        AwsBasicCredentials.create(s3Properties.getAccessKey()
                                , s3Properties.getSecretKey()))
                .build();
    }
}

这个配置类不但可以使我们拿到S3的客户端。还可以获得S3的Presigner,S3Presigner的作用就是生成预签名的 URL。这点比上面的Minio SDK稍微繁琐一点。

测试Controller类

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/minio")
public class S3Controller {

    private final S3Client s3Client;
    private final S3Presigner s3Presigner;
    private final S3Properties s3Properties;

    /**
     * 上传文件
     */
    @RequestMapping("/upload")
    public String upload(@RequestParam("file") MultipartFile file) {
        try (InputStream inputStream = file.getInputStream()) {
            // 创建上传请求
            PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                    .bucket(s3Properties.getBucketName())
                    .key("test/" + file.getOriginalFilename())
                    .contentType(file.getContentType())
                    .build();
            // 上传文件到 Minio
            s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(inputStream, file.getSize()));
            return "上传成功";
        } catch (Exception e) {
            log.error("上传文件失败", e);
            return "上传失败";
        }
    }

    /**
     * 下载文件
     */
    @RequestMapping("/download")
    public void download(@RequestParam("fileName") String fileName, HttpServletResponse response) {
        try {
            // 创建 GetObjectRequest 用于获取对象
            GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                    .bucket(s3Properties.getBucketName())
                    .key(fileName)
                    .build();
            ResponseInputStream<GetObjectResponse> object = s3Client.getObject(getObjectRequest);

            // 设置响应头
            response.setContentType(object.response().contentType());
            response.setHeader("Content-Disposition",
                    "attachment; filename=" + java.net.URLEncoder.encode(fileName, "UTF-8"));
            // 写入响应流
            IoUtil.copy(object, response.getOutputStream());
            response.getOutputStream().flush();
        } catch (Exception e) {
            log.error("下载文件失败", e);
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    /**
     * 获取文件访问的完整URL (AS3接口方式)
     */
    @RequestMapping("/fileUrl")
    public String getFileUrl(@RequestParam("fileName") String fileName) {
        try {
//            // 直接用 s3Client 构造 URL 。适用于public bucket
//            URL url = s3Client.utilities().getUrl(builder -> builder.bucket(s3Properties.getBucketName()).key(fileName));
//            return url.toString();

            // 生成预签名 URL
            PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(builder ->
                    builder.getObjectRequest(getBuilder ->
                                    getBuilder.bucket(s3Properties.getBucketName()).key(fileName))
                            // 签名有效期为1个小时
                            .signatureDuration(Duration.ofHours(1))
            );

            return presignedRequest.url().toString();
        } catch (Exception e) {
            return "获取文件URL失败:" + e.getMessage();
        }
    }


    /**
     * 删除文件
     */
    @RequestMapping("/delete")
    public String deleteFile(@RequestParam("fileName") String fileName) {
        try {
            // 调用 S3Client 的 deleteObject 方法删除文件
            s3Client.deleteObject(builder ->
                    builder.bucket(s3Properties.getBucketName()).key(fileName)
            );
            log.info("文件 {} 删除成功", fileName);
            return "删除成功";
        } catch (Exception e) {
            log.error("删除文件失败", e);
            return "删除失败:" + e.getMessage();
        }
    }

}

功能和上面的Minio SDK方式一样,只不过通过用S3再实现一遍。这里getFileUrl注释掉的获得url方式是针对bucket是public的。如果真有应用场景,可以试试。你会发现生成的url短很多。

总结

  • MinIO可以作为我们项目的文件服务器。但是受到开源协议影响,别动它代码;别深度集成。避免“传染性”。

  • 如果你明确OSS服务就用MinIO的话,那么用MinIO 提供的SDK方式继承到我们的springboot项目比较方便;

    但是如果有可能换其他的云存储服务,那么用S3方式不失为一个好的选择。

  • MinIO上传的文件就会保存在启动脚本指定的目录下。

    image-20241224145405922

  • 在某些场景下可以把bucket设置为public的,里面的图片url就固定为:http://localhost:9000/my-bucket/ccc/%E9%AD%94%E6%96%B9%E5%9B%BE.jpg