详细介绍:FastDFS分布式文件系统
FastDFS 完整使用指南
本文档基于实际项目,全面讲解 FastDFS 分布式文件系统的使用方法、工作原理和最佳实践
目录
FastDFS 是什么
核心定位
FastDFS 是一个开源的分布式文件系统,专为互联网应用设计,具有:
- ✅ 高性能 - 单台服务器可支撑百万级文件存储
- ✅ 高可用 - 支持冗余备份,自动故障转移
- ✅ 分布式 - 支持横向扩展,无中心节点设计
- ✅ 负载均衡 - 自动负载均衡,智能选择存储服务器
- ✅ 轻量级 - 使用 C 语言实现,占用资源少
- ✅ 简单易用 - API 简单,易于集成
为什么需要 FastDFS?
不使用分布式文件系统的后果:
- ❌ 文件存储在应用服务器,占用大量磁盘空间
- ❌ 文件访问消耗应用服务器带宽和性能
- ❌ 水平扩展困难,无法共享文件
- ❌ 单点故障风险高
- ❌ 缺乏文件管理和监控手段
使用 FastDFS 的好处:
- ✅ 文件与应用分离,互不影响
- ✅ 支持高并发访问,提供 HTTP 服务
- ✅ 自动负载均衡和故障转移
- ✅ 支持主从备份,数据安全可靠
- ✅ 适合存储大量小文件(4KB - 500MB)
- ✅ 内置防盗链、限速、访问控制等功能
️ FastDFS 架构组成
FastDFS 架构
│
├─ Tracker Server(跟踪服务器)
│ ├─ 管理 Storage Server
│ ├─ 记录文件存储位置
│ ├─ 负载均衡调度
│ └─ 集群协调
│
├─ Storage Server(存储服务器)
│ ├─ 实际存储文件
│ ├─ 提供文件上传下载
│ ├─ 文件同步备份
│ └─ 文件元数据管理
│
└─ Client(客户端)
├─ 应用程序集成
├─ 调用 FastDFS API
└─ 连接 Tracker/Storage
角色说明:
Tracker Server(跟踪服务器)
- 负责调度和管理
- 记录所有 Storage Server 的状态
- 接收客户端请求,返回可用的 Storage Server
- 不存储实际文件,只存储元数据
- 支持集群部署(建议至少 2 台)
Storage Server(存储服务器)
- 实际存储文件
- 提供文件上传、下载、删除等操作
- 支持分组(Group),同组内服务器互为备份
- 每个组可以独立扩容
- 内置 HTTP 服务器(通过 Nginx 模块)
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 客户端,提供文件操作 APIcommons-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
配置说明:
tracker_servers:
- 这是最重要的配置
- 指定 Tracker Server 的地址和端口
- 如果有多个 Tracker,用逗号分隔
- 示例:
192.168.1.110:22122,192.168.1.111:22122
连接超时和网络超时:
- 根据网络环境调整
- 内网环境可以设置较小值(5-10 秒)
- 跨机房访问建议设置较大值(30-60 秒)
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='花卉信息表';
为什么要保存这些字段?
orname(原始文件名)
- 保存用户上传的原始文件名
- 用于下载时设置文件名
- 用于显示文件信息
groupname(组名)
- FastDFS 返回的组名(如:group1)
- 下载和删除时需要提供
- 示例:
group1
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() {
}
}
核心理解点:
静态初始化块的作用:
- 在类加载时执行一次
- 初始化 FastDFS 连接
- 连接失败会抛出 Error 终止程序
为什么使用静态成员:
- 避免重复创建连接
- 提高性能(连接复用)
- 线程安全(StorageClient 是线程安全的)
上传方法的选择:
- Web 应用:使用
uploadFile(InputStream, String) - 本地文件:使用
uploadFile(File, String)
- Web 应用:使用
文件路径的组成:
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();
}
}
关键理解点:
MultipartFile 参数自动绑定:
// 表单中的 name="photo" 会自动绑定到参数 public String save(Flower flower, MultipartFile photo)文件下载的响应头:
// attachment:附件(下载) response.setHeader("content-disposition", "attachment;filename=" + fileName); // inline:内联(在线预览,适用于图片、PDF) response.setHeader("content-disposition", "inline;filename=" + fileName);流的复制:
// 手动复制(不推荐) 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>
关键点:
enctype=“multipart/form-data”
- 必须设置!否则无法上传文件
- 告诉浏览器使用 multipart 编码
- 支持文件二进制传输
name 属性的对应关系
<!-- 前端 --> <input type="text" name="name"/> <input type="file" name="photo"/> <!-- 后端 --> public String save(Flower flower, MultipartFile photo) // name 字段自动绑定到 flower.name // photo 字段自动绑定到 photo 参数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到数据库
关键理解点:
Tracker 不存储文件
- Tracker 只负责调度和管理
- 文件实际存储在 Storage Server
- Tracker 存储的是元数据(哪个文件在哪个 Storage)
文件路径的含义
M00/00/00/wKgBbmjd17yABDG1AAp2ubcUNr8317.jpg \_/ \___/ \________________________________/ | | | | | 文件名(自动生成,包含时间戳、IP等信息) | 二级目录(根据文件数量自动创建) 存储路径(M00 对应配置文件中的第一个 store_path)负载均衡策略
- 轮询(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: 排查步骤:
检查 Tracker Server 是否启动
# Linux 命令 ps -ef | grep fdfs_trackerd # 如果没有运行,启动 Tracker /usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf restart检查网络连接
# 测试端口是否开放 telnet 192.168.1.110 22122 # 检查防火墙 firewall-cmd --list-ports检查配置文件
# fdfs.properties fastdfs.tracker_servers=192.168.1.110:22122 # IP 和端口是否正确? fastdfs.connect_timeout_in_seconds=10 # 超时时间是否太短?增加超时时间
fastdfs.connect_timeout_in_seconds=30 fastdfs.network_timeout_in_seconds=60
Q3:图片无法显示(404 错误)怎么办?
A: 排查步骤:
检查 Storage Server 的 Nginx 是否启动
ps -ef | grep nginx # 启动 Nginx /usr/local/nginx/sbin/nginx检查 Nginx 配置
# /usr/local/nginx/conf/nginx.conf location ~ /group[0-9]/ { ngx_fastdfs_module; }检查访问 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 (缺少目录)检查文件是否真实存在
# 在 Storage Server 上查看 ls -l /data/fastdfs/storage/data/M00/00/00/
Q4:文件上传大小限制怎么调整?
A: 需要修改多处配置:
Spring Boot 配置(application.yml)
spring: servlet: multipart: max-file-size: 100MB # 单个文件最大 100MB max-request-size: 100MB # 请求最大 100MBNginx 配置(如果通过 Nginx 上传)
# /usr/local/nginx/conf/nginx.conf http { client_max_body_size 100m; # 允许上传 100MB }重启服务
# 重启 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_servers | Tracker 服务器地址 | 192.168.1.110:22122 |
| connect_timeout | 连接超时时间(秒) | 10 |
| network_timeout | 网络超时时间(秒) | 30 |
| max-file-size | 最大文件大小 | 10MB |
| HTTP 端口 | Nginx 端口 | 8888 |
最佳实践
文件命名规范
使用 FastDFS 自动生成的文件名,不要自定义 原因: - 自动包含时间戳 - 自动包含服务器信息 - 防止文件名冲突数据库设计
-- 必须保存的字段 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(用于秒传)异常处理
try { String[] result = FastDFSUtils.uploadFile(...); if (result == null) { throw new RuntimeException("文件上传失败"); } } catch (Exception e) { log.error("文件上传异常", e); throw new BusinessException("文件上传失败,请稍后重试"); }性能优化
// 1. 使用连接池(FastDFSUtils 已实现单例) // 2. 大文件使用异步上传 @Async public void uploadAsync(MultipartFile file) { // 异步上传 } // 3. 图片压缩后再上传 BufferedImage compressed = Thumbnails.of(file.getInputStream()) .scale(0.5) // 缩小到 50% .asBufferedImage();安全防护
// 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 的核心价值
解决文件存储问题
- 文件与应用分离
- 支持海量文件存储
- 提供高性能访问
提高系统可用性
- 支持主从备份
- 自动故障转移
- 负载均衡
简化开发
- 提供简单的 API
- 支持 HTTP 直接访问
- 无需关心存储细节
下次开发时记住这些
- ✅ 复制 FastDFSUtils 工具类
- ✅ 配置 fdfs.properties(Tracker 地址)
- ✅ 实体类添加三个字段(orname、groupname、remotefilename)
- ✅ 表单设置 enctype=“multipart/form-data”
- ✅ 图片访问格式:http://IP:8888/group1/M00/00/00/xxx.jpg
关键概念回顾
| 概念 | 说明 |
|---|---|
| Tracker Server | 跟踪服务器,负责调度和管理 |
| Storage Server | 存储服务器,实际存储文件 |
| Group | 组,同组内服务器互为备份 |
| FileID | 文件ID,包含组名和路径 |
| StorageClient | 存储客户端,用于文件操作 |
| MultipartFile | Spring 提供的文件上传接口 |
恭喜您!现在您已经全面掌握了 FastDFS 的使用方法和工作原理!
下次开发时,只需按照本文档的步骤操作,就能快速集成 FastDFS!

浙公网安备 33010602011771号