FastDFS分布式文件系统

FastDFS 完整使用指南

本文档基于实际项目,全面讲解 FastDFS 分布式文件系统的使用方法、工作原理和最佳实践


📚 目录

  1. FastDFS 是什么
  2. 项目书写流程概览
  3. 详细开发步骤
  4. FastDFS 工作原理深度解析
  5. 完整的文件操作流程
  6. FastDFS 核心功能详解
  7. 常见问题与解答
  8. 下次开发时的快速上手指南

FastDFS 是什么

🎯 核心定位

FastDFS 是一个开源的分布式文件系统,专为互联网应用设计,具有:

  • 高性能 - 单台服务器可支撑百万级文件存储
  • 高可用 - 支持冗余备份,自动故障转移
  • 分布式 - 支持横向扩展,无中心节点设计
  • 负载均衡 - 自动负载均衡,智能选择存储服务器
  • 轻量级 - 使用 C 语言实现,占用资源少
  • 简单易用 - API 简单,易于集成

🤔 为什么需要 FastDFS?

不使用分布式文件系统的后果:

  • ❌ 文件存储在应用服务器,占用大量磁盘空间
  • ❌ 文件访问消耗应用服务器带宽和性能
  • ❌ 水平扩展困难,无法共享文件
  • ❌ 单点故障风险高
  • ❌ 缺乏文件管理和监控手段

使用 FastDFS 的好处:

  • ✅ 文件与应用分离,互不影响
  • ✅ 支持高并发访问,提供 HTTP 服务
  • ✅ 自动负载均衡和故障转移
  • ✅ 支持主从备份,数据安全可靠
  • ✅ 适合存储大量小文件(4KB - 500MB)
  • ✅ 内置防盗链、限速、访问控制等功能

🏗️ FastDFS 架构组成

FastDFS 架构
│
├─ Tracker Server(跟踪服务器)
│  ├─ 管理 Storage Server
│  ├─ 记录文件存储位置
│  ├─ 负载均衡调度
│  └─ 集群协调
│
├─ Storage Server(存储服务器)
│  ├─ 实际存储文件
│  ├─ 提供文件上传下载
│  ├─ 文件同步备份
│  └─ 文件元数据管理
│
└─ Client(客户端)
   ├─ 应用程序集成
   ├─ 调用 FastDFS API
   └─ 连接 Tracker/Storage

角色说明:

  1. Tracker Server(跟踪服务器)

    • 负责调度和管理
    • 记录所有 Storage Server 的状态
    • 接收客户端请求,返回可用的 Storage Server
    • 不存储实际文件,只存储元数据
    • 支持集群部署(建议至少 2 台)
  2. Storage Server(存储服务器)

    • 实际存储文件
    • 提供文件上传、下载、删除等操作
    • 支持分组(Group),同组内服务器互为备份
    • 每个组可以独立扩容
    • 内置 HTTP 服务器(通过 Nginx 模块)
  3. Client(客户端)

    • 集成在应用程序中
    • 通过 Java API 连接 FastDFS
    • 上传时连接 Tracker 获取 Storage 地址
    • 下载时可直接连接 Storage 或通过 HTTP

项目书写流程概览

📋 开发步骤清单

第一步:基础配置
  ├─ pom.xml(Maven 依赖 - FastDFS 客户端)
  ├─ application.yml(数据库配置、文件上传大小限制)
  ├─ fdfs.properties(FastDFS 连接配置)
  └─ 数据库表结构设计(存储文件元信息)

第二步:工具类开发 ⭐ 核心
  └─ FastDFSUtils.java(封装上传、下载、删除、修改操作)

第三步:数据层开发
  ├─ 实体类(Flower.java)
  └─ Mapper 接口(FlowerMapper.java)

第四步:业务层开发
  ├─ Service 接口(FlowerService.java)
  └─ Service 实现(FlowerServiceImpl.java - 调用 FastDFS 工具类)

第五步:控制层开发
  └─ Controller(FlowerController.java - 处理文件上传下载)

第六步:启动类
  └─ SpringBootMain.java

第七步:前端页面
  ├─ save.html(文件上传页面 - multipart/form-data)
  └─ success.html(文件展示页面 - 显示图片和下载链接)

第八步:测试
  └─ DemoTest.java(单元测试 - 测试上传、下载、删除)

详细开发步骤


第一步:基础配置

1.1 创建 Maven 项目

为什么使用 Maven?

  • 依赖管理自动化(不需要手动下载 jar 包)
  • 统一的项目结构
  • 方便的版本管理
  • 简化项目打包和发布

项目结构:

fastdfs01/
├─ src/
│  ├─ main/
│  │  ├─ java/
│  │  │  └─ com/jr/
│  │  │     ├─ controller/      # 控制器
│  │  │     ├─ mapper/          # 数据访问层
│  │  │     ├─ pojo/            # 实体类
│  │  │     ├─ service/         # 业务层
│  │  │     ├─ util/            # 工具类(FastDFS 封装)
│  │  │     └─ SpringBootMain.java  # 启动类
│  │  └─ resources/
│  │     ├─ application.yml     # Spring Boot 配置
│  │     ├─ fdfs.properties     # FastDFS 配置
│  │     ├─ static/             # 静态资源
│  │     └─ templates/          # Thymeleaf 模板
│  └─ test/                     # 测试代码
└─ pom.xml                      # Maven 配置

1.2 配置 pom.xml

<?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>

    <groupId>com.jr.dz18</groupId>
    <artifactId>fastdfs01</artifactId>
    <version>1.0-SNAPSHOT</version>
    
    <!-- 继承 Spring Boot 父项目,用于版本管理 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.2</version>
    </parent>

    <dependencies>
        <!-- ⭐ FastDFS 客户端核心依赖 -->
        <dependency>
            <groupId>cn.bestwu</groupId>
            <artifactId>fastdfs-client-java</artifactId>
            <version>1.27</version>
        </dependency>
        
        <!-- Apache Commons Lang3:提供字符串工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
        
        <!-- Spring Boot Web 启动器:提供 Web 功能 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Thymeleaf 模板引擎:用于渲染 HTML 页面 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- MyBatis 启动器:持久层框架 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>

        <!-- MySQL 数据库驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- Lombok:简化实体类代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- JUnit 测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <!-- 资源拷贝插件:确保配置文件、页面等被正确打包 -->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.yml</include>
                    <include>**/*.xml</include>
                    <include>**/*.html</include>
                    <include>**/*.js</include>
                    <include>**/*.properties</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>

💡 核心依赖说明:

  • fastdfs-client-java:FastDFS 的 Java 客户端,提供文件操作 API
  • commons-lang3:字符串工具类,用于文件扩展名处理
  • spring-boot-starter-web:提供文件上传功能(MultipartFile)

1.3 配置 application.yml

# 服务器端口配置
server:
  port: 8080

# Spring 配置
spring:
  # 文件上传配置
  servlet:
    multipart:
      max-file-size: 10MB      # 单个文件最大大小
      max-request-size: 10MB   # 请求最大大小(适用于多文件上传)
  
  # 数据源配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/jdbc?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: root

# MyBatis 配置
mybatis:
  # 实体类包路径
  type-aliases-package: com.jr.pojo
  # Mapper XML 文件位置
  mapper-locations: classpath:com/jr/mapper/*.xml
  # 配置
  configuration:
    # 控制台输出 SQL 语句(开发时方便调试)
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

⚙️ 配置说明:

  • max-file-size:限制单个文件大小,防止超大文件占用资源
  • max-request-size:限制整个请求大小
  • 如果上传大文件,需要相应调整这两个参数

1.4 配置 fdfs.properties

# 连接超时时间(秒)
fastdfs.connect_timeout_in_seconds=10

# 网络超时时间(秒)
fastdfs.network_timeout_in_seconds=30

# 字符编码
fastdfs.charset=UTF-8

# ⭐ Tracker 服务器地址(多个用逗号分隔)
# 格式:IP:端口号
# 端口号默认为 22122
fastdfs.tracker_servers=192.168.1.110:22122

# HTTP 访问端口(如果配置了 Nginx)
# 默认为 8888,与 Storage 服务器的 Nginx 配置一致
# fastdfs.http_tracker_http_port=8888

🔧 配置说明:

  1. tracker_servers

    • 这是最重要的配置
    • 指定 Tracker Server 的地址和端口
    • 如果有多个 Tracker,用逗号分隔
    • 示例:192.168.1.110:22122,192.168.1.111:22122
  2. 连接超时和网络超时

    • 根据网络环境调整
    • 内网环境可以设置较小值(5-10 秒)
    • 跨机房访问建议设置较大值(30-60 秒)
  3. HTTP 端口

    • 用于直接通过 HTTP 访问文件
    • 需要在 Storage Server 上配置 Nginx + FastDFS 模块
    • 访问格式:http://IP:8888/组名/文件路径

1.5 数据库表结构设计

核心表结构:

-- 花卉表(示例业务表)
CREATE TABLE `flower` (
  `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '花卉ID',
  `name` VARCHAR(50) NOT NULL COMMENT '花卉名称',
  `price` DOUBLE NOT NULL COMMENT '花卉价格',
  `production` VARCHAR(100) COMMENT '花卉产地',
  
  -- ⭐ FastDFS 相关字段
  `orname` VARCHAR(200) COMMENT '原始文件名',
  `groupname` VARCHAR(50) COMMENT 'FastDFS 组名(如:group1)',
  `remotefilename` VARCHAR(200) COMMENT 'FastDFS 远程文件路径'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='花卉信息表';

💡 为什么要保存这些字段?

  1. orname(原始文件名)

    • 保存用户上传的原始文件名
    • 用于下载时设置文件名
    • 用于显示文件信息
  2. groupname(组名)

    • FastDFS 返回的组名(如:group1)
    • 下载和删除时需要提供
    • 示例:group1
  3. remotefilename(远程文件路径)

    • FastDFS 返回的文件存储路径
    • 下载和删除时需要提供
    • 示例:M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.png

完整的文件访问 URL:

http://192.168.1.110:8888/group1/M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.png
                         \_____/ \___________________________________________/
                           组名              远程文件路径

示例数据:

-- 插入测试数据
INSERT INTO flower VALUES 
(1, '玫瑰花', 25.50, '云南', 
 'rose.jpg', 
 'group1', 
 'M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.jpg');

INSERT INTO flower VALUES 
(2, '百合花', 30.00, '山东', 
 'lily.png', 
 'group1', 
 'M00/00/00/wKgBbmjd18yAHJK2BBq3vcdVOs9428.png');

第二步:FastDFS 工具类开发 ⭐ 核心

FastDFSUtils.java(完整版)

package com.jr.util;

import org.apache.commons.lang3.StringUtils;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;

import java.io.*;
import java.util.Properties;

/**
 * FastDFS 工具类
 * 
 * 功能:
 * 1. 文件上传(支持 InputStream 和 File)
 * 2. 文件下载
 * 3. 文件删除
 * 4. 文件修改
 * 5. 获取文件元数据
 * 
 * 使用静态初始化块在类加载时初始化 FastDFS 客户端连接
 */
public final class FastDFSUtils {
    
    /**
     * 定义静态属性,Properties 和 StorageClient
     * 
     * Properties:存储 fdfs.properties 配置
     * StorageClient:FastDFS 存储客户端,用于文件操作
     */
    private final static Properties PROPERTIES;
    private final static StorageClient STORAGE_CLIENT;

    /**
     * ⭐ 静态初始化代码块
     * 
     * 作用:在类加载时执行,初始化 FastDFS 连接
     * 
     * 执行时机:
     * 1. 第一次使用 FastDFSUtils 时
     * 2. 在任何方法调用之前
     * 3. 只执行一次(单例模式)
     * 
     * 异常处理:
     * - 静态初始化块中的异常无法被外部捕获
     * - 抛出 ExceptionInInitializerError 终止类加载
     * - 确保不会在连接失败的情况下继续执行
     */
    static {
        try {
            // 第一步:创建 Properties 对象
            PROPERTIES = new Properties();
            
            // 第二步:加载 fdfs.properties 配置文件
            // 使用类加载器从 classpath 读取配置文件
            PROPERTIES.load(
                FastDFSUtils.class
                    .getClassLoader()
                    .getResourceAsStream("fdfs.properties")
            );
            
            // 第三步:使用 ClientGlobal 初始化 FastDFS 客户端全局配置
            // 解析配置文件中的 tracker_servers、超时时间等
            ClientGlobal.initByProperties(PROPERTIES);
            
            // 第四步:创建 Tracker 客户端对象
            TrackerClient trackerClient = new TrackerClient();
            
            // 第五步:连接到 Tracker Server
            // 返回 TrackerServer 对象,代表与 Tracker 的连接
            TrackerServer trackerServer = trackerClient.getConnection();
            
            // 第六步:通过 Tracker 获取可用的 Storage Server
            // Tracker 会根据负载均衡策略选择一个 Storage
            StorageServer storageServer = trackerClient.getStoreStorage(trackerServer);
            
            // 第七步:创建 Storage 客户端对象
            // 用于执行文件上传、下载、删除等操作
            STORAGE_CLIENT = new StorageClient(trackerServer, storageServer);
            
        } catch (Exception e) {
            // 静态初始化异常,抛出 Error 终止程序
            throw new ExceptionInInitializerError(e);
        }
    }

    /**
     * 文件上传(通过 InputStream)⭐ 推荐
     * 
     * 优点:
     * 1. 支持保存文件元数据(原始文件名、文件大小)
     * 2. 适合处理 Web 上传(MultipartFile.getInputStream())
     * 3. 节省内存(流式处理)
     * 
     * @param inputStream 上传的文件输入流
     * @param fileName    上传的文件原始名(用于提取扩展名和保存元数据)
     * @return String[2] - [0]:组名(如:group1),[1]:远程文件路径
     */
    public static String[] uploadFile(InputStream inputStream, String fileName) {
        try {
            // 第一步:准备文件元数据(Meta Data)
            // 元数据会存储在 FastDFS 中,可以通过 API 查询
            NameValuePair[] meta_list = new NameValuePair[2];
            
            // 元数据1:原始文件名
            meta_list[0] = new NameValuePair("file name", fileName);
            
            // 元数据2:文件大小
            meta_list[1] = new NameValuePair("file length", inputStream.available() + "");
            
            // 第二步:将 InputStream 转换为字节数组
            byte[] file_buff = null;
            if (inputStream != null) {
                // 获取文件大小
                int len = inputStream.available();
                // 创建字节数组
                file_buff = new byte[len];
                // 读取输入流到字节数组
                inputStream.read(file_buff);
            }
            
            // 第三步:调用 FastDFS API 上传文件
            // 参数:
            // - file_buff:文件内容(字节数组)
            // - getFileExt(fileName):文件扩展名(如:jpg)
            // - meta_list:元数据
            String[] fileids = STORAGE_CLIENT.upload_file(file_buff, getFileExt(fileName), meta_list);
            
            // 第四步:返回结果
            // fileids[0] = 组名(如:group1)
            // fileids[1] = 远程文件路径(如:M00/00/00/xxx.jpg)
            return fileids;
            
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }

    /**
     * 文件上传(通过 File 对象)
     * 
     * 特点:不保存元数据
     * 
     * 适用场景:
     * - 本地文件上传
     * - 批量处理文件
     * 
     * @param file     文件对象
     * @param fileName 文件名
     * @return String[2] - [0]:组名,[1]:远程文件路径
     */
    public static String[] uploadFile(File file, String fileName) {
        FileInputStream fis = null;
        try {
            // 不保存元数据
            NameValuePair[] meta_list = null;
            
            // 打开文件输入流
            fis = new FileInputStream(file);
            
            // 读取文件内容到字节数组
            byte[] file_buff = null;
            if (fis != null) {
                int len = fis.available();
                file_buff = new byte[len];
                fis.read(file_buff);
            }

            // 上传文件
            String[] fileids = STORAGE_CLIENT.upload_file(file_buff, getFileExt(fileName), meta_list);
            return fileids;
            
        } catch (Exception ex) {
            return null;
        } finally {
            // 关闭输入流
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 文件删除
     * 
     * 注意:
     * - 删除操作不可逆!
     * - 删除后文件无法恢复
     * - 建议使用软删除(数据库标记删除,文件保留)
     * 
     * @param groupName      组名(如:group1)
     * @param remoteFileName 远程文件路径(如:M00/00/00/xxx.jpg)
     * @return 0 为成功,非 0 为失败(具体错误代码)
     */
    public static int deleteFile(String groupName, String remoteFileName) {
        try {
            // 调用 FastDFS API 删除文件
            // 如果 groupName 为空,默认使用 group1
            int result = STORAGE_CLIENT.delete_file(
                groupName == null ? "group1" : groupName, 
                remoteFileName
            );
            return result;
        } catch (Exception ex) {
            return 0;
        }
    }

    /**
     * 文件修改
     * 
     * 实现原理:
     * 1. 上传新文件
     * 2. 删除旧文件
     * 
     * 注意:
     * - 不是真正的"修改",而是"替换"
     * - 文件路径会改变
     * - 需要更新数据库中的文件路径
     * 
     * @param oldGroupName 旧文件组名
     * @param oldFileName  旧文件路径
     * @param file         新文件
     * @param fileName     新文件名
     * @return String[2] - [0]:新文件组名,[1]:新文件路径
     */
    public static String[] modifyFile(String oldGroupName, String oldFileName, File file, String fileName) {
        String[] fileids = null;
        try {
            // 第一步:上传新文件
            fileids = uploadFile(file, fileName);
            if (fileids == null) {
                return null;
            }
            
            // 第二步:删除旧文件
            int delResult = deleteFile(oldGroupName, oldFileName);
            if (delResult != 0) {
                return null;
            }
        } catch (Exception ex) {
            return null;
        }
        return fileids;
    }

    /**
     * 文件下载
     * 
     * 返回 InputStream,可以:
     * 1. 直接输出到浏览器(在线预览或下载)
     * 2. 保存到本地文件
     * 3. 进行其他处理(如:压缩、转码)
     * 
     * @param groupName      组名
     * @param remoteFileName 远程文件路径
     * @return InputStream 文件输入流
     */
    public static InputStream downloadFile(String groupName, String remoteFileName) {
        try {
            // 调用 FastDFS API 下载文件
            // 返回字节数组
            byte[] bytes = STORAGE_CLIENT.download_file(groupName, remoteFileName);
            
            // 将字节数组转换为 InputStream
            InputStream inputStream = new ByteArrayInputStream(bytes);
            return inputStream;
        } catch (Exception ex) {
            return null;
        }
    }

    /**
     * 获取文件元数据
     * 
     * 可以查询:
     * - 原始文件名
     * - 文件大小
     * - 上传时间
     * - 自定义元数据
     * 
     * @param groupName      组名
     * @param remoteFileName 远程文件路径
     * @return NameValuePair[] 元数据数组
     */
    public static NameValuePair[] getMetaDate(String groupName, String remoteFileName) {
        try {
            NameValuePair[] nvp = STORAGE_CLIENT.get_metadata(groupName, remoteFileName);
            return nvp;
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }

    /**
     * 获取文件后缀名(不带点)
     * 
     * 示例:
     * - "test.jpg" -> "jpg"
     * - "test.tar.gz" -> "gz"
     * - "test" -> ""
     * 
     * @param fileName 文件名
     * @return 文件扩展名
     */
    private static String getFileExt(String fileName) {
        if (StringUtils.isBlank(fileName) || !fileName.contains(".")) {
            return "";
        } else {
            return fileName.substring(fileName.lastIndexOf(".") + 1);
        }
    }

    /**
     * 提供获取 Storage 客户端对象的工具方法
     * 
     * 用于高级操作(如:Appender 文件、分片上传)
     * 
     * @return StorageClient 对象
     */
    public static StorageClient getStorageClient() {
        return STORAGE_CLIENT;
    }

    /**
     * 私有构造器,防止实例化
     * 
     * 工具类不应该被实例化,所有方法都是静态的
     */
    private FastDFSUtils() {
    }
}

🔑 核心理解点:

  1. 静态初始化块的作用

    • 在类加载时执行一次
    • 初始化 FastDFS 连接
    • 连接失败会抛出 Error 终止程序
  2. 为什么使用静态成员

    • 避免重复创建连接
    • 提高性能(连接复用)
    • 线程安全(StorageClient 是线程安全的)
  3. 上传方法的选择

    • Web 应用:使用 uploadFile(InputStream, String)
    • 本地文件:使用 uploadFile(File, String)
  4. 文件路径的组成

    group1/M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.jpg
    \_____/ \___________________________________________/
      组名              远程文件路径
    

第三步:实体类

Flower.java

package com.jr.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

import java.io.Serializable;

/**
 * 花卉实体类
 * 对应数据库的 flower 表
 */
@Component          // 注册为 Spring Bean
@AllArgsConstructor // Lombok:自动生成全参构造器
@NoArgsConstructor  // Lombok:自动生成无参构造器
@Data               // Lombok:自动生成 getter/setter/toString/equals/hashCode
public class Flower implements Serializable {
    
    private Integer id;            // 花卉ID
    private String name;           // 花卉名称
    private Double price;          // 花卉价格
    private String production;     // 花卉产地
    
    // ⭐ FastDFS 相关字段
    private String orname;         // 原始文件名
    private String groupname;      // FastDFS 组名(如:group1)
    private String remotefilename; // FastDFS 远程文件路径
}

💡 为什么要实现 Serializable?

  • 支持对象序列化
  • 可以存储到 Session
  • 可以通过网络传输
  • 可以缓存到 Redis

第四步:Mapper 层(数据访问层)

FlowerMapper.java

package com.jr.mapper;

import com.jr.pojo.Flower;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * 花卉数据访问层
 */
@Component
@Mapper  // MyBatis 注解,标记为 Mapper 接口
public interface FlowerMapper {
    
    /**
     * 插入花卉信息
     * 
     * 包含 FastDFS 返回的组名和文件路径
     * 
     * @param flower 花卉对象
     * @return 影响行数
     */
    @Insert("INSERT INTO flower VALUES(" +
            "DEFAULT, #{name}, #{price}, #{production}, " +
            "#{orname}, #{groupname}, #{remotefilename})")
    int insert(Flower flower);

    /**
     * 查询所有花卉
     * 
     * 用于前端展示列表
     * 
     * @return 花卉列表
     */
    @Select("SELECT * FROM flower")
    List<Flower> selectAll();
}

🔍 SQL 解析:

-- 插入语句
INSERT INTO flower VALUES(
    DEFAULT,              -- id 自增
    '玫瑰花',              -- name
    25.50,                -- price
    '云南',                -- production
    'rose.jpg',           -- orname(原始文件名)
    'group1',             -- groupname(FastDFS 组名)
    'M00/00/00/xxx.jpg'   -- remotefilename(FastDFS 路径)
)

第五步:Service 层(业务层)

5.1 FlowerService.java(接口)

package com.jr.service;

import com.jr.pojo.Flower;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

/**
 * 花卉业务接口
 */
public interface FlowerService {
    
    /**
     * 保存花卉信息(包含文件上传)
     * 
     * @param flower 花卉信息
     * @param photo  上传的文件
     * @return 影响行数
     * @throws IOException IO 异常
     */
    int save(Flower flower, MultipartFile photo) throws IOException;

    /**
     * 查询所有花卉
     * 
     * @return 花卉列表
     */
    List<Flower> findAll();
}

5.2 FlowerServiceImpl.java(实现类)⭐ 核心

package com.jr.service.Impl;

import com.jr.mapper.FlowerMapper;
import com.jr.pojo.Flower;
import com.jr.service.FlowerService;
import com.jr.util.FastDFSUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

/**
 * 花卉业务实现类
 * 
 * ⭐ 核心逻辑:
 * 1. 将上传的文件保存到 FastDFS
 * 2. 获取 FastDFS 返回的组名和文件路径
 * 3. 将文件信息和业务数据一起保存到数据库
 */
@Service  // 注册为 Spring Bean
public class FlowerServiceImpl implements FlowerService {

    @Autowired
    private FlowerMapper flowerMapper;

    /**
     * 保存花卉信息(包含文件上传)
     * 
     * 执行流程:
     * 1. 获取上传文件的输入流
     * 2. 调用 FastDFSUtils 上传文件到 FastDFS
     * 3. 获取 FastDFS 返回的组名和路径
     * 4. 将文件信息设置到 flower 对象
     * 5. 插入数据库
     * 
     * @param flower 花卉信息
     * @param photo  上传的文件(MultipartFile)
     * @return 影响行数
     * @throws IOException IO 异常
     */
    @Override
    public int save(Flower flower, MultipartFile photo) throws IOException {
        // 第一步:获取上传文件的输入流
        InputStream inputStream = photo.getInputStream();
        
        // 第二步:调用 FastDFSUtils 上传文件
        // 返回数组:[0]=组名,[1]=文件路径
        String[] strings = FastDFSUtils.uploadFile(inputStream, photo.getOriginalFilename());
        
        // 第三步:设置文件信息到 flower 对象
        flower.setOrname(photo.getOriginalFilename());  // 原始文件名
        flower.setGroupname(strings[0]);                // 组名(如:group1)
        flower.setRemotefilename(strings[1]);           // 文件路径(如:M00/00/00/xxx.jpg)
        
        // 第四步:插入数据库
        return flowerMapper.insert(flower);
    }

    /**
     * 查询所有花卉
     * 
     * @return 花卉列表
     */
    @Override
    public List<Flower> findAll() {
        return flowerMapper.selectAll();
    }
}

🔍 深度解析:MultipartFile 是什么?

// MultipartFile 是 Spring 提供的文件上传接口
// 常用方法:

MultipartFile photo = ...;

// 1. 获取原始文件名
String fileName = photo.getOriginalFilename();
// 示例:rose.jpg

// 2. 获取文件大小(字节)
long size = photo.getSize();
// 示例:1024000(约 1MB)

// 3. 获取文件类型(MIME Type)
String contentType = photo.getContentType();
// 示例:image/jpeg

// 4. 获取输入流(⭐ 最常用)
InputStream inputStream = photo.getInputStream();
// 用于读取文件内容

// 5. 判断是否为空
boolean isEmpty = photo.isEmpty();
// true 表示用户没有选择文件

// 6. 保存到本地(不推荐,应该用 FastDFS)
photo.transferTo(new File("D:/upload/rose.jpg"));

第六步:Controller 层

FlowerController.java

package com.jr.controller;

import com.jr.pojo.Flower;
import com.jr.service.FlowerService;
import com.jr.util.FastDFSUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;

/**
 * 花卉控制器
 * 
 * 功能:
 * 1. 页面路由
 * 2. 文件上传
 * 3. 文件下载
 * 4. 数据查询
 */
@Controller
public class FlowerController {
    
    @Autowired
    private FlowerService flowerService;

    /**
     * 动态路由处理
     * 
     * 示例:
     * - 访问 /save → 返回 "save" → 渲染 save.html
     * - 访问 /success → 返回 "success" → 渲染 success.html
     * 
     * @param url 路径变量
     * @return 视图名称
     */
    @RequestMapping("/{url}")
    public String url(@PathVariable String url) {
        return url;
    }

    /**
     * 文件上传处理 ⭐ 核心
     * 
     * 处理流程:
     * 1. 接收表单参数(flower 对象)
     * 2. 接收上传的文件(photo)
     * 3. 调用 Service 保存(包含文件上传到 FastDFS)
     * 4. 返回结果页面
     * 
     * @param flower 花卉信息
     * @param photo  上传的文件
     * @return 视图名称
     * @throws IOException IO 异常
     */
    @RequestMapping("/save1")
    public String save(Flower flower, MultipartFile photo) throws IOException {
        int save = flowerService.save(flower, photo);
        if (save > 0) {
            return "success";  // 跳转到成功页面
        } else {
            return "save";     // 返回上传页面
        }
    }

    /**
     * 查询所有花卉(Ajax 接口)
     * 
     * @ResponseBody:将返回值转换为 JSON
     * 
     * 返回示例:
     * [
     *   {
     *     "id": 1,
     *     "name": "玫瑰花",
     *     "price": 25.5,
     *     "production": "云南",
     *     "orname": "rose.jpg",
     *     "groupname": "group1",
     *     "remotefilename": "M00/00/00/xxx.jpg"
     *   }
     * ]
     * 
     * @return 花卉列表(JSON)
     */
    @RequestMapping("/getAll")
    @ResponseBody
    public List<Flower> getAll() {
        return flowerService.findAll();
    }

    /**
     * 文件下载 ⭐ 核心
     * 
     * 处理流程:
     * 1. 从 FastDFS 下载文件(得到 InputStream)
     * 2. 设置响应头(告诉浏览器这是一个下载文件)
     * 3. 将 InputStream 写入响应的 OutputStream
     * 4. 浏览器弹出下载对话框
     * 
     * @param gname    组名(如:group1)
     * @param orname   远程文件路径(如:M00/00/00/xxx.jpg)
     * @param response HttpServletResponse 对象
     * @throws IOException IO 异常
     */
    @RequestMapping("/download")
    @ResponseBody
    public void download(String gname, String orname, HttpServletResponse response) throws IOException {
        
        // 第一步:生成随机文件名(防止中文乱码)
        // UUID 确保文件名唯一
        String uuname = UUID.randomUUID() + ".png";
        
        // 第二步:设置响应头
        // content-disposition:告诉浏览器这是一个附件,需要下载
        // attachment:以附件形式下载
        // filename:下载时的文件名
        response.setHeader("content-disposition", "attachment;filename=" + uuname);
        
        // 第三步:从 FastDFS 下载文件
        InputStream inputStream = FastDFSUtils.downloadFile(gname, orname);
        
        // 第四步:获取响应的输出流
        ServletOutputStream outputStream = response.getOutputStream();
        
        // 第五步:将输入流的内容复制到输出流
        // IOUtils.copy():Apache Commons IO 提供的工具方法
        IOUtils.copy(inputStream, outputStream);
        
        // 第六步:关闭流
        outputStream.close();
        inputStream.close();
    }
}

🔑 关键理解点:

  1. MultipartFile 参数自动绑定

    // 表单中的 name="photo" 会自动绑定到参数
    public String save(Flower flower, MultipartFile photo)
    
  2. 文件下载的响应头

    // attachment:附件(下载)
    response.setHeader("content-disposition", "attachment;filename=" + fileName);
    
    // inline:内联(在线预览,适用于图片、PDF)
    response.setHeader("content-disposition", "inline;filename=" + fileName);
    
  3. 流的复制

    // 手动复制(不推荐)
    byte[] buffer = new byte[1024];
    int len;
    while ((len = inputStream.read(buffer)) != -1) {
        outputStream.write(buffer, 0, len);
    }
    
    // 使用工具类(推荐)
    IOUtils.copy(inputStream, outputStream);
    

第七步:启动类

SpringBootMain.java

package com.jr;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Spring Boot 启动类
 */
@SpringBootApplication  // 标记为 Spring Boot 应用
public class SpringBootMain {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootMain.class, args);
        System.out.println("========================================");
        System.out.println("⭐ FastDFS 应用启动成功!");
        System.out.println("⭐ 访问地址:http://localhost:8080/save");
        System.out.println("========================================");
    }
}

启动时会发生什么?

1. Spring Boot 启动
2. 加载 FastDFSUtils 类
3. 执行静态初始化块
   - 读取 fdfs.properties
   - 连接 Tracker Server
   - 创建 StorageClient
4. 扫描所有 @Component, @Service, @Controller 注解的类
5. 创建 Bean 并注入依赖关系
6. MyBatis 扫描 Mapper 接口
7. Thymeleaf 配置模板路径
8. 启动内置 Tomcat,监听 8080 端口
9. 应用就绪,可以接受请求

第八步:前端页面

8.1 save.html(文件上传页面)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>花卉添加</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f5f5f5;
            padding: 50px;
        }
        h2 {
            color: #333;
        }
        form {
            background-color: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            max-width: 500px;
        }
        p {
            margin-bottom: 15px;
        }
        input[type="text"], input[type="file"] {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            box-sizing: border-box;
        }
        input[type="submit"] {
            width: 100%;
            padding: 12px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
        }
        input[type="submit"]:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>
    <h2>花卉信息添加</h2>
    
    <!-- 
        ⭐ 文件上传表单的三个要点:
        1. method="post"      - 必须是 POST 请求
        2. enctype="multipart/form-data"  - 必须设置(支持文件上传)
        3. input type="file"  - 文件选择控件
    -->
    <form action="/save1" method="post" enctype="multipart/form-data">
        
        <p>
            花卉名称:<input type="text" name="name" required/>
        </p>
        
        <p>
            花卉价格:<input type="text" name="price" required/>
        </p>
        
        <p>
            花卉产地:<input type="text" name="production" required/>
        </p>
        
        <p>
            花卉图片:<input type="file" name="photo" accept="image/*" required/>
        </p>
        
        <p>
            <input type="submit" value="提交"/>
        </p>
    </form>
    
    <p style="margin-top: 20px;">
        <a href="/success">查看已上传的花卉</a>
    </p>
</body>
</html>

🔑 关键点:

  1. enctype=“multipart/form-data”

    • 必须设置!否则无法上传文件
    • 告诉浏览器使用 multipart 编码
    • 支持文件二进制传输
  2. name 属性的对应关系

    <!-- 前端 -->
    <input type="text" name="name"/>
    <input type="file" name="photo"/>
    
    <!-- 后端 -->
    public String save(Flower flower, MultipartFile photo)
    // name 字段自动绑定到 flower.name
    // photo 字段自动绑定到 photo 参数
    
  3. accept 属性

    <!-- 只允许上传图片 -->
    <input type="file" accept="image/*"/>
    
    <!-- 只允许上传 PDF -->
    <input type="file" accept="application/pdf"/>
    
    <!-- 允许多种类型 -->
    <input type="file" accept="image/*,.pdf,.doc,.docx"/>
    

8.2 success.html(文件展示页面)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>花卉列表</title>
    <!-- 引入 jQuery -->
    <script type="text/javascript" src="../js/jquery-1.8.3.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f5f5f5;
            padding: 50px;
        }
        h2 {
            color: #333;
        }
        table {
            width: 100%;
            background-color: white;
            border-collapse: collapse;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        thead {
            background-color: #4CAF50;
            color: white;
        }
        td {
            padding: 12px;
            text-align: center;
            border-bottom: 1px solid #ddd;
        }
        img {
            cursor: pointer;
            transition: transform 0.3s;
        }
        img:hover {
            transform: scale(3);
        }
        a {
            color: #4CAF50;
            text-decoration: none;
        }
        a:hover {
            text-decoration: underline;
        }
    </style>
    <script type="text/javascript">
        $(document).ready(function () {
            // 页面加载完成后,发送 Ajax 请求获取花卉列表
            $.get("getAll", function (dt) {
                // dt 是服务器返回的 JSON 数组
                JSON.stringify(dt);
                
                // 清空表格内容
                $("tbody").empty();
                
                // 遍历数据,动态生成表格行
                for (var i = 0; i < dt.length; i++) {
                    // 构造完整的图片 URL
                    // 格式:http://IP:端口/组名/文件路径
                    var imgUrl = 'http://192.168.1.110:8888/' + 
                                 dt[i].groupname + '/' + 
                                 dt[i].remotefilename;
                    
                    // 创建表格行
                    $("<tr>" +
                        "<td>" + dt[i].id + "</td>" +
                        "<td>" + dt[i].name + "</td>" +
                        "<td>" + dt[i].price + "</td>" +
                        "<td>" + dt[i].production + "</td>" +
                        "<td>" +
                            "<img height='20px' width='20px' " +
                                 "title='" + dt[i].orname + "' " +
                                 "src='" + imgUrl + "'/>" +
                        "</td>" +
                        "<td>" +
                            "<a href='download?gname=" + dt[i].groupname + 
                                     "&&orname=" + dt[i].remotefilename + "'>下载</a>" +
                        "</td>" +
                      "</tr>").appendTo("tbody");
                }
            });
        });
    </script>
</head>
<body>
    <h2>花卉信息列表</h2>
    <table>
        <thead>
        <tr>
            <td>花卉编号</td>
            <td>花卉名称</td>
            <td>价钱</td>
            <td>产地</td>
            <td>图片</td>
            <td>操作</td>
        </tr>
        </thead>
        <tbody></tbody>
    </table>
    <p style="margin-top: 20px;">
        <a href="/save">添加新花卉</a>
    </p>
</body>
</html>

🔍 图片访问原理:

1. 前端构造 URL:
   http://192.168.1.110:8888/group1/M00/00/00/xxx.jpg
   
2. 浏览器发送请求到 Storage Server 的 Nginx

3. Nginx + FastDFS 模块解析请求:
   - 组名:group1
   - 文件路径:M00/00/00/xxx.jpg
   
4. Nginx 从磁盘读取文件:
   /data/fastdfs/storage/data/M00/00/00/xxx.jpg
   
5. 返回文件内容给浏览器

6. 浏览器显示图片

第九步:单元测试

DemoTest.java

package com.jr;

import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.junit.Test;
import com.jr.util.FastDFSUtils;

import java.io.*;
import java.util.Arrays;

/**
 * FastDFS 功能测试
 */
public class DemoTest {
    
    /**
     * 测试文件上传
     * 
     * 两种上传方式:
     * 1. File 对象:不保存元数据
     * 2. InputStream:保存元数据(推荐)
     */
    @Test
    public void test01() throws FileNotFoundException {
        // 方式1:使用 File 上传(不保存元数据)
        /*
        String[] strings = FastDFSUtils.uploadFile(
            new File("C:\\Users\\CuiDa\\Desktop\\壁纸\\1.png"), 
            "1.png"
        );
        System.out.println(Arrays.toString(strings));
        */

        // 方式2:使用 InputStream 上传(保存元数据)⭐ 推荐
        String[] strings = FastDFSUtils.uploadFile(
            new FileInputStream(new File("C:\\Users\\CuiDa\\Desktop\\壁纸\\1.png")),
            "1.png"
        );
        
        // 输出结果:[group1, M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.png]
        System.out.println(Arrays.toString(strings));
        System.out.println("组名:" + strings[0]);
        System.out.println("文件路径:" + strings[1]);
        
        // ⭐ 访问 URL:
        // http://192.168.1.110:8888/group1/M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.png
    }

    /**
     * 测试文件下载
     */
    @Test
    public void test2() throws IOException {
        // 第一步:从 FastDFS 下载文件(得到 InputStream)
        InputStream inputStream = FastDFSUtils.downloadFile(
            "group1", 
            "M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.png"
        );

        // 第二步:指定本地保存路径
        OutputStream outputStream = new FileOutputStream("D:\\fastdfs\\11.png");
        
        // 第三步:复制流
        IOUtils.copy(inputStream, outputStream);
        
        // 第四步:关闭流
        inputStream.close();
        outputStream.close();
        
        System.out.println("文件下载成功!保存到:D:\\fastdfs\\11.png");
    }

    /**
     * 测试文件删除
     */
    @Test
    public void test3() {
        // 删除文件
        // 返回值:0 表示成功,非 0 表示失败
        int result = FastDFSUtils.deleteFile(
            "group1", 
            "M00/00/00/wKgBbmjeGuyAWIWOAAp2ubcUNr8520.png"
        );
        
        if (result == 0) {
            System.out.println("文件删除成功!");
        } else {
            System.out.println("文件删除失败!错误代码:" + result);
        }
    }
}

FastDFS 工作原理深度解析

🔍 核心概念

1. FastDFS 文件上传流程
客户端(应用程序)
   ↓
① 发送上传请求到 Tracker Server
   "我要上传一个文件,请分配 Storage"
   ↓
Tracker Server
   ↓
② 根据负载均衡策略选择一个 Storage Server
   策略:轮询、按剩余空间、按上传次数等
   ↓
③ 返回 Storage Server 的 IP 和端口
   "你可以将文件上传到 192.168.1.110:23000"
   ↓
客户端
   ↓
④ 连接到指定的 Storage Server
   ↓
⑤ 上传文件内容(二进制流)
   ↓
Storage Server
   ↓
⑥ 将文件保存到磁盘
   路径生成规则:/data/fastdfs/storage/data/M00/00/00/xxx.jpg
   
⑦ 生成文件ID(包含组名和路径)
   group1/M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.jpg
   
⑧ 如果配置了主从复制,同步到从Storage
   ↓
⑨ 返回文件ID给客户端
   ↓
客户端
   ↓
⑩ 保存文件ID到数据库

💡 关键理解点:

  1. Tracker 不存储文件

    • Tracker 只负责调度和管理
    • 文件实际存储在 Storage Server
    • Tracker 存储的是元数据(哪个文件在哪个 Storage)
  2. 文件路径的含义

    M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.jpg
    \_/ \___/ \________________________________/
     |    |              |
     |    |              文件名(自动生成,包含时间戳、IP等信息)
     |    二级目录(根据文件数量自动创建)
     存储路径(M00 对应配置文件中的第一个 store_path)
    
  3. 负载均衡策略

    • 轮询(Round Robin)
    • 随机(Random)
    • 按剩余空间(Free Space)
    • 按上传次数(Upload Count)
2. FastDFS 文件下载流程

方式1:通过客户端API下载

客户端
   ↓
① 发送下载请求到 Tracker(提供文件ID)
   "我要下载 group1/M00/00/00/xxx.jpg"
   ↓
Tracker Server
   ↓
② 根据组名(group1)查找对应的 Storage Server
   ↓
③ 返回 Storage Server 的 IP 和端口
   "文件在 192.168.1.110:23000"
   ↓
客户端
   ↓
④ 连接到 Storage Server
   ↓
⑤ 发送文件路径
   ↓
Storage Server
   ↓
⑥ 从磁盘读取文件
   ↓
⑦ 返回文件内容(二进制流)
   ↓
客户端
   ↓
⑧ 接收文件内容

方式2:通过HTTP直接访问(推荐)

浏览器
   ↓
① 访问 URL
   http://192.168.1.110:8888/group1/M00/00/00/xxx.jpg
   ↓
Nginx(Storage Server 上)
   ↓
② FastDFS Nginx 模块解析 URL
   - 组名:group1
   - 文件路径:M00/00/00/xxx.jpg
   ↓
③ 根据路径读取文件
   /data/fastdfs/storage/data/M00/00/00/xxx.jpg
   ↓
④ 返回文件内容
   ↓
浏览器
   ↓
⑤ 显示图片或下载文件

💡 为什么推荐HTTP方式?

  • ✅ 直接访问,无需经过应用服务器
  • ✅ 减轻应用服务器压力
  • ✅ 充分利用 Nginx 的性能优势
  • ✅ 支持浏览器缓存
  • ✅ 支持断点续传
3. FastDFS 文件同步机制
主 Storage Server                   从 Storage Server
    ↓                                   ↑
① 接收客户端上传                         |
    ↓                                   |
② 保存文件到磁盘                         |
    ↓                                   |
③ 将文件写入 binlog                      |
    ↓                                   |
④ 通过 binlog 同步到从 Storage  ----------┘

同步特点:

  • 异步同步(不影响上传性能)
  • 断点续传(网络故障后自动恢复)
  • 增量同步(只同步变化的文件)
  • 一主多从(一个主 Storage 可以有多个从 Storage)

完整的文件操作流程

场景 1:用户上传图片

用户在浏览器选择图片(rose.jpg)→ 点击"提交"按钮
   ↓
POST /save1(multipart/form-data)
   ↓
1. FlowerController.save() 接收请求
   - Flower 对象:{name:"玫瑰花", price:25.5, production:"云南"}
   - MultipartFile 对象:{originalFilename:"rose.jpg", size:102400, ...}
   ↓
2. FlowerService.save() 业务处理
   - 获取文件输入流:photo.getInputStream()
   ↓
3. FastDFSUtils.uploadFile() 上传到 FastDFS
   - 连接 Tracker Server
   - Tracker 返回 Storage Server 地址
   - 连接 Storage Server
   - 上传文件内容
   - Storage 保存文件到磁盘
   - 返回文件ID:["group1", "M00/00/00/xxx.jpg"]
   ↓
4. 设置文件信息到 Flower 对象
   - flower.setOrname("rose.jpg")
   - flower.setGroupname("group1")
   - flower.setRemotefilename("M00/00/00/xxx.jpg")
   ↓
5. FlowerMapper.insert() 保存到数据库
   - 插入记录到 flower 表
   ↓
6. 返回 "success" 视图
   ↓
7. Thymeleaf 渲染 success.html
   ↓
8. 浏览器显示成功页面

场景 2:用户查看图片列表

用户访问:http://localhost:8080/success
   ↓
1. FlowerController.url("success")
   - 返回 "success" 视图
   ↓
2. Thymeleaf 渲染 success.html
   - 返回 HTML 页面给浏览器
   ↓
3. 浏览器执行 JavaScript
   - jQuery 发送 Ajax 请求:$.get("/getAll")
   ↓
4. FlowerController.getAll()
   - 调用 FlowerService.findAll()
   - 调用 FlowerMapper.selectAll()
   - 从数据库查询所有花卉记录
   - 返回 JSON 数组
   ↓
5. JavaScript 接收数据
   - 遍历数据,动态生成表格行
   - 构造图片 URL:http://192.168.1.110:8888/group1/M00/00/00/xxx.jpg
   - 插入到表格
   ↓
6. 浏览器加载图片
   - 向 FastDFS 的 Nginx 发送请求
   - Nginx 返回图片内容
   - 浏览器显示图片

场景 3:用户下载文件

用户点击"下载"链接
   ↓
GET /download?gname=group1&orname=M00/00/00/xxx.jpg
   ↓
1. FlowerController.download()
   - 接收参数:gname="group1", orname="M00/00/00/xxx.jpg"
   ↓
2. FastDFSUtils.downloadFile()
   - 连接 Tracker Server
   - Tracker 返回 Storage Server 地址
   - 连接 Storage Server
   - 发送下载请求
   - Storage 返回文件内容(字节数组)
   - 转换为 InputStream
   ↓
3. 设置响应头
   - content-disposition: attachment;filename=xxx.png
   - 告诉浏览器这是一个下载文件
   ↓
4. 将 InputStream 写入响应流
   - IOUtils.copy(inputStream, outputStream)
   ↓
5. 浏览器弹出下载对话框
   - 用户选择保存位置
   - 文件保存到本地

FastDFS 核心功能详解

功能 1:文件上传

实现方式:

// 方式1:通过 InputStream(推荐)
InputStream is = multipartFile.getInputStream();
String[] result = FastDFSUtils.uploadFile(is, "photo.jpg");

// 方式2:通过 File 对象
File file = new File("D:/test.jpg");
String[] result = FastDFSUtils.uploadFile(file, "test.jpg");

返回值:

String[] result = ["group1", "M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.jpg"];
result[0] // 组名
result[1] // 文件路径

完整的访问URL:

http://192.168.1.110:8888/group1/M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.jpg
\___________________/\_____/\___________________________________________/
        服务器地址         组名              文件路径

功能 2:文件下载

实现方式:

// 下载文件(返回 InputStream)
InputStream is = FastDFSUtils.downloadFile("group1", "M00/00/00/xxx.jpg");

// 保存到本地
FileOutputStream fos = new FileOutputStream("D:/download.jpg");
IOUtils.copy(is, fos);
fos.close();
is.close();

// 或者直接输出到浏览器(在 Controller 中)
ServletOutputStream os = response.getOutputStream();
IOUtils.copy(is, os);
os.close();
is.close();

功能 3:文件删除

实现方式:

int result = FastDFSUtils.deleteFile("group1", "M00/00/00/xxx.jpg");
if (result == 0) {
    System.out.println("删除成功");
} else {
    System.out.println("删除失败,错误代码:" + result);
}

注意事项:

  • ⚠️ 删除操作不可逆!
  • ⚠️ 建议使用软删除(数据库标记,文件保留)
  • ⚠️ 删除前确认文件没有被其他地方引用

功能 4:文件修改

实现方式:

// 上传新文件并删除旧文件
String[] result = FastDFSUtils.modifyFile(
    "group1",              // 旧文件组名
    "M00/00/00/old.jpg",   // 旧文件路径
    new File("D:/new.jpg"), // 新文件
    "new.jpg"              // 新文件名
);

// 更新数据库中的文件路径
flower.setGroupname(result[0]);
flower.setRemotefilename(result[1]);
flowerMapper.update(flower);

功能 5:获取文件元数据

实现方式:

NameValuePair[] metadata = FastDFSUtils.getMetaDate("group1", "M00/00/00/xxx.jpg");
for (NameValuePair pair : metadata) {
    System.out.println(pair.getName() + " = " + pair.getValue());
}

// 输出:
// file name = rose.jpg
// file length = 102400

常见问题与解答

Q1:FastDFS 和传统文件存储的区别?

A: 主要区别:

特性传统存储(应用服务器)FastDFS(分布式)
存储位置应用服务器磁盘独立的存储服务器
扩展性难以扩展易于横向扩展
性能占用应用服务器资源专用存储,性能高
高可用单点故障支持主从备份
负载均衡需要手动实现自动负载均衡
访问方式通过应用服务器直接 HTTP 访问

Q2:上传文件时出现"连接超时"错误怎么办?

A: 排查步骤:

  1. 检查 Tracker Server 是否启动

    # Linux 命令
    ps -ef | grep fdfs_trackerd
    
    # 如果没有运行,启动 Tracker
    /usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf restart
    
  2. 检查网络连接

    # 测试端口是否开放
    telnet 192.168.1.110 22122
    
    # 检查防火墙
    firewall-cmd --list-ports
    
  3. 检查配置文件

    # fdfs.properties
    fastdfs.tracker_servers=192.168.1.110:22122  # IP 和端口是否正确?
    fastdfs.connect_timeout_in_seconds=10         # 超时时间是否太短?
    
  4. 增加超时时间

    fastdfs.connect_timeout_in_seconds=30
    fastdfs.network_timeout_in_seconds=60
    

Q3:图片无法显示(404 错误)怎么办?

A: 排查步骤:

  1. 检查 Storage Server 的 Nginx 是否启动

    ps -ef | grep nginx
    
    # 启动 Nginx
    /usr/local/nginx/sbin/nginx
    
  2. 检查 Nginx 配置

    # /usr/local/nginx/conf/nginx.conf
    location ~ /group[0-9]/ {
        ngx_fastdfs_module;
    }
    
  3. 检查访问 URL 是否正确

    正确格式:
    http://192.168.1.110:8888/group1/M00/00/00/xxx.jpg
    
    常见错误:
    - 缺少组名:http://...//M00/00/00/xxx.jpg
    - 端口错误:http://...:22122/... (应该是 8888)
    - 路径错误:http://.../group1/M00/xxx.jpg (缺少目录)
    
  4. 检查文件是否真实存在

    # 在 Storage Server 上查看
    ls -l /data/fastdfs/storage/data/M00/00/00/
    

Q4:文件上传大小限制怎么调整?

A: 需要修改多处配置:

  1. Spring Boot 配置(application.yml)

    spring:
      servlet:
        multipart:
          max-file-size: 100MB      # 单个文件最大 100MB
          max-request-size: 100MB   # 请求最大 100MB
    
  2. Nginx 配置(如果通过 Nginx 上传)

    # /usr/local/nginx/conf/nginx.conf
    http {
        client_max_body_size 100m;  # 允许上传 100MB
    }
    
  3. 重启服务

    # 重启 Nginx
    /usr/local/nginx/sbin/nginx -s reload
    
    # 重启 Spring Boot 应用
    

Q5:如何实现文件秒传(相同文件只存一份)?

A: FastDFS 默认不支持,需要自己实现:

@Service
public class FileService {
    
    /**
     * 文件上传(支持秒传)
     */
    public String upload(MultipartFile file) throws Exception {
        // 1. 计算文件 MD5
        String md5 = DigestUtils.md5Hex(file.getInputStream());
        
        // 2. 查询数据库,看是否已存在相同 MD5 的文件
        FileInfo existFile = fileMapper.selectByMd5(md5);
        if (existFile != null) {
            // 文件已存在,秒传成功(返回已有的文件路径)
            return existFile.getFilePath();
        }
        
        // 3. 文件不存在,上传到 FastDFS
        String[] result = FastDFSUtils.uploadFile(file.getInputStream(), file.getOriginalFilename());
        
        // 4. 保存文件信息到数据库(包含 MD5)
        FileInfo fileInfo = new FileInfo();
        fileInfo.setMd5(md5);
        fileInfo.setGroupName(result[0]);
        fileInfo.setRemoteFileName(result[1]);
        fileMapper.insert(fileInfo);
        
        return fileInfo.getFilePath();
    }
}

下次开发时的快速上手指南

🚀 快速开发步骤(FastDFS 项目)

第一步:引入依赖
<!-- pom.xml -->
<dependency>
    <groupId>cn.bestwu</groupId>
    <artifactId>fastdfs-client-java</artifactId>
    <version>1.27</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>
第二步:配置文件
# fdfs.properties
fastdfs.connect_timeout_in_seconds=10
fastdfs.network_timeout_in_seconds=30
fastdfs.charset=UTF-8
fastdfs.tracker_servers=192.168.1.110:22122
# application.yml
spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB
第三步:复制工具类
复制 FastDFSUtils.java 到项目的 util 包
第四步:实体类添加字段
private String orname;         // 原始文件名
private String groupname;      // FastDFS 组名
private String remotefilename; // FastDFS 文件路径
第五步:Service 层调用
@Service
public class FileServiceImpl {
    public int upload(Entity entity, MultipartFile file) throws IOException {
        // 上传到 FastDFS
        String[] result = FastDFSUtils.uploadFile(file.getInputStream(), file.getOriginalFilename());
        
        // 设置文件信息
        entity.setOrname(file.getOriginalFilename());
        entity.setGroupname(result[0]);
        entity.setRemotefilename(result[1]);
        
        // 保存到数据库
        return mapper.insert(entity);
    }
}
第六步:Controller 处理
@Controller
public class FileController {
    // 上传
    @RequestMapping("/upload")
    public String upload(Entity entity, MultipartFile file) throws IOException {
        service.upload(entity, file);
        return "success";
    }
    
    // 下载
    @RequestMapping("/download")
    @ResponseBody
    public void download(String gname, String rname, HttpServletResponse response) throws IOException {
        response.setHeader("content-disposition", "attachment;filename=" + UUID.randomUUID() + ".jpg");
        InputStream is = FastDFSUtils.downloadFile(gname, rname);
        IOUtils.copy(is, response.getOutputStream());
        is.close();
    }
}
第七步:前端页面
<!-- 上传表单 -->
<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file" required/>
    <button type="submit">上传</button>
</form>

<!-- 显示图片 -->
<img src="http://192.168.1.110:8888/{{groupname}}/{{remotefilename}}"/>

<!-- 下载链接 -->
<a href="/download?gname={{groupname}}&rname={{remotefilename}}">下载</a>

📋 核心配置清单

配置项说明示例
tracker_serversTracker 服务器地址192.168.1.110:22122
connect_timeout连接超时时间(秒)10
network_timeout网络超时时间(秒)30
max-file-size最大文件大小10MB
HTTP 端口Nginx 端口8888

🎯 最佳实践

  1. 文件命名规范

    使用 FastDFS 自动生成的文件名,不要自定义
    原因:
    - 自动包含时间戳
    - 自动包含服务器信息
    - 防止文件名冲突
    
  2. 数据库设计

    -- 必须保存的字段
    CREATE TABLE file_info (
        orname VARCHAR(200),         -- 原始文件名
        groupname VARCHAR(50),       -- 组名
        remotefilename VARCHAR(200)  -- 文件路径
    );
    
    -- 可选字段
    file_size BIGINT,              -- 文件大小
    file_type VARCHAR(50),         -- 文件类型
    upload_time DATETIME,          -- 上传时间
    md5 VARCHAR(32)                -- 文件 MD5(用于秒传)
    
  3. 异常处理

    try {
        String[] result = FastDFSUtils.uploadFile(...);
        if (result == null) {
            throw new RuntimeException("文件上传失败");
        }
    } catch (Exception e) {
        log.error("文件上传异常", e);
        throw new BusinessException("文件上传失败,请稍后重试");
    }
    
  4. 性能优化

    // 1. 使用连接池(FastDFSUtils 已实现单例)
    // 2. 大文件使用异步上传
    @Async
    public void uploadAsync(MultipartFile file) {
        // 异步上传
    }
    
    // 3. 图片压缩后再上传
    BufferedImage compressed = Thumbnails.of(file.getInputStream())
        .scale(0.5)  // 缩小到 50%
        .asBufferedImage();
    
  5. 安全防护

    // 1. 文件类型校验
    String contentType = file.getContentType();
    if (!contentType.startsWith("image/")) {
        throw new RuntimeException("只允许上传图片");
    }
    
    // 2. 文件大小校验
    if (file.getSize() > 10 * 1024 * 1024) {
        throw new RuntimeException("文件大小不能超过 10MB");
    }
    
    // 3. 文件名校验
    String fileName = file.getOriginalFilename();
    if (fileName.contains("../") || fileName.contains("..\\")) {
        throw new RuntimeException("文件名非法");
    }
    

🎓 总结

FastDFS 的核心价值

  1. 解决文件存储问题

    • 文件与应用分离
    • 支持海量文件存储
    • 提供高性能访问
  2. 提高系统可用性

    • 支持主从备份
    • 自动故障转移
    • 负载均衡
  3. 简化开发

    • 提供简单的 API
    • 支持 HTTP 直接访问
    • 无需关心存储细节

下次开发时记住这些

  1. ✅ 复制 FastDFSUtils 工具类
  2. ✅ 配置 fdfs.properties(Tracker 地址)
  3. ✅ 实体类添加三个字段(orname、groupname、remotefilename)
  4. ✅ 表单设置 enctype=“multipart/form-data”
  5. ✅ 图片访问格式:http://IP:8888/group1/M00/00/00/xxx.jpg

关键概念回顾

概念说明
Tracker Server跟踪服务器,负责调度和管理
Storage Server存储服务器,实际存储文件
Group组,同组内服务器互为备份
FileID文件ID,包含组名和路径
StorageClient存储客户端,用于文件操作
MultipartFileSpring 提供的文件上传接口

🎉 恭喜您!现在您已经全面掌握了 FastDFS 的使用方法和工作原理!

下次开发时,只需按照本文档的步骤操作,就能快速集成 FastDFS! 🚀

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值