51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

在 Spring Boot 中上传文件到 Minio

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

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

安装 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 {#创建-bucket}

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

创建一个名为 "images" 的 Bucket。

Minio 控制台 - 创建 Bucket

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

为匿名用户设置只读权限 {#为匿名用户设置只读权限}

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

设置 Bucket 的匿名访问权限

Prefix 是资源的前缀匹配。

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

创建 Access Key {#创建-access-key}

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

Minio 创建 Access Key

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

Minio 创建 的 Access Key

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

示例应用 {#示例应用}

创建 Spring Boot 项目 {#创建-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}

创建 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 的更多细节,推荐你阅读 官方文档 进行了解。

赞(3)
未经允许不得转载:工具盒子 » 在 Spring Boot 中上传文件到 Minio