一、介绍

1、对象存储和分布式文件简介

阿里云OSS对象存储:https://help.aliyun.com/product/31815.html

对象存储服务OSS(Object Storage Service)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本。对象存储最大的优势就在于它可以存储大容量的非结构化数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等。对于大多数的企业来说,这可以说是最为理想的存储媒介了

分布式文件系统是指文件系统管理的物理存储资源不一定直接连接在本地节点上,而是通过计算机网络与节点相连

2、MinIO简介

MinIO 是一个基于 Go 实现的高性能、兼容 S3 协议的对象存储。它采用 GNU AGPL v3 开源协议,项目地址是 https://github.com/minio/minio,官网是 https://min.io,中文官网是www.minio.org.cn
它适合存储海量的非结构化的数据,例如说图片、音频、视频等常见文件,备份数据、容器、虚拟机镜像等等,小到 1 KB,大到 5 TB 都可以支持。

3、MinIO特点

minio原理和使用

  • 高性能:作为高性能对象存储,在标准硬件条件下它能达到55GB/s的读、35GG/s的写速率

  • 可扩容:不同MinIO集群可以组成联邦,并形成一个全局的命名空间,并跨越多个数据中心

  • 云原生:容器化、基于K8S的编排、多租户支持

  • Amazon S3兼容:Minio使用Amazon S3 v2 / v4 API。可以使用Minio SDK,Minio Client,AWS SDK和AWS CLI访问Minio服务器。

  • 可对接后端存储: 除了Minio自己的文件系统,还支持DAS、 JBODs、NAS、Google云存储和Azure Blob存储。

  • SDK支持: 基于Minio轻量的特点,它得到类似Java、Python或Go等语言的sdk支持

  • Lambda计算: Minio服务器通过其兼容AWS SNS / SQS的事件通知服务触发Lambda功能。支持的目标是消息队列,如Kafka,NATS,AMQP,MQTT,Webhooks以及Elasticsearch,Redis,Postgres和MySQL等数据库。

  • 有操作页面

  • 功能简单: 这一设计原则让MinIO不容易出错、更快启动

  • 支持纠删码:MinIO使用纠删码、Checksum来防止硬件错误和静默数据污染。在最高冗余度配置下,即使丢失1/2的磁盘也能恢复数据

    纠删码是一种用来重建丢失或损坏数据的数学算法。MinIO 使用 Reed-Solomon 码将需要存储的对象切分为可变数据块和奇偶校验块。例如,在由 12 个驱动器构成的存储架构中,对象分片范围可以是 6 个数据块、6 个奇偶校验块到 10 个数据块、2 个奇偶校验块。

二、MinIO安装入门

1、介绍

中文开发文档:http://docs.minio.org.cn/docs/

由于 MinIO 是 Go 写的,所以就一个运行程序,因此安装部署 MinIO 就非常简单。在文档 https://min.io/download 中,有 Windows、Linux、MacOS、Docker、Kubernetes、Source 六种安装方式。

2、快速安装

2.1 Windows

需要在 Windows PowerShell 中执行。

## 国外资源,龟速下载,建议科学上网
setx MINIO_ROOT_USER admin
Invoke-WebRequest -Uri "https://dl.min.io/server/minio/release/windows-amd64/minio.exe" -OutFile "C:\minio.exe" 
setx MINIO_ROOT_PASSWORD password
C:\minio.exe server F:\Data --console-address ":9001" ## F:\Data 存储目录;--console-address 是 UI 界面的端口

2.2 Linux

## 国外资源,龟速下载
wget https://dl.min.io/server/minio/release/linux-amd64/minio 
chmod +x minio
MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server ./minio --console-address ":9001" ## /Users/yunai/minio 存储目录;--console-address 是 UI 界面的端口

2.3 MacOS

## 国外资源,龟速下载
wget https://dl.min.io/server/minio/release/darwin-amd64/minio 
chmod +x minio
MINIO_ROOT_USER=admin MINIO_ROOT_PASSWORD=password ./minio server F:\Data --console-address ":9001" ## F:\Data 存储目录;--console-address 是 UI 界面的端口

2.4 Docker(推荐)

linux单个docker启动

docker pull minio/minio
# --console-address 是 UI 界面的端口
docker run --name minio -p 9000:9000 -p 9001:9001 -d --restart=always -e "MINIO_ACCESS_KEY=admin" -e "MINIO_SECRET_KEY=password" -v "~/minio/data":"/data" -v "~/minio/config":"/root/.minio" minio/minio server /data --console-address ":9001"

linux docker-compose启动(推荐),首先编写docker-compose.yml文件,最后在该目录下docker-compose up -d成功启动

version: '3'
services:
  minio:
    image: minio/minio
    hostname: "minio"
    ports:
      - "9000:9000" # api 端口
      - "9001:9001" # 控制台端口
    environment:
      MINIO_ACCESS_KEY: admin    #管理后台用户名
      MINIO_SECRET_KEY: password #管理后台密码,最小8个字符
    volumes:
      - /home/deepsoft/minio/data:/data               #映射当前目录下的data目录至容器内/data目录
      - /home/deepsoft/minio/config:/root/.minio/     #映射配置目录
    command: server --console-address ':9001' /data  #指定容器中的目录 /data
    privileged: true
    restart: always
    logging:
      options:
        max-size: "50M" # 最大文件上传限制
        max-file: "10"
      driver: json-file
    networks:
      - minio
networks:
  minio:
      name: minio

启动成功后打开防火墙,浏览http://localhost:9001即可进入可视化页面

3、UI界面界面简单使用

https://docs.min.io/minio/baremetal/console/minio-console.html#

3.1 新建存储桶

首先登录http://localhost:9001,点击 [Create Bucket] 按钮,新建一个 Bucket 存储桶,用于稍后文件的上传

在这里插入图片描述

这里存储桶名称长度必须⾄少为3且不超过63个字符,不得包含⼤写字符或下划线,必须以⼩写字母或数字开头。

在这里插入图片描述

3.2 添加访问规则

默认配置下,访问存储桶是需要请求授权的。但是在实际场景下,我们往往希望允许直接访问,此时就需要添加一条 readonly 或readwrite访问规则;或者直接在[Access Policy]直接设置public(不安全)

① 点击右上角的 [Manage] 设置图标,然后选择 [Access Rules] 菜单。

在这里插入图片描述

② 点击 [Add Access Rule] 按钮,添加一条 Prefix 为 /或者* ,Access 为 readwrite的规则。

在这里插入图片描述

3.3 上传文件

点击 [Upload] 按钮,点击 [Upload File] 选项,选择一个文件上传

在这里插入图片描述

3.4 访问文件

文件的访问地址的格式为 <http://127.0.0.1:9000/{bucket}/{name}>,注意是 9000 端口。比如我的是http://192.168.31.34:9000/textbook/yuanshen.png

三、SpringBoot整合MinIO

1、引入项目依赖

注意7.x和8.x版本有一定差异,这里在pom.xml引入最新版

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.4.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2、配置文件设置

application.yml中设置

# Tomcat
server:
  port: 8888
  
spring:
  # 配置文件上传大小限制
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB
      
# Minio配置
minio:
  url: http://localhost:9000
  accessKey: minioadmin
  secretKey: minioadmin
  bucketName: test

3、配置类设置

读取配置文件的信息,这里的存储桶名称可以设置多个

@Configuration
@ConfigurationProperties(prefix = "minio")
@Data
public class MinioConfig
{
    /**
     * 服务地址
     */
    private String url;

    /**
     * 用户名
     */
    private String accessKey;

    /**
     * 密码
     */
    private String secretKey;

    /**
     * 存储桶名称
     */
    private String TextBookBucketName;
    

    @Bean
    public MinioClient getMinioClient()
    {
        return MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
    }
}

设置返回类

@Data
public class FileVO {
    /**
    * 展示url
    */
    private String previewUrl;
    /**
    * 存数据库的uri
    */
    private String uri;
}

4、工具类整合

4.1 简单整合MinIO上传工具

@RestController
@RequestMapping("/file")
public class FileController{

    @Resource
    private MinioClient minioClient;

    @Resource
    private MinioConfig minioConfig;

    /**
     * 上传文件
     */
    @PostMapping("/upload")
    public CommonResult upload(@RequestParam("file") MultipartFile file) throws Exception {
        //可以直接原来的文件名,也可以自定义
        //String originalFilename = file.getOriginalFilename();
        // 文件名
        String originalFilename = file.getOriginalFilename();
        // 新的文件名 = 时间戳.后缀名
        String fileName = System.currentTimeMillis() + originalFilename.substring(originalFilename.lastIndexOf("."));
        // 上传
        minioClient.putObject(PutObjectArgs.builder()
                // 存储桶
                .bucket(minioConfig.getTextBookBucketName())
                // 文件名 ,可以添加路径,会自动创建
                .object(fileName)
                // 文件内容
                .stream(file.getInputStream(), file.getSize(), -1)
                // 文件类型
                .contentType(file.getContentType())
                .build());
        FileVO fileVO = new FileVO();
        fileVO.setUri("/" + minioConfig.getTextBookBucketName()+ "/" + fileName);
        fileVO.setPreviewUrl(minioConfig.getUrl()+"/" + fileVO.getUri());
        // 拼接路径
        return new CommonResult(fileVO);
    }

    /**
     * 删除文件
     */
    @DeleteMapping("/delete")
    public void delete(@RequestParam("path") String path) throws Exception {
        minioClient.removeObject(RemoveObjectArgs.builder()
                // 存储桶
                .bucket(minioConfig.getTextBookBucketName())
                // 文件名,可以文件名
                .object(path)
                .build());
    }
}

4.2 MinIO工具类

首先创建工具类,这里很多操作都在工具类里了,需要用到的地方通过MinioUtils.xxx()方法调用即可。另外,工具类中传递的endpoint、bucketName、accessKey、ecretKey等参数,都是在minio后台可以拿到的,没有的话也可以自己设置,同时要保证后台的存储桶是开放的,默认代码创建的都是私有的

@Slf4j
public class MinIOUtils {

    private static MinioClient minioClient;

    private static String endpoint;
    private static String bucketName;
    private static String accessKey;
    private static String secretKey;
    private static Integer imgSize;
    private static Integer fileSize;


    private static final String SEPARATOR = "/";

    public MinIOUtils() {
    }

    public MinIOUtils(String endpoint, String bucketName, String accessKey, String secretKey, Integer imgSize, Integer fileSize) {
        MinIOUtils.endpoint = endpoint;
        MinIOUtils.bucketName = bucketName;
        MinIOUtils.accessKey = accessKey;
        MinIOUtils.secretKey = secretKey;
        MinIOUtils.imgSize = imgSize;
        MinIOUtils.fileSize = fileSize;
        createMinioClient();
    }

    /**
     * 创建基于Java端的MinioClient
     */
    public void createMinioClient() {
        try {
            if (null == minioClient) {
                log.info("开始创建 MinioClient...");
                minioClient = MinioClient
                                .builder()
                                .endpoint(endpoint)
                                .credentials(accessKey, secretKey)
                                .build();
                createBucket(bucketName);
                log.info("创建完毕 MinioClient...");
            }
        } catch (Exception e) {
      log.error("[Minio工具类]>>>> MinIO服务器异常:", e);
        }
    }

    /**
     * 获取上传文件前缀路径
     * @return
     */
    public static String getBasisUrl() {
        return endpoint + SEPARATOR + bucketName + SEPARATOR;
    }

    /******************************  Operate Bucket Start  ******************************/

    /**
     * 启动SpringBoot容器的时候初始化Bucket
     * 如果没有Bucket则创建
     * @throws Exception
     */
    private static void createBucket(String bucketName) throws Exception {
        if (!bucketExists(bucketName)) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }

    /**
     *  判断Bucket是否存在,true:存在,false:不存在
     * @return
     * @throws Exception
     */
    public static boolean bucketExists(String bucketName) throws Exception {
        return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }


    /**
     * 获得Bucket的策略
     * @param bucketName
     * @return
     * @throws Exception
     */
    public static String getBucketPolicy(String bucketName) throws Exception {
    return minioClient
                .getBucketPolicy(
                    GetBucketPolicyArgs
                        .builder()
                        .bucket(bucketName)
                        .build()
                );
    }


    /**
     * 获得所有Bucket列表
     * @return
     * @throws Exception
     */
    public static List<Bucket> getAllBuckets() throws Exception {
        return minioClient.listBuckets();
    }

    /**
     * 根据bucketName获取其相关信息
     * @param bucketName
     * @return
     * @throws Exception
     */
    public static Optional<Bucket> getBucket(String bucketName) throws Exception {
        return getAllBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
    }

    /**
     * 根据bucketName删除Bucket,true:删除成功; false:删除失败,文件或已不存在
     * @param bucketName
     * @throws Exception
     */
    public static void removeBucket(String bucketName) throws Exception {
        minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
    }

    /******************************  Operate Bucket End  ******************************/

    /******************************  Operate Files Start  ******************************/

    /**
     * 判断文件是否存在
     * @param bucketName 存储桶
     * @param objectName 文件名
     * @return
     */
    public static boolean isObjectExist(String bucketName, String objectName) {
        boolean exist = true;
        try {
            minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
        } catch (Exception e) {
      log.error("[Minio工具类]>>>> 判断文件是否存在, 异常:", e);
            exist = false;
        }
        return exist;
    }

    /**
     * 判断文件夹是否存在
     * @param bucketName 存储桶
     * @param objectName 文件夹名称
     * @return
     */
    public static 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("[Minio工具类]>>>> 判断文件夹是否存在,异常:", e);
            exist = false;
        }
        return exist;
    }

    /**
     * 根据文件前置查询文件
     * @param bucketName 存储桶
     * @param prefix 前缀
     * @param recursive 是否使用递归查询
     * @return MinioItem 列表
     * @throws Exception
     */
    public static List<Item> getAllObjectsByPrefix(String bucketName,
                                                   String prefix,
                                                   boolean recursive) throws Exception {
        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;
    }

    /**
     * 获取文件流
     * @param bucketName 存储桶
     * @param objectName 文件名
     * @return 二进制流
     */
    public static InputStream getObject(String bucketName, String objectName) throws Exception {
        return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }

    /**
     * 断点下载
     * @param bucketName 存储桶
     * @param objectName 文件名称
     * @param offset 起始字节的位置
     * @param length 要读取的长度
     * @return 二进制流
     */
    public InputStream getObject(String bucketName, String objectName, long offset, long length)throws Exception {
        return minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .offset(offset)
                        .length(length)
                        .build());
    }

    /**
     * 获取路径下文件列表
     * @param bucketName 存储桶
     * @param prefix 文件名称
     * @param recursive 是否递归查找,false:模拟文件夹结构查找
     * @return 二进制流
     */
    public static Iterable<Result<Item>> listObjects(String bucketName, String prefix,
                                                     boolean recursive) {
        return minioClient.listObjects(
                ListObjectsArgs.builder()
                        .bucket(bucketName)
                        .prefix(prefix)
                        .recursive(recursive)
                        .build());
    }

    /**
     * 使用MultipartFile进行文件上传
     * @param bucketName 存储桶
     * @param file 文件名
     * @param objectName 对象名
     * @param contentType 类型
     * @return
     * @throws Exception
     */
    public static ObjectWriteResponse uploadFile(String bucketName, MultipartFile file,
                                                String objectName, String contentType) throws Exception {
        InputStream inputStream = file.getInputStream();
        return minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .contentType(contentType)
                        .stream(inputStream, inputStream.available(), -1)
                        .build());
    }


    /**
     * 上传本地文件
     * @param bucketName 存储桶
     * @param objectName 对象名称
     * @param fileName 本地文件路径
     */
    public static ObjectWriteResponse uploadFile(String bucketName, String objectName,
                                                String fileName) throws Exception {
        return minioClient.uploadObject(
                UploadObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .filename(fileName)
                        .build());
    }

    /**
     * 通过流上传文件
     *
     * @param bucketName 存储桶
     * @param objectName 文件对象
     * @param inputStream 文件流
     */
    public static ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) throws Exception {
        return minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .stream(inputStream, inputStream.available(), -1)
                        .build());
    }

    /**
     * 创建文件夹或目录
     * @param bucketName 存储桶
     * @param objectName 目录路径
     */
    public static ObjectWriteResponse createDir(String bucketName, String objectName) throws Exception {
        return minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .stream(new ByteArrayInputStream(new byte[]{}), 0, -1)
                        .build());
    }

    /**
     * 获取文件信息, 如果抛出异常则说明文件不存在
     *
     * @param bucketName 存储桶
     * @param objectName 文件名称
     */
    public static String getFileStatusInfo(String bucketName, String objectName) throws Exception {
        return minioClient.statObject(
                StatObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build()).toString();
    }

    /**
     * 拷贝文件
     *
     * @param bucketName 存储桶
     * @param objectName 文件名
     * @param srcBucketName 目标存储桶
     * @param srcObjectName 目标文件名
     */
    public static ObjectWriteResponse copyFile(String bucketName, String objectName,
                                                 String srcBucketName, String srcObjectName) throws Exception {
        return minioClient.copyObject(
                CopyObjectArgs.builder()
                        .source(CopySource.builder().bucket(bucketName).object(objectName).build())
                        .bucket(srcBucketName)
                        .object(srcObjectName)
                        .build());
    }

    /**
     * 删除文件
     * @param bucketName 存储桶
     * @param objectName 文件名称
     */
    public static void removeFile(String bucketName, String objectName) throws Exception {
        minioClient.removeObject(
                RemoveObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build());
    }

    /**
     * 批量删除文件
     * @param bucketName 存储桶
     * @param keys 需要删除的文件列表
     * @return
     */
    public static 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("[Minio工具类]>>>> 批量删除文件,异常:", e);
            }
        });
    }

    /**
     * 获取文件外链
     * @param bucketName 存储桶
     * @param objectName 文件名
     * @param expires 过期时间 <=7 秒 (外链有效时间(单位:秒))
     * @return url
     * @throws Exception
     */
    public static String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) throws Exception {
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder().expiry(expires).bucket(bucketName).object(objectName).build();
        return minioClient.getPresignedObjectUrl(args);
    }

    /**
     * 获得文件外链
     * @param bucketName
     * @param objectName
     * @return url
     * @throws Exception
     */
    public static String getPresignedObjectUrl(String bucketName, String objectName) throws Exception {
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
                                                                    .bucket(bucketName)
                                                                    .object(objectName)
                                                                    .method(Method.GET).build();
        return minioClient.getPresignedObjectUrl(args);
    }

    /**
     * 将URLDecoder编码转成UTF8
     * @param str
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException {
        String url = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25");
        return URLDecoder.decode(url, "UTF-8");
    }

    /******************************  Operate Files End  ******************************/

}

创建controller层,这里要首先初始化静态类才能使用

@RestController
@RequestMapping("/file")
@Slf4j
public class FileController {

    @Resource
    private MinioClient minioClient;

    @Autowired
    private MinioConfig minioConfig;

    @PostConstruct
    public void initializeMinIO(){
        // 新建minIO
        new MinIOUtils(minioConfig.getUrl(),
                minioConfig.getTextBookBucketName(),
                minioConfig.getAccessKey(),
                minioConfig.getSecretKey(),
                1000,1000);
    }
    /**
     * 上传文件
     */
    @PostMapping("/resourceFileUpload")
    public CommonResult resourceFileUpload(@RequestParam("dirPath")String dirPath,
                                     @RequestParam("file") MultipartFile file) throws Exception {

        if(file == null){
            return new CommonResult<>("参数错误");
        }

        // 路径
        String path;
        if(StringUtils.isBlank(dirPath)){
            path = file.getOriginalFilename();
        }else{
            path = dirPath + "/" + file.getOriginalFilename();
        }
        // 上传
        MinIOUtils.uploadFile(minioConfig.getTextBookBucketName(), file, path, file.getContentType());
        FileVO fileVO = new FileVO();
        fileVO.setUri("/" + minioConfig.getTextBookBucketName()+ "/" + path);
        fileVO.setPreviewUrl(minioConfig.getUrl()+"/" + fileVO.getUri());
        log.info("文件上传成功!");
        return new CommonResult<>(fileVO);
    }
}

参考文章

SpringBoot+Minio搭建

16 分钟搭建高性能的文件服务器

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐