前置准备
- 环境: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);
});
}
}