java每日精进 5.18【文件存储】

1.文件存储思路

支持将文件上传到三类存储器:

  1. 兼容 S3 协议的对象存储:支持 MinIO、腾讯云 COS、七牛云 Kodo、华为云 OBS、亚马逊 S3 等等。
  2. 磁盘存储:本地、FTP 服务器、SFTP 服务器。
  3. 数据库存储: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 表示路径有效。

验证

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 是否可访问(在浏览器中打开)。
    • 错误 :Invoke-WebRequest : Access is denied
      • 解决:以管理员身份运行 PowerShell,或检查 E:\youkeProject\Minio 目录权限: icacls "E:\youkeProject\Minio" /grant "Users:(W)"
    • 错误 :网络连接失败
      • 解决:检查网络(运行 ping mirrors.tuna.tsinghua.edu.cn),或使用代理(如有): $env:HTTP_PROXY="http://proxy:port" $env:HTTPS_PROXY="http://proxy:port"

验证

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
    • 问题 :环境变量未生效
      • 解决:重启 PowerShell 或运行以下命令刷新: $env:MINIO_ROOT_USER = [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_USER", "Machine") $env:MINIO_ROOT_PASSWORD = [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_PASSWORD", "Machine")

验证

[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
    • 错误 :sc start : [SC] StartService FAILED 1057: The account name is invalid
      • 解决:确保服务以正确账户运行,默认使用 LocalSystem: sc.exe config MinIO obj= 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"

验证

Get-Service MinIO | Select-Object Name, Status, StartType

  • 预期输出: Name Status StartType ---- ------ --------- MinIO Running Automatic

5. 验证安装

步骤

  1. 访问 Web 管理界面
    • 打开浏览器,访问 http://localhost:9001
    • 使用用户名 admin 和密码(例如 P@ssw0rd123)登录。
    • 登录成功后,应看到 MinIO 的 Web 控制台,可管理存储桶和文件。
  2. 检查服务状态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

验证

  • 从另一台机器访问 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

验证

  • 停止后: Get-Service MinIO | Select-Object Status
    • 预期输出:Stopped
  • 删除后: Get-Service MinIO -ErrorAction SilentlyContinue
    • 预期输出:无结果(服务不存在)

8. 使用 MinIO 客户端(mc)验证(可选)

步骤

  1. 安装 mc 客户端Invoke-WebRequest -Uri "https://dl.min.io/client/mc/release/windows-amd64/mc.exe" -OutFile "C:\Windows\mc.exe"
  2. 配置别名mc alias set myminio http://localhost:9000 admin P@ssw0rd123
  3. 列出存储桶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

验证

  • 预期输出示例: [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)"

验证

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 可访问。

完整验证流程

  1. 检查目录和文件Test-Path E:\youkeProject\Minio\minio.exe Test-Path E:\youkeProject\Minio\data
  2. 检查环境变量[System.Environment]::GetEnvironmentVariable("MINIO_ROOT_USER", "Machine") [System.Environment]::GetEnvironmentVariable("MINIO_ROOT_PASSWORD", "Machine")
  3. 检查服务状态Get-Service MinIO | Select-Object Name, Status, StartType
  4. 访问 Web 界面
  5. 使用 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
  • 备份数据:定期备份 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。
    • 流程
      1. 解析 uploadReqVO 获取文件和路径。
      2. 使用 IoUtil.readBytes 转换文件流为字节数组。
      3. 调用 fileService.createFile 上传文件到 MinIO,返回 URL。
      4. 封装响应为 CommonResult<String>。
      5. 方法详解
  • 文件上传:createFile 方法实现文件的上传,接收文件名、路径和内容,上传到存储器并保存元数据到数据库,返回文件访问 URL。
  • 文件客户端管理
    • getMasterFileClient:获取主文件客户端(FileClient),从缓存中加载。
    • clientCache:缓存文件客户端,支持异步刷新,减少数据库查询。
  • 文件客户端工厂
    • FileClientFactoryImpl:管理文件客户端的创建和更新,基于配置动态生成客户端实例(如 S3FileClient、LocalFileClient)。
    • 支持多种存储器,通过 FileStorageEnum 映射存储类型到具体客户端类。

3.1.2调用关系

  1. 外部调用:createFile 方法由上层(如 FileController 或其他服务)调用,用于上传文件。
  2. 内部调用
    • createFile 调用 fileConfigService.getMasterFileClient 获取客户端。
    • getMasterFileClient 通过 clientCache 获取缓存的 FileClient。
    • clientCache 的 load 方法调用 fileConfigService 和 fileClientFactory 创建或更新客户端。
    • FileClientFactoryImpl 的 createOrUpdateFileClient 和 getFileClient 方法管理客户端实例。
  3. 依赖
    • 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);


fileMapper.insert(file);

  • 作用:将文件元数据插入数据库。
  • 调用:FileMapper.insert,MyBatis 的 Mapper 方法。
  • 逻辑:保存 FileDO 到 infra_file 表。
  • 示例:插入记录,生成自增 ID。

return url;

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 整体调用流程
  1. 外部调用
    • FileController.uploadFile 调用 FileServiceImpl.createFile,传递文件名、路径和内容。
  2. 文件上传(createFile)
    • 计算 MIME 类型(FileTypeUtils.getMineType)。
    • 生成默认路径(FileUtils.generatePath)。
    • 获取主客户端(getMasterFileClient)。
    • 上传文件(FileClient.upload)。
    • 保存元数据(FileMapper.insert)。
    • 返回 URL。
  3. 获取客户端(getMasterFileClient)
    • 从 clientCache 获取客户端(clientCache.getUnchecked)。
  4. 缓存加载(clientCache.load)
    • 查询配置(fileConfigMapper.selectByMaster 或 selectById)。
    • 创建或更新客户端(FileClientFactory.createOrUpdateFileClient)。
    • 返回客户端(FileClientFactory.getFileClient)。
  5. 客户端工厂(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。
      • 流程
        1. 推断文件类型(FileTypeUtils.getMineType)。
        2. 生成默认路径(如果未提供)。
        3. 获取 MinIO 客户端(FileClient),上传文件(client.upload)。
        4. 保存文件元数据(FileDO)到数据库。
        5. 返回文件 URL。

解析

  • 后端主导:上传由后端代码或服务调用触发,文件内容以字节数组形式传入,无需用户通过浏览器选择文件。
  • 特点:适合服务器端批量处理、自动上传或从其他来源(如本地文件、URL 下载)获取文件的场景。
  • 无前端交互:不依赖浏览器,直接由后端 API 或服务调用。

3.1.6. 回答问题

前端上传和后端上传的区别
  1. 触发方式
    • 前端上传 :由用户通过浏览器界面(<el-upload>、裁剪组件)选择文件,触发 HTTP 请求。
      • 场景:用户上传头像、文档、图片等。
      • 示例:用户点击"上传文件"按钮,发送 POST /admin-api/infra/file/upload。
    • 后端上传 :由后端代码或服务调用触发,文件内容以字节数组形式传入,通常从服务器本地、数据库或其他来源获取。
      • 场景:批量导入文件、服务器端文件处理、从 URL 下载并上传。
      • 示例:后端从本地磁盘读取文件,调用 fileService.createFile。
  2. 文件来源
    • 前端上传 :文件来自用户设备(通过浏览器选择)。
      • 示例:用户选择 C:\test.jpg。
    • 后端上传 :文件来自服务器环境(如本地文件、远程 URL、数据库)。
      • 示例:后端读取 /tmp/test.jpg 或从 URL 下载文件。
  3. 请求格式
    • 前端上传 :使用 multipart/form-data 格式,文件通过 MultipartFile 传输。
      • 示例:form-data: file=(binary), path=/avatars/.
    • 后端上传 :文件以字节数组(byte[])传入,通常通过内部方法调用或 API(如 FileApi.createFile)。
      • 示例:createFile("test.jpg", "/avatars/", fileBytes)。
  4. 用户交互
    • 前端上传 :需要用户交互(如选择文件、点击上传),前端提供界面和反馈(如进度条、成功提示)。
      • 示例:<el-upload> 显示上传进度,成功后弹出 上传成功。
    • 后端上传 :无用户交互,后端自动处理,适合后台任务。
      • 示例:定时任务批量上传文件。
  5. 适用场景
    • 前端上传:用户驱动的场景,如个人中心上传头像、文件管理系统上传文档。
    • 后端上传:系统驱动的场景,如服务器迁移文件、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)
    • 流程:
      1. 前端向后端请求预签名 URL(Presigned URL),包含临时访问权限。
      2. 前端使用预签名 URL 直接上传文件到 S3。
      3. 上传成功后,通知后端记录文件信息(如 URL、路径)。
    • 优势:
      • 速度快:文件直接上传到 S3,利用用户带宽(如 100MB/s,10MB 文件只需 0.1 秒)。
      • 减轻后端压力:后端仅处理轻量请求(如生成预签名 URL),无需传输大文件。
      • 高并发:S3 天然支持高并发上传,适合多用户场景。
  • 适用场景:大文件上传(如视频、图片)、高并发上传(如社交平台用户上传头像)。

1.3 七牛云 S3 存储的特点

  • 七牛云提供 S3 兼容的对象存储服务,支持 AWS S3 的 API 和 SDK。
  • 需要配置:
  • 七牛云要求配置自定义域名(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 }
}
  • 逻辑
    1. 检查模式:通过 VITE_UPLOAD_TYPE 判断是否为 client 模式。
    2. 生成文件名:使用 generateFileName 计算文件的 SHA256 哈希值,拼接后缀(如 .jpg),生成唯一文件名。
    3. 获取预签名 URL:调用后端 /presigned-url 接口,获取上传用的临时 URL。
    4. 上传文件:使用 axios.put 直接将文件上传到预签名 URL,设置 Content-Type 为文件类型。
    5. 记录文件:上传成功后,调用 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

  • 逻辑
    1. 获取默认 FileClient(如七牛云的 S3 客户端)。
    2. 调用 fileClient.getPresignedObjectUrl 生成预签名 URL。
    3. 返回包含 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 存储的配置参数,支持七牛云、阿里云等。
  • 字段
  • 校验:七牛云要求 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 存储,极大提升上传效率。流程如下:

  1. 用户选择图片 avatar.jpg(100KB)。
  2. 前端生成唯一文件名(abc1234567890.jpg),请求后端预签名 URL。
  3. 后端使用七牛云 S3 客户端生成预签名 URL,返回上传地址和访问地址。
  4. 前端通过 axios.put 上传文件到七牛云。
  5. 上传成功后,前端通知后端保存文件信息到数据库。
  6. 用户获得文件 URL,可直接访问图片。
相关推荐
休息一下接着来几秒前
C++ I/O多路复用
linux·开发语言·c++
caihuayuan523 分钟前
生产模式下react项目报错minified react error #130的问题
java·大数据·spring boot·后端·课程设计
编程、小哥哥29 分钟前
Java大厂面试:从Web框架到微服务技术的场景化提问与解析
java·spring boot·微服务·面试·技术栈·数据库设计·分布式系统
界面开发小八哥38 分钟前
「Java EE开发指南」如何使用MyEclipse的可视化JSF编辑器设计JSP?(二)
java·ide·人工智能·java-ee·myeclipse
代码狂人1 小时前
Lua中使用module时踩过的坑
开发语言·lua
繁依Fanyi1 小时前
ColorAid —— 一个面向设计师的色盲模拟工具开发记
开发语言·前端·vue.js·编辑器·codebuddy首席试玩官
易只轻松熊1 小时前
C++(23):容器类<vector>
开发语言·数据结构·c++
Lu Yao_2 小时前
用golang实现二叉搜索树(BST)
开发语言·数据结构·golang
沐土Arvin2 小时前
前端图片上传组件实战:从动态销毁Input到全屏预览的全功能实现
开发语言·前端·javascript
找不到、了2 小时前
Spring-Beans的生命周期的介绍
java·开发语言·spring