详细介绍: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!

posted @ 2025-11-07 17:09  gccbuaa  阅读(7)  评论(0)    收藏  举报