外观
文件存储(nebula-framework-file)
2026-04-01
nebula-framework-file提供了统一的文件存储抽象,支持本地文件系统、MinIO、阿里云 OSS、腾讯云 COS、华为云 OBS、七牛云 Kodo、AWS S3 等多种存储后端,业务代码通过统一接口操作,无需关心底层存储实现。
目录
一、支持的存储类型
StorageType 枚举 | 存储后端 | 对应实现类 |
|---|---|---|
LOCAL | 本地文件系统 | LocalFileStorageService |
MINIO | MinIO 对象存储 | MinioFileStorageService |
ALIYUN | 阿里云 OSS | AliyunOssFileStorageService |
TENCENT | 腾讯云 COS | TencentCosFileStorageService |
HUAWEI | 华为云 OBS | HuaweiObsFileStorageService |
QINIU | 七牛云 Kodo | QiniuKodoFileStorageService |
AWS | AWS S3 | AwsS3FileStorageService |
切换存储后端只需修改配置,业务代码不变。
二、FileStorageService 核心接口
public interface FileStorageService {
// 普通上传
FileUploadResult upload(FileUploadRequest request);
// 分片上传:初始化
MultipartInitResult initiateMultipart(MultipartInitRequest request);
// 分片上传:上传单个分片
MultipartPartResult uploadPart(MultipartPartRequest request);
// 分片上传:完成合并
void completeMultipart(MultipartCompleteRequest request);
// 分片上传:取消(释放资源)
void abortMultipart(String uploadId, String storagePath, String bucketName);
// 下载(返回 InputStream)
InputStream download(String storagePath, String bucketName);
// 生成临时访问 URL(带签名,适用于私有 Bucket)
String generateAccessUrl(String storagePath, String bucketName, Duration expiry);
// 删除
void delete(String storagePath, String bucketName);
// 判断文件是否存在
boolean exists(String storagePath, String bucketName);
// 当前实现的存储类型
StorageType storageType();
}三、普通文件上传
3.1 构建上传请求
@Service
@RequiredArgsConstructor
public class DemoFileService {
private final FileStorageService fileStorageService;
public FileUploadResult uploadFile(MultipartFile file) throws IOException {
FileUploadRequest request = FileUploadRequest.builder()
.inputStream(file.getInputStream()) // 文件流
.originalName(file.getOriginalFilename()) // 原始文件名(用于推断 mimeType)
.size(file.getSize()) // 文件大小(字节)
.mimeType(file.getContentType()) // MIME 类型
.accessPolicy(FileAccessPolicy.PRIVATE) // 访问策略:PUBLIC / PRIVATE
.bizType("demo") // 业务类型(用于生成路径)
.build();
return fileStorageService.upload(request);
}
}3.2 上传结果
public class FileUploadResult {
private String fileId; // 文件唯一 ID(雪花算法)
private String storagePath; // 存储路径(内部使用,不对外暴露)
private String accessUrl; // 访问 URL(PUBLIC 策略时为永久 URL)
private String bucketName; // Bucket 名称(内部使用)
private String etag; // 文件 MD5 / ETag
private String mimeType; // MIME 类型
private long size; // 文件大小(字节)
}重要:
storagePath、bucketName等存储内部字段不应暴露给前端或 RPC 调用方,在对外 VO/DTO 中只返回fileId和accessUrl。
3.3 访问策略说明
FileAccessPolicy | 说明 |
|---|---|
PUBLIC | 文件可直接访问,上传后 accessUrl 为永久 URL |
PRIVATE | 文件受保护,需调用 generateAccessUrl 生成带签名的临时 URL |
四、分片上传(大文件)
大文件(通常 > 10MB)建议使用分片上传,避免超时和占用过多内存。
4.1 分片上传流程
前端 → [初始化] → 获取 uploadId
前端 → [循环上传分片] → 获取每片 ETag
前端 → [完成合并] → 获取最终文件 ID4.2 后端接口实现示例
// 1. 初始化分片上传
public MultipartInitResult initMultipart(String fileName, String mimeType) {
MultipartInitRequest request = MultipartInitRequest.builder()
.originalName(fileName)
.mimeType(mimeType)
.bucketName("your-bucket")
.bizType("demo")
.build();
return fileStorageService.initiateMultipart(request);
}
// 2. 上传单个分片
public MultipartPartResult uploadPart(String uploadId, String storagePath,
String bucketName, int partNumber, InputStream partData, long partSize) {
MultipartPartRequest request = MultipartPartRequest.builder()
.uploadId(uploadId)
.storagePath(storagePath)
.bucketName(bucketName)
.partNumber(partNumber)
.inputStream(partData)
.partSize(partSize)
.build();
return fileStorageService.uploadPart(request);
}
// 3. 完成合并
public void complete(String uploadId, String storagePath, String bucketName,
List<MultipartPartResult> parts) {
MultipartCompleteRequest request = MultipartCompleteRequest.builder()
.uploadId(uploadId)
.storagePath(storagePath)
.bucketName(bucketName)
.parts(parts)
.build();
fileStorageService.completeMultipart(request);
}
// 4. 取消(用户主动取消或异常时清理)
public void abort(String uploadId, String storagePath, String bucketName) {
fileStorageService.abortMultipart(uploadId, storagePath, bucketName);
}五、文件下载
文件下载逻辑必须放在 Service 层,Controller 只做透传(参见后端开发规范):
// Controller:只透传,不处理流
@IgnoreResponseWrap
@GetMapping("/{fileId}/download")
public void download(@PathVariable String fileId, HttpServletResponse response) {
fileService.download(fileId, response);
}
// Service:完整的下载逻辑
public void download(String fileId, HttpServletResponse response) {
// 1. 查询文件元数据
FileMetaDO meta = fileMetaService.getByIdOrThrow(fileId);
// 2. 设置响应头
response.setContentType(meta.getMimeType());
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=" + URLEncoder.encode(meta.getOriginalName(), StandardCharsets.UTF_8));
// 3. 流式写出(使用 StreamUtils.copy,禁止手动 byte[] 循环)
try (InputStream in = fileStorageService.download(meta.getStoragePath(), meta.getBucketName());
OutputStream out = response.getOutputStream()) {
StreamUtils.copy(in, out);
out.flush();
} catch (IOException e) {
throw new SystemException(GlobalErrorCode.INTERNAL_SERVER_ERROR, "文件下载失败", e);
}
}六、访问 URL 生成
对于 PRIVATE 策略的文件,需要通过 generateAccessUrl 生成带签名的临时访问链接:
// 生成 1 小时有效期的临时访问 URL
String tempUrl = fileStorageService.generateAccessUrl(
meta.getStoragePath(),
meta.getBucketName(),
Duration.ofHours(1)
);
// 对于 PUBLIC 策略的文件,上传时 FileUploadResult.accessUrl 即为永久 URL,无需重新生成API 设计建议: 对外 RPC DTO/VO 不应包含
storagePath、bucketName字段,而是提供一个getAccessUrl(fileId)的查询接口,由服务内部决定是返回永久 URL 还是生成临时 URL。
七、文件预览
FilePreviewService 提供文件预览能力(如 PDF 转图片、Office 文档转 PDF 等):
@Autowired
private FilePreviewService filePreviewService;
// 生成预览结果
PreviewResult result = filePreviewService.preview(storagePath, bucketName);
// PreviewResult 包含:
// - type: PreviewType(IMAGE / PDF / OFFICE / VIDEO / AUDIO / UNKNOWN)
// - previewUrl: 预览地址(图片直接访问,文档可能是转换后的 PDF URL)八、配置参考
nebula:
file:
# 默认存储类型
storage-type: MINIO
# MinIO 配置
minio:
endpoint: http://localhost:9000
access-key: minioadmin
secret-key: minioadmin
default-bucket: nebula-files
# 阿里云 OSS 配置
aliyun:
endpoint: oss-cn-hangzhou.aliyuncs.com
access-key-id: your-access-key-id
access-key-secret: your-access-key-secret
default-bucket: nebula-files
# 腾讯云 COS 配置
tencent:
region: ap-shanghai
secret-id: your-secret-id
secret-key: your-secret-key
default-bucket: nebula-files-1234567890
# 华为云 OBS 配置
huawei:
endpoint: obs.cn-east-3.myhuaweicloud.com
access-key: your-access-key
secret-key: your-secret-key
default-bucket: nebula-files
# 七牛云 Kodo 配置
qiniu:
access-key: your-access-key
secret-key: your-secret-key
default-bucket: nebula-files
domain: http://your-domain.com
# AWS S3 配置
aws:
region: us-east-1
access-key-id: your-access-key-id
secret-access-key: your-secret-access-key
default-bucket: nebula-files
# 本地存储配置(开发环境)
local:
root-path: /data/nebula/files
url-prefix: http://localhost:8080/files