在 Spring Boot 中上传文件到 Minio

Minio 是一个用 Golang 开发的开源的对象存储服务器,它基于 Amazon S3 协议,提供了简单而强大的存储解决方案。可以在本地部署或云环境中使用。也支持分布式部署,并具有高可用性和容错性。

本文将会带你了解如何在 Linux 中通过 Docker 的方式来安装、配置 Minio,以及如何在 Spring Boot 应用中通过 Minio 官方 SDK 上传文件资源到 Minio 服务器。

安装 Minio

在 Linux 下,使用 Docker 的方式安装 Minio 最简单。首先确保你在服务器上安装了 Docker,并且需要 root 用户来执行下面的安装过程。

首先,创建存放文件资源的目录:

mkdir -p ~/minio/data

上述命令在 $HOME 目录下创建了 /minio/data 文件夹,用于存放资源。

接着,使用 Docker 运行 Minio 容器:

docker run \
   -d \
   -p 9000:9000 \
   -p 9090:9090 \
   --name minio-server \
   -v ~/minio/data:/data \
   -e "MINIO_ROOT_USER=admin" \
   -e "MINIO_ROOT_PASSWORD=minio858896" \
   quay.io/minio/minio server /data --console-address ":9090"
  • -d:以守护进程的形式启动容器。
  • -p 9000:9000:指定了 Minio 资源的访问端口(也是 API 端口)。
  • -p 9090:9090:指定了管理控制台的访问端口。
  • --name minio-server:指定了容器的名称。
  • -v ~/minio/data:/data:挂载主机上的资源目录到容器的 /data 目录。
  • -e "MINIO_ROOT_USER=admin":指定了管理控制台的用户名。
  • -e "MINIO_ROOT_PASSWORD=minio858896":指定了管理控制台的密码(这里为了演示,故意设置得很简单)。

注意,用户名和密码有安全要求,用户名最低 3 个字符长度,密码最低 8 个字符串长度。否则会提示异常:

ERROR Unable to validate credentials inherited from the shell environment: Invalid credentials
     > Please provide correct credentials
     HINT:
       Access key length should be at least 3, and secret key length at least 8 characters

为了更接近实际应用,本文专门解析了一个域名到 Minio 服务器:oss.springboot.io。本文接下来就会使用这个域名来进行资源访问、后台管理和 API 调用!

你如果没有域名,直接使用 ip 也是没任何问题的。

登录控制台

安装就绪后,使用浏览器访问登录页:http://oss.springboot.io:9090/login。然后使用安装时设置的用户名和密码进行登录。

Minio 登录页面

创建 Bucket

进入管理页面后,点击左侧 “Buckets” 按钮,进入 Bucket 管理面板。

创建一个名为 “images” 的 Bucket。

Minio 控制台 - 创建 Bucket

Bucket 就是存储对象资源的基本单位,你可以简单理解为系统中的 “文件夹”。

为匿名用户设置只读权限

接着,点击刚创建的 Bucket,进入 “Anonymous” 配置。为匿名用户添加 “readonly” 权限。

设置 Bucket 的匿名访问权限

Prefix 是资源的前缀匹配。

默认情况下,Bucket 是私有的,匿名用户无法访问其中的资源。

创建 Access Key

点击左侧 “Access Keys” 菜单,生成一个新的 Access Key。

Minio 创建 Access Key

生成后,点击 “Create” 保存 Access Key。

Minio 创建 的 Access Key

千万要注意保存这俩 Key,因为这是你最后一次可以看到 Secret Key 的值了。

示例应用

创建 Spring Boot 项目

添加 Minio 官方的 SDK 依赖 minio

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/io.minio/minio -->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.7</version>
</dependency>

配置上传信息

application.yaml 中配置 Access Key 等信息:

app:
  minio:
    # 访问资源的 URL
    base-url: "http://oss.springboot.io:9000/"
    # API 端点
    endpoint: "http://oss.springboot.io:9000/"
    # 上传的 Bucket
    bucket: images
    # Access Key
    access-key: Umt2UtK5vp7njhM4BFjP
    # Secret Key
    secret-key: S3ZJayIxxv3AZfkyCitmrksugzrABbYGJQ4v8OGB

如上,在配置文件中指定了访问文件的URL、API 端点地址、要上传到哪个 Bucket 以及在 Minio 控制台生成的 Access Key 和 Secret Key。

UploadController

创建 UploadController,实现 /upload API。

接收客户端上传的资源文件,进行基本的校验后通过 Minio SDK 上传到 Minio 服务器,最后返回访问地址。

package cn.springdoc.demo.web.controller;

import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import io.minio.MinioClient;
import io.minio.PutObjectArgs;

@RestController
@RequestMapping("/upload")
public class UploadController {

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

    // 日期格式化
    static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("/yyy/MM/dd/");

    // 资源的 访问 URL
    @Value("${app.minio.base-url}")
    private String baseUrl;

    // API 端点
    @Value("${app.minio.endpoint}")
    private String endpoint;

    // Bucket 存储桶
    @Value("${app.minio.bucket}")
    private String bucket;

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

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

    /**
     * 上传文件到 Minio 服务器,返回访问地址
     * @param file
     * @return
     * @throws Exception
     */
    @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<String> upload(@RequestParam("file") MultipartFile file) throws Exception{
        
        // 文件大小
        long size = file.getSize();
        if (size == 0) {
            return ResponseEntity.badRequest().body("禁止上传空文件");
        }
        
        // 文件名称
        String fileName = file.getOriginalFilename();
        
        // 文件后缀
        String ext = "";
        
        int index = fileName.lastIndexOf(".");
        if (index ==-1) {
            return ResponseEntity.badRequest().body("禁止上传无后缀的文件");
        }
        
        ext = fileName.substring(index);

        // 文件类型
        String contentType = file.getContentType();
        if (contentType == null) {
            contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        }
        
        // 根据日期打散目录,使用 UUID 重命名文件
        String filePath = formatter.format(LocalDate.now()) + 
                        UUID.randomUUID().toString().replace("-", "") + 
                        ext;
        
        log.info("文件名称:{}", fileName);
        log.info("文件大小:{}", size);
        log.info("文件类型:{}", contentType);
        log.info("文件路径:{}", filePath);
        
        // 实例化客户端
        MinioClient client = MinioClient.builder()
                .endpoint(this.endpoint)
                .credentials(this.accessKey, this.secretKey)
                .build();

        
        // 上传文件到客户端
        try (InputStream inputStream = file.getInputStream()){
            client.putObject(PutObjectArgs.builder()
                    .bucket(this.bucket)		// 指定 Bucket 
                    .contentType(contentType)	// 指定 Content Type
                    .object(filePath)			// 指定文件的路径
                    .stream(inputStream, size, -1) // 文件的 Inputstream 流
                    .build());
        }
        
        
        // 返回最终的访问路径
        return ResponseEntity.ok(this.baseUrl + this.bucket + filePath);
    }
}

通过 @Value 注解,把配置文件中的属性值注入到 Controller 成员变量中。

在上传方法中,首先校验了上传的文件。不允许上大小为 0 和无后缀的文件。然后根据日期 /yyy/MM/dd/ 格式打散目录,并且使用 UUID 重命名文件防止同名文件覆盖。

然后使用端点 API 地址、accessKey 和 secretKey 参数构建 MinioClient 实例。

调用 MinioClientputObject 方法进行上传,通过 PutObjectArgs Builder 构建上传参数,其中指定了要上传的 Bucket、文件的媒体类型、文件的保存路径以及文件的 InputStream

如果没有发生异常,则上传成功。最后,拼接完整的访问路径,返回给客户端(文件的访问路径包含了 Bucket 名称)。

测试

启动服务器,使用 Postman 上传图片文件(图片文件是 “Spring 中文网” 的 Logo - 512 x 512):

Postman 上传图片资源

上传成功,返回资源的访问地址如下:

http://oss.springboot.io:9000/images/2023/11/13/090c2b7daa20457c8cfdfbb1cc32009b.png

接着,尝试在浏览器中访问上传的资源。

Spring 中文网 Logo

一切 OK。

日志

最后附上客户端的请求日志:

POST /upload HTTP/1.1
Accept-Language: en_US
User-Agent: PostmanRuntime/7.29.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 7f213edc-7506-4652-bdc5-f783823fc978
Host: localhost:8080
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--------------------------815963622984394675083411
Content-Length: 20029
 
----------------------------815963622984394675083411
Content-Disposition: form-data; name="file"; filename="512.png"
<512.png>
----------------------------815963622984394675083411--
 
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 84
Date: Mon, 13 Nov 2023 10:30:09 GMT
Keep-Alive: timeout=60
Connection: keep-alive
 
http://oss.springboot.io:9000/images/2023/11/13/090c2b7daa20457c8cfdfbb1cc32009b.png

以及服务端的日志:

INFO 13068 --- [nio-8080-exec-5] c.s.d.web.controller.UploadController    : 文件名称:512.png
INFO 13068 --- [nio-8080-exec-5] c.s.d.web.controller.UploadController    : 文件大小:19825
INFO 13068 --- [nio-8080-exec-5] c.s.d.web.controller.UploadController    : 文件类型:image/png
INFO 13068 --- [nio-8080-exec-5] c.s.d.web.controller.UploadController    : 文件路径:/2023/11/13/090c2b7daa20457c8cfdfbb1cc32009b.png

最后

Minio 的功能十分强大,不仅仅是基本的对象存储,还包括数据加密、访问控制、版本管理等等功能。是代替 FastDFS 的理想选择。

得益于官方提供的 SDK,可以很轻松地在应用中完成整合,只需要几行代码就能实现文件的上传。当然,SDK 提供的功能不仅限于此,还包括 Bucket 管理、存储对象管理等等。

对于 Minio 和 SDK 的更多细节,推荐你阅读 官方文档 进行了解。