Minio结合SpringBoot搭建文件服务器

189 阅读5分钟

Minio结合SpringBoot搭建文件服务器

项目地址:宫静雨/microservice_spc - 码云 - 开源中国 (gitee.com)

子工程:minio

本文章先编写了项目代码,然后再配置部署Minio和SpringBoot服务。

如果有需要,可以先直接看使用Docker部署项目,先把Minio部署起来,docker-compose.yaml文件中把springboot项去掉就可以了。

新建SpringBoot项目并配置

配置pom.xml

由于我采用的是父子工程创建的,所以该pom.xml中没有SpringBoot版本。

主要是依赖(dependencies)和build,其他的可以自行选择。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
    <modelVersion>4.0.0</modelVersion>

    <artifactId>minio</artifactId>
    <groupId>com.gjy</groupId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.2.1</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
    </dependencies>

    <build>
        <finalName>minio-service</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.gjy.minio.MinioApp</mainClass>
                    <layout>ZIP</layout>
                    <includeSystemScope>true</includeSystemScope>
                    <addResources>true</addResources>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

配置application.yml

这里的端口号和项目名可以改成自己的。minio的配置主要参考docker-compose.yaml中minio的配置。

server:
  port: 18000
spring:
  application:
    name: minio-service
minio:
  username: minio
  password: minio123
  address: http://192.168.200.162:9000

启动类MinioApp

package com.gjy.minio;

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

@SpringBootApplication
public class MinioApp {

    public static void main(String[] args) {

        SpringApplication.run(MinioApp.class, args);
    }

}

添加配置类

MinioYaml

此类的作用是为了获取配置文件中的值。

package com.gjy.minio.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(prefix = "minio")
@Component
@Data
public class MinioYaml {

    private String address;
    private String username;
    private String password;
}

MinioConfig

package com.gjy.minio.config;

import com.gjy.minio.properties.MinioYaml;
import io.minio.MinioClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

@Configuration
public class MinioConfig {

    private static final Logger log = LoggerFactory.getLogger(MinioConfig.class);

    @Resource
    private MinioYaml minioYaml;

    @Bean(name = "minio")
    public MinioClient minio() {

        log.info("minio yaml: {}", minioYaml);

        return MinioClient.builder()
                .endpoint(minioYaml.getAddress())
                .credentials(minioYaml.getUsername(), minioYaml.getPassword())
                .build();
    }
}

添加实体类

先添加实体类,避免编写服务类的时候报错。

BucketInfo

package com.gjy.minio.domain;

import lombok.Data;

import java.io.Serializable;
import java.time.ZonedDateTime;

@Data
public class BucketInfo implements Serializable {

    private String name;

    private ZonedDateTime creationDate;

}

BucketConvert

package com.gjy.minio.convert;

import com.gjy.minio.domain.BucketInfo;
import io.minio.messages.Bucket;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.stream.Collectors;

@Component(value = "bucketConvert")
public class BucketConvert {

    public List<BucketInfo> convert(List<Bucket> bucket) {
        return bucket.stream()
                .map(b -> {
                    BucketInfo info = new BucketInfo();
                    info.setName(b.name());
                    info.setCreationDate(b.creationDate());
                    return info;
                })
                .collect(Collectors.toList());
    }
}

AjaxResult

package com.gjy.minio.domain;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AjaxResult implements Serializable {

    private Integer code;
    private String message;
    private Object data;

}

FileInfo

package com.gjy.minio.domain;

import lombok.Data;

import java.io.Serializable;

@Data
public class FileInfo implements Serializable {

    private String name;

    private String url;

    public FileInfo() {
    }

    public FileInfo(String name, String url) {
        this.name = name;
        this.url = url;
    }

    public static FileInfo empty() {
        return new FileInfo("", "");
    }
}

FileItem

package com.gjy.minio.domain;

import lombok.Data;

import java.io.Serializable;

@Data
public class FileItem implements Serializable {

    private String name;

    public FileItem() {
    }

    public FileItem(String name) {
        this.name = name;
    }
}

BucketRequest

package com.gjy.minio.domain.vo;

import lombok.Data;

import javax.validation.constraints.NotBlank;
import java.io.Serializable;

@Data
public class BucketRequest implements Serializable {

    @NotBlank
    private String bucket;

    @NotBlank
    private String operation;

    private String type;

}

BucketResponse

package com.gjy.minio.domain.vo;

import lombok.Data;

import java.io.Serializable;

@Data
public class BucketResponse implements Serializable {

    private Object result;

}

添加功能类

MinioService

package com.gjy.minio.service;

import com.gjy.minio.domain.BucketInfo;
import com.gjy.minio.domain.FileInfo;
import com.gjy.minio.domain.FileItem;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.util.List;

public interface MinioService {

    boolean existsBucket(String bucket);

    boolean createBucket(String bucket, String type);

    boolean deleteBucket(String bucket);

    boolean deleteBucketForce(String bucket);

    List<BucketInfo> getBuckets();

    FileInfo uploadFile(String bucket, MultipartFile file);

    List<FileInfo> uploadFile(String bucket, List<MultipartFile> files);

    void downloadFile(String bucket, String filename, HttpServletResponse response);

    FileInfo previewFile(String bucket, String filename);

    boolean deleteFile(String bucket, String filename);

    List<FileItem> getFiles(String bucket);

}

MinioServiceImpl

package com.gjy.minio.service.imp;

import com.gjy.minio.convert.BucketConvert;
import com.gjy.minio.domain.BucketInfo;
import com.gjy.minio.domain.FileInfo;
import com.gjy.minio.domain.FileItem;
import com.gjy.minio.service.MinioService;
import com.google.common.collect.Lists;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Item;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
public class MinioServiceImpl implements MinioService {

    private static final Logger log = LoggerFactory.getLogger(MinioServiceImpl.class);

    private static final String BUCKET_PARAM = "${bucket}";
    /**
     * bucket权限-读写, 可以生产永久可访问的路径
     */
    private static final String READ_WRITE = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucket\",\"s3:ListBucketMultipartUploads\"],\"Resource\":[\"arn:aws:s3:::" + BUCKET_PARAM + "\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:DeleteObject\",\"s3:GetObject\",\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\"],\"Resource\":[\"arn:aws:s3:::" + BUCKET_PARAM + "/*\"]}]}";

    /**
     * bucket权限-只读, 只能生成最多7天的临时访问路径
     */
    private static final String WRITE_ONLY = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:GetBucketLocation\",\"s3:ListBucketMultipartUploads\"],\"Resource\":[\"arn:aws:s3:::" + BUCKET_PARAM + "\"]},{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:ListMultipartUploadParts\",\"s3:PutObject\"],\"Resource\":[\"arn:aws:s3:::" + BUCKET_PARAM + "/*\"]}]}";

    @Resource
    private MinioClient minio;
    @Resource
    private BucketConvert bucketConvert;

    @Override
    public boolean existsBucket(String bucket) {
        try {
            return minio.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
        } catch (Exception e) {
            log.info("判断bucket[{}] 失败: {}", bucket, e.getMessage());
            return false;
        }
    }

    @Override
    public boolean createBucket(String bucket, String type) {
        try {
            minio.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
            if (type.equals("READ_WRITE")) {
                minio.setBucketPolicy(SetBucketPolicyArgs.builder()
                        .bucket(bucket)
                        .config(READ_WRITE.replace(BUCKET_PARAM, bucket))
                        .build());
            } else {
                minio.setBucketPolicy(SetBucketPolicyArgs.builder()
                        .bucket(bucket)
                        .config(WRITE_ONLY.replace(BUCKET_PARAM, bucket))
                        .build());
            }
            return true;
        } catch (Exception e) {
            log.info("创建bucket[{}] 失败: {}", bucket, e.getMessage());
            return false;
        }
    }

    @Override
    public boolean deleteBucket(String bucket) {
        try {
            minio.removeBucket(RemoveBucketArgs.builder().bucket(bucket).build());
            return true;
        } catch (Exception e) {
            log.info("删除bucket[{}] 失败: {}", bucket, e.getMessage());
            return false;
        }
    }

    @Override
    public boolean deleteBucketForce(String bucket) {
        try {
            getFiles(bucket).stream().map(FileItem::getName).forEach(n -> {
                try {
                    minio.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(n).build());
                } catch (Exception e) {
                    log.error("删除bucket[{}], 删除文件[{}], 失败: {}", bucket, n, e.getMessage());
                }
            });
            minio.removeBucket(RemoveBucketArgs.builder().bucket(bucket).build());
            return true;
        } catch (Exception e) {
            log.info("删除bucket[{}] 失败: {}", bucket, e.getMessage());
            return false;
        }
    }

    @Override
    public List<BucketInfo> getBuckets() {
        try {
            return bucketConvert.convert(minio.listBuckets());
        } catch (Exception e) {
            log.error("获取bucket 失败: {}", e.getMessage());
            return Lists.newArrayList();
        }
    }

    @Override
    public FileInfo uploadFile(String bucket, MultipartFile file) {

        try {
            InputStream inputStream = file.getInputStream();
            String originalFilename = file.getOriginalFilename();
            long fileSize = file.getSize();

            String filename = UUID.randomUUID().toString().replaceAll("-", "");

            minio.putObject(PutObjectArgs.builder().bucket(bucket).object(filename).stream(inputStream, fileSize, -1).contentType(file.getContentType()).build());

            return previewFile(bucket, filename);
        } catch (Exception e) {
            return FileInfo.empty();
        }
    }

    @Override
    public List<FileInfo> uploadFile(String bucket, List<MultipartFile> files) {
        return files.stream().map(f -> uploadFile(bucket, f)).collect(Collectors.toList());
    }

    @Override
    public void downloadFile(String bucket, String filename, HttpServletResponse response) {
        try {
            GetObjectResponse object = minio.getObject(GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(filename).build());
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buf = new byte[1024];
            int len;
            while ((len = object.read(buf)) != -1) {
                baos.write(buf, 0, len);
            }
            baos.flush();
            byte[] bytes = baos.toByteArray();

            response.setHeader("Content-Disposition", "attachment;filename=" +
                    URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            ServletOutputStream outputStream = response.getOutputStream();
            outputStream.write(bytes);
            outputStream.flush();
            outputStream.close();
            baos.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public FileInfo previewFile(String bucket, String filename) {
        try {
            String url = minio.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .bucket(bucket).object(filename).method(Method.GET).build());
            return new FileInfo(filename, url);
        } catch (Exception e) {
            log.error("{}", e.getMessage());
            return FileInfo.empty();
        }
    }

    @Override
    public boolean deleteFile(String bucket, String filename) {
        try {
            minio.removeObject(RemoveObjectArgs.builder()
                    .bucket(bucket)
                    .object(filename).build());
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    @Override
    public List<FileItem> getFiles(String bucket) {
        try {
            Iterable<Result<Item>> results = minio.listObjects(ListObjectsArgs.builder().recursive(true).bucket(bucket).build());
            List<FileItem> list = Lists.newArrayList();

            results.forEach(r -> {
                try {
                    Item item = r.get();
                    String name = item.objectName();
                    list.add(new FileItem(name));
                } catch (Exception e) {
                    log.error("获取文件失败: {}", e.getMessage());
                }
            });
            return list;
        } catch (Exception e) {
            return Lists.newArrayList();
        }

    }

}

MinioController

package com.gjy.minio.controller;

import com.gjy.minio.domain.AjaxResult;
import com.gjy.minio.domain.BucketInfo;
import com.gjy.minio.domain.FileInfo;
import com.gjy.minio.domain.vo.BucketRequest;
import com.gjy.minio.domain.vo.BucketResponse;
import com.gjy.minio.service.MinioService;
import com.google.common.collect.Lists;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.List;

@RestController
@RequestMapping("/minio")
public class MinioController {

    @Resource
    private MinioService minioService;

    private static final List<String> BUCKET_OPERATIONS = Lists.newArrayList("exists", "create", "delete", "delete_force", "get");

    @PostMapping("/bucket")
    public AjaxResult bucket(@RequestBody BucketRequest request) {
        AjaxResult r;
        BucketResponse response = new BucketResponse();
        boolean bucket;
        switch (request.getOperation()) {
            case "exists":
                bucket = minioService.existsBucket(request.getBucket());
                response.setResult(bucket);
                r = AjaxResult.builder().code(200).message("OK").data(response).build();
                break;
            case "create":
                bucket = minioService.createBucket(request.getBucket(), request.getType());
                response.setResult(bucket);
                r = AjaxResult.builder().code(200).message("OK").data(response).build();
                break;
            case "delete":
                bucket = minioService.deleteBucket(request.getBucket());
                response.setResult(bucket);
                r = AjaxResult.builder().code(200).message("OK").data(response).build();
                break;
            case "delete_force":
                bucket = minioService.deleteBucketForce(request.getBucket());
                response.setResult(bucket);
                r = AjaxResult.builder().code(200).message("OK").data(response).build();
                break;
            case "get":
                List<BucketInfo> buckets = minioService.getBuckets();
                response.setResult(buckets);
                r = AjaxResult.builder().code(200).message("OK").data(response).build();
                break;
            default:
                r = AjaxResult.builder().code(400).message("参数错误,operator仅支持: " + BUCKET_OPERATIONS).build();
        }
        return r;
    }

    @PostMapping("/upload")
    public AjaxResult uploadFile(@RequestPart String bucket,
                                 @RequestPart MultipartFile file) {
        FileInfo info = minioService.uploadFile(bucket, file);
        return AjaxResult.builder().code(200).message("OK").data(info).build();
    }

    @PostMapping("/uploads")
    public AjaxResult uploadFiles(@RequestPart String bucket,
                                  @RequestPart List<MultipartFile> files) {
        List<FileInfo> info = minioService.uploadFile(bucket, files);
        return AjaxResult.builder().code(200).message("OK").data(info).build();
    }

    @PostMapping("/preview")
    public AjaxResult previewFile(@RequestParam String bucket,
                                  @RequestParam String filename) {
        FileInfo info = minioService.previewFile(bucket, filename);
        return AjaxResult.builder().code(200).message("OK").data(info).build();
    }

    @PostMapping("/delete")
    public AjaxResult deleteFile(@RequestParam String bucket,
                                 @RequestParam String filename) {
        boolean info = minioService.deleteFile(bucket, filename);
        return AjaxResult.builder().code(200).message("OK").data(info).build();
    }

    @PostMapping("/download")
    public void downloadFile(@RequestParam String bucket,
                             @RequestParam String filename,
                             HttpServletResponse response) {
        minioService.downloadFile(bucket, filename, response);
    }

}

使用Docker部署项目

编写Dockerfile

此处的minio-service.jar是在pom.xml中build的finalName配置的,可以更改为自己喜欢的名称。

FROM openjdk:8
ADD minio-service.jar /minio-service.jar
EXPOSE 18000
ENTRYPOINT ["java","-jar","/minio-service.jar"]

编写docker-compose.yaml

version: '3.8'
services:
  minio:
    image: minio/minio
    ports:
      - "9000:9000"
      - "9090:9090"
    volumes:
      - /data/minio:/data
    environment:
      - MINIO_ACCESS_KEY=minio
      - MINIO_SECRET_KEY=minio123
    command: server /data --console-address ":9090" -address ":9000"
  springboot:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "18000:18000"
    depends_on:
      - minio

执行

  1. 将Dockerfile,docker-compose.yaml,minio-service.jar上传至服务器

  2. 执行命令启动项目

    // 后台运行

    docker-compose up -d

  3. 停止项目

    docker-compsoe down

  4. 如果想改了SpringBoot项目文件,需要先删除minio-server镜像

    docker rmi minio-service-springboot:latest

  5. 不用担心minio服务关掉会丢失数据,前提是在服务器新建目录 /data/minio

    mkdir -p /data/minio

测试用例

可以在Gitee上下载查看

minio/minio-service-test.html · 宫静雨/microservice_spc - 码云 - 开源中国 (gitee.com)

附录

虚拟机安装CentOS

可以参考这篇文章

VMware安装CentOS7 - 掘金 (juejin.cn)