minio使用入门

【引言】

Minio为java开发提供了一些API,以方便我们通过这些API直接控制Minio,从而使用java程序实现对象的存取。

↓ 您可以参考下面的 官方 的英文api说明
https://min.io/docs/minio/linux/developers/java/API.html

↓ 或参考 书栈网 提供的中文api说明
https://www.bookstack.cn/read/MinioCookbookZH/22.md

  • 接下来,我们将对官方的原生API进行二次封装,实现以下三点功能
  1. 将连接Minio用的配置写在了配置文件中,并编写一个类读取配置文件中的信息,并将其注册为一个Bean对象
    2.将MinioClient初始化后注册为一个单例的Bean,不用每次使用的时候再去创建。
  2. 将Minio原生的方法进行封装,简化方法的调用,提高易用性,并提供更多重载方法,为一个功能提供多种参数选择,减少默认参数的重复填写。

1.1. 结构及功能

目录结构

minio                   
 └─ config
     └─ MinioConfig
 └─ service
     └─ MinioService
     └─ MinioServiceImpl
 └─ utils
     └─ MinioUtils  

功能

  1. 操作桶相关(桶是否存在,新建桶,删除桶,获取所有桶,获取桶内对象信息)
  2. 对象操作:
    1. 上传到指定的桶的指定位置
    2. 下载指定桶内的指定文件
    3. 删除指定文件
    4. 获取文件的url
    5. 获取文件的二进制数组
  3. 工具
    1. 对图片附件进行压缩,可以按最大尺寸或按质量压缩

1.2. 配置

pom.xml

<!-- minio核心 -->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.4.0</version>
</dependency>

<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
</dependency>
<!--读取配置信息-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<!--图片处理-->
<dependency>
    <groupId>net.coobird</groupId>
    <artifactId>thumbnailator</artifactId>
    <version>0.4.8</version>
</dependency>
<!--springboot单元测试,MutipartFile转换需要使用-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

application.yml

spring:
  #上传文件大小限制
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB

minio:
  endpoint: http://IP:端口
  bucketName: 默认桶名称
  accessKey: 实际值
  secretKey: 实际值

1.3. 其他代码

MinioConfig.java 配置Bean

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

@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
    private String endpoint;
    private String accessKey;
    private String secretKey;
    private String bucketName;

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

MinioService.java 业务接口

import io.minio.messages.Bucket;
import io.minio.messages.Item;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.util.List;

/**
 * Minio业务接口
 *
 * @Author Mosfield
 */
public interface MinioService {

    //////////////////// 桶操作相关 /////////////////////

    /**
     * 查看bucket是否存在
     *
     * @param bucketName
     * @return
     */
    Boolean bucketExists(String bucketName);

    /**
     * 创建存储bucket
     *
     * @param bucketName
     */
    void makeBucket(String bucketName);

    /**
     * 删除存储bucket
     *
     * @param bucketName
     */
    void removeBucket(String bucketName);

    /**
     * 获取全部bucket
     *
     * @return
     */
    List<Bucket> listBuckets();

    /**
     * 递归查看桶内的所有文件信息
     *
     * @return 存储bucket内文件对象信息
     */
    List<Item> listObjects();

    /**
     * 列出某个存储桶中的所有对象。
     *
     * @param bucketName  存储桶名称
     * @param prefix      对象名称的前缀
     * @param recursive   是否递归查找,如果是false,就模拟文件夹结构查找
     * @param useVersion1 如果是true, 使用版本1 REST API
     * @return
     */
    List<Item> listObjects(String bucketName, String prefix, Boolean recursive, Boolean useVersion1);

    //////////////////// 对象操作相关 /////////////////////

    /**
     * 判断文件夹是否存在
     *
     * @param folderName 文件夹名称
     * @return true存在, 反之
     */
    Boolean checkFolderIsExist(String folderName);

    /**
     * 判断文件夹是否存在
     *
     * @param folderName 文件夹名称
     * @param bucket     桶名称
     * @return true存在, 反之
     */
    public Boolean checkFolderIsExist(String folderName, String bucket);

    /**
     * 判断文件是否存在
     *
     * @param fileName 文件名称, 如果要带文件夹请用 / 分割, 例如 /help/index.html
     * @return true存在, 反之
     */
    Boolean checkFileIsExist(String fileName);

    /**
     * 判断文件是否存在
     *
     * @param fileName 文件名称, 如果要带文件夹请用 / 分割, 例如 /help/index.html
     * @param bucket   桶名称
     * @return true存在, 反之
     */
    Boolean checkFileIsExist(String fileName, String bucket);

    /**
     * 删除默认桶内的指定文件
     *
     * @param fileName
     * @return
     * @throws Exception
     */
    void remove(String fileName);

    /**
     * 删除指定桶内的指定文件
     *
     * @param fileName
     * @return
     * @throws Exception
     */
    void remove(String fileName, String bucket);

    /**
     * 把文件上传到默认桶,名字为原名字
     *
     * @param file
     * @return 临时访问url
     */
    String uploadFileObject(MultipartFile file);

    /**
     * 把文件上传到默认桶
     *
     * @param file
     * @param name 路径和新名字
     * @return 临时访问url
     */
    String uploadFileObject(MultipartFile file, String name);

    /**
     * 把文件上传到指定桶的指定路径下,桶名为空则上传到默认桶,路径为空则上传到桶的根目录
     *
     * @param file
     * @param name
     * @param bucket
     * @return 临时访问url
     */
    String uploadFileObject(MultipartFile file, String name, String bucket);

    /**
     * 把文件流上传到默认桶
     * @param inputStream
     * @param name
     * @param contentType
     * @param size
     * @return
     */
    String uploadFileObject(InputStream inputStream, String name, String contentType, Long size);

    /**
     * 把文件流上传到指定桶
     * @param inputStream
     * @param name
     * @param bucket
     * @param contentType
     * @param size
     * @return
     */
    String uploadFileObject(InputStream inputStream, String name, String bucket, String contentType, Long size);

    /**
     * 从默认的桶里下载文件
     *
     * @param fileName
     * @return
     */
    InputStream downloadFileObject(String fileName);

    /**
     * 从指定的桶里下载文件
     *
     * @param fileName
     * @param bucket
     * @return
     */
    InputStream downloadFileObject(String fileName, String bucket);

    /**
     * 获取文件的url
     *
     * @param fileName
     * @return
     */
    String getPresignedObjectUrl(String fileName);

    /**
     * 从指定的桶中获取文件的url
     *
     * @param fileName
     * @return
     */
    String getPresignedObjectUrl(String fileName, String bucket);

    /**
     * 获取公开桶里文件的url
     *
     * @param fileName
     * @return
     */
    String getPublicUrlByName(String fileName);

    /**
     * 获取指定的公开桶里文件的url
     *
     * @param fileName
     * @return
     */
    String getPublicUrlByName(String fileName, String bucket);

    /**
     * 根据路径和名字,获取默认桶内对应的二进制数组
     *
     * @param fileName
     * @return
     */
    byte[] getByteArrByName(String fileName);

    /**
     * 根据路径和名字,获取指定桶内对应的二进制数组
     *
     * @param fileName
     * @param bucket
     * @return
     */
    byte[] getByteArrByName(String fileName, String bucket);
}

MinioServiceImpl.java 业务实现类


import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import link.xiaomo.minio.config.MinioConfig;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * Minio业务实现类
 *
 * @Author Mosfield
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class MinioServiceImpl implements MinioService {
    private final MinioClient minioClient;
    private final MinioConfig minioConfig;

    @Override
    @SneakyThrows
    public Boolean bucketExists(String bucketName) {
        return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    @Override
    @SneakyThrows
    public void makeBucket(String bucketName) {
        minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
    }

    @Override
    @SneakyThrows
    public void removeBucket(String bucketName) {
        minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
    }

    @Override
    @SneakyThrows
    public List<Bucket> listBuckets() {
        return minioClient.listBuckets();
    }

    @Override
    @SneakyThrows
    public List<Item> listObjects() {
        return this.listObjects(minioConfig.getBucketName(), null, false, false);
    }

    @Override
    @SneakyThrows
    public List<Item> listObjects(String bucketName, String prefix, Boolean recursive, Boolean useVersion1) {

        ListObjectsArgs args = ListObjectsArgs.builder().
                bucket(bucketName)
                .prefix(prefix)
                .recursive(recursive)
                .useApiVersion1(useVersion1)
                .build();
        Iterable<Result<Item>> results = minioClient.listObjects(args);
        List<Item> items = new ArrayList<>();
        for (Result<Item> result : results) {
            items.add(result.get());
        }
        return items;
    }

    public Boolean checkFolderIsExist(String folderName) {
        return this.checkFolderIsExist(folderName, minioConfig.getBucketName());
    }

    public Boolean checkFolderIsExist(String folderName, String bucket) {
        try {
            Iterable<Result<Item>> results = minioClient.listObjects(
                    ListObjectsArgs
                            .builder()
                            .bucket(bucket)
                            .prefix(folderName)
                            .recursive(false)
                            .build());
            for (Result<Item> result : results) {
                Item item = result.get();
                if (item.isDir() && folderName.equals(item.objectName())) {
                    return true;
                }
            }
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    public Boolean checkFileIsExist(String fileName) {
        return this.checkFileIsExist(fileName, minioConfig.getBucketName());
    }

    public Boolean checkFileIsExist(String fileName, String bucket) {
        try {
            minioClient.statObject(
                    StatObjectArgs.builder().bucket(bucket).object(fileName).build()
            );
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    @Override
    @SneakyThrows
    public void remove(String fileName) {
        this.remove(fileName, minioConfig.getBucketName());
    }

    @Override
    @SneakyThrows
    public void remove(String fileName, String bucket) {

        RemoveObjectArgs args = RemoveObjectArgs.builder()
                .bucket(bucket)
                .object(fileName)
                .build();
        minioClient.removeObject(args);
    }

    @Override
    public String uploadFileObject(MultipartFile file) {
        return this.uploadFileObject(file, file.getOriginalFilename(), minioConfig.getBucketName());
    }

    @Override
    public String uploadFileObject(MultipartFile file, String name) {
        return this.uploadFileObject(file, name, minioConfig.getBucketName());
    }

    @Override
    @SneakyThrows
    public String uploadFileObject(MultipartFile file, String name, String bucket) {
        if (file.isEmpty()) {
            return null;
        }

        return this.uploadFileObject(file.getInputStream(), name, bucket, file.getContentType(), file.getSize());
    }

   @Override
    @SneakyThrows
    public String uploadFileObject(InputStream inputStream, String name, String contentType, Long size) {
        return this.uploadFileObject(inputStream, name, minioConfig.getBucketName(), contentType, size);
    }

    @Override
    @SneakyThrows
    public String uploadFileObject(InputStream inputStream, String name, String bucket, String contentType, Long size) {
        //构建要上传文件对象的参数,如文件名、类型等
        PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                .object(name)
                .bucket(bucket)
                .contentType(contentType)
                .stream(inputStream, size, -1)
                .build();
        //上传文件
        minioClient.putObject(putObjectArgs);
        return this.getPresignedObjectUrl(name);
    }

    @Override
    @SneakyThrows
    public InputStream downloadFileObject(String fileName) {
        Boolean result = this.checkFileIsExist(fileName);
        return this.downloadFileObject(fileName, minioConfig.getBucketName());
    }

    @Override
    @SneakyThrows
    public InputStream downloadFileObject(String fileName, String bucket) {
        // 先判断是否存在该文件,不存在返回null
        Boolean result = this.checkFileIsExist(fileName, bucket);
        if (!result) {
            return null;
        }

        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                .bucket(bucket)
                .object(fileName)
                .build();

        return minioClient.getObject(getObjectArgs);
    }

    @Override
    @SneakyThrows
    public String getPresignedObjectUrl(String fileName) {
        return this.getPresignedObjectUrl(fileName, minioConfig.getBucketName());
    }

    @Override
    @SneakyThrows
    public String getPresignedObjectUrl(String fileName, String bucket) {
        // 先判断是否存在该文件,不存在返回null
        Boolean result = this.checkFileIsExist(fileName, bucket);
        if (!result) {
            return null;
        }

        GetPresignedObjectUrlArgs build = GetPresignedObjectUrlArgs.builder()
                .bucket(bucket)
                .object(fileName)
                .method(Method.GET)
                .build();
        return minioClient.getPresignedObjectUrl(build);
    }

    @Override
    @SneakyThrows
    public String getPublicUrlByName(String fileName) {
        return this.getPublicUrlByName(fileName, minioConfig.getBucketName());
    }

    @Override
    public String getPublicUrlByName(String fileName, String bucket) {
        // 先判断是否存在该文件,不存在返回null
        Boolean result = this.checkFileIsExist(fileName, bucket);
        if (!result) {
            return null;
        }
        return minioConfig.getEndpoint() + "/" + bucket + "/" + fileName;
    }

    @Override
    @SneakyThrows
    public byte[] getByteArrByName(String fileName) {
        return this.getByteArrByName(fileName, minioConfig.getBucketName());
    }

    @Override
    @SneakyThrows
    public byte[] getByteArrByName(String fileName, String bucket) {
        // 先判断是否存在该文件,不存在返回null
        Boolean result = this.checkFileIsExist(fileName, bucket);
        if (!result) {
            return null;
        }

        InputStream inputStream = this.downloadFileObject(fileName, bucket);
        if (inputStream == null) {
            return null;
        }

        return IOUtils.toByteArray(inputStream);
    }
}

MinioUtils.java 工具类

import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.mock.web.MockMultipartFile;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

@Slf4j
public class MinioUtils {

    /**
     * 压缩图片
     *
     * @param file      原始文件
     * @param maxWidth  最大宽
     * @param maxHeight 最大高
     * @param quality   质量压缩比,范围是(0,1]
     * @return
     */
    public static MultipartFile zipImg(MultipartFile file, Integer maxWidth, Integer maxHeight, Float quality) throws IOException {

        String fileName = file.getName();
        String originalFilename = file.getOriginalFilename();
        String contentType = file.getContentType();

        InputStream is = null;
        // 通过MultipartFile得到InputStream,从而得到BufferedImage
        BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
        // 获取图片的真实长和宽
        int width = bufferedImage.getWidth();
        int height = bufferedImage.getHeight();

        if (maxWidth == null) {
            maxWidth = width;
        }
        if (maxHeight == null) {
            maxHeight = height;
        }

        // 计算图片的压缩比
        float rate = 1f;
        if (width > maxWidth) {
            rate = 1f * maxWidth / width;
        }
        if (height > maxHeight && rate > 1f * maxHeight / height) {
            rate = 1f * maxHeight / height;
        }

        log.info("压缩比" + rate);

        // 质量压缩比
        if(quality == null){
            quality=1f;
        }

        is = file.getInputStream();
        // 此处换成任意路径即可,临时文件存放位置
        File tempFile = new File("/" + originalFilename);

        Thumbnails.of(is)
                .scale(rate)
                .outputQuality(quality)
                .toFile(tempFile);

        FileInputStream fileInputStream = new FileInputStream(tempFile);
        MockMultipartFile newFile = new MockMultipartFile(fileName, originalFilename, contentType, fileInputStream);

        fileInputStream.close();
        // 删除临时文件
        boolean success = tempFile.delete();

        log.info("压缩结果:" + success);

        return newFile;
    }

    /**
     * 通过UUID的规则生成新的文件名,并在前面拼接上路径前缀
     * @param fileName 原文件名
     * @return
     */
    public static String getNewFileNameByUUID(String fileName) {
        return MinioUtils.getNewFileNameByUUID(fileName,"");
    }

    /**
     * 通过UUID的规则生成新的文件名,并在前面拼接上路径前缀
     *
     * @param fileName 原文件名
     * @param prefix   路径名可以为空。如果不为空,那不应该以/开头,应该以/结尾
     * @return
     */
    public static String getNewFileNameByUUID(String fileName, String prefix) {
        // 处理前缀为null的情况
        if (prefix == null) {
            prefix = "";
        }
        // 去除空格
        prefix = prefix.trim();
        // 判断前缀是否以斜线开头,如果是,就删除开头的斜线
        if (prefix.startsWith("/")) {
            prefix = prefix.replaceFirst("/", "");
        }
        // 判断如果内容不为空,且不以斜线结尾,则在尾部加上斜线
        if (prefix.length() > 0 && !prefix.endsWith("/")) {
            prefix = prefix + "/";
        }

        return prefix + UUID.randomUUID().toString().replaceAll("-", "")
                + fileName.substring(fileName.lastIndexOf("."));
    }
}

1.4. 案例实现-图片上传和展示

UserController.java

/**
 * 用户相关controller
 *
 * @Author Mosfield
 * @Date 2023-6-1
 */
@RestController
@CrossOrigin
@RequestMapping("user")
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 根据用户id查询具体用户
     *
     * @param id
     * @return
     */
    @RequestMapping("/loadUserById")
    public Result loadUserById(@RequestParam("id") Integer id) {
        User user = userService.loadUserById(id);
        return Result.success(user);
    }

    /**
     * 把文件附件上传到minio
     * @param file
     * @param id
     * @return
     */
    @PostMapping("uploadImgToMinio")
    public Result uploadImgToMinio(@RequestParam("file") MultipartFile file, @RequestParam(name = "id", required = false) Integer id) {
        if (file == null) {
            return Result.error("文件上传失败!");
        }

        String fileName = userService.uploadImgToMinio(file,id);

        return Result.success(fileName);
    }

}

UserServiceImpl.java (接口省略)

/**
 * 用户业务层实现类
 * @Author Mosfield
 */
@Service
@Slf4j
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MinioService minioService;

    /**
     * 根据id查询用户
     * @param id
     * @return
     */
    @Override
    public User loadUserById(Integer id) {
        // 查询数据库,获得user对象
        User user = userMapper.loadUserById(id);
        // 查询minio,把图片的二进制数组放入user对象
        user.setImgByte(minioService.getByteArrByName(user.getHeadImg()));
        return user;
    }

    /**
     * 把文件附件上传到minio
     * @param file
     * @param id
     * @return
     */
    @Override
    @SneakyThrows
    public String uploadImgToMinio(MultipartFile file, Integer id) {

        log.info("关联id:" + id);
        log.info("文件原名:" + file.getOriginalFilename());

        // 文件原名
        String fileName = file.getOriginalFilename();
        // 通过UUID生成路径+md5随机文件名+原拓展名
        String newName = MinioUtils.getNewFileNameByUUID(fileName);
        log.info("文件新名:" + newName);

        // 压缩图片
        MultipartFile newFile = MinioUtils.zipImg(file, 500, 500, null);
        try{
            // 存储
            minioService.uploadFileObject(newFile, newName);
        }catch (Exception e){
            // 抛出自定义业务异常
            throw new FileUploadFailedException("文件上传失败");
        }

        return newName;
    }
}

img展示二进制数据图片(element-ui组件)

<el-image :src="'data:image/png;base64,'+ 二进制数组变量" fit="contain">
</el-image>

也可以使用 MinioService 中的几个获取url的方法,返回minio对应的外部访问链接。

需要注意的是,要确认浏览器能访问到minio提供的地址,同时需要注意minio分享的连接的过期时间。

如人饮水,冷暖自知。
最后更新于 2023-08-02