做了多年Java全栈,见过太多号称"云盘管理系统"的课设和毕设代码,说实话大部分只是把一个File上传接口包了三层壳:前端一个“选择文件”按钮,后端一个MultipartFile接收,存进本地文件夹就完事了。真正把"个人云盘"当成一个产品来做,而不是当作业来交的,少之又少。
这篇博文想从一个相对完整的角度,拆解一个Spring Boot + 原生JS实现的个人云盘管理系统。不吹架构、不堆中间件,重点讲清楚文件上传链路、数据模型、权限隔离、部署后的真实问题这几个核心模块是怎么设计和落地的。如果你正在做类似的课设、毕设,或者想给自己搭一个私有网盘,这篇内容可以直接当一份“避坑+实现”的参考手册来用。
1. 从“网盘”看技术选型:为什么Spring Boot + 原生JS这个组合够用
很多人一看到"个人云盘管理系统",下意识就觉得要上FastDFS、MinIO、OSS一套全家桶。但对于一个单机部署、面向个人或小规模用户场景的系统,这些重型依赖反而会带来不必要的运维成本。我见过一个课设用上了FastDFS + Nginx + Redis + RabbitMQ,最后答辩时连环境都跑不起来——这属于典型的选型大于需求。
1.1 后端选型:Spring Boot解决的是“开发效率”问题
Spring Boot在这一类管理系统里的核心价值,不是它有多强的文件处理能力,而是它把Web层的配置复杂度降到了极低。你不需要去配web.xml、不需要手动管理DispatcherServlet、不需要纠结jar包版本冲突。一个spring-boot-starter-web依赖,就能把HTTP接口、参数绑定、异常处理、静态资源映射全部串联起来。
在文件上传这个核心场景里,Spring Boot的MultipartFile抽象让接收文件变得非常干净:
java复制@PostMapping("/upload")
public Result upload(@RequestParam("file") MultipartFile file,
@RequestParam("parentId") Long parentId,
@RequestParam("userId") Long userId) {
// 业务处理
}
但要注意,Spring Boot提供的只是“接住文件”的能力,真正决定云盘好不好的,是接住之后你怎么处理。文件落地策略、分片合并、hash校验、目录结构组织,这些都需要自己实现。所以在骨架层面选Spring Boot是对的,但别指望框架帮你解决业务问题。
1.2 前端选型:原生JS在管理系统中的地位依然稳固
标题里写的是“基于JS”,这里的JS指的是前端交互层用JavaScript实现,不依赖重量级框架。很多人会觉得不用Vue/React就是落后的,但从实际经验来看,对于文件管理这种以表单、列表、弹窗为主的中后台界面,原生JS + XHR/Fetch + 手动DOM操作完全够用,而且调试起来更直接。
更重要的是,原生JS在“上传大文件分片”这个场景里有个隐藏优势:你可以直接控制Blob.slice()切割文件,通过XMLHttpRequest逐片发送,完全掌握上传进度和失败重试逻辑。如果用框架封装的multipart上传组件,反而要花很多精力去处理组件的黑盒行为。
javascript复制function uploadChunk(file, chunkIndex, chunkSize) {
const start = chunkIndex * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const blob = file.slice(start, end);
const formData = new FormData();
formData.append('file', blob);
formData.append('fileName', file.name);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', Math.ceil(file.size / chunkSize));
return axios.post('/api/file/uploadChunk', formData);
}
这套代码在任何基于浏览器的管理系统中都能直接迁移,不依赖任何框架的版本更新,这是原生JS在这个项目里最值钱的地方。
1.3 技术栈全景与职责边界
| 层次 | 技术/工具 | 负责的事情 |
|---|---|---|
| 前端 | 原生HTML/CSS/JavaScript | 文件列表渲染、上传进度、弹窗交互 |
| 后端 | Spring Boot 2.x | 业务接口、文件存储、权限校验 |
| 数据库 | MySQL 5.7+ / 内置H2 | 用户信息、文件元数据、分享记录 |
| 文件存储 | 本地磁盘目录 | 物理文件落地、分片临时存储 |
| 认证方式 | JWT + 拦截器 | 登录状态校验、接口访问控制 |
这个组合的优点就两个:部署简单(一个jar包 + 一个数据库 + 一个上传目录),代码结构清晰(每一层做什么一眼能看懂)。这对课程设计、毕业设计、个人私有化部署都是最务实的方案。如果你以后要往企业级扩展,把本地存储换成MinIO或者OSS,只需要改一个StorageService接口的实现类,其他业务代码完全不用动。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 文件上传链路:分片、断点续传与秒传的落地细节
网盘系统的技术核心,90%集中在文件上传这一条链路上。个人云盘最常见的场景是什么?往云盘里扔大文件——视频素材、系统镜像、设计源文件。这种几个GB级别的文件,如果走普通的MultipartFile一次性上传,轻则超时,重则内存溢出直接让服务挂掉。
2.1 为什么一定要做分片上传
Tomcat默认的maxPostSize只有2MB,Spring Boot的spring.servlet.multipart.max-file-size默认也只有1MB。即便你把这几个参数调到很大,一次性接收大文件依然有两个要命的问题:
一是网络不稳定导致的整体失败。传了80%断网,全部作废,只能从头再来。二是服务器内存压力。Spring Boot处理MultipartFile时会把文件先写入临时目录,如果你的目录空间不足或者并发量大,IO和内存都会被拖垮。
分片上传的思路就一句话:前端把大文件切成固定大小的块(比如每片4MB),后端每收到一片就落地一片,等所有分片到齐后合成完整文件。这样任何一个分片失败,只需要重传这个分片,而不影响其他分片。
2.2 分片上传后端设计:临时目录、合并、二次校验
后端的核心逻辑包含三个动作:接收分片、记录分片状态、合并文件。
接收分片时,我会为每次上传任务生成一个唯一标识(基于userId + timestamp + 随机数),然后为这个任务建一个独立的临时目录。每个分片以chunkIndex命名存储。这里有一个非常关键的细节:分片落盘时必须做校验,防止脏数据。推荐的做法是前端在分片上传时附带该分片的MD5,后端落盘后计算一次MD5,不一致就返回失败。
java复制// 分片上传
@PostMapping("/uploadChunk")
public Result uploadChunk(@RequestParam("file") MultipartFile file,
@RequestParam("uploadId") String uploadId,
@RequestParam("chunkIndex") Integer chunkIndex,
@RequestParam("md5") String md5) throws IOException {
// 1. 创建本次上传任务临时目录
String tempDir = uploadConfig.getTempPath() + File.separator + uploadId;
File dir = new File(tempDir);
if (!dir.exists()) dir.mkdirs();
// 2. 分片落盘
File chunkFile = new File(tempDir, chunkIndex + ".part");
file.transferTo(chunkFile.getAbsolutePath());
// 3. MD5校验,不一致则删除并报错
String calcMd5 = DigestUtils.md5DigestAsHex(new FileInputStream(chunkFile));
if (!calcMd5.equalsIgnoreCase(md5)) {
chunkFile.delete();
return Result.error("分片校验失败");
}
return Result.success("分片上传成功");
}
合并操作在最后一个分片到达时触发。合并时要注意顺序——先把所有分片按照chunkIndex排序,然后用FileOutputStream循环写入。这里踩过一个坑:FileOutputStream的write方法频繁调用会导致磁盘IO性能极差,正确做法是使用BufferedOutputStream,缓冲区大小设置为8192字节以上。
合并完成之后,还有一道工序不能省:对合并后的完整文件再算一次MD5,和前端在初始化上传时提交的文件MD5对比。这道工序能兜住分片合并过程中可能出现的字节错乱问题。
2.3 秒传与断点续传:两个大幅提升体验的功能
秒传的原理不复杂:前端在选完文件后,先计算文件的MD5(通过FileReader分片读取计算),然后请求后端接口,看这个MD5在数据库里是否存在。如果存在,说明服务器上已经有一份一模一样的文件了,那就不需要真正传文件,直接在数据库里插入一条文件记录,把物理存储指向已有的文件路径即可。
断点续传则依赖“状态记录”。前端在开始上传前,先请求后端查询该文件已经传了哪些分片(可以通过uploadId查),然后跳过这些分片,只传缺失的部分。这里有个实际经验:前端在每次分片上传成功后,把chunkIndex存到localStorage,刷新页面后可以快速恢复上传状态。
java复制// 查询已上传分片
@GetMapping("/chunkStatus")
public Result chunkStatus(@RequestParam("uploadId") String uploadId) {
String tempDir = uploadConfig.getTempPath() + File.separator + uploadId;
File dir = new File(tempDir);
if (!dir.exists()) {
return Result.success(Collections.emptyList());
}
String[] names = dir.list((d, name) -> name.endsWith(".part"));
List<Integer> uploadedIndexes = new ArrayList<>();
for (String name : names) {
uploadedIndexes.add(Integer.parseInt(name.replace(".part", "")));
}
Collections.sort(uploadedIndexes);
return Result.success(uploadedIndexes);
}
这三个功能(分片、秒传、断点续传)做完,这个云盘才算是有了“网盘”的骨架,而不是一个单纯的文件上传Demo。
3. 数据模型设计:目录树、文件元数据与分享链接
文件系统和数据库的关系,是整个项目里最容易设计糊掉的部分。很多课设代码里只有一个file表,字段就fileName、fileUrl、uploadTime三项,看起来简单,但一旦要实现移动文件、重命名、多级目录、按类型筛选这些基本操作,SQL就开始各种LIKE匹配、各种嵌套查询。归根到底,是没有把“物理文件”和“逻辑文件”分开建模。
3.1 核心表结构:用户表、文件表、分享表
我采用的最小可行模型是三张表:
sql复制-- 用户表
CREATE TABLE `user` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL UNIQUE,
`password` VARCHAR(128) NOT NULL,
`salt` VARCHAR(32) NOT NULL,
`storage_quota` BIGINT DEFAULT 10737418240, -- 默认10GB
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 文件表
CREATE TABLE `file_metadata` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`parent_id` BIGINT DEFAULT 0, -- 父目录ID, 0表示根目录
`file_name` VARCHAR(255) NOT NULL,
`is_folder` TINYINT DEFAULT 0, -- 1表示文件夹
`file_size` BIGINT DEFAULT 0,
`file_md5` VARCHAR(32), -- 物理文件唯一标识
`storage_path` VARCHAR(500), -- 物理存储路径
`file_type` VARCHAR(20), -- 扩展名/类型
`is_deleted` TINYINT DEFAULT 0, -- 软删除标记
`share_status` TINYINT DEFAULT 0, -- 是否被分享
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY `idx_user_parent` (`user_id`, `parent_id`),
KEY `idx_user_md5` (`user_id`, `file_md5`)
);
-- 分享表
CREATE TABLE `file_share` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`file_id` BIGINT NOT NULL,
`share_code` VARCHAR(16) NOT NULL UNIQUE, -- 提取码
`expire_time` DATETIME,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`share_count` INT DEFAULT 0,
KEY `idx_share_code` (`share_code`)
);
3.2 目录树:parentId自关联是成本最低的方案
用parent_id实现多级目录,是关系型数据库处理树形结构的经典做法。查询某个目录下的子文件,一条SQL就够:
sql复制SELECT * FROM file_metadata
WHERE parent_id = #{directoryId} AND user_id = #{userId} AND is_deleted = 0;
但这里有个问题要注意:移动文件夹时不能只改目标文件夹的parent_id,要递归更新所有子孙节点的路径前缀。因为如果前端展示路径用的是一个拼接的全路径字符串(如/文档/工作/项目A),父目录的层级变化会直接影响所有子目录的路径展示。我采用的方案是:文件表里只存parent_id,不存冗余的路径字段,展示时由前端根据目录树的映射关系逐级向上递归拼接。这样虽然多几次查询,但彻底避免了路径字段不一致的问题。
文件重名处理也是必须做的。最简单的策略:在同一parent_id下,如果新增文件的file_name已存在,自动在文件名后追加(1)、(2)之类的后缀。这个逻辑在插入前查一次数据库就能搞定。
java复制private String resolveDuplicateName(String fileName, Long parentId, Long userId) {
String baseName = fileName;
String ext = "";
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex > 0) {
baseName = fileName.substring(0, dotIndex);
ext = fileName.substring(dotIndex);
}
String candidate = fileName;
int suffix = 1;
// 循环检测是否重名
while (fileMetadataMapper.countByParentAndName(candidate, parentId, userId) > 0) {
candidate = baseName + "(" + suffix + ")" + ext;
suffix++;
}
return candidate;
}
3.3 物理文件去重:MD5带来的空间节省
个人云盘有个隐藏需求:用户可能同时上传多份相同的文件(比如同一个安装包存到不同目录)。如果每次都物理存一份,空间浪费非常明显。我利用file_md5字段实现了物理级去重:
上传完成后算出文件MD5,在插入file_metadata之前先查一下该用户是否已经有相同MD5的文件记录。如果存在,直接复用已有的storage_path,不为新文件分配新的物理存储空间。删除其中一个文件时,检查还有没有其他记录引用这个storage_path,没有才真正删除物理文件。这个逻辑叫引用计数,二十分钟就能实现,但非常影响空间利用率。
3.4 软删除:回收站是个必须认真设计的功能
直接DELETE物理记录很容易出现尴尬情况:用户误删重要文件,想找回却发现已经彻底没了。我采用is_deleted软删除方案,删除操作只是把is_deleted置为1,然后定期(比如每晚3点)扫描超过30天的垃圾数据做物理清理。
这里有个细节:删除文件夹时,要递归把该文件夹下所有子文件都标记为软删除。否则文件夹删掉了,但子文件的parent_id还指向一个不存在的节点,会变成孤儿数据。写递归SQL或者Java循环都可以,但务必把逻辑放在一个@Transactional事务里。
4. 权限与安全:个人云盘最容易翻车的地方
网盘类系统的安全等级要求比普通博客系统高一个级别。因为文件即资产,一旦出现越权访问,用户的隐私数据直接裸奔。在这一块的实现上,我踩过、也见过太多坑,挑重点说说。
4.1 登录认证:JWT比Session更适合前后端分离
Spring Boot传统的HttpSession方案在前后端分离架构下有个痛点:跨域场景下Session Cookie的管理非常让人头疼。JWT(JSON Web Token)的方案则是把用户信息加密放进token里,后端不需要保存会话状态,天然适合接口鉴权。
实现流程:
- 用户登录成功后,后端生成JWT token,里面包含
userId和过期时间,用HMAC-SHA256签名。 - 前端把token存到
localStorage,每次请求在Authorization头里带上。 - 后端写一个拦截器(
HandlerInterceptor),拦截所有/api/**接口,校验token合法性并解析出userId,放入ThreadLocal供后续业务逻辑使用。
java复制public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
// 放行登录、注册等接口
if (request.getRequestURI().contains("/login") || request.getRequestURI().contains("/register")) {
return true;
}
String token = request.getHeader("Authorization");
try {
Claims claims = JwtUtil.parseToken(token);
UserContext.setUserId(claims.get("userId", Long.class));
return true;
} catch (Exception e) {
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"登录已过期\"}");
return false;
}
}
}
4.2 越权漏洞:文件接口必须做归属校验
这是个人云盘里最致命的安全漏洞。很多课设代码里下载文件的接口长这样:
java复制@GetMapping("/download")
public ResponseEntity<Resource> download(@RequestParam("fileId") Long fileId) {
FileMetadata file = fileMetadataMapper.selectById(fileId);
// 直接读取file.getStoragePath()返回文件流
}
这个接口只要把fileId从1改到2,就能下载到别人的第一个文件。正确的做法是:查询文件时强制加上user_id = 当前登录用户ID条件。所以下载接口的SQL必须是:
sql复制SELECT * FROM file_metadata
WHERE id = #{fileId} AND user_id = #{userId} AND is_deleted = 0
而不是selectById单字段查询。同理,重命名、移动、删除、获取下载链接——所有涉及文件实体的接口,都要带userId条件过滤。这种“强制数据归属校验”的习惯,值得成为肌肉记忆。
4.3 路径穿越与上传校验
文件系统的../路径穿越攻击,在网盘系统里同样存在。比如用户把文件名改成../../etc/passwd上传,如果后端直接把文件名拼进存储路径,就能实现目录穿越。安全做法是:存储路径的后半段永远用系统生成的名,绝不用用户原始文件名拼路径。
我的做法是用UUID作为存储文件名:
java复制String storedName = UUID.randomUUID().toString().replace("-", "");
String fileExtension = extractExtension(originalFilename);
String fullPath = uploadConfig.getStoragePath() + File.separator + userId
+ File.separator + storedName + fileExtension;
同时,上传白名单校验不能只拦截扩展名。文件内容类型也要看——比如把恶意脚本改成.png上传,再用某些方式触发解析,还是会有风险。比较稳妥的做法:保存文件后通过Files.probeContentType(path)获取真实的MIME类型,和数据库记录的file_type对比,不一致就拒绝入库。
4.4 分享链接鉴权:提取码与时效控制
分享功能给文件加了一个“匿名访问”通道,这正是权限控制最容易漏的地方。我的实现思路是:分享记录生成一个6位随机提取码,访问分享链接时必须同时提供文件ID和提取码才能换取一个临时的只读下载token,这个token有效期通常设为10分钟。文件归属校验在这个场景里替换为“分享码校验”,逻辑等价,但主体从用户变成了分享记录。
存储在file_share表里的expire_time字段,每次访问分享链接都要检查是否过期。过期后自动删除分享记录,避免垃圾数据堆积。
5. 部署之后:真实遇到的性能瓶颈与调优记录
把系统跑在本地IDE里和部署到服务器上是两种完全不同的体验。这里记录几个真实的性能问题,供做完项目要上线部署的同学参考。
5.1 MultipartFile的临时文件与内存溢出
Spring Boot接收上传文件时,默认的spring.servlet.multipart.max-file-size是1MB。我一开始把这两个参数调大:
properties复制spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB
结果并发上传两个大文件时,Java堆内存直接飙升到接近上限。原因在于Spring的StandardMultipartFile在大文件场景下会先把文件写入系统临时目录(java.io.tmpdir),如果这个目录所在磁盘空间不足,或者文件没有及时转移(transferTo),就会占用大量临时磁盘空间甚至导致IO堆积。
优化方案分两步:一是所有上传分片都走流式处理,用InputStream逐段读取写入目标文件,不持有整个文件的字节数组;二是定期清理临时目录中的过期.part文件,否则宕机或异常中断留下的临时文件会越积越多。
5.2 文件下载时的内存与流关闭问题
下载文件最忌讳的做法是把整个文件读进字节数组再返回ResponseEntity。我一开始图省事,写过这样的代码:
java复制byte[] data = Files.readAllBytes(Paths.get(filePath));
return ResponseEntity.ok().body(data);
2GB的文件这么搞,JVM直接OutOfMemoryError。正确的做法是用StreamingResponseBody或FileSystemResource配合ResponseEntity,让Spring MVC把文件流直接写到HTTP响应,不经过堆内存:
java复制@GetMapping("/download")
public ResponseEntity<Resource> download(@RequestParam("fileId") Long fileId) {
FileMetadata file = getOwnedFile(fileId);
FileSystemResource resource = new FileSystemResource(file.getStoragePath());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode(file.getFileName(), "UTF-8"))
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}
5.3 Tomcat连接数配置与并发上传
默认的Tomcat最大线程数是200,最大排队数是100。个人网盘虽然并发量不高,但如果用户同时上传大量分片(每个分片一个HTTP请求),连接数很容易被打满。我在分片上传接口上做了一层并发控制:同一个uploadId的分片请求用Semaphore限制并发数为5。多出来的请求排队等待,避免文件合并时出现多个线程同时写同一个文件的竞争条件。
java复制private final ConcurrentHashMap<String, Semaphore> uploadLocks = new ConcurrentHashMap<>();
@PostMapping("/uploadChunk")
public Result uploadChunk(...) {
Semaphore semaphore = uploadLocks.computeIfAbsent(uploadId, k -> new Semaphore(5));
semaphore.acquire();
try {
// 分片写入逻辑
} finally {
semaphore.release();
// 合并完成后移除锁,避免内存泄漏
}
}
5.4 静态资源映射:存储目录与项目目录分离
一个很容易被忽略的问题是:如果你把上传文件存放在项目内部目录(比如src/main/resources/upload),那么每次重新打包部署,文件就全丢了。正确的落地路径应该完全独立于应用目录。
我在配置里单独定义了一个上传根目录,并通过配置类映射为静态资源路径:
properties复制upload.storage-path=/data/pan/storage
upload.temp-path=/data/pan/temp
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${upload.storage-path}")
private String storagePath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/file/**")
.addResourceLocations("file:" + storagePath + "/");
}
}
这样,应用升级、jar包替换都不会影响已上传的文件,备份时也只需要单独备份/data/pan目录即可。
6. 从“能跑”到“好用”的进阶改造思路
到这里为止,一个个人云盘已经具备了上传下载、目录管理、分享、权限控制、秒传分片这些核心能力。但如果要让它真正好用,或者说在答辩/面试时多几个亮点,下面几个方向是性价比比较高的进阶点。
6.1 在线预览:补齐文件操作闭环
文件传上去了,不能只下载不看。在线预览是整个体验里很重要的一块。实现路径按文件类型拆:
| 文件类型 | 预览方案 | 复杂度 |
|---|---|---|
| 图片 | <img>标签直接加载 |
低 |
| 文本/代码 | 后端读取纯文本返回,前端<pre>展示 |
低 |
前端<iframe>或PDF.js展示 |
低 | |
| Excel/Word | 用配置好的本地Office服务转PDF,再走PDF预览 | 高 |
前两种直接做,PDF用iframe指向文件URL就能在大多数浏览器里预览。Office转PDF的方案,如果环境允许,可以引入本地的LibreOffice命令转格式。这一步做完,云盘的“管理感”会提升很多。
6.2 文件搜索:从目录遍历到倒排索引
随着文件数量增长,按目录一层层点击的效率太低了。我做了个简单的文件名模糊搜索接口,基于MySQL的LIKE '%keyword%',数据量小时够用:
sql复制SELECT * FROM file_metadata
WHERE user_id = #{userId} AND is_deleted = 0
AND file_name LIKE CONCAT('%', #{keyword}, '%')
ORDER BY updated_at DESC
LIMIT 50;
如果以后文件涨到几万条,可以做两级优化:一是用全文索引FULLTEXT(ngram解析器支持中文),二是把频繁查询放到缓存中间件里。但对于个人云盘的规模,把SQL写对、索引建好,基本已经够快了。
6.3 配额管理:存储空间的可视化
用户表的storage_quota字段如果不用,就是个摆设。我做了一个简单的配额检查:每次文件上传前算一下该用户已用空间+本次文件大小,超过配额就拒绝上传。已用空间可以单独维护一张统计表,或者在file_metadata表上做SUM(file_size)聚合查询。文件量在万级以内时聚合查询性能完全没问题。
6.4 异步任务:从同步卡顿到顺滑体验
文件合并、MD5计算、缩略图生成这类耗时操作,如果都放在请求线程里同步执行,用户会明显感觉到卡顿。我引入了一个简单的线程池做异步处理:
java复制@Bean("fileTaskExecutor")
public Executor fileTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("file-task-");
return executor;
}
合并文件、清理临时分片这些非关键操作全部丢给这个线程池执行,前端接口立刻返回“上传成功”,用户感知不到后台还在忙。这个异步化改完,整体的交互流畅度提升非常明显。
最后说两句实在的
做完这个项目,我最大的感受是:网盘类系统是一个“看着简单,越做越深”的典型。文件上传人人都写过,但分片策略、MD5去重、目录树维护、越权防护、临时文件清理,每一个点展开都有一大堆学问。这篇博客记录的所有设计决策,都是基于“单机部署、个人使用、低运维成本”这三个前提,如果你要面向大规模用户,那存储层和缓存层的架构又会有完全不同的取舍。
如果正在做类似系统,我的建议是:先把“文件上传-存储-列表展示-下载”这个最小闭环跑通,再逐步加秒传、分片、分享、回收站这些进阶功能。每一步都在验证前面的设计是否合理,比一开始就想做一个完美系统要实际得多。每个模块的踩坑细节,都是真实项目里换来的一手经验,希望你能少走几步弯路。
