java每日精进 5.18【文件存储】
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:
<?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:
spring:servlet:multipart:max-file-size: 10MBmax-request-size: 10MB
minio:endpoint: http://127.0.0.1:9000access-key: adminsecret-key: password123bucket-name: bucketone
config:
@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;@Beanpublic 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:
// 标记为 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:
// 标记为 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 裁剪图片后上传。
- 关键代码:
- 文件上传:
<el-uploadref="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 并刷新列表。
- 头像上传:
<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):
@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:
/*** 文件 API 实现类*/ @Service @Validated public class FileApiImpl implements FileApi {@Resourceprivate FileService fileService;@Overridepublic String createFile(String name, String path, byte[] content) {return fileService.createFile(name, path, content);}}
- 作用:实现 FileApi 接口,提供 createFile 方法,接收文件名、路径和字节数组,调用 fileService 上传文件。
- FileServiceImpl:
@Override@SneakyThrowspublic 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。
- FileApiImpl:
解析:
- 后端主导:上传由后端代码或服务调用触发,文件内容以字节数组形式传入,无需用户通过浏览器选择文件。
- 特点:适合服务器端批量处理、自动上传或从其他来源(如本地文件、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 文件下载
// 标记为 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) {// 若输入字符串为空,返回空或 nullif (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 {// 获取文件内容@Overridepublic 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 文件客户端
// 定义文件客户端接口,抽象文件操作方法,支持多种存储器(如 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 方法
export const useUpload = () => {const uploadUrl = getUploadUrl() // 获取上传 URLconst 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. 请求后端获取预签名 URLconst presignedInfo = await FileApi.getFilePresignedUrl(fileName)// 3. 直接上传文件到 S3return 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
async function generateFileName(file: UploadRawFile) {const data = await file.arrayBuffer() // 读取文件内容const wordArray = CryptoJS.lib.WordArray.create(data) // 转换为 CryptoJS 格式const sha256 = CryptoJS.SHA256(wordArray).toString() // 计算 SHA256const ext = file.name.substring(file.name.lastIndexOf('.')) // 获取文件后缀return `${sha256}${ext}` // 返回唯一文件名
}
- 作用:基于文件内容的 SHA256 哈希生成唯一文件名,避免文件名冲突。
- 优点:即使文件名相同,内容不同也会生成不同文件名,确保文件不被覆盖。
记录文件信息:createFile
function createFile(vo: FileApi.FilePresignedUrlRespVO, name: string, file: UploadRawFile) {const fileVo = {configId: vo.configId, // 存储配置 IDurl: vo.url, // 文件访问 URLpath: 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
@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
@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 不能为空")@JsonIgnorepublic 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。
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();}@Overridepublic 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 和访问 URLreturn 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,可直接访问图片。