springboot集成minio SDK实现文件上传下载

1,590 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情

为了在项目中实现文件的管理,我们打算在项目中使用minio对象存储服务。如何搭建minio服务,可以查看Docker compose快速部署minio服务

在minio服务搭建好的情况下,我们要准备在springboot项目当中集成minio SDK,以便我们项目中的文件能全部交由minio服务来管理。

准备工作

访问http://ip:9001,通过管理员用户名密码登陆。

  • 创建文件桶

    从正常项目管理的角度来看,我们一般会提前把文件桶创建好。在团队开发中制定规范,达成共识,确定当前项目对应的文件桶名称,不同业务类型的文件放在什么名称的文件夹下面。

    比如以当前项目名称创建一个文件桶,在这个文件桶里面再创建不同的文件夹,类似image、vedio等。这样的话,我们针对头像、音视频等文件就可以放到指定的文件夹里面了。

  • 创建指定的用户,赋予对应的权限

image-20220805142907488.png 先把需要创建的用户名和密码填好,其他可暂时不选,然后创建用户。

其次创建权限:

1.点击左侧栏Access;

2.点击Create Policy

image-20220805143251954.png

image-20220805143438468.png 权限配置:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetBucketLocation",
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::awesome-spring"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::awesome-spring/*"
            ]
        }
    ]
}

首先awesome-spring是文件桶的名称,里面一共包含两段权限:

  • 第一段权限是允许在awesome-spring文件桶中执行GetBucketLocationGetObject(下载)PutObject(上传)操作,基本上满足上传下载的业务。
  • 第二段权限是允许在awesome-spring的子文件夹中做所有的操作。我们可以根据实际情况把权限设置得更细粒度。

权限和用户都创建完毕后,我们再回到用户列表,我们需要做的步骤就是把用户和权限关联起来:

image-20220805144319780.png

image-20220805144548957.png

image-20220805144712958.png

  • 通过以上步骤,我们把权限和用户关联起来了,这样的话,我们新创建的这个用户就有了操作这个bucket的权限了。

    如果团队人员比较多的话,我建议通过创建Group的方式,给对应的Group赋予权限,最后把新创建的用户放进Group,这样的话,可以避免一个一个给用户配置权限。

  • 创建开发需要的AccessKey和SecretKey

    当我们拿到新创建的用户后,登陆http://ip:9001

image-20220805145239336.png 我们可以看到,目前的新用户只能看到当前拥有权限的bucket。

接下来,我们立马创建一个service account来为我们顺利使用minio SDK作准备:

image-20220805145621053.png 【注意】secret key只有再第一次创建的时候会显示,创建成功后就无法查看,建议下载下来保存好。

Minio SDK依赖

想要把Minio服务集成到springboot项目中,我们可以通过它所提供的SDK来做的,下面我们找到了Maven依赖:

<!--minio文件存储-->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.4.3</version>
</dependency>

如果是gradle或者其他依赖管理工具可以去mvnrepository.com/artifact/io… 查找相关的依赖。

文件上传下载功能实现

  • 准备自定义minio config

    minio:
      config:
        # 请填写自己minio服务的ip和端口
        endpoint: "http://ip:9000"
        bucket-name: 文件桶名称
        access-key: "pXLVexkIGrvhfPbC"
        secret-key: "wcI9GE9UeX4qVQik2dV9zK6DkeZVZ3TR"
    

    通过springboot的ConfigurationProperties的功能,将配置转换成java bean:

    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Configuration;
    ​
    /**
     * @author zouwei
     * @className MinioConfigProperties
     * @date: 2022/8/4 下午4:35
     * @description:
     */
    @Data
    @Configuration
    @ConfigurationProperties(prefix = "minio.config")
    public class MinioConfigProperties {
      /**
       * minio服务API访问入口
       */
      private String endpoint;
      /**
       * 桶名称
       */
      private String bucketName;
      /**
       * 公钥
       */
      private String accessKey;
      /**
       * 私钥
       */
      private String secretKey;
    }
    
  • 实例化minio client

    import io.minio.MinioClient;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    ​
    /**
     * @author zouwei
     * @className MinioConfig
     * @date: 2022/8/4 下午4:27
     * @description:
     */
    @Configuration
    public class MinioConfig {
    ​
      @Autowired
      private MinioConfigProperties properties;
    ​
      @Bean
      public MinioClient minioClient() {
        return MinioClient.builder()
            .credentials(properties.getAccessKey(), properties.getSecretKey())
            .endpoint(properties.getEndpoint())
            .build();
      }
    }
    

    通过java config的方式把MinioClient实例化交给spring ioc容器来管理,接下来就可以直接在spring框架体系下正常使用SDK的功能了。

  • 上传下载业务实现

    // 文件上传
    @PostMapping("/image/upload")
    String upload(@RequestPart("userImage") MultipartFile userImage) throws Exception {
        fileService.putObject("image", userImage);
        return "success";
    }
    ​
    /**
       * 下载
       *
       * @param fileId
       * @param response
       * @throws Exception
       */
    @GetMapping("/download/{fileId}")
    public void download(@PathVariable("fileId") String fileId, HttpServletResponse response) throws Exception {
        fileService.getObject(fileId, response);
    }
    

    controller接收到前端上传的文件,马上调用接口fileService的上传接口,把文件上传到minio服务;

    import com.example.awesomespring.bo.FileUploadResult;
    import org.springframework.web.multipart.MultipartFile;
    ​
    import javax.servlet.http.HttpServletResponse;
    ​
    /**
     * @author zouwei
     * @className FileService
     * @date: 2022/8/4 下午3:45
     * @description:
     */
    public interface FileService {
      // 上传功能,把文件提交到minio服务,并把提交结果持久化
      FileUploadResult putObject(String dirs, MultipartFile file) throws Exception;
      // 下载功能,把minio服务中的文件下载并写入响应
      void getObject(String fileId, HttpServletResponse response) throws Exception;
    }
    

    我们来看看具体实现:

    import lombok.Data;
    /**
    * @author zouwei
    * @className FileUploadResult
    * @date: 2022/8/4 下午11:59
    * @description:
    */
    @Data
    public class FileUploadResult {
        // 文件桶名称
        private String bucketName;
        // 文件存储的路径
        private String filePath;
        // 文件名称
        private String filename;
        // 文件上传类型
        private String contentType;
        // 文件大小
        private int length;
    }
    
    import com.example.awesomespring.bo.FileUploadResult;
    import com.example.awesomespring.config.MinioConfigProperties;
    import com.example.awesomespring.dao.entity.FileUploadRecord;
    import com.example.awesomespring.dao.mapper.FileUploadRecordMapper;
    import com.example.awesomespring.service.FileService;
    import io.minio.*;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;
    ​
    import javax.servlet.http.HttpServletResponse;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.nio.file.Paths;
    import java.util.Date;
    import java.util.UUID;
    ​
    /**
     * @author zouwei
     * @className FileServiceImpl
     * @date: 2022/8/4 下午3:45
     * @description:
     */
    @Service
    public class FileServiceImpl implements FileService {
    ​
      @Autowired
      private MinioClient client;
    ​
      @Autowired
      private MinioConfigProperties properties;
    ​
      @Autowired
      private FileUploadRecordMapper fileUploadRecordMapper;
    ​
      /**
       * 上传文件
       *
       * @param dirs 目标文件夹; 比如image、video
       * @param file 上传的文件
       * @return
       * @throws Exception
       */
      @Override
      public FileUploadResult putObject(String dirs, MultipartFile file) throws Exception {
        FileUploadResult result = putObject(dirs, file, true);
        // 保存到数据库
        FileUploadRecord row = new FileUploadRecord();
        row.setFileName(result.getFilename());
        row.setCreateTime(new Date());
        row.setFilePath(result.getFilePath());
        row.setContentType(result.getContentType());
        row.setBucketName(result.getBucketName());
        row.setId(UUID.randomUUID().toString());
        row.setSize(result.getLength());
        fileUploadRecordMapper.insert(row);
        return result;
      }
    ​
      /**
       * 下载文件并写入响应中
       *
       * @param fileId
       * @param response
       * @throws Exception
       */
      @Override
      public void getObject(String fileId, HttpServletResponse response) throws Exception {
        FileUploadRecord row = fileUploadRecordMapper.selectByPrimaryKey(fileId);
        String path = row.getFilePath();
        // 构建下载参数
        GetObjectArgs objectArgs = GetObjectArgs.builder()
            .bucket(bucketName())
            .object(path)
            .build();
        // 下载并写入响应中
        try (InputStream input = client.getObject(objectArgs); OutputStream outputStream = response.getOutputStream()) {
          response.setContentType(row.getContentType());
          response.setHeader("Accept-Ranges", "bytes");
          response.setHeader("Content-Length", String.valueOf(length));
          response.setHeader("Content-disposition", "attachment; filename=" + filename);
          outputStream.write(input.readAllBytes());
          outputStream.flush();
        } catch (Exception e) {
          // 建议包装成自定义异常,以便自定义异常处理捕获到
          throw e;
        }
      }
    ​
      /**
       * 获取文件桶
       *
       * @return
       */
      private String bucketName() {
        return properties.getBucketName();
      }
    ​
      /**
       * 如果文件桶不存在就创建
       *
       * @param bucketName
       * @throws Exception
       */
      private void createIfNotExistBucket(String bucketName) throws Exception {
        if (!client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
          client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
      }
    ​
    ​
      /**
       * 这个实现只针对于minio服务实现,所以不建议以接口暴露给外部调用
       *
       * @param dirs
       * @param file
       * @param createIfNotExistBucket
       * @return
       * @throws Exception
       */
      private FileUploadResult putObject(final String dirs, MultipartFile file, boolean createIfNotExistBucket) throws Exception {
        // 获取桶名称
        final String bucketName = bucketName();
        // 获取文件名称
        final String filename = file.getOriginalFilename();
        // 获取文件类型
        final String contentType = file.getContentType();
        // 拼接路径;因为不会把所有文件直接放在桶下面
        String path = filename;
        if (StringUtils.isNotBlank(dirs)) {
          path = Paths.get(dirs, filename).toString();
        }
        // 从上传来的文件中取流
        try (InputStream fileStream = file.getInputStream()) {
          // 如果要求文件桶不存在就创建
          if (createIfNotExistBucket) {
            createIfNotExistBucket(bucketName);
          }
          int length = fileStream.available();
          // 准备好文件上传的参数
          PutObjectArgs objectArgs = PutObjectArgs.builder()
              .bucket(bucketName)
              .object(path)
              .contentType(contentType)
              .stream(fileStream, length, -1)
              .build();
          // 上传文件
          client.putObject(objectArgs);
          // 返回上传结果
          FileUploadResult result = new FileUploadResult();
          result.setContentType(contentType);
          result.setFilePath(dirs);
          result.setFilename(filename);
          result.setBucketName(bucketName);
          result.setLength(length);
          return result;
        } catch (Exception e) {
          // 建议包装成自定义异常,以便自定义异常处理捕获到
          throw e;
        }
      }
    }
    

    以上代码有几个需要注意的点:

1.文件上传成功到minio服务中后,并不会返回统一的哈希等唯一标识字段,所以我建议我们需要把上传结果保存一条记录到数据库。

2.我们提供给外面的下载链接应该尽可能的简单,比如:http://127.0.0.1/download/{fileId};所以我在设计上传和下载的时候,上传结果用fileId来表示一个文件,下载的时候也只需要使用fileId就可以下载目标文件。

3.在文件上传下载处理过程中,产生的Exception应该全部转换成自定义的异常抛出去,这样的话,可以方便后续的统一异常处理逻辑一次性解决服务端的异常问题。

至此,基于minio对象存储中间件的集成就完成了,小伙伴们可以根据自己的实际情况修改文件存储逻辑的具体实现。