1.文件存储思路
支持将文件上传到三类存储器:
- 兼容 S3 协议的对象存储:支持 MinIO、腾讯云 COS、七牛云 Kodo、华为云 OBS、亚马逊 S3 等等。
- 磁盘存储:本地、FTP 服务器、SFTP 服务器。
- 数据库存储:MySQL、Oracle、PostgreSQL、SQL Server 等等。
技术选型?
- 优先,✔ 推荐方案 1。如果无法使用云服务,可以自己搭建一个 MinIO 服务。
- 其次,推荐方案 3。数据库的主从机制可以实现高可用,备份也方便,少量小文件问题不大。
- 最后,× 不推荐方案 2。主要是实现高可用比较困难,无法实现故障转移。
2.MinIo服务
1. 创建存储目录
命令:
mkdir E:\youkeProject\Minio\data -Force
说明:
- 作用:在 E:\youkeProject\Minio\data 创建用于存储 MinIO 数据的目录,-Force 确保即使目录已存在也不会报错。
- 注意事项 :
- 确保 E:\ 磁盘存在且有足够空间(建议至少预留几 GB,视存储需求而定)。
- 磁盘需有写入权限,当前用户必须能访问 E:\youkeProject。
- 错误排查 :
- 错误 :mkdir : Access is denied(权限不足)
- 解决:以管理员身份运行 PowerShell(右键 PowerShell,选择"以管理员身份运行")。
- 验证:运行 whoami 检查用户是否为管理员(如 NT AUTHORITY\SYSTEM 或 yourdomain\admin)。
- 错误 :mkdir : The device is not ready(磁盘不可用)
- 解决:检查 E:\ 是否挂载(运行 Get-Disk 或 Get-Volume),确保磁盘可用。
- 验证:运行 Test-Path E:\,返回 True 表示路径有效。
- 错误 :mkdir : Access is denied(权限不足)
验证:
Test-Path E:\youkeProject\Minio\data
- 预期输出:True
2. 下载 MinIO 可执行文件
命令:
Invoke-WebRequest -Uri "https://mirrors.tuna.tsinghua.edu.cn/minio/releases/windows-amd64/minio.exe" -OutFile "E:\youkeProject\Minio\minio.exe"
说明:
- 作用:从清华大学镜像源下载 MinIO 的 Windows 可执行文件,保存到 E:\youkeProject\Minio\minio.exe。
- 注意事项 :
- 确保网络连接稳定,清华大学镜像源通常比官方源更快。
- 确认系统为 64 位 Windows(minio.exe 为 windows-amd64 架构)。
- 下载路径 E:\youkeProject\Minio 必须存在且可写。
- 错误排查 :
- 错误 :Invoke-WebRequest : The remote server returned an error: (404) Not Found
- 解决:镜像源 URL 可能失效,尝试官方源:
Invoke-WebRequest -Uri "https://dl.min.io/server/minio/release/windows-amd64/minio.exe" -OutFile "E:\youkeProject\Minio\minio.exe"
- 验证:检查 URL 是否可访问(在浏览器中打开)。
- 解决:镜像源 URL 可能失效,尝试官方源:
- 错误 :Invoke-WebRequest : Access is denied
- 解决:以管理员身份运行 PowerShell,或检查 E:\youkeProject\Minio 目录权限:
icacls "E:\youkeProject\Minio" /grant "Users:(W)"
- 解决:以管理员身份运行 PowerShell,或检查 E:\youkeProject\Minio 目录权限:
- 错误 :网络连接失败
- 解决:检查网络(运行 ping mirrors.tuna.tsinghua.edu.cn),或使用代理(如有):
$env:HTTP_PROXY="http://proxy:port" $env:HTTPS_PROXY="http://proxy:port"
- 解决:检查网络(运行 ping mirrors.tuna.tsinghua.edu.cn),或使用代理(如有):
- 错误 :Invoke-WebRequest : The remote server returned an error: (404) Not Found
验证:
Test-Path E:\youkeProject\Minio\minio.exe
- 预期输出:True
- 检查文件版本:
(Get-Item E:\youkeProject\Minio\minio.exe).VersionInfo
3. 设置环境变量
命令:
setx MINIO_ROOT_USER "admin" /M setx MINIO_ROOT_PASSWORD "password123" /M
说明:
- 作用:设置 MinIO 的管理员用户名和密码为系统环境变量,/M 表示设置系统级变量(需要管理员权限)。
- 注意事项 :
- MINIO_ROOT_PASSWORD 必须至少 8 位,建议包含大小写字母、数字和符号,例如 P@ssw0rd123。
- 环境变量在当前 PowerShell 会话中不会立即生效,需重启 PowerShell 或系统。
- 运行命令后,变量会存储在注册表(HKLM\System\CurrentControlSet\Control\Session Manager\Environment)。
- 错误排查 :
- 错误 :setx : Access is denied
- 解决:以管理员身份运行 PowerShell。
- 验证:运行 whoami 确认用户为管理员。
- 错误 :密码不符合要求
- 解决:确保密码长度 ≥ 8 位,包含复杂字符:
setx MINIO_ROOT_PASSWORD "P@ssw0rd123" /M
- 解决:确保密码长度 ≥ 8 位,包含复杂字符:
- 问题 :环境变量未生效
- 解决:重启 PowerShell 或运行以下命令刷新:
$env:MINIO_ROOT_USER = [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_USER", "Machine") $env:MINIO_ROOT_PASSWORD = [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_PASSWORD", "Machine")
- 解决:重启 PowerShell 或运行以下命令刷新:
- 错误 :setx : Access is denied
验证:
[System.Environment]::GetEnvironmentVariable("MINIO_ROOT_USER", "Machine") [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_PASSWORD", "Machine")
- 预期输出:admin 和 P@ssw0rd123(或您设置的密码)
4. 注册为 Windows 服务
命令:
# 创建服务
sc.exe create MinIO binPath= "E:\youkeProject\Minio\minio.exe server E:\youkeProject\Minio\data --console-address :9001" start= auto displayname= "MinIO Object Storage"
# 启动服务
sc start MinIO
说明:
- 作用 :
- sc.exe create:创建名为 MinIO 的 Windows 服务,指定可执行文件路径和参数。
- binPath:运行 MinIO 服务器,数据目录为 E:\youkeProject\Minio\data,Web 控制台端口为 9001。
- start= auto:服务随系统启动自动运行。
- displayname:服务在服务管理器中的显示名称。
- sc start:启动 MinIO 服务。
- 注意事项 :
- 必须以管理员身份运行 sc.exe。
- 确保 E:\youkeProject\Minio\minio.exe 和 E:\youkeProject\Minio\data 路径正确。
- 端口 9001(控制台)和 9000(API)不能被占用。
- 错误排查 :
- 错误 :sc.exe : [SC] CreateService FAILED 5: Access is denied
- 解决:以管理员身份运行 PowerShell。
- 错误 :sc.exe : [SC] CreateService FAILED 1053: The service did not respond
- 解决:检查 binPath 是否正确,路径或文件可能有误:
Test-Path E:\youkeProject\Minio\minio.exe Test-Path E:\youkeProject\Minio\data
- 验证:手动运行命令检查错误:
E:\youkeProject\Minio\minio.exe server E:\youkeProject\Minio\data --console-address :9001
- 解决:检查 binPath 是否正确,路径或文件可能有误:
- 错误 :sc start : [SC] StartService FAILED 1057: The account name is invalid
- 解决:确保服务以正确账户运行,默认使用 LocalSystem:
sc.exe config MinIO obj= LocalSystem
- 解决:确保服务以正确账户运行,默认使用 LocalSystem:
- 错误 :端口被占用
- 解决:检查端口 9000 和 9001:
netstat -ano | findstr ":9000" netstat -ano | findstr ":9001"
- 如果占用,修改端口(例如 --console-address :9002)并重新创建服务:
sc.exe delete MinIO sc.exe create MinIO binPath= "E:\youkeProject\Minio\minio.exe server E:\youkeProject\Minio\data --console-address :9002" start= auto displayname= "MinIO Object Storage"
- 解决:检查端口 9000 和 9001:
- 错误 :sc.exe : [SC] CreateService FAILED 5: Access is denied
验证:
Get-Service MinIO | Select-Object Name, Status, StartType
- 预期输出:
Name Status StartType ---- ------ --------- MinIO Running Automatic
5. 验证安装
步骤:
- 访问 Web 管理界面 :
- 打开浏览器,访问 http://localhost:9001。
- 使用用户名 admin 和密码(例如 P@ssw0rd123)登录。
- 登录成功后,应看到 MinIO 的 Web 控制台,可管理存储桶和文件。
- 检查服务状态 :
Get-Service MinIO
- 预期输出:Status 为 Running。
错误排查:
- 问题 :无法访问 http://localhost:9001
- 解决:
- 确认服务是否运行:
Get-Service MinIO
- 检查端口是否监听:
netstat -ano | findstr ":9001"
- 如果端口未监听,尝试手动启动:
E:\youkeProject\Minio\minio.exe server E:\youkeProject\Minio\data --console-address :9001
- 检查防火墙是否阻止访问(见步骤 6)。
- 确认服务是否运行:
- 解决:
- 问题 :登录失败
- 解决:
- 确认环境变量:
[System.Environment]::GetEnvironmentVariable("MINIO_ROOT_USER", "Machine") [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_PASSWORD", "Machine")
- 如果密码错误,重新设置:
setx MINIO_ROOT_PASSWORD "P@ssw0rd123" /M
- 重启服务:
sc stop MinIO sc start MinIO
- 确认环境变量:
- 解决:
- 问题 :服务未启动
- 解决:
- 查看服务日志:
Get-EventLog -LogName System -Newest 100 | Where-Object { $_.Source -eq "Service Control Manager" -and $_.Message -like "*MinIO*" } | Format-List
- 检查 MinIO 日志(如果有文件输出):
Get-Content -Path "E:\youkeProject\Minio\logs\minio.log" -Tail 10
- 查看服务日志:
- 解决:
6. 配置防火墙规则(可选)
命令:
New-NetFirewallRule -DisplayName "MinIO Console" -Direction Inbound -Protocol TCP -LocalPort 9001 -Action Allow New-NetFirewallRule -DisplayName "MinIO API" -Direction Inbound -Protocol TCP -LocalPort 9000 -Action Allow
说明:
- 作用:开放 MinIO 的控制台端口(9001)和 API 端口(9000),允许外部访问。
- 注意事项 :
- 仅当需要从其他机器访问 MinIO 时执行。
- 确保防火墙规则不会影响其他服务。
- 错误排查 :
- 错误 :New-NetFirewallRule : Access is denied
- 解决:以管理员身份运行 PowerShell。
- 问题 :规则未生效
-
解决:检查防火墙状态:
Get-NetFirewallProfile | Select-Object Name, Enabled
-
启用防火墙(如果禁用): powershell
Copy
Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True
-
验证规则: powershell
Copy
Get-NetFirewallRule -DisplayName "MinIO*" | Format-Table Name, DisplayName, Enabled, Direction, Action
-
- 错误 :New-NetFirewallRule : Access is denied
验证:
- 从另一台机器访问 http://<your-ip>:9001 或 http://<your-ip>:9000。
- 检查防火墙规则:
Get-NetFirewallRule -DisplayName "MinIO*" | Format-List
7. 服务管理命令
命令:
# 停止服务 sc stop MinIO
# 重启服务 sc restart MinIO
# 删除服务(卸载时使用) sc delete MinIO
说明:
- 作用 :
- sc stop:停止 MinIO 服务。
- sc restart:重启服务(等效于 stop 后 start)。
- sc delete:移除 MinIO 服务(用于卸载)。
- 注意事项 :
- 所有命令需以管理员身份运行。
- 删除服务前确保服务已停止:
sc query MinIO
- 错误排查 :
- 错误 :sc stop : [SC] ControlService FAILED 1062: The service has not been started
- 解决:确认服务状态:
Get-Service MinIO
- 如果已停止,无需再次停止。
- 解决:确认服务状态:
- 错误 :sc delete : [SC] DeleteService FAILED 1072: The specified service has been marked for deletion
- 解决:等待几秒或重启系统后重试,或者手动删除注册表项(谨慎操作):
Remove-Item -Path "HKLM:\System\CurrentControlSet\Services\MinIO" -Force
- 解决:等待几秒或重启系统后重试,或者手动删除注册表项(谨慎操作):
- 错误 :sc stop : [SC] ControlService FAILED 1062: The service has not been started
验证:
- 停止后:
Get-Service MinIO | Select-Object Status
- 预期输出:Stopped
- 删除后:
Get-Service MinIO -ErrorAction SilentlyContinue
- 预期输出:无结果(服务不存在)
8. 使用 MinIO 客户端(mc)验证(可选)
步骤:
- 安装 mc 客户端 :
Invoke-WebRequest -Uri "https://dl.min.io/client/mc/release/windows-amd64/mc.exe" -OutFile "C:\Windows\mc.exe"
- 配置别名 :
mc alias set myminio http://localhost:9000 admin P@ssw0rd123
- 列出存储桶 :
mc ls myminio
错误排查:
- 错误 :mc: Unable to initialize new alias
- 解决:确认 MinIO 服务运行,端口 9000 可访问,用户名和密码正确。
- 验证:
Test-NetConnection -ComputerName localhost -Port 9000
- 错误 :mc: No such file or directory
- 解决:确保 mc.exe 在 PATH 中:
setx PATH "%PATH%;C:\Windows" /M
- 解决:确保 mc.exe 在 PATH 中:
验证:
- 预期输出示例:
[2025-05-18 16:00:00 JST] 0B mybucket/
9. 手动启动 MinIO(调试用)
命令:
E:\youkeProject\Minio\minio.exe server E:\youkeProject\Minio\data --console-address :9001

说明:
- 作用:在命令行手动启动 MinIO,用于调试或临时运行。
- 注意事项 :
- 需保持命令行窗口打开,关闭窗口会停止 MinIO。
- 确保环境变量 MINIO_ROOT_USER 和 MINIO_ROOT_PASSWORD 已设置。
- 错误排查 :
- 错误 :ERROR Unable to validate credentials
- 解决:检查环境变量:
$env:MINIO_ROOT_USER $env:MINIO_ROOT_PASSWORD
- 手动指定:
$env:MINIO_ROOT_USER="admin" $env:MINIO_ROOT_PASSWORD="P@ssw0rd123"
- 解决:检查环境变量:
- 错误 :ERROR Unable to use the drive
- 解决:检查 E:\youkeProject\Minio\data 权限:
icacls "E:\youkeProject\Minio\data" /grant "Users:(F)"
- 解决:检查 E:\youkeProject\Minio\data 权限:
- 错误 :ERROR Unable to validate credentials
验证:
- 访问 http://localhost:9001,登录控制台。
- 检查日志输出,确认启动成功。
10.Java交互
pom:
java
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>minio-file-manager</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>minio-file-manager</name>
<description>Spring Boot MinIO File Management System</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.13</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml:
java
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
minio:
endpoint: http://127.0.0.1:9000
access-key: admin
secret-key: password123
bucket-name: bucketone
config:
java
@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.access-key}")
private String accessKey;
@Value("${minio.secret-key}")
private String secretKey;
@Value("${minio.bucket-name}")
private String bucketName;
@Bean
public MinioClient minioClient() throws Exception {
// 创建 MinIO 客户端
MinioClient minioClient = MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
// 检查存储桶是否存在,不存在则创建
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
return minioClient;
}
}
comtroller:
java
// 标记为 Spring REST 控制器,返回 JSON 响应
@RestController
// 设置控制器基础路径,所有端点以 /files 开头
@RequestMapping("/files")
public class FileController {
// 声明 MinIO 文件服务依赖,通过构造函数注入
private final MinioFileService minioFileService;
// 构造函数,注入 MinioFileService 实例
public FileController(MinioFileService minioFileService) {
this.minioFileService = minioFileService;
}
// 处理文件上传请求,POST /files/upload
@PostMapping("/upload")
// 接收上传的文件,绑定请求中的 file 字段,抛出异常由全局处理器处理
public String uploadFile(@RequestParam("file") MultipartFile file) throws Exception {
// 调用服务层上传文件,返回生成的文件名
return minioFileService.uploadFile(file);
}
// 处理文件下载请求,GET /files/download/{fileName}
@GetMapping("/download/{fileName}")
// 接收路径中的文件名,返回文件流响应
public ResponseEntity<InputStreamResource> downloadFile(@PathVariable String fileName) throws Exception {
// 调用服务层获取文件输入流
InputStream inputStream = minioFileService.downloadFile(fileName);
// 构建 HTTP 响应,设置下载头和二进制流
return ResponseEntity.ok()
// 设置 Content-Disposition 头,提示浏览器下载文件
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
// 设置内容类型为通用二进制流
.contentType(MediaType.APPLICATION_OCTET_STREAM)
// 包装输入流为响应体
.body(new InputStreamResource(inputStream));
}
// 处理文件删除请求,DELETE /files/{fileName}
@DeleteMapping("/{fileName}")
// 接收路径中的文件名,删除文件并返回提示
public String deleteFile(@PathVariable String fileName) throws Exception {
// 调用服务层删除文件
minioFileService.deleteFile(fileName);
// 返回删除成功的提示消息
return "File deleted: " + fileName;
}
// 处理文件列表请求,GET /files/list
@GetMapping("/list")
// 返回存储桶中的所有文件名列表
public List<String> listFiles() throws Exception {
// 调用服务层获取文件列表
return minioFileService.listFiles();
}
}
service:
java
// 标记为 Spring 服务组件,封装 MinIO 文件操作
@Service
public class MinioFileService {
// 声明 MinIO 客户端实例,用于与 MinIO 服务器交互
private final MinioClient minioClient;
// 从配置文件注入存储桶名称(如 application.yml 中的 minio.bucket-name)
@Value("${minio.bucket-name}")
private String bucketName;
// 构造函数,注入 MinioClient 实例
public MinioFileService(MinioClient minioClient) {
this.minioClient = minioClient;
}
// 上传文件到 MinIO 存储桶
public String uploadFile(MultipartFile file) throws Exception {
// 生成唯一文件名:时间戳 + 原始文件名,防止冲突
String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();
// 调用 MinIO 客户端上传文件
minioClient.putObject(
// 配置上传参数
PutObjectArgs.builder()
// 指定存储桶
.bucket(bucketName)
// 指定对象名(文件名)
.object(fileName)
// 设置文件流、大小和分片阈值(-1 表示自动分片)
.stream(file.getInputStream(), file.getSize(), -1)
// 设置文件 MIME 类型
.contentType(file.getContentType())
.build()
);
// 返回上传后的文件名
return fileName;
}
// 从 MinIO 下载文件,返回输入流
public InputStream downloadFile(String fileName) throws Exception {
// 调用 MinIO 客户端获取文件流
return minioClient.getObject(
// 配置下载参数
GetObjectArgs.builder()
// 指定存储桶
.bucket(bucketName)
// 指定对象名(文件名)
.object(fileName)
.build()
);
}
// 从 MinIO 删除指定文件
public void deleteFile(String fileName) throws Exception {
// 调用 MinIO 客户端删除文件
minioClient.removeObject(
// 配置删除参数
RemoveObjectArgs.builder()
// 指定存储桶
.bucket(bucketName)
// 指定对象名(文件名)
.object(fileName)
.build()
);
}
// 列出存储桶中的所有文件名
public List<String> listFiles() throws Exception {
// 创建文件名列表
List<String> fileNames = new ArrayList<>();
// 调用 MinIO 客户端列出存储桶中的对象
Iterable<Result<Item>> results = minioClient.listObjects(
// 配置列表参数
ListObjectsArgs.builder().bucket(bucketName).build()
);
// 迭代对象列表,提取文件名
for (Result<Item> result : results) {
fileNames.add(result.get().objectName());
}
// 返回文件名列表
return fileNames;
}
}
错误排查总结
以下是常见错误及其解决方法的汇总:
错误 | 可能原因 | 解决方法 |
---|---|---|
mkdir : Access is denied | 权限不足 | 以管理员身份运行 PowerShell。 |
Invoke-WebRequest : 404 Not Found | 镜像源失效 | 使用官方源或检查 URL。 |
setx : Access is denied | 非管理员运行 | 以管理员身份运行 PowerShell。 |
sc.exe : CreateService FAILED 1053 | binPath 错误 | 验证路径和文件存在,尝试手动运行命令。 |
sc start : StartService FAILED 1057 | 服务账户无效 | 设置服务账户为 LocalSystem。 |
无法访问 http://localhost:9001 | 服务未启动/端口被占 | 检查服务状态、端口占用,开放防火墙规则。 |
登录失败 | 用户名/密码错误 | 验证环境变量,重新设置密码并重启服务。 |
mc: Unable to initialize alias | 服务不可达 | 确认 MinIO 运行,端口 9000 可访问。 |
完整验证流程
- 检查目录和文件 :
Test-Path E:\youkeProject\Minio\minio.exe Test-Path E:\youkeProject\Minio\data
- 检查环境变量 :
[System.Environment]::GetEnvironmentVariable("MINIO_ROOT_USER", "Machine") [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_PASSWORD", "Machine")
- 检查服务状态 :
Get-Service MinIO | Select-Object Name, Status, StartType
- 访问 Web 界面 :
- 浏览器打开 http://localhost:9001,登录。
- 使用 mc 客户端 :
mc alias set myminio http://localhost:9000 admin P@ssw0rd123 mc ls myminio
注意事项
- 管理员权限:所有命令需在管理员权限的 PowerShell 中运行。
- 密码安全:MINIO_ROOT_PASSWORD 需满足 ≥8 位,包含大小写、数字和符号,避免弱密码(如 password123)。
- 端口冲突:检查 9000 和 9001 端口是否被占用,若冲突可修改为其他端口(如 9002)。
- 日志查看 :
- MinIO 默认日志可能在 E:\youkeProject\Minio\logs\minio.log:
Get-Content -Path "E:\youkeProject\Minio\logs\minio.log" -Tail 10
- 检查 Windows 事件日志(如果配置了事件日志输出):
Get-WinEvent -LogName System -MaxEvents 100 | Where-Object { $_.ProviderName -like "*minio*" } | Format-List
- MinIO 默认日志可能在 E:\youkeProject\Minio\logs\minio.log:
- 备份数据:定期备份 E:\youkeProject\Minio\data 中的数据,避免意外丢失。
总结
通过以上步骤,您可以在 Windows 系统上成功安装并启动 MinIO 作为服务,数据存储在 E:\youkeProject\Minio\data,Web 控制台通过 http://localhost:9001 访问。
3.系统实现


3.1文件上传代码实现
3.1.1 方式一:前端上传
前端代码(Vue 组件):
-
文件:InfraFile.vue 和头像上传组件(未命名,假设为 AvatarUpload.vue)。
-
功能 :
- 文件列表展示:显示文件列表(el-table),支持搜索(路径、创建时间)、分页、删除。
- 文件上传:通过 <el-upload> 组件实现文件上传,支持拖拽、类型限制(.jpg, .png, .gif)。
- 头像上传:使用 vue-cropper 裁剪图片后上传。
-
关键代码 :
-
文件上传 :
javascript<el-upload ref="upload" :limit="1" accept=".jpg, .png, .gif" :auto-upload="false" :headers="upload.headers" :action="upload.url" :data="upload.data" :on-change="handleFileChange" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess"> <i class="el-icon-upload"></i> <div class="el-upload__text">将文件拖到此处,或 <em>点击上传</em></div> <div class="el-upload__tip" style="color:red" slot="tip">提示:仅允许导入 jpg、png、gif 格式文件!</div> </el-upload>
- 作用:用户选择文件后,点击"确定"按钮触发上传,发送 POST /admin-api/infra/file/upload 请求。
- 配置 :
- action:上传接口 URL(/admin-api/infra/file/upload)。
- headers:携带认证 token(Authorization: Bearer xxx)。
- data:附加参数(如路径)。
- on-success:处理上传成功的响应,显示 URL 并刷新列表。
-
头像上传 :
javascript<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload"> <el-button size="small">选择<i class="el-icon-upload el-icon--right"></i></el-button> </el-upload>
- 作用:选择图片后裁剪,上传到 /admin-api/system/user/avatar。
- 配置 :
- http-request:自定义上传逻辑,调用 uploadAvatar API。
- before-upload:验证文件类型(必须为图片)。
- uploadImg:将裁剪后的图片作为 FormData 上传。
-
-
后端代码(FileController.java) :
javascript@PostMapping("/upload") @Operation(summary = "上传文件") public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception { MultipartFile file = uploadReqVO.getFile(); String path = uploadReqVO.getPath(); return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()))); }
- 作用:接收前端上传的 MultipartFile 和路径参数,调用 fileService.createFile 存储文件,返回文件 URL。
- 流程 :
- 解析 uploadReqVO 获取文件和路径。
- 使用 IoUtil.readBytes 转换文件流为字节数组。
- 调用 fileService.createFile 上传文件到 MinIO,返回 URL。
- 封装响应为 CommonResult<String>。
- 方法详解
- 文件上传:createFile 方法实现文件的上传,接收文件名、路径和内容,上传到存储器并保存元数据到数据库,返回文件访问 URL。
- 文件客户端管理 :
- getMasterFileClient:获取主文件客户端(FileClient),从缓存中加载。
- clientCache:缓存文件客户端,支持异步刷新,减少数据库查询。
- 文件客户端工厂 :
- FileClientFactoryImpl:管理文件客户端的创建和更新,基于配置动态生成客户端实例(如 S3FileClient、LocalFileClient)。
- 支持多种存储器,通过 FileStorageEnum 映射存储类型到具体客户端类。
3.1.2调用关系
- 外部调用:createFile 方法由上层(如 FileController 或其他服务)调用,用于上传文件。
- 内部调用 :
- createFile 调用 fileConfigService.getMasterFileClient 获取客户端。
- getMasterFileClient 通过 clientCache 获取缓存的 FileClient。
- clientCache 的 load 方法调用 fileConfigService 和 fileClientFactory 创建或更新客户端。
- FileClientFactoryImpl 的 createOrUpdateFileClient 和 getFileClient 方法管理客户端实例。
- 依赖 :
- FileConfigService:提供存储配置查询。
- FileClientFactory:创建和管理文件客户端。
- FileMapper:操作数据库,保存文件元数据。
3.1.3主要类和接口
- FileServiceImpl:文件服务实现类,包含 createFile 和 getMasterFileClient。
- FileClient:文件客户端接口,定义上传、删除、获取内容等方法。
- FileClientFactoryImpl:文件客户端工厂,动态创建客户端。
- AbstractFileClient:抽象文件客户端,提供通用逻辑。
- FileConfigDO:文件配置实体,存储存储器配置。
- FileDO:文件元数据实体,存储文件名、路径、URL 等。
3.1.4代码逐行分析与调用链
3.1.4.1 FileServiceImpl.createFile
@Override @SneakyThrows public String createFile(String name, String path, byte[] content) {
- 作用:上传文件到存储器,保存元数据到数据库,返回文件 URL。
- 入参 :
- name:文件名(如 image.jpg)。
- path:存储路径(如 /avatars/image.jpg)。
- content:文件内容(字节数组)。
- 返回值:String,文件访问 URL(如 http://minio.example.com/mybucket/avatars/image.jpg)。
- 注解 :
- @Override:实现 FileService 接口的 createFile 方法。
- @SneakyThrows:Lombok 注解,简化异常处理,抛出 Exception。
- 调用者:FileController(前端上传)、其他服务(如后端上传)。
- 调用链:FileController.uploadFile -> FileServiceImpl.createFile。
String type = FileTypeUtils.getMineType(content, name);
- 作用:获取文件的 MIME 类型(如 image/jpeg)。
- 逻辑:使用 FileTypeUtils(可能是 Hutool 或自定义工具)分析文件内容和名称。
- 调用:FileTypeUtils.getMineType(静态方法)。
- 示例:name="image.jpg", content 为 JPEG 数据,返回 image/jpeg。
if (StrUtil.isEmpty(path)) { path = FileUtils.generatePath(content, name); }
- 作用:如果 path 为空,生成默认存储路径。
- 逻辑 :
- StrUtil.isEmpty:Hutool 工具,检查 path 是否为空。
- FileUtils.generatePath:生成路径,可能基于文件名或日期(如 /2025/05/image.jpg)。
- 调用:FileUtils.generatePath(静态方法)。
- 示例:name="image.jpg", 返回 /2025/05/image.jpg。
if (StrUtil.isEmpty(name)) { name = path; }
- 作用:如果 name 为空,使用 path 作为文件名。
- 逻辑:确保文件名有效,避免空值。
- 示例:name="", path="/avatars/image.jpg", 设置 name="/avatars/image.jpg"。
FileClient client = fileConfigService.getMasterFileClient();
- 作用:获取主文件客户端(FileClient),用于上传文件。
- 调用:FileConfigService.getMasterFileClient,实际调用 FileServiceImpl.getMasterFileClient。
- 返回值:FileClient 实例(如 S3FileClient)。
- 调用链:createFile -> getMasterFileClient -> clientCache.getUnchecked。
Assert.notNull(client, "客户端(master) 不能为空");
- 作用:校验客户端是否为空,若为空抛出异常。
- 逻辑:Spring 的 Assert 工具,确保 client 有效。
- 示例:若 client=null,抛出 IllegalArgumentException: 客户端(master) 不能为空。
String url = client.upload(content, path, type);
- 作用:调用文件客户端上传文件,返回文件 URL。
- 调用:FileClient.upload,由具体实现(如 S3FileClient)处理。
- 入参 :
- content:文件内容。
- path:存储路径。
- type:MIME 类型。
- 返回值:文件 URL。
- 示例:上传 image.jpg,返回 http://minio.example.com/mybucket/avatars/image.jpg。
FileDO file = new FileDO();
- 作用:创建文件元数据实体,准备保存到数据库。
- 逻辑:FileDO 是文件表对应的实体类,包含配置 ID、名称、路径等字段。
file.setConfigId(client.getId());
- 作用:设置文件配置 ID。
- 调用:FileClient.getId,返回客户端的配置 ID。
- 示例:client.getId() 返回 1(主配置 ID)。
file.setName(name); file.setPath(path); file.setUrl(url); file.setType(type); file.setSize(content.length);
- 作用:设置文件元数据,包括文件名、路径、URL、类型和大小。
- 示例 :
- name="image.jpg"
- path="/avatars/image.jpg"
- url="http://minio.example.com/mybucket/avatars/image.jpg"
- type="image/jpeg"
- size=102400(100KB)
fileMapper.insert(file);
- 作用:将文件元数据插入数据库。
- 调用:FileMapper.insert,MyBatis 的 Mapper 方法。
- 逻辑:保存 FileDO 到 infra_file 表。
- 示例:插入记录,生成自增 ID。
return url;
- 作用:返回文件 URL,供调用者使用。
- 示例:返回 http://minio.example.com/mybucket/avatars/image.jpg。
3.1.4.2 FileServiceImpl.getMasterFileClient
@Override public FileClient getMasterFileClient() {
- 作用:获取主文件客户端,从缓存中加载。
- 返回值:FileClient 实例。
- 调用者:createFile 方法。
- 调用链:createFile -> getMasterFileClient -> clientCache.getUnchecked。
return clientCache.getUnchecked(CACHE_MASTER_ID);
- 作用:从缓存获取主客户端,CACHE_MASTER_ID 表示主配置 ID(通常为固定值,如 0)。
- 调用:LoadingCache.getUnchecked,Guava 缓存方法。
- 逻辑:若缓存命中,直接返回;若未命中,调用缓存的 load 方法。
- 示例:返回 S3FileClient 实例。
3.1.4.3 FileServiceImpl.clientCache
@Getter private final LoadingCache<Long, FileClient> clientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),
- 作用:定义文件客户端缓存,支持异步刷新。
- 字段 :
- clientCache:Guava 的 LoadingCache,键为配置 ID,值为 FileClient。
- Duration.ofSeconds(10L):缓存刷新间隔为 10 秒。
- 逻辑:缓存避免频繁查询数据库或创建客户端。
- 调用:buildAsyncReloadingCache
- 调用者:getMasterFileClient
new CacheLoader<Long, FileClient>() {
- 作用:定义缓存加载器,当缓存未命中时加载 FileClient。
- 逻辑:实现 load 方法,动态创建客户端。
@Override public FileClient load(Long id) {
- 作用:加载指定 ID 的文件客户端。
- 入参:id,配置 ID(CACHE_MASTER_ID 表示主配置)。
- 返回值:FileClient 实例。
- 调用者:clientCache.getUnchecked。
FileConfigDO config = Objects.equals(CACHE_MASTER_ID, id) ? fileConfigMapper.selectByMaster() : fileConfigMapper.selectById(id);
- 作用:查询存储配置。
- 逻辑 :
- 若 id 是 CACHE_MASTER_ID,调用 fileConfigMapper.selectByMaster 获取主配置。
- 否则,调用 fileConfigMapper.selectById 获取指定 ID 的配置。
- 调用 :
- fileConfigMapper.selectByMaster:MyBatis 查询主配置。
- fileConfigMapper.selectById:MyBatis 查询指定配置。
- 示例:返回 FileConfigDO(包含 endpoint、bucket 等)。
if (config != null) { fileClientFactory.createOrUpdateFileClient(config.getId(), config.getStorage(), config.getConfig()); }
- 作用:若配置存在,创建或更新文件客户端。
- 调用:FileClientFactory.createOrUpdateFileClient。
- 入参 :
- config.getId():配置 ID。
- config.getStorage():存储类型(如 S3、本地)。
- config.getConfig():存储配置(如 MinIO 的 endpoint、accessKey)。
- 示例:创建 S3FileClient。
return fileClientFactory.getFileClient(null == config ? id : config.getId());
- 作用:获取文件客户端。
- 调用:FileClientFactory.getFileClient。
- 逻辑 :
- 若 config 为空,使用原始 id。
- 否则,使用 config.getId()。
- 示例:返回 S3FileClient。
3.1.4.4 FileClientFactoryImpl
@Slf4j public class FileClientFactoryImpl implements FileClientFactory {
- 作用:文件客户端工厂实现类,管理客户端的创建和更新。
- 注解 :
- @Slf4j:Lombok 注解,提供日志记录器(log)。
- 调用者:clientCache.load。
private final ConcurrentMap<Long, AbstractFileClient<?>> clients = new ConcurrentHashMap<>();
- 作用:存储文件客户端实例,键为配置 ID,值为客户端。
- 逻辑:ConcurrentHashMap 确保线程安全。
@Override public FileClient getFileClient(Long configId) {
- 作用:获取指定配置 ID 的文件客户端。
- 入参:configId,配置 ID。
- 返回值:FileClient 实例。
- 调用者:clientCache.load。
AbstractFileClient<?> client = clients.get(configId);
- 作用:从 clients 映射获取客户端。
- 示例:configId=1,返回 S3FileClient。
if (client == null) { log.error("[getFileClient][配置编号({}) 找不到客户端]", configId); }
- 作用:若客户端不存在,记录错误日志。
- 示例:configId=999,日志输出 [getFileClient][配置编号(999) 找不到客户端]。
return client;
- 作用:返回客户端实例(可能为 null)。
- 示例:返回 S3FileClient。
@Override @SuppressWarnings("unchecked") public <Config extends FileClientConfig> void createOrUpdateFileClient(Long configId, Integer storage, Config config) {
- 作用:创建或更新文件客户端。
- 入参 :
- configId:配置 ID。
- storage:存储类型(如 S3、本地)。
- config:存储配置(泛型,子类如 S3FileClientConfig)。
- 调用者:clientCache.load。
- 注解:@SuppressWarnings("unchecked") 抑制类型转换警告。
AbstractFileClient<Config> client = (AbstractFileClient<Config>) clients.get(configId);
- 作用:尝试从 clients 获取现有客户端。
- 逻辑:强制类型转换为 AbstractFileClient<Config>。
if (client == null) { client = this.createFileClient(configId, storage, config); client.init(); clients.put(client.getId(), client);
- 作用:若客户端不存在,创建新客户端。
- 逻辑 :
- 调用 createFileClient 创建客户端。
- 调用 client.init 初始化客户端(如连接 MinIO)。
- 将客户端存入 clients 映射。
- 调用 :
- createFileClient
- AbstractFileClient.init
} else { client.refresh(config); }
- 作用:若客户端存在,刷新配置。
- 调用:AbstractFileClient.refresh。
- 示例:更新 MinIO 的 endpoint 或 accessKey。
@SuppressWarnings("unchecked") private <Config extends FileClientConfig> AbstractFileClient<Config> createFileClient( Long configId, Integer storage, Config config) {
- 作用:创建文件客户端实例。
- 入参 :
- configId:配置 ID。
- storage:存储类型。
- config:存储配置。
- 返回值:AbstractFileClient<Config>。
- 调用者:createOrUpdateFileClient。
FileStorageEnum storageEnum = FileStorageEnum.getByStorage(storage);
- 作用:根据存储类型获取枚举值。
- 调用:FileStorageEnum.getByStorage(静态方法)。
- 示例:storage=1,返回 FileStorageEnum.S3。
Assert.notNull(storageEnum, String.format("文件配置(%s) 为空", storageEnum));
- 作用:校验存储类型是否有效。
- 示例:若 storageEnum=null,抛出 IllegalArgumentException: 文件配置(null) 为空。
return (AbstractFileClient<Config>) ReflectUtil.newInstance(storageEnum.getClientClass(), configId, config);
- 作用:创建客户端实例。
- 调用 :
- FileStorageEnum.getClientClass:获取客户端类(如 S3FileClient.class)。
- ReflectUtil.newInstance:Hutool 工具,通过反射创建实例。
- 逻辑:调用客户端构造函数,传入 configId 和 config。
- 示例:storageEnum=S3,创建 S3FileClient。
3.1.4.5 整体调用流程
- 外部调用 :
- FileController.uploadFile 调用 FileServiceImpl.createFile,传递文件名、路径和内容。
- 文件上传(createFile) :
- 计算 MIME 类型(FileTypeUtils.getMineType)。
- 生成默认路径(FileUtils.generatePath)。
- 获取主客户端(getMasterFileClient)。
- 上传文件(FileClient.upload)。
- 保存元数据(FileMapper.insert)。
- 返回 URL。
- 获取客户端(getMasterFileClient) :
- 从 clientCache 获取客户端(clientCache.getUnchecked)。
- 缓存加载(clientCache.load) :
- 查询配置(fileConfigMapper.selectByMaster 或 selectById)。
- 创建或更新客户端(FileClientFactory.createOrUpdateFileClient)。
- 返回客户端(FileClientFactory.getFileClient)。
- 客户端工厂(FileClientFactoryImpl) :
- getFileClient:从 clients 获取客户端。
- createOrUpdateFileClient:
- 若客户端不存在,调用 createFileClient 创建。
- 若存在,调用 refresh 更新。
- createFileClient:通过反射创建客户端实例(如 S3FileClient)。
3.1.4.6 每一步作用总结
代码部分 作用 调用关系 createFile 上传文件,保存元数据,返回 URL 调用 getMasterFileClient, FileClient.upload, FileMapper.insert getMasterFileClient 获取主文件客户端 调用 clientCache.getUnchecked clientCache 缓存文件客户端,支持异步刷新 调用 load(查询配置,创建客户端) clientCache.load 加载客户端,查询配置并创建 调用 fileConfigMapper, fileClientFactory.createOrUpdateFileClient FileClientFactoryImpl.getFileClient 获取客户端实例 从 clients 获取 createOrUpdateFileClient 创建或更新客户端 调用 createFileClient, AbstractFileClient.init/refresh createFileClient 通过反射创建客户端 调用 FileStorageEnum, Refl
- 前端主导:用户通过浏览器界面选择文件,触发 HTTP 请求,上传过程由前端组件控制(<el-upload>)。
- 后端辅助:后端仅负责接收文件、存储到 MinIO、返回 URL,不主动发起上传。
- 特点:上传流程由前端用户交互驱动,适合需要用户选择文件的场景(如上传头像、文档)。
3.1.5方式二:后端上传
后端代码:
- 文件:FileApiImpl.java 和 FileServiceImpl.java。
- 功能 :
- 提供文件管理 API,包括创建、删除、查询文件内容和生成签名 URL。
- 支持直接通过字节数组上传文件(无需 MultipartFile)。
- 关键代码 :
-
FileApiImpl :
java/** * 文件 API 实现类 */ @Service @Validated public class FileApiImpl implements FileApi { @Resource private FileService fileService; @Override public String createFile(String name, String path, byte[] content) { return fileService.createFile(name, path, content); } }
- 作用:实现 FileApi 接口,提供 createFile 方法,接收文件名、路径和字节数组,调用 fileService 上传文件。
-
FileServiceImpl :
java@Override @SneakyThrows public String createFile(String name, String path, byte[] content) { // 计算默认的 path 名 String type = FileTypeUtils.getMineType(content, name); if (StrUtil.isEmpty(path)) { path = FileUtils.generatePath(content, name); } // 如果 name 为空,则使用 path 填充 if (StrUtil.isEmpty(name)) { name = path; } // 上传到文件存储器 FileClient client = fileConfigService.getMasterFileClient(); Assert.notNull(client, "客户端(master) 不能为空"); String url = client.upload(content, path, type); // 保存到数据库 FileDO file = new FileDO(); file.setConfigId(client.getId()); file.setName(name); file.setPath(path); file.setUrl(url); file.setType(type); file.setSize(content.length); fileMapper.insert(file); return url; }
- 作用:处理文件上传逻辑,保存文件到 MinIO,记录元数据到数据库,返回 URL。
- 流程 :
- 推断文件类型(FileTypeUtils.getMineType)。
- 生成默认路径(如果未提供)。
- 获取 MinIO 客户端(FileClient),上传文件(client.upload)。
- 保存文件元数据(FileDO)到数据库。
- 返回文件 URL。
-
解析:
- 后端主导:上传由后端代码或服务调用触发,文件内容以字节数组形式传入,无需用户通过浏览器选择文件。
- 特点:适合服务器端批量处理、自动上传或从其他来源(如本地文件、URL 下载)获取文件的场景。
- 无前端交互:不依赖浏览器,直接由后端 API 或服务调用。
3.1.6. 回答问题
前端上传和后端上传的区别
- 触发方式 :
- 前端上传 :由用户通过浏览器界面(<el-upload>、裁剪组件)选择文件,触发 HTTP 请求。
- 场景:用户上传头像、文档、图片等。
- 示例:用户点击"上传文件"按钮,发送 POST /admin-api/infra/file/upload。
- 后端上传 :由后端代码或服务调用触发,文件内容以字节数组形式传入,通常从服务器本地、数据库或其他来源获取。
- 场景:批量导入文件、服务器端文件处理、从 URL 下载并上传。
- 示例:后端从本地磁盘读取文件,调用 fileService.createFile。
- 前端上传 :由用户通过浏览器界面(<el-upload>、裁剪组件)选择文件,触发 HTTP 请求。
- 文件来源 :
- 前端上传 :文件来自用户设备(通过浏览器选择)。
- 示例:用户选择 C:\test.jpg。
- 后端上传 :文件来自服务器环境(如本地文件、远程 URL、数据库)。
- 示例:后端读取 /tmp/test.jpg 或从 URL 下载文件。
- 前端上传 :文件来自用户设备(通过浏览器选择)。
- 请求格式 :
- 前端上传 :使用 multipart/form-data 格式,文件通过 MultipartFile 传输。
- 示例:form-data: file=(binary), path=/avatars/.
- 后端上传 :文件以字节数组(byte[])传入,通常通过内部方法调用或 API(如 FileApi.createFile)。
- 示例:createFile("test.jpg", "/avatars/", fileBytes)。
- 前端上传 :使用 multipart/form-data 格式,文件通过 MultipartFile 传输。
- 用户交互 :
- 前端上传 :需要用户交互(如选择文件、点击上传),前端提供界面和反馈(如进度条、成功提示)。
- 示例:<el-upload> 显示上传进度,成功后弹出 上传成功。
- 后端上传 :无用户交互,后端自动处理,适合后台任务。
- 示例:定时任务批量上传文件。
- 前端上传 :需要用户交互(如选择文件、点击上传),前端提供界面和反馈(如进度条、成功提示)。
- 适用场景 :
- 前端上传:用户驱动的场景,如个人中心上传头像、文件管理系统上传文档。
- 后端上传:系统驱动的场景,如服务器迁移文件、API 集成、自动化脚本。
命名原因:
- 前端上传:被称为"前端上传",因为上传流程由前端用户交互发起,文件通过前端组件(如 <el-upload>)选择并发送到后端。后端仅处理请求,核心动作(文件选择、触发上传)发生在前端。
- 后端上传:被称为"后端上传",因为上传由后端代码或服务主动调用,文件来源和上传逻辑完全由后端控制,无需前端参与。核心动作(文件读取、上传)发生在后端。
3.2 文件下载
java
// 标记为 Spring REST 控制器,返回 JSON 或文件流响应
@RestController
// 设置日志记录器
@Slf4j
public class FileController {
// 处理文件下载请求,GET /admin-api/infra/file/{configId}/get/**
@GetMapping("/{configId}/get/**")
// 允许未认证用户访问
@PermitAll
// Swagger 文档:接口描述
@Operation(summary = "下载文件")
// Swagger 文档:参数描述
@Parameter(name = "configId", description = "配置编号", required = true)
// 接收请求、响应和配置 ID,处理文件下载
public void getFileContent(HttpServletRequest request,
HttpServletResponse response,
@PathVariable("configId") Long configId) throws Exception {
// 从请求 URI 中提取文件路径(/get/ 之后的部分)
String path = StrUtil.subAfter(request.getRequestURI(), "/get/", false);
// 校验路径是否为空,若为空抛出异常
if (StrUtil.isEmpty(path)) {
throw new IllegalArgumentException("结尾的 path 路径必须传递");
}
// 解码路径,解决中文路径的编码问题
path = URLUtil.decode(path);
// 调用文件服务获取文件内容
byte[] content = fileService.getFileContent(configId, path);
// 若文件不存在,记录警告日志并返回 404 状态
if (content == null) {
log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path);
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
// 返回文件内容作为附件
FileTypeUtils.writeAttachment(response, path, content);
}
}
// Hutool 的 StrUtil 工具类,提供字符串操作
class StrUtil {
// 从字符串中提取指定分隔符后的子串
public static String subAfter(CharSequence string, CharSequence separator, boolean isLastSeparator) {
// 若输入字符串为空,返回空或 null
if (isEmpty(string)) {
return null == string ? null : "";
} else if (separator == null) {
// 若分隔符为空,返回空字符串
return "";
} else {
// 转换为字符串
String str = string.toString();
String sep = separator.toString();
// 根据 isLastSeparator 决定使用最后一个或第一个分隔符
int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep);
// 若找到分隔符且不是字符串末尾,返回分隔符后的子串
return -1 != pos && string.length() - 1 != pos ? str.substring(pos + separator.length()) : "";
}
}
// 检查字符串是否为空(Hutool 工具方法)
public static boolean isEmpty(CharSequence str) {
return str == null || str.length() == 0;
}
}
// 文件服务接口实现类
class FileServiceImpl {
// 获取文件内容
@Override
public byte[] getFileContent(Long configId, String path) throws Exception {
// 获取指定配置 ID 的文件客户端
FileClient client = fileConfigService.getFileClient(configId);
// 校验客户端是否存在
Assert.notNull(client, "客户端({}) 不能为空", configId);
// 调用客户端获取文件内容
return client.getContent(path);
}
}
// Servlet 工具类,提供响应处理方法
class ServletUtils {
// 将文件内容作为附件写入响应
public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
// 设置 Content-Disposition 头,指定文件名(UTF-8 编码)
response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtil.encodeUtf8(filename));
// 获取文件 MIME 类型
String contentType = FileTypeUtils.getMineType(content, filename);
// 设置响应内容类型
response.setContentType(contentType);
// 针对视频文件的特殊处理,解决移动端播放兼容性
if (StrUtil.containsIgnoreCase(contentType, "video")) {
response.setHeader("Content-Length", String.valueOf(content.length - 1));
response.setHeader("Content-Range", String.valueOf(content.length - 1));
response.setHeader("Accept-Ranges", "bytes");
}
// 将文件内容写入响应输出流
IoUtil.write(response.getOutputStream(), false, content);
}
}
3.3 文件客户端
java
// 定义文件客户端接口,抽象文件操作方法,支持多种存储器(如 S3、本地磁盘、数据库等)
public interface FileClient {
/**
* 获取客户端编号
*
* @return 客户端编号,用于标识存储配置(如 MinIO、S3 的配置 ID)
*/
Long getId();
/**
* 上传文件到存储器
*
* @param content 文件内容,字节数组形式
* @param path 相对路径,指定文件在存储器中的位置(如 "/avatars/image.jpg")
* @return 完整路径,即文件的 HTTP 访问地址(如 "http://minio.example.com/mybucket/avatars/image.jpg")
*/
String upload(byte[] content, String path);
/**
* 从存储器删除指定文件
*
* @param path 相对路径,指定要删除的文件位置(如 "/avatars/image.jpg")
*/
void delete(String path);
/**
* 获取存储器中指定文件的内容
*
* @param path 相对路径,指定要读取的文件位置(如 "/avatars/image.jpg")
* @return 文件内容,字节数组形式,若文件不存在可能返回 null 或抛出异常
*/
byte[] getContent(String path);
}
4.前端直传S3存储
1. S3 存储和前端直传 S3 的解释
1.1 什么是 S3 存储?
- 定义:S3(Simple Storage Service)是 Amazon 提供的一种对象存储服务,广泛用于存储文件(如图片、视频、文档)。七牛云、阿里云 OSS、腾讯云 COS 等提供了 S3 兼容的存储服务,允许使用类似 AWS S3 的 API 操作文件。
- 核心概念 :
- Bucket:存储文件的容器,类似文件夹,名称全局唯一。
- Object:存储的文件,每个对象有唯一的 Key(路径,如 avatars/image.jpg)。
- URL 访问:文件通过 HTTP URL 访问(如 http://bucket.qiniucs.com/avatars/image.jpg)。
- 特点 :
- 高可用性:数据多副本存储,耐久性达 99.999999999%。
- 可扩展性:支持无限存储容量,适合大文件和海量数据。
- 安全性:通过访问密钥(Access Key/Secret Key)和权限策略控制访问。
- 适用场景:用户头像上传、视频存储、静态网站托管、数据备份等。
1.2 什么是前端直传 S3?
- 定义:前端直传 S3 是指前端(浏览器或客户端)直接将文件上传到 S3 存储(如七牛云),而不通过后端服务器中转。相比传统方式(前端 → 后端 → S3),它减少了后端带宽压力。
- 传统上传(前端 → 后端 → S3) :
- 流程:前端将文件发送到后端,后端再上传到 S3。
- 问题:文件流量经过后端,若后端带宽有限(如 1MB/s),上传大文件(如 10MB)会很慢(需 10 秒),多用户上传可能导致带宽瓶颈。
- 前端直传(前端 → S3) :
- 流程:
- 前端向后端请求预签名 URL(Presigned URL),包含临时访问权限。
- 前端使用预签名 URL 直接上传文件到 S3。
- 上传成功后,通知后端记录文件信息(如 URL、路径)。
- 优势:
- 速度快:文件直接上传到 S3,利用用户带宽(如 100MB/s,10MB 文件只需 0.1 秒)。
- 减轻后端压力:后端仅处理轻量请求(如生成预签名 URL),无需传输大文件。
- 高并发:S3 天然支持高并发上传,适合多用户场景。
- 流程:
- 适用场景:大文件上传(如视频、图片)、高并发上传(如社交平台用户上传头像)。
1.3 七牛云 S3 存储的特点
- 七牛云提供 S3 兼容的对象存储服务,支持 AWS S3 的 API 和 SDK。
- 需要配置:
- Endpoint:存储服务地址(如 s3-cn-south-1.qiniucs.com)。
- Bucket:存储桶名称。
- Access Key/Secret Key:用于认证的密钥。
- Domain:自定义访问域名(如 http://bucket.qiniucs.com)。
- 七牛云要求配置自定义域名(domain),否则无法生成可访问的 URL。
2. 代码分析
2.1 前端代码(yudao-ui-admin-vue3)
前端代码基于 Vue3 和 Element Plus,使用 ElUpload 组件实现文件上传,支持两种模式:前端直传(client)和后端上传(server)。
关键代码:useUpload 方法
javascript
export const useUpload = () => {
const uploadUrl = getUploadUrl() // 获取上传 URL
const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE // 判断是否前端直传
// 重写 ElUpload 的上传方法
const httpRequest = async (options: UploadRequestOptions) => {
if (isClientUpload) {
// 模式一:前端直传
// 1. 生成唯一文件名(基于 SHA256)
const fileName = await generateFileName(options.file)
// 2. 请求后端获取预签名 URL
const presignedInfo = await FileApi.getFilePresignedUrl(fileName)
// 3. 直接上传文件到 S3
return axios
.put(presignedInfo.uploadUrl, options.file, {
headers: { 'Content-Type': options.file.type }
})
.then(() => {
// 4. 异步记录文件信息到后端
createFile(presignedInfo, fileName, options.file)
// 返回与后端上传一致的格式
return { data: presignedInfo.url }
})
} else {
// 模式二:后端上传
return new Promise((resolve, reject) => {
FileApi.updateFile({ file: options.file })
.then((res) => {
if (res.code === 0) resolve(res)
else reject(res)
})
.catch((res) => reject(res))
})
}
}
return { uploadUrl, httpRequest }
}
- 逻辑 :
- 检查模式:通过 VITE_UPLOAD_TYPE 判断是否为 client 模式。
- 生成文件名:使用 generateFileName 计算文件的 SHA256 哈希值,拼接后缀(如 .jpg),生成唯一文件名。
- 获取预签名 URL:调用后端 /presigned-url 接口,获取上传用的临时 URL。
- 上传文件:使用 axios.put 直接将文件上传到预签名 URL,设置 Content-Type 为文件类型。
- 记录文件:上传成功后,调用 createFile 通知后端保存文件信息(如 URL、路径)。
- 注意 :
- 不使用 FormData 上传,因为 MinIO(或七牛云)不支持 multipart/form-data 格式。
- 返回格式与后端上传一致,确保 ElUpload 组件兼容。
生成文件名:generateFileName
javascript
async function generateFileName(file: UploadRawFile) {
const data = await file.arrayBuffer() // 读取文件内容
const wordArray = CryptoJS.lib.WordArray.create(data) // 转换为 CryptoJS 格式
const sha256 = CryptoJS.SHA256(wordArray).toString() // 计算 SHA256
const ext = file.name.substring(file.name.lastIndexOf('.')) // 获取文件后缀
return `${sha256}${ext}` // 返回唯一文件名
}
- 作用:基于文件内容的 SHA256 哈希生成唯一文件名,避免文件名冲突。
- 优点:即使文件名相同,内容不同也会生成不同文件名,确保文件不被覆盖。
记录文件信息:createFile
javascript
function createFile(vo: FileApi.FilePresignedUrlRespVO, name: string, file: UploadRawFile) {
const fileVo = {
configId: vo.configId, // 存储配置 ID
url: vo.url, // 文件访问 URL
path: name, // 文件路径
name: file.name, // 原始文件名
type: file.type, // 文件类型
size: file.size // 文件大小
}
FileApi.createFile(fileVo) // 调用后端保存文件信息
return fileVo
}
- 作用:将文件信息(如 URL、路径、大小)发送到后端,保存到数据库,便于后续管理。
2.2 后端代码
后端基于 Spring Boot,提供预签名 URL 和文件信息保存接口,支持七牛云等 S3 兼容存储。
获取预签名 URL:FileController.getFilePresignedUrl
java
@GetMapping("/presigned-url")
@Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception {
return success(fileService.getFilePresignedUrl(path));
}
- 作用:接收前端请求的文件路径,返回预签名 URL 和相关信息。
- 输入:path(如 avatars/abc123.jpg)。
- 输出:FilePresignedUrlRespVO(包含 uploadUrl、url、configId)。
文件服务:FileServiceImpl.getFilePresignedUrl
- 逻辑 :
- 获取默认 FileClient(如七牛云的 S3 客户端)。
- 调用 fileClient.getPresignedObjectUrl 生成预签名 URL。
- 返回包含 uploadUrl(上传地址)、url(访问地址)和 configId 的对象。
S3 配置类:S3FileClientConfig
java
@Data
public class S3FileClientConfig implements FileClientConfig {
public static final String ENDPOINT_QINIU = "qiniucs.com";
@NotNull(message = "endpoint 不能为空") private String endpoint; // 节点地址
@URL(message = "domain 必须是 URL 格式") private String domain; // 自定义域名
@NotNull(message = "bucket 不能为空") private String bucket; // 存储桶
@NotNull(message = "accessKey 不能为空") private String accessKey; // 访问密钥
@NotNull(message = "accessSecret 不能为空") private String accessSecret; // 秘密密钥
@AssertTrue(message = "domain 不能为空")
@JsonIgnore
public boolean isDomainValid() {
if (StrUtil.contains(endpoint, ENDPOINT_QINIU) && StrUtil.isEmpty(domain)) {
return false; // 七牛云必须配置 domain
}
return true;
}
}
- 作用:定义 S3 存储的配置参数,支持七牛云、阿里云等。
- 字段 :
- endpoint:存储服务地址(如 s3-cn-south-1.qiniucs.com)。
- domain:访问域名(如 http://bucket.qiniucs.com)。
- bucket:存储桶名称。
- accessKey/accessSecret:认证密钥。
- 校验:七牛云要求 domain 必填。
2.3 七牛云 S3 客户端(假设实现)
虽然您未提供 S3FileClient 的具体实现,但可以参考之前的 S3FileClient(基于 MinIO SDK)。七牛云的实现类似,使用 AWS SDK 或七牛云 SDK 生成预签名 URL。
javascript
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> implements FileClient {
private final AmazonS3 s3Client;
public S3FileClient(Long configId, S3FileClientConfig config) {
super(configId, config);
s3Client = AmazonS3ClientBuilder
.standard()
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(
config.getEndpoint(), "auto"))
.withCredentials(new AWSStaticCredentialsProvider(
new BasicAWSCredentials(config.getAccessKey(), config.getSecretKey())))
.build();
}
@Override
public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception {
// 生成预签名 URL(有效期 1 小时)
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(config.getBucket(), path)
.withMethod(HttpMethod.PUT)
.withExpiration(Date.from(Instant.now().plusSeconds(3600)));
URL uploadUrl = s3Client.generatePresignedUrl(request);
// 返回预签名 URL 和访问 URL
return new FilePresignedUrlRespDTO()
.setUploadUrl(uploadUrl.toString())
.setUrl(config.getDomain() + "/" + path);
}
}
逻辑:使用 AWS SDK 生成预签名 URL,设置 HTTP 方法为 PUT,有效期 1 小时。
总结:
前端代码作用
前端基于 Vue3 和 Element Plus,使用 ElUpload 组件实现文件上传,核心代码在 useUpload 方法中。支持两种模式:前端直传(client)和后端上传(server)。以下是前端直传的逻辑和作用:
- 配置文件 (.env.local):
- 设置 VITE_UPLOAD_TYPE=client,启用前端直传。
- 配置后端 API 地址(VITE_BASE_URL 和 VITE_API_URL)。
- 生成文件名 (generateFileName):
- 使用 SHA256 算法基于文件内容生成唯一文件名,防止冲突。
- 作用:确保文件名全局唯一,避免覆盖。
- 获取预签名 URL (FileApi.getFilePresignedUrl):
- 调用后端 /presigned-url 接口,获取上传用的预签名 URL 和访问 URL。
- 作用:获得 S3 存储的临时上传权限。
- 上传文件 (axios.put):
- 使用预签名 URL 直接将文件上传到 S3,设置 Content-Type 为文件类型。
- 作用:将文件存储到 S3,无需后端中转。
- 记录文件信息 (createFile):
- 将文件信息(路径、URL、大小等)发送到后端,保存到数据库。
- 作用:确保后端能跟踪和管理文件。
后端代码作用
后端基于 Spring Boot,提供预签名 URL 和文件信息保存接口,支持 S3 兼容存储(如七牛云)。核心代码在 FileController 和 FileServiceImpl 中。
- 获取预签名 URL (FileController.getFilePresignedUrl):
- 接收前端的路径参数,返回预签名 URL 和访问 URL。
- 作用:为前端提供上传 S3 的临时权限。
- 生成预签名 URL (FileServiceImpl.getFilePresignedUrl):
- 使用默认 FileClient(如七牛云的 S3 客户端)生成预签名 URL。
- 作用:调用 S3 客户端生成安全的上传地址。
- S3 客户端 (S3FileClient.getPresignedObjectUrl):
- 使用 AWS SDK 或七牛云 SDK 生成预签名 URL,设置上传方法(PUT)和有效期(如 1 小时)。
- 作用:与 S3 存储交互,生成临时访问令牌。
- 保存文件信息 (FileApi.createFile):
- 接收前端发送的文件元数据,保存到数据库。
- 作用:记录文件信息,便于后续查询和管理。
- S3 配置 (S3FileClientConfig):
- 定义 S3 存储的配置参数(endpoint、bucket、accessKey 等)。
- 作用:初始化 S3 客户端,确保连接正确。
整体流程
S3 直传通过前端直接上传文件到七牛云 S3 存储,极大提升上传效率。流程如下:
- 用户选择图片 avatar.jpg(100KB)。
- 前端生成唯一文件名(abc1234567890.jpg),请求后端预签名 URL。
- 后端使用七牛云 S3 客户端生成预签名 URL,返回上传地址和访问地址。
- 前端通过 axios.put 上传文件到七牛云。
- 上传成功后,前端通知后端保存文件信息到数据库。
- 用户获得文件 URL,可直接访问图片。