【工具篇】Spring Boot 整合 Minio OSS 存储服务

421 阅读5分钟

写在最前

Spring Boot 整合 Minio

Demo 地址:mingyue-springboot-minio

1.添加依赖

<!--  minio 依赖  -->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.3.9</version>
</dependency>
<!--  minio 依赖于 okhttp  -->
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.9.0</version>
</dependency>

2.修改配置文件

minio:
  endpoint: http://IP:9000
  port: 9000
  accessKey: 登录账号
  secretKey: 登录密码
  secure: false
  bucket-name: mingyue-test # 桶名
  image-size: 10485760 # 图片文件的最大大小
  file-size: 1073741824 # 文件的最大大小

3.创建 Minio 配置类

import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

/**
 * Minio 配置
 *
 * @author Strive
 * @date 2022/4/25 10:20
 * @description
 */
@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioPropertiesConfig {
  /** 服务地址 */
  private String endpoint;

  /** TCP/IP端口号 */
  private Integer port;

  /** 账户 */
  private String accessKey;

  /** 密码 */
  private String secretKey;

  /** 如果是 true,则用的是 https 而不是 http,默认值是 true" */
  private boolean secure;

  /** 存储桶 */
  private String bucketName;

  /** 图片的最大大小 */
  private long imageSize;

  /** 其他文件的最大大小 */
  private long fileSize;

  /** 初始化 MinIO 客户端 */
  @Bean
  public MinioClient minioClient() {
    return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();
  }
}

4.创建 Minio 工具类

import com.csp.mingyue.minio.config.MinioPropertiesConfig;
import io.minio.BucketExistsArgs;
import io.minio.GetObjectArgs;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.ListObjectsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveBucketArgs;
import io.minio.RemoveObjectArgs;
import io.minio.RemoveObjectsArgs;
import io.minio.Result;
import io.minio.StatObjectArgs;
import io.minio.StatObjectResponse;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

/**
 * Minio 工具类
 *
 * @author Strive
 * @date 2022/4/25 10:24
 * @description minio工具类
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class MinioUtil {

  private final MinioPropertiesConfig minioPropertiesConfig;

  private final MinioClient minioClient;

  /**
   * 检查存储桶是否存在
   *
   * @param bucketName 存储桶名称
   */
  @SneakyThrows
  public boolean bucketExists(String bucketName) {
    boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    if (found) {
      log.info("{} exists", bucketName);
    } else {
      log.info("{} does not exist", bucketName);
    }
    return found;
  }

  /**
   * 创建存储桶
   *
   * @param bucketName 存储桶名称
   */
  @SneakyThrows
  public boolean makeBucket(String bucketName) {
    boolean flag = bucketExists(bucketName);
    if (!flag) {
      minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
      return true;
    } else {
      return false;
    }
  }

  /** 列出所有存储桶名称 */
  @SneakyThrows
  public List<String> listBucketNames() {
    List<Bucket> bucketList = listBuckets();
    List<String> bucketListName = new ArrayList<>();
    for (Bucket bucket : bucketList) {
      bucketListName.add(bucket.name());
    }
    return bucketListName;
  }

  /** 列出所有存储桶 */
  @SneakyThrows
  public List<Bucket> listBuckets() {
    return minioClient.listBuckets();
  }

  /**
   * 删除存储桶
   *
   * @param bucketName 存储桶名称
   */
  @SneakyThrows
  public boolean removeBucket(String bucketName) {
    boolean flag = bucketExists(bucketName);
    if (flag) {
      Iterable<Result<Item>> myObjects = listObjects(bucketName);
      for (Result<Item> result : myObjects) {
        Item item = result.get();
        // 有对象文件,则删除失败
        if (item.size() > 0) {
          return false;
        }
      }
      // 删除存储桶,注意,只有存储桶为空时才能删除成功。
      minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
      flag = bucketExists(bucketName);
      return !flag;
    }
    return false;
  }

  /**
   * 列出存储桶中的所有对象名称
   *
   * @param bucketName 存储桶名称
   */
  @SneakyThrows
  public List<String> listObjectNames(String bucketName) {
    List<String> listObjectNames = new ArrayList<>();
    boolean flag = bucketExists(bucketName);
    if (flag) {
      Iterable<Result<Item>> myObjects = listObjects(bucketName);
      for (Result<Item> result : myObjects) {
        Item item = result.get();
        listObjectNames.add(item.objectName());
      }
    } else {
      listObjectNames.add("存储桶不存在");
    }
    return listObjectNames;
  }

  /**
   * 列出存储桶中的所有对象
   *
   * @param bucketName 存储桶名称
   */
  @SneakyThrows
  public Iterable<Result<Item>> listObjects(String bucketName) {
    boolean flag = bucketExists(bucketName);
    if (flag) {
      return minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).build());
    }
    return null;
  }

  /**
   * 文件上传
   *
   * @param bucketName 桶名称
   * @param multipartFile 上传的文件
   */
  @SneakyThrows
  public void putObject(
      String bucketName, MultipartFile multipartFile, String filename, String fileType) {
    InputStream inputStream = new ByteArrayInputStream(multipartFile.getBytes());
    minioClient.putObject(
        PutObjectArgs.builder().bucket(bucketName).object(filename).stream(
                inputStream, -1, minioPropertiesConfig.getFileSize())
            .contentType(fileType)
            .build());
  }

  /**
   * 文件访问路径
   *
   * @param bucketName 存储桶名称
   * @param objectName 存储桶里的对象名称
   * @return
   */
  @SneakyThrows
  public String getObjectUrl(String bucketName, String objectName) {
    boolean flag = bucketExists(bucketName);
    String url = "";
    if (flag) {
      url =
          minioClient.getPresignedObjectUrl(
              GetPresignedObjectUrlArgs.builder()
                  .method(Method.GET)
                  .bucket(bucketName)
                  .object(objectName)
                  .expiry(2, TimeUnit.MINUTES)
                  .build());
      log.info("url:{}", url);
    }
    return url;
  }

  /**
   * 删除一个对象
   *
   * @param bucketName 存储桶名称
   * @param objectName 存储桶里的对象名称
   */
  @SneakyThrows
  public boolean removeObject(String bucketName, String objectName) {
    boolean flag = bucketExists(bucketName);
    if (flag) {
      minioClient.removeObject(
          RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
      return true;
    }
    return false;
  }

  /**
   * 以流的形式获取一个文件对象
   *
   * @param bucketName 存储桶名称
   * @param objectName 存储桶里的对象名称
   */
  @SneakyThrows
  public InputStream getObject(String bucketName, String objectName) {
    boolean flag = bucketExists(bucketName);
    if (flag) {
      StatObjectResponse statObject = statObject(bucketName, objectName);
      if (statObject != null && statObject.size() > 0) {
        return minioClient.getObject(
            GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
      }
    }
    return null;
  }

  /**
   * 获取对象的元数据
   *
   * @param bucketName 存储桶名称
   * @param objectName 存储桶里的对象名称
   */
  @SneakyThrows
  public StatObjectResponse statObject(String bucketName, String objectName) {
    boolean flag = bucketExists(bucketName);
    if (flag) {
      return minioClient.statObject(
          StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }
    return null;
  }

  /**
   * 删除指定桶的多个文件对象,返回删除错误的对象列表,全部删除成功,返回空列表
   *
   * @param bucketName 存储桶名称
   * @param objectNames 含有要删除的多个object名称的迭代器对象
   */
  @SneakyThrows
  public boolean removeObject(String bucketName, List<String> objectNames) {
    boolean flag = bucketExists(bucketName);
    if (flag) {
      List<DeleteObject> objects = new LinkedList<>();
      for (String objectName : objectNames) {
        objects.add(new DeleteObject(objectName));
      }
      Iterable<Result<DeleteError>> results =
          minioClient.removeObjects(
              RemoveObjectsArgs.builder().bucket(bucketName).objects(objects).build());
      for (Result<DeleteError> result : results) {
        DeleteError error = result.get();
        log.info("Error in deleting object {}: {}", error.objectName(), error.message());
      }
    }
    return true;
  }

  /**
   * 以流的形式获取一个文件对象(断点下载)
   *
   * @param bucketName 存储桶名称
   * @param objectName 存储桶里的对象名称
   * @param offset 起始字节的位置
   * @param length 要读取的长度 (可选,如果无值则代表读到文件结尾)
   */
  @SneakyThrows
  public InputStream getObject(String bucketName, String objectName, long offset, Long length) {
    boolean flag = bucketExists(bucketName);
    if (flag) {
      StatObjectResponse statObject = statObject(bucketName, objectName);
      if (statObject != null && statObject.size() > 0) {
        return minioClient.getObject(
            GetObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .offset(offset)
                .length(length)
                .build());
      }
    }
    return null;
  }

  /**
   * 通过InputStream上传对象
   *
   * @param bucketName 存储桶名称
   * @param objectName 存储桶里的对象名称
   * @param inputStream 要上传的流
   * @param contentType 要上传的文件类型 MimeTypeUtils.IMAGE_JPEG_VALUE
   */
  @SneakyThrows
  public boolean putObject(
      String bucketName, String objectName, InputStream inputStream, String contentType) {
    boolean flag = bucketExists(bucketName);
    if (flag) {
      minioClient.putObject(
          PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(
                  inputStream, -1, minioPropertiesConfig.getFileSize())
              .contentType(contentType)
              .build());
      StatObjectResponse statObject = statObject(bucketName, objectName);
      return statObject != null && statObject.size() > 0;
    }
    return false;
  }
}

5.创建 Minio Service

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.csp.mingyue.minio.config.MinioPropertiesConfig;
import com.csp.mingyue.minio.util.MinioUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

/**
 * Minio 服务层
 * 
 * @author Strive
 * @date 2022/4/25 10:32
 * @description Minio Service 层
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class MinioService {

  private final MinioPropertiesConfig minioPropertiesConfig;

  private final MinioUtil minioUtil;

  /**
   * 检查存储桶是否存在
   *
   * @param bucketName 存储桶名称
   */
  public boolean bucketExists(String bucketName) {
    return minioUtil.bucketExists(bucketName);
  }

  /**
   * 创建存储桶
   *
   * @param bucketName 存储桶名称
   */
  public void makeBucket(String bucketName) {
    minioUtil.makeBucket(bucketName);
  }

  /**
   * 文件上传
   *
   * @param file 上传的文件
   * @param bucketName 桶名称
   * @param fileType 文件类型
   */
  public String upload(MultipartFile file, String bucketName, String fileType) {
    if (StrUtil.isBlank(bucketName)) {
      log.info("bucketName is null");
    }

    try {
      if (!this.bucketExists(bucketName)) {
        this.makeBucket(bucketName);
      }
      String fileName = file.getOriginalFilename();

      assert fileName != null;
      String objectName =
          IdUtil.randomUUID().replace("-", "") + fileName.substring(fileName.lastIndexOf("."));
      minioUtil.putObject(bucketName, file, objectName, fileType);
      return minioPropertiesConfig.getEndpoint() + "/" + bucketName + "/" + objectName;
    } catch (Exception e) {
      e.printStackTrace();
      return "上传失败";
    }
  }

  /**
   * 获取文件路径
   *
   * @param bucketName 桶名称
   * @param objectName 对象名称
   */
  public String getObjectUrl(String bucketName, String objectName) {
    return minioUtil.getObjectUrl(bucketName, objectName);
  }
}

6.Minio 接口

import com.csp.mingyue.minio.config.MinioPropertiesConfig;
import com.csp.mingyue.minio.service.MinioService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

/**
 * Minio 接口
 *
 * @author Strive
 * @date 2022/4/25 10:48
 * @description
 */
@RestController
@RequestMapping("/minio")
@RequiredArgsConstructor
public class MinioController {

  private final MinioPropertiesConfig minioPropertiesConfig;

  private final MinioService minioService;

  @PostMapping("/upload")
  public String upload(@RequestParam(name = "multipartFile") MultipartFile multipartFile) {
    return minioService.upload(
        multipartFile, minioPropertiesConfig.getBucketName(), multipartFile.getContentType());
  }

  @GetMapping("/getObjectUrl")
  public String getObjectUrl(String objectName) {
    return minioService.getObjectUrl(minioPropertiesConfig.getBucketName(), objectName);
  }
}

7.测试接口

补充说明

只写了两个接口,想要查询桶列表或者其他功能具体看 MinioUtil 函数,主要可以参考如下文档: