本文从本人博客搬运,原文格式更加美观,可以移步原文阅读:MinIO实战

基本介绍

1.简介

MinIO是一个开源的对象存储服务,非常适合存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等。而一个对象文件可以是任意大小,从几十kb到最大5T不等。MinIO是一个非常轻量的服务,可以很简单地和其他应用结合,类似Redis、MySQL

MinIO的优点如下:

  • 部署简单:一个二进制文件即是一切,还可以支持各种平台

  • 支持海量存储,可以按zone扩展,支持单个对象最大5TB

  • 低冗余且磁盘损坏高容忍,标准且最高的数据冗余系数为2(即存储一个1M的数据对象,实际占用磁盘空间为2M)。但在任意n/2块disk损坏的情况下依然可以读出数据(n为一个纠删码集合中的disk数量)。并且这种损坏恢复是基于单个对象的,而不是基于整个存储卷的

  • 读写性能优异

关于项目文件存储的技术选型:

  • 如果不想自己搭建文件服务器,那么可以选择购买阿里云、腾讯云等对象存储服务
  • 如果自己搭建对象存储服务器,可以选择MinIO

2.基础概念

MinIO中涉及以下几个基础概念:

  • Object:存储到MinIO的基本对象,如文件、字节流等
  • Bucket:用来存储Object的逻辑空间。每个Bucket之间的数据是相互隔离的。对于客户端而言,就相当于一个存放文件的顶层文件夹
  • Drive:即存储数据的磁盘,在MinIO启动时,以参数的方式传入。MinIO中的所有对象数据都会存储在Drive
  • Set:即一组Drive的集合。分布式部署根据集群规模自动划分一个或多个Set,每个Set中的Drive分布在不同位置。一个对象存储在一个Set
    • 一个Object存储在一个Set
    • 一个集群划分为多个Set
    • 一个Set包含的Drive数量是固定的,默认由系统根据集群规模自动计算得出
    • 一个Set中的Drive尽可能分布在不同节点上

3.纠删码EC

在存储领域,保证数据可靠性主要有2大类方法:

  • 冗余:简单直接,对存储的数据进行副本备份。当数据出现丢失、损坏,即可使用备份内容进行恢复,而副本备份的多少,决定了数据可靠性的高低。这其中会有成本的考量,副本数据越多,数据越可靠,但需要的设备越多,成本就越高。当前已经有很多分布式系统是采用这种方式实现,如Hadoop的文件系统,Redis集群,MySQL主备模式等
  • 校验:校验法即通过校验码的数学计算方式,对出现丢失、损坏的数据进行校验、还原。注意,这里有2个作用,一个是校验,通过对数据进行checksum计算,可以检查数据是否完整,有无损坏或更改,在数据传输和保存时经常用到,如TCP协议;二是恢复还原,通过对数据结合校验码,通过数学计算,还原丢失或损坏的数据,可以在保证数据可靠的前提下,降低冗余。如单机硬盘存储中的RAID技术,纠删码技术等

MinIO使用纠删码机制来保证高可靠性,使用hignwayhash来处理数据损坏。关于纠删码,简单来说就是可以通过数学计算,把丢失的数据进行还原。它可以将n份原始数据,增加m份数据,并能通过n+m份中的任意n份数据,还原为原始数据。即如果有任意小于等于m份的数据失效,仍然能通过剩下的数据还原出来

MinIO使用纠删码和校验checksum来保护数据免受硬件故障和无声数据损坏,即使丢失一半数量的硬盘,仍然可以恢复数据

MinIO环境搭建

1.单机部署

单机模式中,要管理的磁盘都在host本地,该模式一般仅用于实验环境、测试环境。在这种模式下,还可以分为非纠删码模式和纠删码模式:

  • 非纠删码模式:在此启动模式下,对于每一份对象数据,MinIO直接在磁盘中存储这份数据,不会建立副本,也不会启用纠删码机制。因为这种模式无论是服务实例还是磁盘都是单点,无任何高可用保障,磁盘损坏就表示数据丢失
  • 纠删码模式:此模式为MinIO服务实例启动时传入多个本地磁盘参数,一旦遇到多个磁盘参数,MinIO服务器会自动启用纠删码模式,纠删码对磁盘的个数是有要求的,如果不满足要求,那么服务实例启动将会失败。纠删码启用后,要求传给MinIO服务器的endpoint(在单机模式下,即为本地磁盘上的目录)至少为4个

1.1 Centos7普通安装

首先下载MinIO

wget http://dl.minio.org.cn/server/minio/release/linux-amd64/minio
# 添加可执行权限
chmod +x minio

然后启动minio,注意启动时需要传入一个本机磁盘路径的参数,告诉MinIO将文件保存在哪里

# 将/minio/data作为文件存储目录
./minio server /minio/data

启动后控制台有一些输出,其中有几个关键信息:

  • 操作MinIO的端口是9000
  • 访问MinIO控制台界面的端口是42579,这是一个动态生成的端口,建议启动时添加--console-address ":port"来指定一个固定的端口
  • 默认的用户名和密码都是minioadmin,建议通过修改环境变量MINIO_ROOT_USERMINIO_ROOT_PASSWORD来修改

我们根据建议来重新启动MinIO

# 修改环境变量,自定义minio的root用户名和密码(密码至少要8位)
export MINIO_ROOT_USER=admin
export MINIO_ROOT_PASSWORD=12345678

# 启动minio时指定控制台端口
./minio server --console-address ":9001" /minio/data

此时/minio/data已经创建

我们访问控制台:http://ip:9001,登录后界面如下

在Bucket页面可以创建Bucket

此时MinIO会在磁盘目录中也生成bucket对应的目录

我们可以利用控制台在bucket中上传文件

还可以直接浏览文件、下载或者删除,浏览文件时可以直接播放视频,查看图片,非常方便

此时对应bucket目录中保存了上传的文件

注意:

  • 上述的启动形式,由于只传了1个磁盘路径参数,所以默认是非纠删码模式,没有冗余数据保护,磁盘中只保存了一份原始文件

  • 实际开发中建议传入至少4个磁盘路径,启用纠删码模式,这样即使某些磁盘损坏了,也能恢复数据

    # 指定多个磁盘路径,启用纠删码模式
    ./minio server --console-address ":9001" /minio/disk{1...4}
    

    在纠删码模式下再上传文件,可以发现每个磁盘路径中都保存了一份原始文件和纠删码数据,这样就可以实现一定的冗余,保障数据恢复

1.2 Docker安装

我们直接开启纠删码模式,指定多个磁盘路径

docker run -d -p 9000:9000 -p 50000:50000 --name minio \
  -e "MINIO_ROOT_USER=admin" \
  -e "MINIO_ROOT_PASSWORD=12345678" \
  -v /minio/docker/disk1:/disk1 \
  -v /minio/docker/disk2:/disk2 \
  -v /minio/docker/disk3:/disk3 \
  -v /minio/docker/disk4:/disk4 \
  -v /minio/docker/config:/root/.minio \
  minio/minio server --console-address ":50000" /disk{1...4}

启动容器时注意以下几点:

  • 设置用户名密码的环境变量
  • 暴露操作端口和控制台端口
  • 映射数据目录和配置文件目录
  • 启动时指定命令,server --console-address ":50000" /disk{1...4},指定控制台端口和磁盘路径参数

2.分布式集群部署

单机部署虽然可以指定多个路径参数利用纠删码模式实现数据安全,但是一旦节点宕机,就无法访问数据了。分布式部署可以将多块分布在不同物理机器上的硬盘组成一个对象存储服务,这样避免了单点故障带来的影响

分布式MinIO有如下优势:

  • 数据保护:分布式MinIO至少需要传入4个磁盘路径参数,所以自动引入了纠删码
  • 高可用:一个有n块硬盘的分布式MinIO,只要有n/2的硬盘在线,你的数据就是安全的,即可读。但是如果要保证数据可写入,至少需要有n/2 + 1块硬盘在线。例如,一个16个节点的MinIO集群,每个节点16块硬盘,就算有8台服务器宕机,这个集群仍然是可读的,不过需要9台服务器在线才可以写入数据
  • 一致性:MinIO在分布式和单机模式下,所有读写操作都严格遵循read-after-write一致性模型

启动一个分布式MinIO实例非常简单,只需要在传递给启动命令的多个磁盘路径参数前面加上主机地址即可,如下列命令启动了一个有8个节点,每个节点1块硬盘的分布式MinIO集群

export MINIO_ROOT_USER=admin
export MINIO_ROOT_PASSWORD=12345678

./minio server --console-address ":9001" \
http://192.168.1.11/disk1 \
http://192.168.1.12/disk2 \
http://192.168.1.13/disk3 \
http://192.168.1.14/disk4 \
http://192.168.1.15/disk5 \
http://192.168.1.16/disk6 \
http://192.168.1.17/disk7 \
http://192.168.1.18/disk8

但是有几点需要注意:

  • 分布式MinIO的所有节点需要有相同的用户名和密码,这样才能互相建立连接。所以运行之前需要设置环境变量MINIO_ROOT_USERMINIO_ROOT_PASSWORD
  • 分布式MinIO使用的磁盘必须是干净的,里面没有数据
  • 分布式MinIO里的节点时间差不能超过3秒,可以使用NTP来保证时间一致
  • 在Windows平台下的分布式MinIO仍然处于实验阶段,不建议使用

MinIO客户端使用

之前我们操作MinIO都是在图形化的控制台进行,但是如果不能连接控制台该怎么办。MinIO提供了客户端,可以让我们以类似linux命令的方式操作MinIO

ls       列出文件和文件夹。
mb       创建一个存储桶或一个文件夹。
cat      显示文件和对象内容。
pipe     将一个STDIN重定向到一个对象或者文件或者STDOUT。
share    生成用于共享的URL。
cp       拷贝文件和对象。
mirror   给存储桶和文件夹做镜像。
find     基于参数查找文件。
diff     对两个文件夹或者存储桶比较差异。
rm       删除文件和对象。
events   管理对象通知。
watch    监听文件和对象的事件。
policy   管理访问策略。
session  为cp命令管理保存的会话。
config   管理mc配置文件。
update   检查软件更新。
version  输出版本信息。

1.安装mc客户端

首先要下载mc客户端,下载地址如下:

  • Linux:http://dl.minio.org.cn/client/mc/release/linux-amd64/mc
  • Windows:http://dl.minio.org.cn/client/mc/release/windows-amd64/mc.exe

我们尝试在windows上安装,来操作Linux上的MinIO服务端。下载完成后可以mc.exe --help查看是否安装成功

然后我们需要配置客户端需要操作的服务端host地址,可以用如下命令查看已经配置的服务端host列表

mc config host ls

默认配置了以下几个host

我们可以添加host,指定主机端口,用户名密码即可

mc config host add my-minio-server http://192.168.153.130:9000 admin 12345678

如果想要删除host,可以用以下命令

mc config host remove my-minio-server

2.mc基本命令使用

2.1 查看文件

我们可以通过如下命令查看某个host的存储的文件

# 查看host的所有bucket
mc ls my-minio-server
# 查看某个bucket中的文件
mc ls my-minio-server/test

2.2 上传下载

上传和下载文件使用mc cp命令,指定本地文件名和远程host的名称以及bucket即可

# 上传文件
mc cp consul.exe my-minio-server/test

# 下载文件
mc cp my-minio-server/test/consul.exe D:\test

2.3 删除文件

mc rm my-minio-server/test/consul.exe

2.4 Bucket管理

# 创建bucket
mc mb my-minio-server/bucket01

# 删除bucket(如果bucket中有文件数据,删除将会失败)
mc rb my-minio-server/bucket01

# 强制删除有文件数据的bucket
mc rb --force my-minio-server/bucket01

3.mc admin命令使用

MinIO Client(mc)提供了 admin子命令来对您的MinIO部署执行管理任务

service     服务重启并停止所有MinIO服务器
update      更新更新所有MinIO服务器
info        信息显示MinIO服务器信息
user        用户管理用户
group       小组管理小组
policy      MinIO服务器中定义的策略管理策略
config      配置管理MinIO服务器配置
heal        修复MinIO服务器上的磁盘,存储桶和对象
profile     概要文件生成概要文件数据以进行调试
top         顶部提供MinIO的顶部统计信息
trace       跟踪显示MinIO服务器的http跟踪
console     控制台显示MinIO服务器的控制台日志
prometheus  Prometheus管理Prometheus配置
kms         kms执行KMS管理操作

3.1 用户管理

# 查看用户
mc admin user list my-minio-server

# 添加用户
mc admin user add my-minio-server baobao 12345678

# 禁用用户
mc admin user disable my-minio-server baobao

# 启用用户
mc admin user enable my-minio-server baobao

# 查看用户信息
mc admin user info my-minio-server baobao

# 删除用户
mc admin user remove my-minio-server baobao

3.2 策略管理

policy命令用于添加、删除、查看策略,可以给MinIO的用户设置相关策略,控制用户访问权限

# 查看所有策略
mc admin policy list my-minio-server

# 查看某个策略的具体信息
mc admin policy info my-minio-server readwrite

# 添加新的策略
mc admin policy add my-minio-server policyname /root/mypolicy.json
# 给用户设置策略
mc admin policy set my-minio-server policyname user=username

policy策略文件的写法如下

{
   "Version": "2012-10-17",
   "Statement": [
      {
         "Effect": "Allow",
         "Action": [
            "s3:ListAllMyBuckets"
         ],
         "Resource": [
            "arn:aws:s3:::*"
         ]
      }
   ]
}

Java客户端使用

1.普通java项目

引入依赖

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

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.8.1</version>
</dependency>

测试上传

@Test
public void testUpload() throws Exception {
    // 创建minio客户端,指定要操作的服务器地址以及用户密码
    MinioClient minioClient = MinioClient.builder().endpoint("http://192.168.153.130:9000")
        .credentials("admin", "12345678")
        .build();
    // 创建上传参数,指定要上传到哪个bucket,本地文件路径和上传后的文件名
    UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder().bucket("test")
        .object("typora.exe") // 上传到bucket后的文件名
        .filename("D:\\迅雷下载\\typora-update-x64-0522.exe") // 本地文件路径
        .build();
    // 上传文件
    minioClient.uploadObject(uploadObjectArgs);
}

测试下载

@Test
public void testDownload() throws Exception {
    // 创建minio客户端,指定要操作的服务器地址以及用户密码
    MinioClient minioClient = MinioClient.builder().endpoint("http://192.168.153.130:9000")
        .credentials("admin", "12345678")
        .build();
    // 创建下载参数
    DownloadObjectArgs downloadObjectArgs = DownloadObjectArgs.builder().bucket("test")
        .object("typora.exe") // 要下载的文件在bucket中的文件名
        .filename("D:\\typora111.exe") // 下载后保存的本地路径
        .build();
    // 下载文件
    minioClient.downloadObject(downloadObjectArgs);
}

2.SpringBoot整合

由于官方没有提供对应的starter,所以需要我们自己手动整合,引入的依赖与普通项目一致。首先要创建MinIO配置属性类,与yaml中的配置对应

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

在yaml配置中配置对应的minio

minio:
  endpoint: http://192.168.153.130:9000
  accessKey: admin
  secretKey: 12345678
  bucket: test
  
# 设置上传文件最大大小
spring:
  servlet:
    multipart:
      max-request-size: 100MB
      max-file-size: 100MB

创建minio配置类,创建MinioClient放入容器

@Configuration
public class MinioConfig {
    @Autowired
    private MinioProperties minioProperties;
    
    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder().endpoint(minioProperties.getEndpoint())
                .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
                .build();
    }
}

创建Controller,注入MinioClient,实现文件列表、上传、下载、删除接口

2.1 获取文件列表

@RestController
@RequestMapping("minio")
public class MinioController {
    @Autowired
    private MinioClient minioClient;

    @Autowired
    private MinioProperties minioProperties;

    @GetMapping("list")
    public List<String> list() {
        List<String> result = new ArrayList<>();
        // 获取bucket中的文件对象列表
        ListObjectsArgs listObjectsArgs = ListObjectsArgs.builder().bucket(minioProperties.getBucket()).build();
        Iterable<Result<Item>> objects = minioClient.listObjects(listObjectsArgs);
        objects.forEach(obj -> {
            try {
                Item item = obj.get();
                result.add(item.objectName());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        return result;
    }
}

2.2 文件下载

/**
     * 下载文件
     * @param fileName 文件在bucket中的对象名称
     */
@GetMapping("download/{fileName}")
public void download(@PathVariable("fileName") String fileName, HttpServletResponse response) throws Exception {
    String bucket = minioProperties.getBucket();
    // 获取要下载的对象的信息
    StatObjectArgs statObjectArgs = StatObjectArgs.builder().bucket(bucket).object(fileName).build();
    StatObjectResponse stat = minioClient.statObject(statObjectArgs);
    // 设置响应文件类型
    response.setContentType(stat.contentType());
    // 设置下载响应头
    response.setHeader("Content-Disposition", "attachment;filename=" 
                       + URLEncoder.encode(fileName, "UTF-8"));
    // 下载文件流
    GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket).object(fileName).build();
    try (GetObjectResponse object = minioClient.getObject(getObjectArgs);
         OutputStream out = response.getOutputStream()){
        IOUtils.copy(object, out);
    }
}

2.3 文件上传

@PostMapping("upload")
public void upload(MultipartFile file) throws Exception {
    String bucket = minioProperties.getBucket();
    // 上传文件流
    try (InputStream in = file.getInputStream()){
        PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(bucket).object(file.getOriginalFilename())
            .contentType(file.getContentType())
            .stream(in, file.getSize(), -1)
            .build();
        minioClient.putObject(putObjectArgs);
    }
}

注意:上传文件的时候一定要通过file.getContentType()获取文件的类型并通过.contentType()设置,否则默认的文件类型将会是二进制流,这样后续获取文件临时访问url时返回的响应数据的Content-Type也会是二进制流,导致无论任何文件浏览器都会直接下载。而如果是图片之类的文件我们更希望是浏览器进行显示而不是下载,并且一些前端框架拿到图片数据也会根据Content-Type决定是否显示,错误的Content-Type会导致浏览器错误的行为

2.4 文件删除

@DeleteMapping("delete/{fileName}")
public void delete(@PathVariable("fileName") String fileName) throws Exception {
    RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket(minioProperties.getBucket())
        .object(fileName).build();
    minioClient.removeObject(removeObjectArgs);
}

2.5 获取文件临时http url

如果将bucket设置为公共读,返回固定url访问文件会不安全。一般情况下都是将bucket设置为私有,然后返回一个预签名的临时url,这个url有访问超时时间

@GetMapping("url")
public String getObjectUrl(String path) throws Exception {
    // 创建参数
    GetPresignedObjectUrlArgs presignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
        .bucket(minioProperties.getBucket())
        .object(path) // object的路径
        .method(Method.GET) // http请求方式
        .expiry(5, TimeUnit.MINUTES) // url过期时间
        .build();
    return minioClient.getPresignedObjectUrl(presignedObjectUrlArgs);
}

用临时url可以访问文件

注意:

  • 默认情况下生成的url都是基于http的,如果想要用https访问,可以将相关证书放到minio配置文件目录的/cert文件夹中,即可生效

  • 如果endpoint是https协议,minio接口出现javax.net.ssl.SSLHandshakeException异常,那么需要检查证书是否正常。Java在连接自签名、非权威机构颁发、证书安全等级有问题的https地址时会抛上述异常。如果正常流程申请的证书还是出现上述问题,可以去https://myssl.com输入域名检查证书是否异常,比如证书链是否完整

2.6 前端直传

如果按照之前的上传方式,文件会先传到后端服务器,然后再由后端服务传到minio服务。如果后端服务和minio服务在同一台物理机上还好,如果分开部署,将极大浪费后端服务器的带宽。所以一般采用前端直传方式

在minio中实现前端直传有2种方式:PUT临时url方式和POST预签名方式

PUT方式

步骤为:

  1. 前端请求后端,后端通过minio生成一个预签名的上传url(方式与临时访问url一致,只是请求方法要从GET改为PUT)返回给前端

  2. 前端通过这个上传url,将文件直传给minio。注意请求方式为PUT,并且传文件的方式并不是multipart/form-data,而是直接将文件的二进制数据放到请求体中。不管文件原来的名称是什么,上传到bucket中的名称都为参数path所指定的名称

@GetMapping("uploadUrl")
public String getObjectUrl(String path) throws Exception {
    // 创建参数
    GetPresignedObjectUrlArgs presignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
        .bucket(minioProperties.getBucket())
        .object(path) // object的路径
        .method(Method.PUT) // 上传必须为PUT方式
        .expiry(5, TimeUnit.MINUTES) // url过期时间
        .build();
    return minioClient.getPresignedObjectUrl(presignedObjectUrlArgs);
}
POST方式

除了生成PUT方式的上传url,还有一种可以用POST方式上传文件,步骤如下:

  1. 前端请求后端,后端先返回文件上传的预签名信息给到前端,其中包含加密信息以及后端生成的文件在bucket中的实际路径
  2. 前端根据预签名信息,向endpoint + bucket名称组成的url发送POST上传请求,请求体的multipart/form-data中除了带上file字段表示实际文件,还需要携带预签名返回的所有参数
/**
     * 根据原始文件名生成随机化文件名,并散列到日期目录中
     * @param dir 上传到bucket哪个目录,开头不要加/
     * @param originFileName 原始文件名
     * @return 最终生成的文件在bucket中的路径
     */
private String generateFilePath(String dir, String originFileName) {
    // 随机化文件名
    String uuid = UUID.randomUUID().toString(true);
    String fileName = StrUtil.addPrefixIfNot(originFileName, uuid);
    String rootPath;
    if (StrUtil.isEmpty(dir)) {
        rootPath = "";
    }else {
        rootPath = dir.endsWith("/") ? dir : dir + "/";
    }
    // 将文件散列到日期目录
    return rootPath + DateUtil.format(new Date(), "yyyyMMdd") + "/" + fileName;
}

@GetMapping("presignedInfo")
public Map<String, String> getObjectUrl(String fileName) throws Exception {
    String filePath = this.generateFilePath("", fileName);
    // 给指定bucket创建一个上传策略,超时时间为60秒
    PostPolicy policy = new PostPolicy(minioProperties.getBucket(), ZonedDateTime.now().plusSeconds(60));
    // 设置一个参数key,值为上传对象的名称
    policy.addEqualsCondition("key", path);
    // 添加Content-Type以"image/"开头,表示只能上传照片
    // policy.addStartsWithCondition("Content-Type", "image/");
    // 设置上传文件的大小 64kiB to 10MiB.
    // policy.addContentLengthRangeCondition(64 * 1024, 10 * 1024 * 1024);
    Map<String, String> map = minioClient.getPresignedPostFormData(policy);
    // 将文件在bucket中的最终路径回传给前端
    map.put("key", path);
    return map;
}

生成的预签名信息示例如下:

拿到这些信息后,可以用postman模拟前端上传文件:

注意:

  • 创建的上传策略中必须包含一个key参数,即policy.addEqualsCondition("key", path);这句代码,其中值path的含义是文件上传成功后在bucket中的实际路径。如果创建的策略中不包含key,会抛出异常

  • 前端实际上传文件时,需要将之前后端返回的预签名信息作为body中的参数连同文件一起发起请求,其中key为实际上传到bucket中的文件路径+文件名,不管源文件的名称是什么。前端上传成功后就可以将这个key作为文件实际路径,后续需要调用后端具体业务接口保存文件字段到数据库时就这个传递这个key作为参数,这样这个路径保存到数据库后,后续获取到这个值可以请求获取其临时访问url

  • 实际开发中推荐这种POST预签名直传方式

3.自定义Springboot-Starter

首先创建一个Springboot工程,作为starter,引入starter必要的依赖

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.13.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.baobao.minio</groupId>
    <artifactId>minio-spring-boot-starter</artifactId>
    <version>0.0.7-SNAPSHOT</version>
    <name>minio-spring-boot-starter</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <minio.version>8.3.0</minio.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>${minio.version}</version>
        </dependency>

    </dependencies>

</project>

然后创建yaml对应的配置类

@ConfigurationProperties("minio")
@Data
public class MinioConfigurationProperties {
    private String endpoint;
    private String accessKey;
    private String secretKey;
    private String bucket;
    private Integer tempUrlExpire;
}

创建minio自动配置类,将MinioClient存入容器

@Configuration
@EnableConfigurationProperties(MinioConfigurationProperties.class)
public class MinioAutoConfiguration {
    @Autowired
    private MinioConfigurationProperties minioProperties;

    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder().endpoint(minioProperties.getEndpoint())
                .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
                .build();
    }
}

创建MinioService,封装方法

@Service
public class MinioService {
    @Autowired
    private MinioClient minioClient;

    @Autowired
    private MinioConfigurationProperties minioProperties;

    /**
     * 获取文件列表
     * @return 文件列表
     */
    public List<String> listFiles() {
        List<String> result = new ArrayList<>();
        // 获取bucket中的文件对象列表
        ListObjectsArgs listObjectsArgs = ListObjectsArgs.builder().bucket(minioProperties.getBucket()).build();
        Iterable<Result<Item>> objects = minioClient.listObjects(listObjectsArgs);
        objects.forEach(obj -> {
            try {
                Item item = obj.get();
                result.add(item.objectName());
            } catch (Exception e) {
                throw new MinioException("文件服务异常");
            }
        });
        return result;
    }

    /**
     * 获取文件输入流
     * @param filePath 文件在bucket中的相对路径
     * @return 文件输入流
     */
    public InputStream getFile(String filePath) {
        GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(minioProperties.getBucket()).object(filePath).build();
        try {
            return minioClient.getObject(getObjectArgs);
        } catch (Exception e) {
            throw new MinioException("文件服务异常");
        }
    }

    /**
     * 获取文件参数
     * @param filePath 文件在bucket中的相对路径
     * @return 文件参数
     */
    public StatObjectResponse getFileStat(String filePath) {
        StatObjectArgs statObjectArgs = StatObjectArgs.builder().bucket(minioProperties.getBucket()).object(filePath).build();
        try {
            return minioClient.statObject(statObjectArgs);
        } catch (Exception e) {
            throw new MinioException("文件服务异常");
        }
    }

    /**
     * 上传文件
     * @param inputStream 文件输入流
     * @param filePath 文件在bucket中的相对路径
     * @param contentType 文件mime类型
     * @param fileSize 文件大小
     */
    public void uploadFile(InputStream inputStream, String filePath, String contentType, long fileSize) {
        PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(minioProperties.getBucket()).object(filePath)
                .contentType(contentType)
                .stream(inputStream, fileSize, -1)
                .build();
        try {
            minioClient.putObject(putObjectArgs);
        } catch (Exception e) {
            throw new MinioException("文件服务异常");
        }
    }

    /**
     * 删除文件
     * @param filePath 文件在bucket中的相对路径
     */
    public void deleteFile(String filePath) {
        RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket(minioProperties.getBucket())
                .object(filePath).build();
        try {
            minioClient.removeObject(removeObjectArgs);
        } catch (Exception e) {
            throw new MinioException("文件服务异常");
        }
    }

    /**
     * 获取文件临时url
     * @param filePath 文件在bucket中的相对路径
     * @return 文件临时url
     */
    public String getFileTempUrl(String filePath) {
        GetPresignedObjectUrlArgs presignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
                .bucket(minioProperties.getBucket())
                .object(filePath) // object的路径
                .method(Method.GET) // http请求方式
                .expiry(minioProperties.getTempUrlExpire(), TimeUnit.MINUTES) // url过期时间
                .build();
        try {
            return minioClient.getPresignedObjectUrl(presignedObjectUrlArgs);
        } catch (Exception e) {
            throw new MinioException("文件服务异常");
        }
    }
}

其中自定义异常MinioException如下

public class MinioException extends RuntimeException {
    public MinioException(String message) {
        super(message);
    }
}

然后在resources下创建META-INF/spring.factories文件,指定自动配置类

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baobao.minio.MinioAutoConfiguration

最后install到本地maven仓库

这样在其他springboot工程中就可以使用这个starter了

<dependency>
    <groupId>com.baobao.minio</groupId>
    <artifactId>minio-spring-boot-starter</artifactId>
    <version>0.0.7-SNAPSHOT</version>
</dependency>

注意这里有一个坑,引入starter的工程中,需要额外添加一个属性修改okhttp的版本,否则会导致引入的okhttp版本过低,启动项目失败

<properties>
 <okhttp3.version>4.8.1</okhttp3.version>
</properties>

这样我们就可以在Controller中非常方便地使用minio

@RestController
@RequestMapping("minio")
public class MinioController {
    @Autowired
    private MinioService minioService;

    @GetMapping("list")
    public List<String> list() {
        return minioService.listFiles();
    }

    /**
     * 下载文件
     *
     * @param filePath 文件在bucket中的相对路径
     */
    @GetMapping("download/{filePath}")
    public void download(@PathVariable("filePath") String filePath, HttpServletResponse response) throws Exception {
        // 获取要下载的对象的信息
        StatObjectResponse stat = minioService.getFileStat(filePath);
        // 设置响应文件类型
        response.setContentType(stat.contentType());
        // 设置下载响应头
        response.setHeader("Content-Disposition", "attachment;filename="
                + URLEncoder.encode(filePath, "UTF-8"));
        // 下载文件流
        try (InputStream object = minioService.getFile(filePath);
             OutputStream out = response.getOutputStream()) {
            IOUtils.copy(object, out);
        }
    }


    @PostMapping("upload")
    public void upload(MultipartFile file) throws Exception {
        // 上传文件流
        try (InputStream in = file.getInputStream()) {
            minioService.uploadFile(in, file.getOriginalFilename(), file.getContentType(), file.getSize());
        }
    }

    @DeleteMapping("delete/{filePath}")
    public void delete(@PathVariable("filePath") String filePath) {
        minioService.deleteFile(filePath);
    }

    @GetMapping("url")
    public String getObjectUrl(String filePath) {
        return minioService.getFileTempUrl(filePath);
    }
}
Logo

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

更多推荐