SpringBoot+Minio搭建不再爆肝秃头的分布式文件服务器

4,159 阅读12分钟

前言

1)、有人一定会问,为什么不用FastDFS?众所周知,FastDFS的原生安装非常复杂,有过安装经验的人大体都明白,虽然可以利用别人做好的docker直接安装,但真正使用过程中也可能出现许多莫名其妙的问题;
2)、还有人会问,为什么不用oss或其他现有云产品?道理很简单,你不能保证自己所在的公司拥有的项目一定会上云,据我个人了解,大部分公司要么依托于甲方使用内网服务器,要么是公司自己内部搭建的,比如我公司就是依托于医院自己的服务器,所有部署以安全为首,只能自己搭建内部文件服务器;
3)、Minio是GO语言开发的,性能很好,安装简单,可分布式存储海量图片、音频、视频等文件,且拥有自己的管理后台界面,十分友好。
4)、我有个习惯,每年会观察流量大的培训机构会新增什么技术去除什么技术,虽然Minio出来也有些时日了,但这两年陆续有知名机构的讲师开始引入Minio了,意味着这个产品的接受度正在升高,随着培训机构培养的人员扩散到各个IT公司,接受度只会越来越高。
所以,此时不了解更待何时~


minio官网地址: docs.min.io/docs/minio-…
minio中文网地址: docs.minio.org.cn/docs/

特别说明:大部分内容直接看中文网即可,但下载minio时,最好看官网,因为minio版本更新非常快,经常会出现资源文件更换目录的情况,同时中文网的地址可能会失效,导致下载失败。



搭建Minio

分为下载、安装、启动、访问、自定义启动脚本及设置永久访问链接等几步操作。

1. 下载minio

  1)、手工下载:docs.min.io/docs/minio-…

  找到这个位置,下载自己需要的版本,我这里使用Linux,所以下载第一个就行。

1643606976(1).jpg


  2)、远程拉取
  创建自己的minio目录
  远程拉取: wget http://dl.minio.org.cn/server/minio/release/darwin-amd64/minio 微信图片_20220131171328.png


2. 安装minio

  1)、给minio二进制文件赋权限,否则无法执行:chmod +x minio
  2)、在二进制文件所在目录执行 ./minio ,成功后可看到最下面的版本号,我这里安装的是当前最新版。

微信图片_20220131172535.png


3. 启动minio

  1)、在minio安装目录新建data目录,用来存放minio的数据:mkdir data ;

微信图片_20220131173728.png


  2)、在后台进程启动minio: ./minio server /data/minio/data > /data/minio/minio.log 2>&1 &
    查看后台运行日志: tail -f minio.log

微信图片_20220131173737.png

特别说明: 这里日志可以看出来,新版的minio和老版是有区别的,这里API后面的地址是9000端口,console也就是控制台地址的端口是33587,而且最后一句WARNING有提示,控制台端口是动态生成的,请使用命令选择一个静态端口,意思就是如果重启了,那么这个控制台的端口又会发生改变,需要自己设置一个固定不变的静态端口,具体的设置方法可以按照提示的命令设置。
命令如下:(注意重启时要执行 kill -9 [进程号] 把之前后台进程启动的minio杀掉)

# 指定后台端口为9999
./minio server --console-address 0.0.0.0:9999 /data/minio/data > /data/minio/minio.log 2>&1 &

4. 访问minio

  设置固定的静态端口后,日志提示的访问地址是 http://127.0.0.1:9999 ,这里我们就替换成自己服务器的ip地址即可,我这里用的是腾讯云服务器。
  访问地址:http://42.193.14.144:9999
  效果如下,和老版的界面也不一样了:
  默认账号密码: minioadmin minioadmin

微信图片_20220131173751.png


5. 自定义脚本启动minio

  1)、新建一个shell脚本,把启动时需要设置的命令放进来即可。这里新增了设置账号密码的命令,不再用之前的默认账号密码minioadmin。
  新建shell脚本:vim minio-start.sh

# 设置账号密码
export MINIO_ACCESS_KEY=root
export MINIO_SECRET_KEY=123456

# 后台进程启动minio
./minio server --console-address 0.0.0.0:9999 /data/minio/data > /data/minio/minio.log 2>&1 &

  2)、给这个脚本赋予执行权限:chmod +x minio-start.sh
  3)、执行脚本启动minio:./minio-start.sh

  最终效果和上面一样!

6. 使用minio

  进入后台后便可以简单使用minio上传文件、预览、分享URL等来尝试minio带来的美好。 许多配置使用默认的就好,不明白的就多点点很快就会了,唯一要明白的是Bucket概念,因为调用minio的API时经常会用到它,简单点就可以理解为存放鸡蛋的篮子(存放文件的目录)。

图像_2022-01-31_183516.png

  PS: 刚开始使用的同学可能会习惯点击文件右侧几个按钮中的share按钮copy后台生成的文件链接,然后粘贴到浏览器打开,基本上都会遭遇打不开的情况,因为你仔细看链接就发现,这个链接地址的ip端口是错误的,这是一个误区,我们一般使用minio会通过mc客户端来执行命令进行一些配置,达到永久访问文件及直接下载文件的效果。

图像_2022-01-31_200351.png


7. 设置永久访问链接

  1)、安装mc客户端
  可以参考官网,写的很详细:docs.min.io/docs/minio-…
  也可以参考中文网: docs.minio.org.cn/docs/master…
  当你打开文档读一会儿后,你会发现写的很棒,但是看不懂。没关系,有许多踩过坑的人已经把障碍扫清了。 图像_2022-01-31_184636.png
安装MC客户端: wget https://dl.min.io/client/mc/release/linux-amd64/mc

微信图片_20220131185049.png

这里就印证了我前面讲的,安装minio相关文件最好看官网,这里的中文网地址无法下载了,所以无法安装成功。

  官网mc安装地址:

gw.png   中文网mc安装地址:(这个我使用时已经失效了)

zww.png


  同样的,要给mc执行文件赋予权限,否则会提示权限不足的错误。
  chmod +x mc

  设置永久访问链接,这里官网和中文网都讲的不清楚,个人认为这里就是设置了一个可访问的前缀地址,方便之后开放桶权限后能直接访问到图片,方便理解你可以想象为nginx做代理。

微信图片_20220131185636.png

  设置配置名称为minio,设置访问前缀为http://42.193.14.144 ,这是前面说的我的腾讯云服务器地址,端口设为9000,当然也可以设为别的,我这里设为9000是因为腾讯云安全组的规则已经存在9000端口,我不需要重新添加规则了。这里的root和123456就是前面自定义启动脚本设置的账号密码,你改成自己的就好。其他都不需要改。

./mc config host add minio http://42.193.14.144:9000 root 123456 --api S3v4

特别说明:切记,这里设置端口,如果用的是本地虚拟机,要么关闭防火墙,要么就打开你设定的这个端口;如果用的是和我一样的云服务器,不管有没有打开防火墙,都要在云服务器后台管理中添加规则开放这个端口,否则你依然打不开文件。


  设置某个桶(即文件目录)中的文件可直接下载的权限:./mc policy set download minio/hospitalimages

这里的hospitalimages就是我自己建的存放互联网医院文件的桶了,记得一定要加上前面的minio,是上一步命令设定的配置名。 微信图片_20220131190759.png

  执行命令后,这个桶下面的文件就可以直接访问到了。
  设置永久访问链接和下载权限的命令执行完后,最终效果如下:
可通过 http://服务器ip:端口/桶名称/文件名称 直接访问到了! 微信图片_20220131190807.png



SpringBoot整合Minio

特别说明:minio引入不同版本依赖使用过程是有较大区别的,比如7和8就区别很大,本人也踩过不少坑,搜过不少资料,虽然7版本算是用上了,但目前版本较新,就使用8版本,8的坑也很多,后来在某网站的风间影月老师那里终于找到了能使用的方案,也已经用在了公司的项目中,在这里直接分享给大家。

1. 引入依赖
<!-- MinIO -->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.2.1</version>
</dependency>
2. MinioUtils工具类
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.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;

/**
 * MinIO工具类
 *
 * @author guoj
 * @date 2021/12/14 19:30
 */
@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  ******************************/


}



总结

  这样其实就完成了整合,是不是So Easy?咔咔,在需要用到的地方通过MinioUtils.xxx()方法调用即可,比如我在公司项目中用到的就是MinioUtils.getPresignedObjectUrl()这个获取文件外链的方法,因为大多数时候不需要你对文件本身进行修改删除操作,正常来讲只会用到上传和查询文件的操作,在设计上许多产品老师也会规避这种风险问题。另外,工具类中传递的endpoint、bucketName、accessKey、ecretKey等参数,都是在minio后台可以拿到的,没有的话也可以自己设置。



啥也不说了,纯手打,看在这点辛苦的份上,各位看官点个赞给个关注可好?
可惜没有一键三连…… (=_=!)


其他文章推荐:
Springboot+Redisson自定义注解一次解决重复提交问题(含源码):juejin.cn/post/706328…