Spring Boot轻松整合Minio实现文件上传下载功能

一、Linux 安装Minio

安装

/root/xxkfz/soft目录下面创建文件minio文件夹,进入minio文件夹,并创建data目录;

csharp 复制代码
[root@xxkfz soft]# mkdir minio
[root@xxkfz soft]# cd minio
[root@xxkfz minio]# mkdir data

执行如下命令进行下载

csharp 复制代码
[root@xxkfz minio]# wget https://dl.min.io/server/minio/release/linux-amd64/minio
[root@xxkfz minio]# chmod +x minio  # 赋权

下载完成后如下所示:

设置账号密码

minio 默认账号密码为 minioadmin/minioadmin

csharp 复制代码
[root@xxkfz minio]# export MINIO_ACCESS_KEY=admin # 设置控制台账号(最少3位)
[root@xxkfz minio]# export MINIO_SECRET_KEY=12345678 # 设置密码(最少8位)

直接设置管理员账号密码 编辑 /etc/profile 文件即可

csharp 复制代码
[root@xxkfz minio]# vim /etc/profile

编辑/etc/profile文件,追加如下内容:

ini 复制代码
#===============================Minio=============================================
# set minio environment
export MINIO_ROOT_USER=admin
export MINIO_ROOT_PASSWORD=admin123

可省略 设置账号密码 此步骤!12

启动

进入执行文件目录/root/xxkfz/soft/minio,自定义端口启动(默认端口:9000)

typescript 复制代码
[root@xxkfz minio]# nohup /root/xxkfz/soft/minio/minio server --address :9001 --console-address :9002 /root/xxkfz/soft/minio/data >/root/xxkfz/soft/minio/minio.log 2>&1 &

说明:

  • nohup 为后台启动
  • ./minio server 启动命令
  • --address :9001 指定API端口
  • --console-address :9002 指定控制台端口
  • /usr/local/minio/data 指定存储目录
  • >/usr/local/minio/minio.log 2>&1 控制台日志重定向到/usr/local/minio/minio.log文件中
  • & 后台运行

启动成功:

启动成功

注意:浏览器访问需要开启防火墙端口!

阿里云配置开放9001、9002端口

测试访问:http://IP地址:9002

输入账号密码: admin/12345678 登录成功!

设置开机自启动

设置Minio服务器宕机后自动重启

进入init.d目录

csharp 复制代码
[root@xxkfz minio]# cd /etc/rc.d/init.d

新建minio.sh shell脚本文件

csharp 复制代码
[root@xxkfz init.d]# vim minio.sh 

shell脚本内容

bash 复制代码
#!/bin/bash
#chkconfig: 2345 10 90
#description: ping10
nohup /root/xxkfz/soft/minio/minio server --address :9001 --console-address :9002 /root/xxkfz/soft/minio/data >/root/xxkfz/soft/minio/minio.log 2>&1 &

给shell脚本赋权

bash 复制代码
chmod +x minio.sh

添加到开机自启动服务中

csharp 复制代码
chkconfig --add minio.sh

设置开机自启动

csharp 复制代码
chkconfig minio.sh on

查看是否添加成功

css 复制代码
chkconfig --list

二、Spring Boot整合Minio

项目搭建

项目基本结构

引入依赖

pom.xml

xml 复制代码
   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.2.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>

配置MinIo

application.yml

yaml 复制代码
minio:
  endpoint: http://IP地址:9001
  accessKey: admin
  secretKey: 12345678
  bucketName: xk-admin

# 配置端口号
server:
  port: 8099

编写配置类

MinioConfig.java

kotlin 复制代码
/**
 * @program: xxkfz-minio
 * @ClassName MinioConfig.java
 * @author: 公众号:小小开发者
 * @create: 2024-03-13 10:53
 * @description: Minio 配置类
 **/
@Data
@Configuration
public class MinioConfig {

    /**
     * 访问地址
     */
    @Value("${minio.endpoint}")
    private String endpoint;

    /**
     * accessKey类似于用户ID,用于唯一标识你的账户
     */
    @Value("${minio.accessKey}")
    private String accessKey;

    /**
     * secretKey是你账户的密码
     */
    @Value("${minio.secretKey}")
    private String secretKey;

    /**
     * 默认存储桶
     */
    @Value("${minio.bucketName}")
    private String bucketName;

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

编写Minio操作工具类

MinioUtils.java

scss 复制代码
/**
 * @program: xxkfz-minio
 * @ClassName MinioUtils.java
 * @author: 公众号:小小开发者
 * @create: 2024-03-13 10:55
 * @description: MinIO操作工具类
 **/
@Slf4j
@Component
public class MinioUtils {

    @Autowired
    private MinioClient minioClient;


    /**
     * 启动SpringBoot容器的时候初始化Bucket
     * 如果没有Bucket则创建
     *
     * @param bucketName
     */
    public void createBucket(String bucketName) {
        try {
            if (!bucketExists(bucketName)) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
                log.info("创建bucketName = {}完成!", bucketName);
                return;
            }
            log.info("bucketName = {}已存在!策略为:{}", bucketName, getBucketPolicy(bucketName));
        } catch (Exception e) {
            log.error("创建bucketName = {}异常!e = {}", bucketName, e);
        }
    }

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

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

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

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

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


    /**
     * 判断文件是否存在
     *
     * @param bucketName
     * @param objectName
     * @return
     */
    public 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 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 列表
     */
    @SneakyThrows(Exception.class)
    public List<Item> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive) {
        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 二进制流
     */
    @SneakyThrows(Exception.class)
    public InputStream getObject(String bucketName, String objectName) {
        return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }

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

    /**
     * 获取路径下文件列表
     *
     * @param bucketName 存储桶
     * @param prefix     文件名称
     * @param recursive  是否递归查找,false:模拟文件夹结构查找
     * @return 二进制流
     */
    public 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
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse uploadFile(String bucketName, MultipartFile file, String objectName, String contentType) {
        InputStream inputStream = file.getInputStream();
        return minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).contentType(contentType).stream(inputStream, inputStream.available(), -1).build());
    }

    /**
     * 图片上传
     *
     * @param bucketName
     * @param imageBase64
     * @param imageName
     * @return
     */
    public ObjectWriteResponse uploadImage(String bucketName, String imageBase64, String imageName) {
        if (!StringUtils.isEmpty(imageBase64)) {
            InputStream in = base64ToInputStream(imageBase64);
            String newName = System.currentTimeMillis() + "_" + imageName + ".jpg";
            String year = String.valueOf(new Date().getYear());
            String month = String.valueOf(new Date().getMonth());
            return uploadFile(bucketName, year + "/" + month + "/" + newName, in);

        }
        return null;
    }

    public static InputStream base64ToInputStream(String base64) {
        ByteArrayInputStream stream = null;
        try {
            byte[] bytes = Base64.getEncoder().encode(base64.trim().getBytes());
            stream = new ByteArrayInputStream(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return stream;
    }


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

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

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

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

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

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

    /**
     * 批量删除文件
     *
     * @param bucketName 存储桶
     * @param keys       需要删除的文件列表
     * @return
     */
    public 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
     */
    @SneakyThrows(Exception.class)
    public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) {
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder().expiry(expires).bucket(bucketName).object(objectName).build();
        return minioClient.getPresignedObjectUrl(args);
    }

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

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

项目启动初始化配置

创建配置类InitConfig.java,并实现了InitializingBean接口重写afterPropertiesSet方法。

该方法主要实现逻辑:在项目启动的时候初始化Bucket,如果没有则进行创建!

InitConfig.java

java 复制代码
/**
 * @program: xxkfz-minio
 * @ClassName Init.java
 * @author: wust
 * @create: 2024-03-16 10:34
 * @description: 项目启动初始化配置
 **/
@Component
@Slf4j
    public class InitConfig implements InitializingBean {

    @Autowired
    private MinioUtils minioUtils;


    @Autowired
    private MinioConfig minioConfig;

    @Override
    public void afterPropertiesSet() throws Exception {
        // 项目启动创建Bucket,不存在则进行创建
        minioUtils.createBucket(minioConfig.getBucketName());
    }
}

编写测试接口

MinioController.java

less 复制代码
/**
 * @program: xxkfz-minio
 * @ClassName OSSController.java
 * @author: wust
 * @create: 2024-03-13 11:01
 * @description:
 **/
@Slf4j
@RestController
@RequestMapping("/oss")
public class MinioController {

    @Autowired
    private MinioUtils minioUtils;

    @Autowired
    private MinioConfig minioConfig;

    /**
     * 文件上传
     *
     * @param file
     */
    @PostMapping("/upload")
    public String upload(@RequestParam("file") MultipartFile file) {
        try {
            //文件名
            String fileName = file.getOriginalFilename();
            String newFileName = System.currentTimeMillis() + "." + StringUtils.substringAfterLast(fileName, ".");
            //类型
            String contentType = file.getContentType();
            minioUtils.uploadFile(minioConfig.getBucketName(), file, newFileName, contentType);
            return "上传成功,文件名:" + newFileName;
        } catch (Exception e) {
            e.printStackTrace();
            return "上传失败";
        }
    }

    /**
     * 删除
     *
     * @param fileName
     */
    @DeleteMapping("/")
    public void delete(@RequestParam("fileName") String fileName) {
        minioUtils.removeFile(minioConfig.getBucketName(), fileName);
    }

    /**
     * 获取文件信息
     *
     * @param fileName
     * @return
     */
    @GetMapping("/info")
    public String getFileStatusInfo(@RequestParam("fileName") String fileName) {
        return minioUtils.getFileStatusInfo(minioConfig.getBucketName(), fileName);
    }

    /**
     * 获取文件外链
     *
     * @param fileName
     * @return
     */
    @GetMapping("/url")
    public String getPresignedObjectUrl(@RequestParam("fileName") String fileName) {
        return minioUtils.getPresignedObjectUrl(minioConfig.getBucketName(), fileName);
    }

    /**
     * 文件下载
     *
     * @param fileName
     * @param response
     */
    @GetMapping("/download")
    public void download(@RequestParam("fileName") String fileName, HttpServletResponse response) {
        try {
            InputStream fileInputStream = minioUtils.getObject(minioConfig.getBucketName(), fileName);
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
            response.setContentType("application/force-download");
            response.setCharacterEncoding("UTF-8");
            IOUtils.copy(fileInputStream, response.getOutputStream());
        } catch (Exception e) {
            log.error("下载失败");
        }
    }
}

测试验证

启动项目:

上传图片

测试接口:http://localhost:8099/oss/upload

进入服务器查看文件上传情况。

进入目录:/root/xxkfz/soft/minio/data/xk-admin

当然,也可以直接访问minio的地址:http://IP地址:9001/xk-admin/1710558001536.jpg。验证文件是否上传成功。

获取文件信息

测试接口:http://localhost:8099/oss/info

获取文件外链

测试接口:http://localhost:8099/oss/url

下载文件

测试接口:http://localhost:8099/oss/download

相关推荐
爱勇宝4 小时前
第 1 章:别把“需求文档”当成真正的需求
前端·后端·程序员
IT_陈寒9 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码10 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo10 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志11 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..11 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects11 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰11 小时前
一些基础和问题解决
后端
陈随易11 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN11 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp