基于阿里云OSS与MinIO的文件存储集成方案
一、阿里云OSS对象存储接入流程
完成阿里云账号注册并开通OSS服务后,需创建存储桶(Bucket)并生成访问密钥(AccessKey)。配置信息应准确写入应用配置文件。
aliyun:
oss:
endpoint: https://oss-cn-hangzhou.aliyuncs.com
access-key-id: LTAI5tS9qa9H2uftNY
access-key-secret: VNEIKZfxBpa4HO9YLDx2i
bucket-name: big-event993
配置类定义
使用Spring Boot的属性绑定机制,将配置项映射为可注入的实体类。
AliyunOSSConfig.java
package com.example.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSConfig {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
上传工具类实现
封装OSS客户端操作逻辑,支持按文件类型自动分类存储,并对路径进行安全编码处理。
AliOSSUtil.java
package com.example.util;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.PutObjectRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.InputStream;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
@Component
public class AliOSSUtil {
@Autowired
private AliyunOSSConfig config;
public String uploadFile(String originalName, InputStream stream) throws Exception {
String ext = getFileExtension(originalName);
String dir = classifyByType(ext);
String uniqueId = UUID.randomUUID().toString().substring(0, 4);
String newName = originalName.substring(0, originalName.lastIndexOf('.')) + "-" + uniqueId + originalName.substring(originalName.lastIndexOf('.'));
String objectName = dir + "/" + newName;
OSS client = new OSSClientBuilder().build(config.getEndpoint(), config.getAccessKeyId(), config.getAccessKeySecret());
String url = "";
try {
PutObjectRequest request = new PutObjectRequest(config.getBucketName(), objectName, stream);
client.putObject(request);
String encodedName = URLEncoder.encode(newName, StandardCharsets.UTF_8.toString()).replace("+", "%20");
String encodedPath = objectName.substring(0, objectName.lastIndexOf('/') + 1) + encodedName;
url = "https://" + config.getBucketName() + "." + config.getEndpoint().substring(config.getEndpoint().lastIndexOf("/") + 1) + "/" + encodedPath;
} finally {
client.shutdown();
}
return url;
}
private String getFileExtension(String name) {
int idx = name.lastIndexOf('.');
return idx != -1 ? name.substring(idx + 1).toLowerCase() : "";
}
private String classifyByType(String ext) {
switch (ext) {
case "mp3": case "wav": case "aac": case "ogg": case "flac":
return "audio";
case "mp4": case "avi": case "mov": case "mkv": case "webm":
return "video";
case "png": case "jpg": case "jpeg": case "gif": case "bmp":
return "image";
case "doc": case "pdf": case "txt": case "md":
return "document";
default:
return "others";
}
}
}
控制器接口示例
提供标准HTTP接口用于文件上传与删除。
FileUploadController.java
package com.example.controller;
import com.example.result.ApiResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@Tag(name = "文件管理")
public class FileUploadController {
@Autowired
private AliOSSUtil uploader;
@PostMapping("/upload")
@Operation(summary = "上传至OSS")
public ApiResult<String> upload(@RequestParam("file") MultipartFile file) throws Exception {
String url = uploader.uploadFile(file.getOriginalFilename(), file.getInputStream());
return ApiResult.success(url);
}
@PostMapping("/delete")
@Operation(summary = "删除OSS文件")
public ApiResult<String> delete(@RequestParam("url") String fileUrl) {
try {
// 从URL解析出对象名并删除
String bucket = config.getBucketName();
String path = fileUrl.replace("https://" + bucket + ".", "").substring(1);
uploader.deleteObject(bucket, path);
return ApiResult.success("删除成功");
} catch (Exception e) {
return ApiResult.error("删除失败:" + e.getMessage());
}
}
}
二、MinIO本地化部署与集成
通过Docker快速搭建私有对象存储服务,适用于开发测试或内部系统。
Docker启动命令
docker run -d \
--name minio \
-p 9000:9000 \
-p 9001:9001 \
-v /data/minio:/data \
-e MINIO_ROOT_USER=admin \
-e MINIO_ROOT_PASSWORD=123456 \
--restart always \
minio/minio server /data --console-address ":9001"
配置文件设置
minio:
url: http://localhost:9000
access-key: admin
secret-key: 123456
bucket-name: my-bucket
MinIO配置类
MinIOConfig.java
package com.example.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinIOConfig {
private String url;
private String accessKey;
private String secretKey;
private String bucketName;
}
MinIO工具类实现
MinIOUtil.java
package com.example.util;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.errors.MinioException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.InputStream;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
@Component
public class MinIOUtil {
@Autowired
private MinIOConfig config;
public String uploadFile(String fileName, InputStream stream) throws Exception {
String ext = getFileExtension(fileName);
String dir = classifyByType(ext);
String uniqueId = UUID.randomUUID().toString().substring(0, 4);
String newName = fileName.substring(0, fileName.lastIndexOf('.')) + "-" + uniqueId + fileName.substring(fileName.lastIndexOf('.'));
String objectName = dir + "/" + newName;
MinioClient client = MinioClient.builder()
.endpoint(config.getUrl())
.credentials(config.getAccessKey(), config.getSecretKey())
.build();
String url = "";
try {
PutObjectArgs args = PutObjectArgs.builder()
.bucket(config.getBucketName())
.object(objectName)
.stream(stream, stream.available(), -1)
.build();
client.putObject(args);
String encodedName = URLEncoder.encode(newName, StandardCharsets.UTF_8.toString()).replace("+", "%20");
String encodedPath = objectName.substring(0, objectName.lastIndexOf('/') + 1) + encodedName;
url = config.getUrl() + "/" + config.getBucketName() + "/" + encodedPath;
} finally {
if (stream != null) stream.close();
}
return url;
}
private String getFileExtension(String name) {
int idx = name.lastIndexOf('.');
return idx != -1 ? name.substring(idx + 1).toLowerCase() : "";
}
private String classifyByType(String ext) {
switch (ext) {
case "mp3": case "wav": case "aac": case "ogg": case "flac":
return "audio";
case "mp4": case "avi": case "mov": case "mkv": case "webm":
return "video";
case "png": case "jpg": case "jpeg": case "gif": case "bmp":
return "image";
default:
return "others";
}
}
public void deleteFileByUrl(String fileUrl) throws Exception {
String decoded = URLDecoder.decode(fileUrl, StandardCharsets.UTF_8.toString());
String bucket = config.getBucketName();
String objectName = decoded.substring(decoded.indexOf(bucket) + bucket.length() + 1);
MinioClient client = MinioClient.builder()
.endpoint(config.getUrl())
.credentials(config.getAccessKey(), config.getSecretKey())
.build();
RemoveObjectArgs args = RemoveObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.build();
client.removeObject(args);
}
}
三、配置文件与依赖管理
确保Spring Boot项目正确加载外部配置。
POM依赖声明
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
两种配置读取方式对比
- 推荐方式:使用
@ConfigurationProperties+@Autowired注入配置类,便于维护和类型检查。 - 简化方式:对于少量字段,可直接用
@Value("${minio.url}")获取值,但不建议在复杂场景中使用。