spring cloud 集成minIO分布式文件系统

494 阅读4分钟

minIO介绍

MinIO 是在 GNU Affero 通用公共许可证 v3.0 下发布的高性能对象存储。它与 Amazon S3 云存储服务 API 兼容。使用 MinIO 为机器学习、分析和应用程序数据工作负载构建高性能基础架构。

高性能

MinIO 是全球领先的对象存储先锋,目前在全世界有数百万的用户. 在标准硬件上,读/写速度上高达183 GB / 秒 和 171 GB / 秒。对象存储可以充当主存储层,以处理Spark、Presto、TensorFlow、H2O.ai等各种复杂工作负载以及成为Hadoop HDFS的替代品。MinIO用作云原生应用程序的主要存储,与传统对象存储相比,云原生应用程序需要更高的吞吐量和更低的延迟。而这些都是MinIO能够达成的性能指标。

可扩展性

MinIO利用了Web缩放器的来之不易的知识,为对象存储带来了简单的缩放模型。 这是我们坚定的理念 “简单可扩展.” 在 MinIO, 扩展从单个群集开始,该群集可以与其他MinIO群集联合以创建全局名称空间, 并在需要时可以跨越多个不同的数据中心。 通过添加更多集群可以扩展名称空间, 更多机架,直到实现目标。

云的原生支持

MinIO 是在过去4年的时间内从0开始打造的一款软件 ,符合一切原生云计算的架构和构建过程,并且包含最新的云计算的全新的技术和概念。 其中包括支持Kubernetes 、微服和多租户的的容器技术。使对象存储对于 Kubernetes更加友好。

简单

极简主义是MinIO的指导性设计原则。简单性减少了出错的机会,提高了正常运行时间,提供了可靠性,同时简单性又是性能的基础。 只需下载一个二进制文件然后执行,即可在几分钟内安装和配置MinIO。 配置选项和变体的数量保持在最低限度,这样让失败的配置概率降低到接近于0的水平。 MinIO升级是通过一个简单命令完成的,这个命令可以无中断的完成MinIO的升级,并且不需要停机即可完成升级操作 - 降低总使用和运维成本。

minIO安装

使用Docker 安装 Standalone MinIO

生产环境请使用分布式安装 运行docker命令

docker run \
  -p 9000:9000 \
  -p 9001:9001 \
  -e "MINIO_ROOT_USER=AKIAIOSFODNN7EXAMPLE" \
  -e "MINIO_ROOT_PASSWORD=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \
  quay.io/minio/minio server /data --console-address ":9001"

MINIO_ROOT_USER为登录账号,MINIO_ROOT_PASSWORD为登录密码

开放9000和9001端口

控制台

min.png

min2.png

spring cloud 集成

添加依赖

implementation 'io.minio:minio'

FileServiceImpl.java

package com.f.service.impl;

import com.f.config.FileProperties;
import com.f.constant.Constant;
import com.f.dto.file.GetObjectDto;
import com.f.dto.file.PreUrlGetObjectDto;
import com.f.dto.file.PreUrlPutObjectDto;
import com.f.dto.file.PutObjectDto;
import com.f.service.FileService;
import com.f.utils.FileUtils;
import com.f.utils.IdUtils;
import io.minio.GetObjectArgs;
import io.minio.GetObjectResponse;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.MinioClient;
import io.minio.ObjectWriteResponse;
import io.minio.PutObjectArgs;
import io.minio.http.Method;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import okhttp3.HttpUrl;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * 文件服务实现
 *
 * @author liuf
 * @date 2022/2/22 14:18
 */
@Slf4j
@RequiredArgsConstructor
@Service
public class FileServiceImpl implements FileService {

    private final FileProperties fileProperties;

    private MinioClient minioClient;

    @PostConstruct
    private synchronized void init() {
        if (minioClient == null) {
            minioClient = MinioClient.builder()
                    .endpoint(Objects.requireNonNull(HttpUrl.parse(fileProperties.getMinIoUrl())))
                    .credentials(fileProperties.getAccessKey(), fileProperties.getAccessSecret())
                    .region(fileProperties.getRegion())
                    .build();
            log.info("init minioClient");
        }
    }

    @SneakyThrows
    @Override
    public String putObject(PutObjectDto putObjectDto) {
        final ObjectWriteResponse response = minioClient.putObject(PutObjectArgs.builder().bucket(putObjectDto.getBucket())
                .object(getName(putObjectDto.getSuffix()))
                .stream(putObjectDto.getStream(), putObjectDto.getObjectSize(), putObjectDto.getPartSize())
                .build());
        log.info("result:{}", response);
        return response.object();
    }

    @SneakyThrows
    @Override
    public String putMultipartFile(PutObjectDto putObjectDto, MultipartFile file) {
        putObjectDto.setStream(file.getInputStream());
        putObjectDto.setSuffix(Constant.DOT + StringUtils.substringAfterLast(Constant.DOT, file.getOriginalFilename()));
        return putObject(putObjectDto);
    }

    @NotNull
    private String getName(String suffix) {
        return IdUtils.uuid() + suffix;
    }

    @SneakyThrows
    @Override
    public String getPreSignedPutObjectUrl(PreUrlPutObjectDto preUrlObjectDto) {
        return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                .bucket(preUrlObjectDto.getBucket())
                .object(getName(preUrlObjectDto.getSuffix()))
                .expiry(FileUtils.getExpiry(preUrlObjectDto.getExpiry()))
                .method(Method.PUT)
                .build());
    }

    @SneakyThrows
    @Override
    public String getPreSignedGetObjectUrl(PreUrlGetObjectDto preUrlGetObjectDto) {
        return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                .bucket(preUrlGetObjectDto.getBucket())
                .object(preUrlGetObjectDto.getName())
                .expiry(FileUtils.getExpiry(preUrlGetObjectDto.getExpiry()))
                .method(Method.GET)
                .build());
    }

    @Override
    public List<String> getPreSignedPutObjectUrlList(PreUrlPutObjectDto preUrlObjectDto) {
        final int size = preUrlObjectDto.getSize();
        List<String> list = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            list.add(getPreSignedPutObjectUrl(preUrlObjectDto));
        }
        return list;
    }

    @SneakyThrows
    @Override
    public InputStream getObject(GetObjectDto getObjectDto) {
        final GetObjectResponse response = minioClient.getObject(GetObjectArgs.builder()
                .bucket(getObjectDto.getBucket())
                .object(getObjectDto.getName())
                .build());
        log.info("result:{}", response);
        return response;
    }
}

FileController.java

package com.f.controller;

import com.f.base.Result;
import com.f.dto.file.GetObjectDto;
import com.f.dto.file.PreUrlGetObjectDto;
import com.f.dto.file.PreUrlPutObjectDto;
import com.f.dto.file.PutObjectDto;
import com.f.service.FileService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

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

/**
 * 文件服务
 *
 * @author liuf
 * @date 2022/2/22 14:18
 */
@RequiredArgsConstructor
@RestController
public class FileController {

    private final FileService fileService;

    /**
     * 上传
     *
     * @param putObjectDto 上传对象
     * @return 路径
     * @date 2022年2月23日
     */
    @PutMapping("/putMultipartFile")
    public Result<String> putMultipartFile(PutObjectDto putObjectDto, @RequestParam("file") MultipartFile file) {
        return Result.success(fileService.putMultipartFile(putObjectDto, file));
    }

    /**
     * 上传
     *
     * @param putObjectDto 上传对象
     * @return 路径
     * @date 2022年2月23日
     */
    @PutMapping("/putObject")
    public Result<String> putObject(PutObjectDto putObjectDto) {
        return Result.success(fileService.putObject(putObjectDto));
    }

    /**
     * 预上传url生成
     *
     * @param preUrlObjectDto dto
     * @return url
     */
    @GetMapping("/getPreSignedPutObjectUrl")
    public Result<String> getPreSignedPutObjectUrl(PreUrlPutObjectDto preUrlObjectDto) {
        return Result.success(fileService.getPreSignedPutObjectUrl(preUrlObjectDto));
    }

    /**
     * 预下载url生成
     *
     * @param preUrlGetObjectDto dto
     * @return url
     */
    @GetMapping("/getPreSignedGetObjectUrl")
    public Result<String> getPreSignedGetObjectUrl(PreUrlGetObjectDto preUrlGetObjectDto) {
        return Result.success(fileService.getPreSignedGetObjectUrl(preUrlGetObjectDto));
    }

    /**
     * 预上传url生成 批量
     *
     * @param preUrlObjectDto dto
     * @return url
     */
    @GetMapping("/getPreSignedPutObjectUrlList")
    public Result<List<String>> getPreSignedPutObjectUrlList(PreUrlPutObjectDto preUrlObjectDto) {
        return Result.success(fileService.getPreSignedPutObjectUrlList(preUrlObjectDto));
    }

    /**
     * 下载
     *
     * @param getObjectDto 对象
     * @return 文件流
     * @date 2022年2月23日
     */
    @GetMapping("/getObject")
    public InputStream getObject(@ModelAttribute GetObjectDto getObjectDto) {
        return fileService.getObject(getObjectDto);
    }
}

FileApplication.java

package com.f;

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

/**
 * 文件服务启动 @EnableDiscoveryClient 默认会启用
 *
 * @author liuf
 * @date 2022/2/22 15:25
 */
@SpringBootApplication(scanBasePackages = "com.f", exclude = {DataSourceAutoConfiguration.class})
public class FileApplication {

    public static void main(String[] args) {
        SpringApplication.run(FileApplication.class, args);
    }
}

前端集成

api/index.ts

import axios from 'axios';
import { FILE_API, FILE_BUCKET, FILE_EXPIRY } from '/@/api/constant';
import request from '/@/utils/request';

// 配置新建一个 axios 实例
const fileRequest = axios.create({
	timeout: 15000,
	headers: { 'Content-Type': 'application/octet-stream' },
});

export interface MyFile {
	file: File, // 文件
}

/**
 * 预上传请求路径
 * @param suffix 文件后缀
 * @returns 返回预请求路径
 */
export function getPreSignedPutObjectUrl(suffix: string) {
	return request.get(FILE_API + '/getPreSignedPutObjectUrl', {
		params: {
			"expiry": FILE_EXPIRY,
			"suffix": suffix,
			"bucket": FILE_BUCKET
		}
	});
}

/**
 * 上传文件接口
 * @param file 文件
 * @param url 上传路径
 * @returns 路径
 */
export function uploadFileByUrl(file: File, url: string) {
	return fileRequest.put(url, file)
}

view/personal/idnex.vue

<template>
   <div class="personal">
      <el-row>
         <!-- 个人信息 -->
         <el-col :xs="24" :sm="16">
            <el-card shadow="hover" header="个人信息">
               <div class="personal-user">
                  <div class="personal-user-left">
                     <el-upload class="h100 personal-user-left-upload" :http-request="upload" multiple :limit="1">
                        <img :src="getSrc(getUserInfos.headImage)" />
                     </el-upload>
                  </div>
                  <div class="personal-user-right">
                     <el-row>
                        <el-col :span="24" class="personal-title mb18">{{ currentTime }},admin,生活变的再糟糕,也不妨碍我变得更好!</el-col>
                        <el-col :span="24">
                           <el-row>
                              <el-col :xs="24" :sm="8" class="personal-item mb6">
                                 <div class="personal-item-label">昵称:</div>
                                 <div class="personal-item-value">{{ getUserInfos.name }}</div>
                              </el-col>
                              <el-col :xs="24" :sm="16" class="personal-item mb6">
                                 <div class="personal-item-label">身份:</div>
                                 <div class="personal-item-value">{{ getUserInfos.name }}</div>
                              </el-col>
                           </el-row>
                        </el-col>
                        <el-col :span="24">
                           <el-row>
                              <el-col :xs="24" :sm="8" class="personal-item mb6">
                                 <div class="personal-item-label">登录IP:</div>
                                 <div class="personal-item-value">192.168.1.1</div>
                              </el-col>
                              <el-col :xs="24" :sm="16" class="personal-item mb6">
                                 <div class="personal-item-label">登录时间:</div>
                                 <div class="personal-item-value">{{ getUserInfos.createTime }}</div>
                              </el-col>
                           </el-row>
                        </el-col>
                     </el-row>
                  </div>
               </div>
            </el-card>
         </el-col>
      </el-row>
   </div>
</template>

<script lang="ts">
import { toRefs, reactive, computed } from 'vue';
import { formatAxis } from '/@/utils/formatTime';
import { newsInfoList, recommendList } from './mock';
import { useStore } from '/@/store/index';
import { getPreSignedPutObjectUrl, MyFile, uploadFileByUrl } from '/@/api/file';
import { update } from '/@/api/system/user';
import { getPath, getSuffix, getSrc } from '/@/utils/common';
import { SET_USER_INFOS, USER_INFOS_MODULE } from '/@/api/constant';
import { encrypt } from '/@/utils/aes';
export default {
   name: 'personal',
   setup() {
      const store = useStore();
      const state = reactive({
         newsInfoList,
         recommendList,
         personalForm: {
            name: '',
            email: '',
            autograph: '',
            occupation: '',
            phone: '',
            sex: '',
         },
      });
      // 当前时间提示语
      const currentTime = computed(() => {
         return formatAxis(new Date());
      });

      // 获取用户信息 vuex
      const getUserInfos = computed(() => {
         return store.state.userInfos.userInfos;
      });

      const upload = (file: MyFile) => {
         getPreSignedPutObjectUrl(getSuffix(file.file.name)).then(res => {
            uploadFileByUrl(file.file, res.data).then(() => {
               // 修改用户头像
               encrypt(JSON.stringify({ "id": (getUserInfos.value as any).id, "headImage": getPath(res.data) }))
                  .then(data => {
                     update({ data })
                  })
               store.dispatch(USER_INFOS_MODULE + '/' + SET_USER_INFOS, { ...getUserInfos.value, "headImage": getPath(res.data) });
            })
         })
      }

      return {
         currentTime,
         getUserInfos,
         upload,
         getSrc,
         ...toRefs(state),
      };
   },
};
</script>

展示

home.png

参考

docs.min.io/docs/minio-…

集成项目