• 如果SpringBoot搭建还有问题,可以参考我的文章:
  • minio官方网站:https://min.io/
  • 程序包可以官网下载,本次演示的版本下载:

运行步骤

linux:

#!/bin/bash

MINIO_ROOT_USER=xiaohh MINIO_ROOT_PASSWORD=xiaohh1234 ./minio server /data/minio --console-address ":9001" >> /dev/null &

Windows:

setx MINIO_ROOT_USER xiaohh
setx MINIO_ROOT_PASSWORD xiaohh1234
minio.exe server D:\Data\minio --console-address ":9001"

注意 MINIO_ROOT_USER 后面的是用户名,而 MINIO_ROOT_PASSWORD 后面的是密码, --console-address 后面的是minio的控制台端口,minio默认程序端口为9000

SpringBoot项目

首先搭建SpringBoot项目,pom文件:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- SpringBoot 项目 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
    </parent>

    <groupId>love.xiaohh</groupId>
    <artifactId>xiaohh-minio</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <!-- 声明依赖 -->
    <dependencies>
        <!-- SpringMVC 相关的依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 健康检测 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- 缓存 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- mybatis 相关依赖 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <!-- MySQL 驱动包 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- 分布式文件系统 -->
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.0.0</version>
        </dependency>

        <!-- JSON工具 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.78</version>
        </dependency>

        <!-- apache 提供的常用工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- 制作一个可运行的 jar 包需要的插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

重点导入的为:

<!-- 分布式文件系统 -->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.0.0</version>
</dependency>

启动类代码:

package love.xiaohh.minio;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * <p>
 * XiaoHHMinioApplication 模块启动类
 * </p>
 *
 * @author XiaoHH
 * @version 1.0
 * @date 2021-11-21 星期日 9:21:54
 * @file XiaoHHMinioApplication.java
 */
@SpringBootApplication
public class XiaoHHMinioApplication {

    public static void main(String[] args) {
        // 启动文件系统
        SpringApplication.run(XiaoHHMinioApplication.class, args);
        System.out.println("^V^ 文件系统启动成功 ^V^\n" +
                "/-\\ /-\\ /-\\ /-\\ /-\\ /-\\\n" +
                " X   i   a   o   H   H\n" +
                "\\-/ \\-/ \\-/ \\-/ \\-/ \\-/");
    }

}

一些工具类代码:R.java(返回给前端的响应对象)

package love.xiaohh.minio.utils.http;

import love.xiaohh.minio.constants.BizCodeEnum;

import java.io.Serializable;

/**
 * <p>
 * 响应对象
 * </p>
 *
 * @author XiaoHH
 * @version 1.0
 * @date 2021-08-21 星期六 16:08:28
 * @file R.java
 */
public class R<T> implements Serializable {

    /**
     * 只允许内部实例化
     */
    private R() {
    }

    /**
     * 消息状态码
     */
    private int code;

    /**
     * 消息内容
     */
    private String message;

    /**
     * 响应内容
     */
    private T data;

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    /**
     * 创建一个响应对象
     *
     * @param data 响应数据
     * @return 响应内容
     */
    public static <T> R<T> success(T data) {
        R<T> r = new R<T>();
        r.code = BizCodeEnum.SUCCESS.getCode();
        r.message = "success";
        r.data = data;
        return r;
    }

    /**
     * 创建一个响应对象
     *
     * @return 响应内容
     */
    public static <T> R<T> success() {
        return success("success");
    }

    /**
     * 创建一个响应对象
     *
     * @param message 消息内容
     * @return 响应内容
     */
    public static <T> R<T> success(String message) {
        return success(BizCodeEnum.SUCCESS.getCode(), message);
    }

    /**
     * 创建一个响应对象
     *
     * @param code    状态码
     * @param message 消息内容
     * @return 响应内容
     */
    public static <T> R<T> success(int code, String message) {
        R<T> r = new R<T>();
        r.code = code;
        r.message = message;
        return r;
    }

    /**
     * 创建一个响应对象
     *
     * @return 响应内容
     */
    public static <T> R<T> error() {
        return error(BizCodeEnum.SUCCESS.getCode(), "error");
    }

    /**
     * 创建一个响应对象
     *
     * @param message 消息内容
     * @return 响应内容
     */
    public static <T> R<T> error(String message) {
        return error(BizCodeEnum.SUCCESS.getCode(), message);
    }

    /**
     * 创建一个响应对象
     *
     * @param code    状态码
     * @param message 消息内容
     * @return 响应内容
     */
    public static <T> R<T> error(int code, String message) {
        R<T> r = new R<T>();
        r.code = code;
        r.message = message;
        return r;
    }

    /**
     * 创建一个错误信息
     *
     * @param bizCodeEnum 错误信息的对象
     * @return 错误信息
     */
    public static <T> R<T> error(BizCodeEnum bizCodeEnum) {
        R<T> r = new R<T>();
        r.code = bizCodeEnum.getCode();
        r.message = bizCodeEnum.getMessage();
        return r;
    }

    /**
     * 创建一个错误信息
     *
     * @param bizCodeEnum 错误信息的对象
     * @return 错误信息
     */
    public static R<Object> error(Object data, BizCodeEnum bizCodeEnum) {
        R<Object> r = new R<Object>();
        r.code = bizCodeEnum.getCode();
        r.message = bizCodeEnum.getMessage();
        r.data = data;
        return r;
    }
}

BizCodeEnum:

package love.xiaohh.minio.constants;

/**
 * <p>
 * 异常统一声明
 * </p>
 *
 * @author XiaoHH
 * @version 1.0
 * @date 2021-08-21 星期六 16:19:00
 * @file BizCodeEnum.java
 */
public enum BizCodeEnum {

    /**
     * 成功
     */
    SUCCESS(200, "操作成功"),

    /**
     * 未知异常
     */
    UNKNOWN_EXCEPTION(10000, "系统未知异常"),

    /**
     * 参数校验异常
     */
    VALIDATE_EXCEPTION(10001, "参数格式校验失败"),

    /**
     * 数据完整性异常
     */
    DATA_INTEGRITY_VIOLATION_EXCEPTION(10002, "数据完整性校验失败"),

    /**
     * SQL 语句错误
     */
    BAD_SQL_GRAMMAR_EXCEPTION(10003, "SQL 语句错误"),

    /**
     * 客户端异常
     */
    CUSTOMER_EXCEPTION(10004, null),

    /**
     * 图形验证码异常
     */
    CAPTCHA_EXCEPTION(10005, null);

    /**
     * 错误码
     */
    private final int code;

    /**
     * 错误信息
     */
    private final String message;

    /**
     * @param code    错误码
     * @param message 错误信息
     */
    BizCodeEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }

    /**
     * 获取错误码
     *
     * @return 错误码
     */
    public int getCode() {
        return code;
    }

    /**
     * 获取错误信息
     *
     * @return 错误信息
     */
    public String getMessage() {
        return message;
    }
}

FileUtils:

package love.xiaohh.minio.utils.file;

import love.xiaohh.minio.utils.StringUtils;

/**
 * <p>
 * 文件相关的工具类
 * </p>
 *
 * @author XiaoHH
 * @version 1.0
 * @date 2021-11-21 星期日 10:02:17
 * @file FileUtils.java
 */
public class FileUtils {

    /**
     * 不允许被实例化
     */
    private FileUtils() {
    }

    /**
     * 返回文件的扩展名
     *
     * @param filename 文件的名称
     * @return 扩展名
     */
    public static String getExtension(String filename) {
        if (StringUtils.isNotEmpty(filename)) {
            int index = filename.lastIndexOf('.');
            if (index > 0)
                return filename.substring(index);
            else return StringUtils.NULL_STR;
        } else return null;
    }

    /**
     * 根据文件类型获取文件扩展名,如 image/png 则返回 .png
     *
     * @param contentType 文件类型
     * @return 文件扩展名
     */
    public static String getExtensionByContentType(String contentType) {
        if (StringUtils.isNotEmpty(contentType)) {
            int index = contentType.indexOf("/");
            if (index > 0) return "." + contentType.substring(index + 1);
            else return StringUtils.NULL_STR;
        } else return null;
    }

    /**
     * 判断一个内容类型是不是图片类型
     *
     * @param contentType 内容类型
     * @return 判断结果
     */
    public static boolean isImage(String contentType) {
        if (StringUtils.isEmpty(contentType)) return false;
        return contentType.matches("^image/[a-zA-Z]*$");
    }
}

StringUtils:

package love.xiaohh.minio.utils;

import java.io.*;
import java.util.Set;

/**
 * <p>
 * 字符串的工具类
 * </p>
 *
 * @author XiaoHH
 * @version 1.0
 * @date 2021-08-21 星期六 15:58:44
 * @file StringUtils.java
 */
public class StringUtils extends org.apache.commons.lang3.StringUtils {
    /**
     * 空字符串
     */
    public static final String NULL_STR = "";

    /**
     * 不允许被实例化
     */
    private StringUtils() {
    }

    /**
     * 将对象经行toString操作,但可以避免空指针异常
     *
     * @param o 需要被转换为字符串的对象
     * @return 转换结果
     */
    public static String toString(Object o) {
        if (null == o) return null;
        return o.toString();
    }

    /**
     * 判断字符串是否为空
     *
     * @param s 需要被判断的字符串
     * @return 是否为空
     */
    public static boolean isEmpty(String s) {
        if (null == s) return true;
        return s.trim().length() == 0;
    }

    /**
     * 判断字符串是否不为空
     *
     * @param s 需要被判断的字符串
     * @return 是否不为空
     */
    public static boolean isNotEmpty(String s) {
        return !isEmpty(s);
    }

}

Base64.java(主要用decode方法,将base64的图片转换为二进制格式的数组)

package love.xiaohh.minio.utils.sign;

/**
 * <p>
 * Base64工具类
 * </p>
 *
 * @author XiaoHH
 * @version 1.0
 * @date 2021-11-21 星期日 10:29:54
 * @file Base64.java
 */
public final class Base64 {
    static private final int BASELENGTH = 128;
    static private final int LOOKUPLENGTH = 64;
    static private final int FOURBYTE = 4;
    static private final char PAD = '=';
    static final private byte[] base64Alphabet = new byte[BASELENGTH];
    static final private char[] lookUpBase64Alphabet = new char[LOOKUPLENGTH];

    static {
        for (int i = 0; i < BASELENGTH; ++i) {
            base64Alphabet[i] = -1;
        }
        for (int i = 'Z'; i >= 'A'; i--) {
            base64Alphabet[i] = (byte) (i - 'A');
        }
        for (int i = 'z'; i >= 'a'; i--) {
            base64Alphabet[i] = (byte) (i - 'a' + 26);
        }

        for (int i = '9'; i >= '0'; i--) {
            base64Alphabet[i] = (byte) (i - '0' + 52);
        }

        base64Alphabet['+'] = 62;
        base64Alphabet['/'] = 63;

        for (int i = 0; i <= 25; i++) {
            lookUpBase64Alphabet[i] = (char) ('A' + i);
        }

        for (int i = 26, j = 0; i <= 51; i++, j++) {
            lookUpBase64Alphabet[i] = (char) ('a' + j);
        }

        for (int i = 52, j = 0; i <= 61; i++, j++) {
            lookUpBase64Alphabet[i] = (char) ('0' + j);
        }
        lookUpBase64Alphabet[62] = (char) '+';
        lookUpBase64Alphabet[63] = (char) '/';
    }

    private static boolean isWhiteSpace(char octect) {
        return (octect == 0x20 || octect == 0xd || octect == 0xa || octect == 0x9);
    }

    private static boolean isPad(char octect) {
        return (octect == PAD);
    }

    private static boolean isData(char octect) {
        return (octect >= BASELENGTH || base64Alphabet[octect] == -1);
    }

    /**
     * Decodes Base64 data into octects
     *
     * @param encoded string containing Base64 data
     * @return Array containind decoded data.
     */
    public static byte[] decode(String encoded) {
        if (encoded == null) {
            return null;
        }

        char[] base64Data = encoded.toCharArray();
        // remove white spaces
        int len = removeWhiteSpace(base64Data);

        if (len % FOURBYTE != 0) {
            return null;// should be divisible by four
        }

        int numberQuadruple = (len / FOURBYTE);

        if (numberQuadruple == 0) {
            return new byte[0];
        }

        byte[] decodedData;
        byte b1, b2, b3, b4;
        char d1, d2, d3, d4;

        int i = 0;
        int encodedIndex = 0;
        int dataIndex = 0;
        decodedData = new byte[(numberQuadruple) * 3];

        for (; i < numberQuadruple - 1; i++) {

            if (isData((d1 = base64Data[dataIndex++])) || isData((d2 = base64Data[dataIndex++]))
                    || isData((d3 = base64Data[dataIndex++])) || isData((d4 = base64Data[dataIndex++]))) {
                return null;
            } // if found "no data" just return null

            b1 = base64Alphabet[d1];
            b2 = base64Alphabet[d2];
            b3 = base64Alphabet[d3];
            b4 = base64Alphabet[d4];

            decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
            decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
            decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);
        }

        if (isData((d1 = base64Data[dataIndex++])) || isData((d2 = base64Data[dataIndex++]))) {
            return null;// if found "no data" just return null
        }

        b1 = base64Alphabet[d1];
        b2 = base64Alphabet[d2];

        d3 = base64Data[dataIndex++];
        d4 = base64Data[dataIndex++];
        if (isData((d3)) || isData((d4))) {// Check if they are PAD characters
            if (isPad(d3) && isPad(d4)) {
                if ((b2 & 0xf) != 0)// last 4 bits should be zero
                {
                    return null;
                }
                byte[] tmp = new byte[i * 3 + 1];
                System.arraycopy(decodedData, 0, tmp, 0, i * 3);
                tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
                return tmp;
            } else if (!isPad(d3) && isPad(d4)) {
                b3 = base64Alphabet[d3];
                if ((b3 & 0x3) != 0)// last 2 bits should be zero
                {
                    return null;
                }
                byte[] tmp = new byte[i * 3 + 2];
                System.arraycopy(decodedData, 0, tmp, 0, i * 3);
                tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
                tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
                return tmp;
            } else {
                return null;
            }
        } else { // No PAD e.g 3cQl
            b3 = base64Alphabet[d3];
            b4 = base64Alphabet[d4];
            decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4);
            decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
            decodedData[encodedIndex++] = (byte) (b3 << 6 | b4);

        }
        return decodedData;
    }

    /**
     * remove WhiteSpace from MIME containing encoded Base64 data.
     *
     * @param data the byte array of base64 data (with WS)
     * @return the new length
     */
    private static int removeWhiteSpace(char[] data) {
        if (data == null) {
            return 0;
        }

        // count characters that's not whitespace
        int newSize = 0;
        int len = data.length;
        for (int i = 0; i < len; i++) {
            if (!isWhiteSpace(data[i])) {
                data[newSize++] = data[i];
            }
        }
        return newSize;
    }
}

然后开始对接minio的编码,首先我们建立一个数据库,用于存放文件信息:

DROP DATABASE IF EXISTS `minio_file`;
CREATE DATABASE `minio_file`;
USE `minio_file`;

CREATE TABLE `file_store` (
    `uuid` varchar(60) NOT NULL COMMENT '文件的UUID',
    `bucket` varchar(50) NOT NULL COMMENT '文件的桶名称',
    `object_name` varchar(255) NOT NULL COMMENT '文件的对象名称',
    `file_name` varchar(255) NOT NULL COMMENT '文件名称',
    PRIMARY KEY (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文件属性存储表';

还有其对应的实体类:

package love.xiaohh.minio.entities;

import com.alibaba.fastjson.JSON;

import java.io.Serializable;

/**
 * <p>
 * 文件属性存储实体
 * </p>
 *
 * @author XiaoHH
 * @version 1.0
 * @date 2021-11-21 星期日 9:47:26
 * @file FileStore.java
 */
public class FileStore implements Serializable {
    /**
     * 无参构造
     */
    public FileStore() {
    }

    /**
     * 全参构造
     *
     * @param uuid       文件的UUID
     * @param bucket     文件的桶名称
     * @param objectName 文件的对象名称
     * @param fileName   文件名称
     */
    public FileStore(String uuid, String bucket, String objectName, String fileName) {
        this.uuid = uuid;
        this.bucket = bucket;
        this.objectName = objectName;
        this.fileName = fileName;
    }

    /**
     * 文件的UUID
     */
    private String uuid;

    /**
     * 文件的桶名称
     */
    private String bucket;

    /**
     * 文件的对象名称
     */
    private String objectName;

    /**
     * 文件名称
     */
    private String fileName;

    public String getUuid() {
        return uuid;
    }

    public FileStore setUuid(String uuid) {
        this.uuid = uuid;
        return this;
    }

    public String getBucket() {
        return bucket;
    }

    public FileStore setBucket(String bucket) {
        this.bucket = bucket;
        return this;
    }

    public String getObjectName() {
        return objectName;
    }

    public FileStore setObjectName(String objectName) {
        this.objectName = objectName;
        return this;
    }

    public String getFileName() {
        return fileName;
    }

    public FileStore setFileName(String fileName) {
        this.fileName = fileName;
        return this;
    }

    /**
     * 将本对象转换为json格式
     *
     * @return 转换后的json
     */
    @Override
    public String toString() {
        return JSON.toJSONString(this);
    }
}

然后我们建立一个类来存储minio的一些配置:

package love.xiaohh.minio.config;

import com.alibaba.fastjson.JSON;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * <p>
 * minio的属性配置类
 * </p>
 *
 * @author XiaoHH
 * @version 1.0
 * @date 2021-11-21 星期日 9:30:31
 * @file MinioProperties.java
 */
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {

    /**
     * minio的主机地址
     */
    private String host;

    /**
     * minio的端口
     */
    private int port = 9000;

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

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

    /**
     * 是否是https安全的链接
     */
    private boolean secure;

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public String getAccessKey() {
        return accessKey;
    }

    public void setAccessKey(String accessKey) {
        this.accessKey = accessKey;
    }

    public String getSecretKey() {
        return secretKey;
    }

    public void setSecretKey(String secretKey) {
        this.secretKey = secretKey;
    }

    public boolean isSecure() {
        return secure;
    }

    public void setSecure(boolean secure) {
        this.secure = secure;
    }

    /**
     * 将本对象转换为json格式
     *
     * @return 转换后的json
     */
    @Override
    public String toString() {
        return JSON.toJSONString(this);
    }
}

然后我们就可以在 application.yml 当中配置了:

# 文件系统的配置
minio:
  host: 127.0.0.1
  port: 9000
  accessKey: xiaohh
  secretKey: xiaohh1234
  secure: false

整个程序的配置:

server:
  # 端口
  port: 8080

spring:
  datasource:
    # 数据库配置
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/minio_file?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    username: root
    password: xiaohh
  redis:
    # 缓存配置
    host: 127.0.0.1
    port: 6379

# 打印SQL日志
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  type-aliases-package: love.xiaohh.minio.entities
  mapper-locations: classpath:mapper/*Mapper.xml

# 日志设置
logging:
  level:
    root: info

# 文件系统的配置
minio:
  host: 127.0.0.1
  port: 9000
  accessKey: xiaohh
  secretKey: xiaohh1234
  secure: false

我们现在新建一个业务接口,用于与minio对接:

package love.xiaohh.minio.services;

import io.minio.GetObjectResponse;
import io.minio.StatObjectResponse;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;

/**
 * <p>
 * 文件系统的服务层
 * </p>
 *
 * @author XiaoHH
 * @version 1.0
 * @date 2021-11-21 星期日 9:34:50
 * @file MinioService.java
 */
public interface MinioService {

    /**
     * 上传一个文件
     *
     * @param bucket     桶名字
     * @param objectName 对象名字
     * @param file       文件
     */
    void putObject(String bucket, String objectName, MultipartFile file);

    /**
     * 上传一个文件
     *
     * @param bucket      桶名字
     * @param objectName  对象名字
     * @param contentType 文件类型
     * @param inputStream 输入流
     */
    void putObject(String bucket, String objectName, String contentType, InputStream inputStream);

    /**
     * 获取一个文件对象
     *
     * @param bucket     桶名称
     * @param objectName 对象名称
     * @return 输入流
     */
    GetObjectResponse getObject(String bucket, String objectName);

    /**
     * 获取文件状态
     *
     * @param bucket     桶名称
     * @param objectName 对象名称
     * @return 获取文件状态
     */
    StatObjectResponse getStatObject(String bucket, String objectName);

    /**
     * 在分布式文件系统当中删除一个文件
     *
     * @param bucketName 同名称
     * @param objectName 对象名称
     */
    void removeObject(String bucketName, String objectName);
}

然后在实现类中导入minio的配置并且实现链接,minio的链接获取方式是构建者模式,创建的代码(MinioProperties是实现写好的配置类对象):

/**
 * minio文件系统的客户端
 */
private final MinioClient minioClient;

/**
 * 初始化文件系统
 *
 * @param minioConfig 文件系统的配置
 */
public MinioServiceImpl(MinioProperties minioConfig) {
    this.minioClient = MinioClient.builder()
            .endpoint(minioConfig.getHost(), minioConfig.getPort(), false)
            .credentials(minioConfig.getAccessKey(), minioConfig.getSecretKey()).build();
}

在这之前讲一下 对象 的简单概念。桶不用多说,就是存储对象的容器,而对象说白了就是文件,一个文件只能在一个桶里,但是一个桶可以有多个文件。同时桶可以有多个,用于存储不同的文件,接下来我们来看看上面的这些接口如何实现:

package love.xiaohh.minio.services.impl;

import io.minio.*;
import io.minio.errors.*;
import love.xiaohh.minio.config.MinioProperties;
import love.xiaohh.minio.services.MinioService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

/**
 * <p>
 * 文件系统的服务层实现
 * </p>
 *
 * @author XiaoHH
 * @version 1.0
 * @date 2021-11-21 星期日 9:35:42
 * @file MinioServiceImpl.java
 */
@Service
public class MinioServiceImpl implements MinioService {

    /**
     * 记录日志的工具
     */
    private static final Logger log = LoggerFactory.getLogger(MinioServiceImpl.class);

    /**
     * minio文件系统的客户端
     */
    private final MinioClient minioClient;

    /**
     * 初始化文件系统
     *
     * @param minioConfig 文件系统的配置
     */
    public MinioServiceImpl(MinioProperties minioConfig) {
        this.minioClient = MinioClient.builder()
                .endpoint(minioConfig.getHost(), minioConfig.getPort(), false)
                .credentials(minioConfig.getAccessKey(), minioConfig.getSecretKey()).build();
    }

    /**
     * 上传一个文件
     *
     * @param bucket     桶名字
     * @param objectName 对象名字
     * @param file       文件
     */
    @Override
    public void putObject(String bucket, String objectName, MultipartFile file) {
        try {
            this.putObject(bucket, objectName, file.getContentType(), file.getInputStream());
        } catch (IOException e) {
            log.error("上传文件时出错", e);
        }
    }

    /**
     * 上传一个文件
     *
     * @param bucket      桶名字
     * @param objectName  对象名字
     * @param contentType 文件类型
     * @param inputStream 输入流
     */
    @Override
    public void putObject(String bucket, String objectName, String contentType, InputStream inputStream) {
        try {
            // 判断桶是否存在,不存在则创建
            BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucket).build();
            if (!this.minioClient.bucketExists(bucketExistsArgs)) {
                synchronized (this) {
                    if (!this.minioClient.bucketExists(bucketExistsArgs)) {
                        MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucket).build();
                        this.minioClient.makeBucket(makeBucketArgs);
                    }
                }
            }
            // 存入分布式文件系统
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                    .bucket(bucket).object(objectName).contentType(contentType)
                    .stream(inputStream, inputStream.available(), -1).build();
            this.minioClient.putObject(putObjectArgs);
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidResponseException | InvalidKeyException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            log.error("上传文件时出错", e);
        }
    }

    /**
     * 获取一个文件对象
     *
     * @param bucket     桶名称
     * @param objectName 对象名称
     * @return 输入流
     */
    @Override
    public GetObjectResponse getObject(String bucket, String objectName) {
        GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(bucket).object(objectName).build();
        try {
            return this.minioClient.getObject(getObjectArgs);
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            log.error("获取一个文件对象时出错", e);
        }
        return null;
    }

    /**
     * 获取文件状态
     *
     * @param bucket     桶名称
     * @param objectName 对象名称
     * @return 获取文件状态
     */
    @Override
    public StatObjectResponse getStatObject(String bucket, String objectName) {
        try {
            StatObjectArgs statObjectArgs = StatObjectArgs.builder().bucket(bucket).object(objectName).build();
            return this.minioClient.statObject(statObjectArgs);
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            log.error("获取文件状态时出错", e);
            return null;
        }
    }

    /**
     * 在分布式文件系统当中删除一个文件
     *
     * @param bucketName 同名称
     * @param objectName 对象名称
     */
    @Override
    public void removeObject(String bucketName, String objectName) {
        try {
            this.minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            log.error("在分布式文件系统当中删除一个文件时出错", e);
        }
    }
}

大部分都是用构建者模式,具体说明注释当中有,现在我们写几个接口来测试一下(增加、查询、删除的mybatis代码忽略)。

Controller:

package love.xiaohh.minio.controllers;

import love.xiaohh.minio.entities.FileStore;
import love.xiaohh.minio.entities.dto.Base64ImageUploadDTO;
import love.xiaohh.minio.services.FileStoreService;
import love.xiaohh.minio.utils.http.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.SQLException;

/**
 * <p>
 * 文件存取的前端控制器
 * </p>
 *
 * @author XiaoHH
 * @version 1.0
 * @date 2021-11-21 星期日 9:39:39
 * @file FileStoreController.java
 */
@RestController
@RequestMapping("/fileStore")
public class FileStoreController {

    @Autowired
    private FileStoreService fileStoreService;

    /**
     * 上传一个文件
     *
     * @param file 文件对象
     * @return 上传成功之后会返回文件的信息
     * @throws SQLException 与数据库交互的时候可能抛出的数据库异常
     */
    @PostMapping("/upload")
    public R<FileStore> upload(@RequestParam("file") MultipartFile file) throws SQLException {
        return R.success(this.fileStoreService.upload(file));
    }

    /**
     * 上传base64格式的图片的接口
     *
     * @param base64ImageUpload base64格式的图片的传输对象
     * @return 上传成功之后会返回文件的信息
     * @throws IOException 会因为解析base64、上传文件而抛出io异常
     */
    @PostMapping("/base64")
    public R<FileStore> base64(@RequestBody Base64ImageUploadDTO base64ImageUpload) throws IOException {
        return R.success(this.fileStoreService.uploadBase64(base64ImageUpload));
    }

    /**
     * 下载文件时候的uuid
     *
     * @param download 是否下载;0=在线预览,1=下载
     * @param uuid     文件的uuid
     * @param response 响应对象,由Spring向方法里面传递
     * @throws IOException 可能会抛出输入输出流异常
     */
    @GetMapping("/download/{download}/{uuid}")
    public void download(@PathVariable("download") Integer download, @PathVariable("uuid") String uuid, HttpServletResponse response) throws IOException {
        this.fileStoreService.download(download, uuid, response);
    }

    /**
     * 删除一个文件
     *
     * @param uuid 文件的uuid
     * @return 是否成功
     */
    @DeleteMapping("/{uuid}")
    public R<Void> remove(@PathVariable("uuid") String uuid) {
        this.fileStoreService.removeFileByUuid(uuid);
        return R.success();
    }
}

桶名称常量类:

package love.xiaohh.minio.constants;

/**
 * <p>
 *     minio文件系统的常量类
 * </p>
 *
 * @author XiaoHH
 * @version 1.0
 * @date 2021-11-21 星期日 9:41:39
 * @file MinioConstant.java
 */
public class MinioConstant {
    /**
     * 不允许被实例化
     */
    private MinioConstant() {
    }

    /**
     * 用户存储图片的同名称
     */
    public static final String IMAGE_BUCKET = "image-bucket";

    /**
     * 存储普通文件的同名称
     */
    public static final String FILE_BUCKET = "file-bucket";
}

业务层接口:

package love.xiaohh.minio.services;

import love.xiaohh.minio.entities.FileStore;
import love.xiaohh.minio.entities.dto.Base64ImageUploadDTO;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.SQLException;

/**
 * <p>
 * 文件存取的服务层
 * </p>
 *
 * @author XiaoHH
 * @version 1.0
 * @date 2021-11-21 星期日 9:51:33
 * @file FileStoreService.java
 */
public interface FileStoreService {

    /**
     * 上传普通文件
     *
     * @param file    普通文件
     * @return 文件对象
     */
    FileStore upload(MultipartFile file) throws SQLException;

    /**
     * 上传base64格式的图片
     *
     * @param base64ImageUpload base64格式图片的数据传输对象
     * @return 文件对象
     */
    FileStore uploadBase64(Base64ImageUploadDTO base64ImageUpload) throws IOException;

    /**
     * 下载或者在线预览文件
     *
     * @param download 是否下载;0=在线预览,1=下载
     * @param uuid     文件的uuid
     */
    void download(Integer download, String uuid, HttpServletResponse response) throws IOException;

    /**
     * 删除一个文件
     *
     * @param uuid 文件的uuid
     */
    void removeFileByUuid(String uuid);
}

业务层接口实现:

package love.xiaohh.minio.services.impl;

import com.google.common.io.ByteStreams;
import io.minio.GetObjectResponse;
import io.minio.StatObjectResponse;
import love.xiaohh.minio.constants.MinioConstant;
import love.xiaohh.minio.entities.FileStore;
import love.xiaohh.minio.entities.dto.Base64ImageUploadDTO;
import love.xiaohh.minio.mapper.FileStoreMapper;
import love.xiaohh.minio.services.FileStoreService;
import love.xiaohh.minio.services.MinioService;
import love.xiaohh.minio.utils.file.FileUtils;
import love.xiaohh.minio.utils.sign.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 文件存取的服务层实现
 * </p>
 *
 * @author XiaoHH
 * @version 1.0
 * @date 2021-11-21 星期日 9:58:44
 * @file FileStoreServiceImpl.java
 */
@Service
public class FileStoreServiceImpl implements FileStoreService {

    @Autowired
    private MinioService minioService;

    @Autowired
    private FileStoreMapper fileStoreMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 文件目录的分隔符
     */
    private static final String FILE_SEPARATOR = "/";

    /**
     * 文件在redis当中存储的前缀
     */
    private static final String REDIS_KEY_PREFIX = "CostFile:uuid:";

    /**
     * 上传普通文件
     *
     * @param file 普通文件
     * @return 文件对象
     */
    @Override
    public FileStore upload(MultipartFile file) throws SQLException {
        // 文件的uuid
        String uuid = UUID.randomUUID().toString();
        // 文件名
        String originalFilename = file.getOriginalFilename();
        // 存储桶
        String bucket = FileUtils.isImage(file.getContentType()) ? MinioConstant.IMAGE_BUCKET : MinioConstant.FILE_BUCKET;
        // 生成对象名称
        LocalDate localDate = LocalDate.now();
        String objectName = localDate.getYear() + FILE_SEPARATOR + localDate.getMonthValue() + FILE_SEPARATOR
                + localDate.getDayOfMonth() + FILE_SEPARATOR + uuid + FileUtils.getExtension(originalFilename);

        // 文件存储对象
        FileStore fileStore = new FileStore(uuid, bucket, objectName, originalFilename);
        int rows = this.fileStoreMapper.insertFileStore(fileStore);
        if (rows < 1) throw new SQLException("存储文件存储对象时失败");

        // 将文件存入文件系统
        this.minioService.putObject(bucket, objectName, file);
        // 将文件属性存入缓存
        this.storeFile(fileStore);
        return fileStore;
    }

    /**
     * 上传base64格式的图片
     *
     * @param base64ImageUpload base64格式图片的数据传输对象
     * @return 文件对象
     */
    @Override
    public FileStore uploadBase64(Base64ImageUploadDTO base64ImageUpload) throws IOException {
        // 读取文件内容
        String base64 = base64ImageUpload.getBase64();
        // 文件类型
        String contentType = base64.substring(5, base64.indexOf(";base64,"));
        // 提取base64字符串并转换为字节数组
        base64 = base64.substring(base64.indexOf(",") + 1);
        byte[] decode = Base64.decode(base64);
        // 如果为空则代表转换错误
        if (null == decode || decode.length == 0) {
            throw new IOException("传入base64格式错误");
        }
        // 文件唯一标识
        final String uuid = UUID.randomUUID().toString();
        // 生成文件名
        String fileName = uuid + FileUtils.getExtensionByContentType(contentType);
        // 生成对象名称
        LocalDate localDate = LocalDate.now();
        final String objectName = localDate.getYear() + FILE_SEPARATOR + localDate.getMonthValue() + FILE_SEPARATOR
                + localDate.getDayOfMonth() + FILE_SEPARATOR + fileName;
        // 生成文件属性存储实体
        final FileStore fileStore = new FileStore(uuid, MinioConstant.IMAGE_BUCKET, objectName, fileName);
        // 将文件插入数据库
        int rows = this.fileStoreMapper.insertFileStore(fileStore);
        if (rows < 1) throw new IOException("哎呀,累趴了,请稍后再试");

        // 将字节数组转换为输入流并上传文件系统
        InputStream inputStream = new ByteArrayInputStream(decode);
        this.minioService.putObject(MinioConstant.IMAGE_BUCKET, objectName, contentType, inputStream);

        // 将文件对象存入缓存
        storeFile(fileStore);

        return fileStore;
    }

    /**
     * 下载或者在线预览文件
     *
     * @param download 是否下载;0=在线预览,1=下载
     * @param uuid     文件的uuid
     */
    @Override
    public void download(Integer download, String uuid, HttpServletResponse response) throws IOException {
        // 文件信息
        FileStore fileStore = this.getFileStore(uuid);
        if (null == fileStore) {
            response.setStatus(HttpStatus.NOT_FOUND.value());
            throw new IOException("您的文件离家出走了");
        }
        // 在minio中获取文件信息
        final GetObjectResponse minioFileSteam = this.minioService.getObject(fileStore.getBucket(), fileStore.getObjectName());
        final StatObjectResponse statObject = this.minioService.getStatObject(fileStore.getBucket(), fileStore.getObjectName());
        if (null == minioFileSteam || null == statObject) {
            response.setStatus(HttpStatus.NOT_FOUND.value());
            throw new IOException("您的文件离家出走了");
        }

        // 设置响应属性
        response.setContentType(statObject.contentType());
        // 设置响应文件的大小
        response.setContentLength((int) statObject.size());

        // 如果是下载,那么添加下载的文件名
        if (download == 1)
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode(fileStore.getFileName(), "UTF-8"));

        // 输出文件
        ServletOutputStream outputStream = response.getOutputStream();
        ByteStreams.copy(minioFileSteam, outputStream);
        outputStream.flush();
        outputStream.close();
        response.flushBuffer();
        minioFileSteam.close();
    }

    /**
     * 删除一个文件
     *
     * @param uuid 文件的uuid
     */
    @Override
    public void removeFileByUuid(String uuid) {
        // 文件信息
        FileStore fileStore = this.getFileStore(uuid);
        if (null == fileStore) {
            return;
        }

        // 在文件系统中删除文件
        this.minioService.removeObject(fileStore.getBucket(), fileStore.getObjectName());

        // 从缓存中删除文件信息
        this.removeFileOnCache(uuid);
        // 从数据库中删除文件信息
        this.fileStoreMapper.deleteFileStore(uuid);
    }

    /**
     * 将文件对象存入缓存中
     *
     * @param fileStore 文件属性存储对象
     */
    private void storeFile(FileStore fileStore) {
        this.redisTemplate.opsForValue().set(REDIS_KEY_PREFIX + fileStore.getUuid(), fileStore, 1L, TimeUnit.HOURS);
    }

    /**
     * 在缓存中删除对应uuid文件的缓存
     *
     * @param uuid 文件的uuid
     */
    private void removeFileOnCache(String uuid) {
        this.redisTemplate.delete(REDIS_KEY_PREFIX + uuid);
    }

    /**
     * 根据文件的uuid找出文件的属性存储实体
     *
     * @param uuid 属性存储实体的uuid
     * @return 属性存储实体
     */
    private FileStore getFileStore(String uuid) {
        Object fileObject = this.redisTemplate.opsForValue().get(REDIS_KEY_PREFIX + uuid);
        if (null != fileObject) {
            return (FileStore) fileObject;
        } else {
            FileStore fileStore = this.fileStoreMapper.selectFileStoreByUuid(uuid);
            this.storeFile(fileStore);
            return fileStore;
        }
    }
}

(Mapper代码忽略,可去代码仓库查看)

现在我们启动一下minio文件系统,启动命令在文章的开始有,然后访问服务器的9001端口:

springboot创建聚合工程 springboot整合minio_错误信息

登录之后我们来看看当前的桶,当前没有桶:

springboot创建聚合工程 springboot整合minio_maven_02

然后我们启动一下Java程序,上传一个图像文件试试,可以看到上传成功:

springboot创建聚合工程 springboot整合minio_spring_03

根据这个文件的uuid调用文件下载接口下载一下文件:

springboot创建聚合工程 springboot整合minio_springboot创建聚合工程_04

download的值为1则代表下载:

springboot创建聚合工程 springboot整合minio_错误信息_05

文件系统当中也有这个文件的桶了:

springboot创建聚合工程 springboot整合minio_maven_06