安装

Docker 部署 MinIO

拉取镜像

docker pull minio/minio --platform linux/amd64

创建挂载目录

mkdir /Users/clear/docker/data/minio/config
mkdir /Users/clear/docker/data/minio/data

启动

docker run \
-p 19000:9000 \
-p 9090:9090 \
--net=host \
--name minio \
-d --restart=always \
-e "MINIO_ACCESS_KEY=minioadmin" \
-e "MINIO_SECRET_KEY=minioadmin" \
-v /data/clear/docker/minio/data:/data \
-v /data/clear/docker/minio/config:/root/.minio \
 minio/minio server \
/data --console-address ":9090" -address ":19000"

访问可视化页面

访问链接:http://ip:9090/login

账号密码:minioadmin/minioadmin

使用

Spring 搭配开发

引入

implementation group: 'io.minio', name: 'minio', version: '7.1.0'

配置类与工具类

minio.ip=127.0.0.1
minio.port=9090
minio.access-key=minioadmin
minio.secret-key=minioadmin
@Configuration
public class MinioConfig {

    @Value("${minio.ip}")
    private String ip;

    @Value("${minio.port}")
    private int port;

    @Value("${minio.access-key}")
    private String accessKey;

    @Value("${minio.secret-key}")
    private String secretKey;

    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
        .endpoint(ip, port, false) //https or not
        .credentials(accessKey, secretKey)
        .build();
    }
}
@Component
public class MinioUtil {
    private Logger logger = LoggerFactory.getLogger(MinioUtil.class);

    @Autowired
    private MinioClient minioClient;

    /**
	 * 创建bucket
	 */
    private void createBucket(String bucketName) throws Exception {
        BucketExistsArgs existsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
        if(!minioClient.bucketExists(existsArgs)) {
            MakeBucketArgs makeArgs = MakeBucketArgs.builder().bucket(bucketName).build();
            minioClient.makeBucket(makeArgs);
            logger.info("bucket {} 不存在, 自动创建该bucket", bucketName);
        }
    }

    /**
	 * 从给定输入流中传输对象并放入bucket
	 */
    public ObjectWriteResponse putObject(String bucketName, String objectName, InputStream stream, long objectSize, String contentType) throws Exception {
        if (StringUtils.isEmpty(bucketName)) {
            throw new RuntimeException("保存的bucketName为空");
        }
        createBucket(bucketName);
        //long objSize = -1;
        long partSize = -1; //objectSize已知,partSize设为-1意为自动设置
        PutObjectArgs putArgs = PutObjectArgs.builder()
        .bucket(bucketName)
        .object(objectName)
        .stream(stream, objectSize, partSize)
        .contentType(contentType)
        .build();
        ObjectWriteResponse response = minioClient.putObject(putArgs);

        return response;
    }

    /**
	 * 从bucket获取指定对象的输入流,后续可使用输入流读取对象
	 * getObject与minio server连接默认保持5分钟,
	 * 每隔15s由minio server向客户端发送keep-alive check,5分钟后由客户端主动发起关闭连接
	 */
    public InputStream getObject(String bucketName, String objectName) throws Exception{
        GetObjectArgs args = GetObjectArgs.builder()
        .bucket(bucketName)
        .object(objectName)
        .build();
        return minioClient.getObject(args);
    }

    /**
	 * 获取对象的临时访问url,有效期5分钟
	 */
    public String getObjectURL(String bucketName, String objectName) throws Exception{
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
        .bucket(bucketName)
        .object(objectName)
        .expiry(5, TimeUnit.MINUTES)
        .method(Method.GET)
        .build();
        return minioClient.getPresignedObjectUrl(args);
    }

    /**
     * 删除对象
     */
    public void removeObject(String bucketName, String objectName) throws Exception {
        RemoveObjectArgs args = RemoveObjectArgs.builder()
        .bucket(bucketName)
        .object(objectName)
        .build();
        minioClient.removeObject(args);
        logger.info("bucket:{}文件{}已删除", bucketName , objectName);
    }

    /**
     * 上传MultipartFile
     * @param bucketName 文件存放的bucket
     * @param filePath 文件在bucket里的全目录
     */
    public ObjectWriteResponse uploadFile(String bucketName, String filePath, MultipartFile file) throws Exception{
        return putObject(bucketName, filePath, file.getInputStream(), file.getSize(), file.getContentType());
    }

    /**
     * 从minio下载文件,直接通过response传输
     */
    public void downloadFile(String bucketName, String filePath, HttpServletResponse response) throws Exception {
    	
		try (InputStream is = getObject(bucketName, filePath);
			 BufferedInputStream bis = new BufferedInputStream(is); 
			 OutputStream os = response.getOutputStream()) {
		//try {	
			/*InputStream is = getObject(bucketName, filePath);
			BufferedInputStream bis = new BufferedInputStream(is);
			OutputStream os = response.getOutputStream();*/
			response.setContentType("application/force-download;charset=utf-8");// 设置强制下载而不是直接打开
            response.addHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(filePath, "UTF-8"));// 设置文件名
            
            
            byte[] buffer = new byte[1024*1024*1024]; //buffer 1M
            int offset = bis.read(buffer);
            while (offset != -1) {
                os.write(buffer, 0, offset);
                offset = bis.read(buffer);
            }
            os.flush();
		} catch (Exception e) {
			logger.error("下载文件失败"+e.getMessage(), e);
			throw new RuntimeException("下载文件失败" , e);
		}
    }
}

记录一下几个问题:

  1. SpringBoot 2.1.13.RELEASE 环境下集成了 MinIO 客户端 7.1.0 版本,最新的版本貌似集成不进去

  2. 文件下载也就是 minioClient.getObject(args) 场景下,客户端与 MinIO server 连接默认保持 5 分钟,每隔 15s 由 MinIO server 向客户端发送 keep-alive check,5 分钟后由客户端主动发起关闭连接。

  3. 文件下载时使用流的方式传输,上述代码里使用 try-with-resource 的方式确保 finally 关闭流,但是通过 wireshark 观测貌似直接普通 try-catch 最后也没有造成连接的泄露,应该是底层的 okio 做了优化、5 分钟后直接发起了连接关闭。但作为好习惯还是应该主动去关闭流。