【Spring Content】Spring Content Minio(S3)的文件上传下载

21 阅读2分钟

前置准备

  • 环境:JDK17
  • SpringBoot:SpringBoot3
  • SpringContent:3.0.9

依赖引入

注意:fs(本地文件系统)和S3的starter只能设置1个,否则启动时会报错!

引入fs的stater

<dependency>
    <groupId>com.github.paulcwarren</groupId>
    <artifactId>spring-content-s3-boot-starter</artifactId>
    <version>3.0.9</version>
</dependency>

实现

开启SpringContent包扫描

启动类添加@EnableS3Stores注解,标记要扫描的包。

注意:fs(本地文件系统)的注解是@EnableFilesystemStores,两者不能同时存在!

@EnableS3Stores(basePackages = "org.evaltool.*")

配置MinioS3Config

配置Minio的访问地址,用户名密码,地区

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3Configuration;

import java.net.URI;

@Configuration
public class MinioS3Config {
    /**
     * 配置minio
     */
    @Bean
    public S3Client s3Client() {
        return S3Client.builder()
                .endpointOverride(URI.create("http://localhost:9000"))
                .credentialsProvider(
                        StaticCredentialsProvider.create(
                                AwsBasicCredentials.create("minioadmin", "minioadmin")
                        )
                )
                .region(Region.CN_NORTH_1)
                .serviceConfiguration(
                        S3Configuration.builder()
                                .pathStyleAccessEnabled(true)
                                .build()
                )
                .build();
    }
}

定义文件实体对象

  • @ContentId:存储文件时的唯一编号
  • @ContentLength:文件的大小
import lombok.Data;
import org.springframework.content.commons.annotations.ContentId;
import org.springframework.content.commons.annotations.ContentLength;

/**
 * spring content文件定义
 */
@Data
public class FileDocument {
    @ContentId
    private String id;
    private String fileName;
    @ContentLength
    private long fileSize;
    private String fileExt;
}

定义Store接口

自定义接口继承自ContentStore。

ContentStore 是 Spring Content 中的核心接口,用于定义内容存储和检索的标准操作。它提供了一套统一的 API 来处理各种类型的内容存储后端。

import org.evaltool.tool.domain.FileDocument;
import org.springframework.content.commons.store.ContentStore;
import org.springframework.stereotype.Repository;

/**
 * 本地文件存储器
 */
@Repository
public interface LocalFileStore extends ContentStore<FileDocument, String> {
}

AppService中使用Store

import lombok.extern.slf4j.Slf4j;
import org.evaltool.evalengine.common.utils.NanoIdUtils;
import org.evaltool.tool.domain.FileDocument;
import org.evaltool.tool.store.LocalFileStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;

@Slf4j
@Service
public class FileStorageAppService {
    @Autowired
    private LocalFileStore fileStore;

    /**
     * 上传文件
     */
    public String storeFile(MultipartFile file) {
        String original = file.getOriginalFilename();
        String ext = StringUtils.getFilenameExtension(original);
        if (original == null || original.contains(".."))
            throw new RuntimeException("非法文件名");
        FileDocument doc = new FileDocument();
        String nanoId = NanoIdUtils.random();
        doc.setId(nanoId);
        doc.setFileName(original);
        doc.setFileExt(ext);
        doc.setFileSize(file.getSize());
        try (InputStream in = file.getInputStream()) {
            fileStore.setContent(doc, in);
        } catch (IOException e) {
            throw new RuntimeException("存储文件失败", e);
        }
        return doc.getId() + "." + ext;
    }

    /**
     * 下载文件
     */
    public Resource loadFile(String fileName) {
        String ext = StringUtils.getFilenameExtension(fileName);
        ext = ext != null ? ext : "";
        String fileId = StringUtils.replace(fileName, "." + ext, "");
        FileDocument doc = new FileDocument();
        doc.setId(fileId);
        doc.setFileName(fileName);
        doc.setFileExt(ext);
        InputStream content = fileStore.getContent(doc);
        if (content == null) {
            throw new IllegalArgumentException("File not found");
        }
        return new InputStreamResource(content);
    }
}

(可选操作)保存的文件携带后缀

SpringContent默认存储的文件名等于id,不带后缀。如果要携带后缀,需要修改配置,添加Converter来增加后缀。

注意:

  • S3只能使用s3StorePlacementService
  • Minio不能动态创建通,需要提前创建
package org.evaltool.tool.config;

import org.apache.commons.lang3.StringUtils;
import org.evaltool.evalengine.common.utils.DateUtils;
import org.evaltool.tool.domain.FileDocument;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.content.s3.S3ObjectId;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.ConverterRegistry;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.BucketAlreadyExistsException;
import software.amazon.awssdk.services.s3.model.BucketAlreadyOwnedByYouException;

@Configuration
public class ContentStoreConfig {
    @Autowired
    private S3Client s3Client;

    /**
     * Spring Content默认使用id作为文件名不满足要求, 需要给文件添加扩展名
     */
    @Autowired
    public void configureConverter(@Qualifier("s3StorePlacementService") ConverterRegistry registry) {
        // s3对象
        registry.addConverter(FileDocument.class, S3ObjectId.class, doc -> {
            String ext = doc.getFileExt();
            String key = StringUtils.isEmpty(ext)
                    ? doc.getId()
                    : doc.getId() + "." + ext.toLowerCase();
            // 动态建桶
            String bucket = "docs-" + DateUtils.getNowDateStr("yyyyMM");
            try {
                s3Client.createBucket(b -> b.bucket(bucket));
            } catch (BucketAlreadyExistsException | BucketAlreadyOwnedByYouException ignore) {
            }
            return new S3ObjectId(bucket, key);
        });
    }
}