
简介基于Java Web开发的小型云盘系统完整源码包面向Java后端初学者及毕业设计参考者解决个人或小团队的在线文件存储需求。项目模仿百度网盘核心功能涵盖文件上传、下载、分享、管理及用户权限控制技术栈涉及Servlet、JSP、Spring MVC、MyBatis/Hibernate等常用框架适合系统学习Java Web分层开发。压缩包共204个文件约4.55MB包含50个Java源码、65个编译后的class文件、15个依赖jar包、JSP页面、CSS/JS前端资源及SQL数据库脚本源码目录按Controller、Service、Repository、Model等分层组织便于对照理解请求处理、业务逻辑与数据访问流程。目前已有346人学习适合需要完整项目案例、课程设计或快速搭建云盘原型的学习者。通过学习可掌握文件流操作、数据库设计、会话管理、REST接口设计等关键技能同时了解Maven工程结构与项目部署方法。1. 仿百度网盘的小型云盘这个 javaweb 项目到底能做什么以 javaweb 技术仿照百度网盘做一个小型云盘系统常见交付是一个带 MySQL 脚本和 IDEA 配置的 .zip 项目。它把上传、下载、文件列表、分享这几个基本交互用 Servlet JSP MySQL 完整串起来适合正在找 javaweb 完整案例、想用 MySQL 做数据落地的从业者也适合初学后端的人补足完整链路。很多人从网盘下载资源包后以为导入 IDEA 就能跑结果先被 Tomcat 配置卡住又在上传目录和中文乱码上翻车。这个系统看似只是把文件存一存、再列出来但真正复现时文件落盘和数据库元数据的同步就够折腾一天。下面从拆解设计链路开始给出一套能照着跑起来的落地步骤再把常见问题集中排除掉。2. 系统拆解从上传到下载小型云盘的核心链路与设计取舍2.1 功能清单从百度网盘里挑出必须做的四件事小型云盘没有必要把秒传、离线下载、在线预览全做进去先打通主链路更重要。我一般会按百度网盘的常用操作把最小可用范围划成四个功能登录注册、文件上传、文件下载、目录与文件列表。分享链接可以作为扩展项加入但不是第一版必需。登录注册解决这是谁的文件问题。后面所有文件记录都要关联一个 user_id这样下载、删除、重命名时才能做权限校验。文件上传是核心中的核心涉及到 HTTP multipart 解析、文件流落盘、磁盘文件名与上传人绑定。文件下载则要把数据库记录转换成一次真实的文件流回给浏览器。目录与文件列表负责把 MySQL 里的父子关系渲染成网页树同时给上传、分享入口提供上下文。这四个功能之间是依赖关系没有登录就没有归属没有上传就没有下载没有列表就没有办法定位文件。很多 .zip 项目表面上有很多页面实际上都是在这四条链路上加了点前端样式。设计时不要把代码写散最好让一个 Servlet 只负责一类动作比如 UploadServlet、DownloadServlet、FileListServlet这样后续改权限或加秒传都更容易。2.2 技术选型ServletJSPMySQL 为什么在这个场景里够用标题写的是 javaweb最稳妥的落地组合是 Servlet 4.0/5.0 JSP MySQL Tomcat用 Maven 管理依赖。为什么不直接上 Spring Boot两个原因一是很多课程设计和毕业设计要求能讲清楚 Servlet 生命周期、请求转发、重定向、Session 管理如果直接 Spring Boot答辩时说不清底层反而减分二是这类 .zip 项目的原始代码大多不是 Spring Boot而是传统的 Web 目录结构硬改成 Spring Boot 会把原有结构破坏掉。从性能角度看小型云盘是个人练习不需要高并发。Tomcat 的连接池和 NIO 足够跑通几十个同学同时上传。JDK 用 1.8 或 11 都可以Tomcat 用 8.5 或 9 比较常见MySQL 用 5.7 或 8.0 都行。如果代码里用了 javax.servlet 包Java 11 要选 Tomcat 9如果用了 jakarta.servlet 包就是 Tomcat 10。这个差异是导入 .zip 后最容易遇到的版本坑。存储层面文件不塞进数据库而是放在服务器磁盘数据库只记录文件的元数据。这样做的好处是备份简单直接把上传目录拷走就能恢复坏处是必须把上传目录和项目部署目录分开否则 Tomcat 重启会清空。所以我一般会在 2.3 里统一约定存储路径。2.3 核心流程一次上传和一次下载的完整链路与存储方案上传链路是这样的前端页面通过 FormData 把文件拼成 multipart 请求带上 parentId 和当前登录的用户 sessionUploadServlet 收到请求后用 Part 对象把文件流保存到预先配置好的 uploadDir 目录存储名改成 UUID 前缀接着向 file_record 表插入一条记录包含原始文件名、存储路径、大小、父目录 ID、上传人和上传时间最后返回 JSON 给前端刷新列表。下载链路恰好相反DownloadServlet 收到文件 ID 后先从 file_record 查记录校验当前用户是否有权限再把磁盘文件的输入流用 HttpServletResponse 的输出流写回去。这里要注意响应头 Content-Disposition不然浏览器会把文件当页面打开或者中文文件名乱码这一部分在第 5 章会单独展开。存储方案里最容易被忽略的是存储名和显示名的分离。磁盘上的文件名不要直接用用户上传的原始名因为中文名、特殊字符、同名文件都会造成落盘失败或互相覆盖。常规做法是磁盘名用 UUID 时间戳原始名只存在于数据库的 file_name 字段下载时再把原始名拼到响应头里。上传目录可以在 web.xml 里用 context-param 配置也可以在 ServletContextListener 里读取绝对路径强烈不建议写死在 Servlet 里。底线是保证磁盘文件和数据库记录一一对应如果写盘成功但插入数据库失败要用 try-catch 回滚删除文件否则会出现磁盘一大堆幽灵文件。3. 数据库与接口设计MySQL 建表与上传下载接口约定3.1 建表用户表、文件表、分享表与 UTF-8 边界一个 javaweb 项目完整案例MySQL 版本的含金量主要就在建表语句。小型云盘至少需要三张表user、file_record、share_link。user 存账号密码file_record 存文件和目录元数据share_link 存分享码。以下是常见的建表 SQLCREATE DATABASE IF NOT EXISTS cloud_disk DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE cloud_disk; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE file_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, parent_id INT DEFAULT 0, file_name VARCHAR(255) NOT NULL, storage_name VARCHAR(100) DEFAULT NULL, file_size BIGINT DEFAULT 0, is_dir TINYINT DEFAULT 0, upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_parent (user_id, parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE share_link ( id INT PRIMARY KEY AUTO_INCREMENT, file_record_id INT NOT NULL, share_code VARCHAR(32) NOT NULL, expire_time DATETIME DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_share_code (share_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 SQL 里最容易被忽略的是字符集统一为 utf8mb4。MySQL 的 utf8 在 5.7 里最多存三字节用户上传的文件名如果带 Emoji 或生僻字插入时可能直接报错所以建库建表都要用 utf8mb4。password 字段不要存明文用 SHA-256 或 BCrypt 的摘要字符串长度留够 64。file_record 的 storage_name 可以为 NULL因为目录没有磁盘文件只有子节点。parent_id 默认 0 表示根目录这样省去单独建目录表。3.2 上传与下载接口的路径、参数和返回体接口约定不需要太复杂够用就行。上传接口POST /upload参数为 filePart 文件、parentId父目录 ID需要 Cookie/Session 携带登录态。正常返回 JSON{code:0,data:{id:12,fileName:xx.pdf}}。下载接口GET /download?id12正常情况直接回文件流无权限返回 403文件不存在返回 404。获取文件列表GET /list?parentId0返回 JSON 数组每个元素包含 id、fileName、isDir、fileSize。我一般会把接口统一返回为 JSON而不是让 Servlet 直接写 HTML。原因是前端用 fetch 异步刷新更灵活分页和目录跳转都容易处理。Servlet 层用resp.setContentType(application/json;charsetUTF-8)再把 Map 序列化成 JSON 字符串写回去。小项目可以不引第三方库自己拼 JSON 也够但要注意转义引号最好还是用 Gson 或 Jackson。还有一个容易忽略的约定上传接口一定要在业务层判断 Session 是否登录不要在 Filter 里把所有 POST 都拦掉。因为有些前端会先发一个预检请求或探活请求拦死会导致明明是登录状态却上传失败的错觉。3.3 同名文件、目录树和 share_code三个容易埋雷的表结构设计同一个目录下用户上传两个报告.docx是家常便饭。如果 file_record 里只用 file_name 区分后一条会把前一条覆盖或者语义混乱。解决办法是磁盘存储名用 UUID数据库记录维持两条记录但显示名相同。下载时依据 file_record.id 而不是 file_name这样哪怕同名也不会串文件。列表展示时同名文件会在同一目录里出现两次这是可接受的因为用户看到的文件名本来就是一样的。目录树用 parent_id 实现。每一条 file_record 根据 is_dir 区分文件或目录目录也有 parent_id。删目录时要用递归把子孙节点一起标记删除否则会出现悬空父节点。这里我建议不建外键用外键虽然可以保证完整性但递归删除和整表迁移时容易被约束卡住。靠业务代码控制父子关系配合 parent_id 索引对小项目来说更灵活。分享链接不要用 file_record 自增 id 直接暴露。如果 /download?id1 就能下载第一个人的文件那整个云盘毫无私密性。单独建 share_link 表每次分享生成一个 32 位随机字符串作为 share_code访问时先查 share_code再映射到 file_record id。这样别人拿不到数据库 id就无法遍历文件。expire_time 为空表示永久有效不为空则到期后查询接口自动拒绝。3.4 一个容易被忽略的字段is_dir 与 parent_id 的组合约束有人在列表查询时发现目录和文件混在一起。用 SQL 排序时可以在ORDER BY is_dir DESC, file_name ASC让目录排前面。还有删除文件夹时 file_record 的 storage_name 为 NULLdownload 接口要判断不能对目录执行下载。这些细节虽然不影响跑通但直接影响答辩演示的观感也能避免前端拿到一个目录 ID 后直接下载报 500。4. IDEA 运行 javaweb 项目配置ZIP 导入、Tomcat 启动与上传下载代码4.1 解压 ZIP 后导入 IDEA先看目录结构别直接 Open从 .zip 解压下来的项目不要用 IDEA 的 Open 直接打开根目录否则 Web 工程可能没有被正确识别。我先看根目录下有没有 pom.xml 或 build.gradle。如果是 Maven 项目用 File - New - Project from Existing Sources选择 pom.xml导入方式选 MavenIDEA 会自动下载依赖。如果是没有 Maven 的纯 Web 项目需要手动在 Project Structure - Modules 里添加 Web Facet再配置 Artifact。对 Maven 项目pom.xml 里一般会有这样的打包配置packagingwar/packaging build finalNameclouddisk/finalName /buildfinalName 决定了部署后的应用名最终访问路径是http://localhost:8080/clouddisk/。如果没写默认会用 artifactId 加 version 拼目录名路径就会变得很长。导入后要在 Project Structure - Artifacts 里确认有clouddisk:war exploded的 artifact没有的话点 从 Modules 添加。exploded 模式对调试更友好改 Java 代码后不用重新打 war 包。4.2 初始化数据库并配置连接JDBC URL 三个必加参数数据库连接最好集中在一个工具类里。以下是我常用的 DBUtil 写法注释里的三个参数基本能避开 MySQL 8 的大部分坑public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/cloud_disk?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD root; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }serverTimezoneAsia/Shanghai解决 MySQL 8 的时区报错否则插入时间可能和你本地时间差 8 小时。useSSLfalse避免证书校验失败allowPublicKeyRetrievaltrue解决明文密码连接时无法获取公钥的问题。如果你的 MySQL 是 5.7驱动类com.mysql.cj.jdbc.Driver同样能用不用特意换成com.mysql.jdbc.Driver。4.3 上传文件的关键代码MultipartConfig 与落盘上传是云盘的核心入口。用 Servlet 3.0 的 Part API 写上传代码量比 FileInputStream 解析 multipart 少很多。关键代码WebServlet(/upload) MultipartConfig( maxFileSize 1024 * 1024 * 100, maxRequestSize 1024 * 1024 * 200, fileSizeThreshold 1024 * 1024 ) public class UploadServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(application/json;charsetUTF-8); Part part req.getPart(file); String originalName Paths.get(part.getSubmittedFileName()).getFileName().toString(); String parentId req.getParameter(parentId); String storageName UUID.randomUUID().toString().replace(-, ) _ System.currentTimeMillis() _ originalName; String uploadDir req.getServletContext().getInitParameter(uploadDir); File target new File(uploadDir, storageName); try (InputStream is part.getInputStream(); FileOutputStream os new FileOutputStream(target)) { byte[] buffer new byte[8192]; int len; while ((len is.read(buffer)) ! -1) { os.write(buffer, 0, len); } } // 到这里文件已落盘把 originalName/storageName/size/userId/parentId 写入 file_record // 成功后返回 JSON{code:0,data:{id:12,fileName:xx.pdf}} } }MultipartConfig的maxFileSize控制单个文件大小maxRequestSize控制整个请求体积fileSizeThreshold表示超过 1MB 阈值后临时写入磁盘而不是全放内存。手工拷贝流比part.write()更可控因为可以自己拼接目标路径。Paths.get(part.getSubmittedFileName()).getFileName()是处理某些浏览器会传完整路径的情况保证取到的只是文件名。这段代码里有一个关键前提uploadDir必须来自 web.xml 的 context-param而不是getRealPath(/)。否则在 IDEA 里部署到 target 目录重编译后文件全被清空。4.4 下载文件查记录、校验归属、返回流下载接口如果只做找到文件然后输出流会留下越权漏洞所以必须在校验归属之后才读磁盘。核心方法示意WebServlet(/download) public class DownloadServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { int id Integer.parseInt(req.getParameter(id)); // 根据 id 查询 file_record并校验 user_id 与当前 Session 用户一致 FileRecord file fileDao.findById(id); if (file null) { resp.sendError(404, file not found); return; } // 不校验权限就会产生越权下载这一步不能省 if (file.getUserId() ! currentUserId(req)) { resp.sendError(403, forbidden); return; } resp.setContentType(application/octet-stream); String encoded URLEncoder.encode(file.getFileName(), UTF-8).replace(, %20); resp.setHeader(Content-Disposition, attachment;filename*UTF-8 encoded); try (InputStream is new FileInputStream(file.getStoragePath()); OutputStream os resp.getOutputStream()) { byte[] buffer new byte[8192]; int len; while ((len is.read(buffer)) ! -1) { os.write(buffer, 0, len); } } } }Content-Disposition用filename*UTF-8是为了支持中文文件名URLEncoder.encode会把空格变成加号所以要用replace(, %20)还原。每一次读取都走 8KB 缓冲不把整个文件载入内存100MB 文件也不会把 Tomcat 堆打爆。如果之后要支持 Range 断点下载只需要在这个输出流外层加一个支持 seek 的包装类第 6 章会提到思路。5. 部署避坑Tomcat、MySQL、中文乱码与上传目录的 5 个常见问题5.1 上传几百 KB 就失败maxPostSize 和 maxSwallowSize 的坑现象上传不超过 2MB 时一切正常超过后浏览器报 404 或请求没有响应Tomcat 日志出现关于 maximum allowed size 的异常req.getPart()返回空对象。原因Tomcat 的 Connector 默认限制 POST 请求体大小8.5/9 版本默认 maxPostSize 是 2MB。即使 Servlet 上的MultipartConfig设置了 100MBTomcat 容器在早期阶段就把超过限制的请求拦掉了。maxSwallowSize是另一个连带参数它控制请求体读取出错后Tomcat 是否继续把剩余数据吞掉默认 2MB设置不当会导致连接提前关闭前端莫名其妙收到空响应。解决在conf/server.xml的 Connector 节点上调整两个参数并把 URI 编码一起配好Connector port8080 maxPostSize-1 maxSwallowSize-1 URIEncodingUTF-8 /maxPostSize-1表示不限制表单 POST 体积maxSwallowSize-1表示允许吞掉无限长度的剩余数据。如果使用的是 Tomcat Maven 插件或嵌入式 Tomcat同样要把对应 Connector 参数暴露出来否则光改MultipartConfig没用。5.2 重启 Tomcat 后上传文件全没了别把上传目录放到 target 里现象第一次上传成功打开目录看一眼文件也在但重新 build 或 redeploy 之后列表里的文件记录还在点下载却 404上传目录里的文件也不见了。原因IDEA 部署 war exploded 时默认把项目输出到target/clouddisk。Servlet 里如果用getServletContext().getRealPath(/)拼接上传目录得到的就是 target 下的路径。执行 Maven clean 或者 IDE 重新部署时整个 target 目录被清空上传到该目录的文件自然一起被删掉。数据库里有元数据磁盘上的文件却没了这就形成幽灵记录。解决把上传目录与 Web 应用解耦放在系统绝对路径下。在web.xml里配置一个 context-param例如context-param param-nameuploadDir/param-name param-valueD:/clouddisk/upload/param-value /context-paramServlet 通过getServletContext().getInitParameter(uploadDir)读取不要在代码里硬编码路径。Linux 部署时改成/opt/clouddisk/upload并确保 Tomcat 运行用户对该目录有写权限。这样无论怎么重装应用文件都留在系统路径里。5.3 MySQL 8 连接报 Public Key Retrieval 错误是 allowPublicKeyRetrieval 的问题现象启动 Tomcat 后第一次执行数据库查询控制台抛Public Key Retrieval is not allowed或者是在连接池初始化阶段直接报Access denied用 Navicat 连接同样的密码却正常。原因MySQL 8 默认使用 caching_sha2_password 认证客户端连接时需要从服务器获取公钥完成加密握手。JDBC 驱动出于安全考虑默认不允许自动获取公钥所以连接被拒绝。这个问题在 MySQL 5.7 下不会出现很多 .zip 项目里写的是老驱动连 MySQL 8 就翻车。解决不换密码在 JDBC URL 上追加参数jdbc:mysql://localhost:3306/cloud_disk?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueallowPublicKeyRetrievaltrue只是解决本地开发环境的问题如果你部署到公网更好的做法是给 MySQL 配置 SSL 证书但课程设计阶段这样最省事。如果仍然报错把 MySQL 用户的插件改成 mysql_native_password 也可以只是不如改 URL 干净。5.4 控制台和网页乱码编码要一路设到 server.xml现象IDEA 控制台里的中文日志变成??网页提交的中文文件名存入数据库是乱码从数据库查出来又变成问号下载文件名也乱。原因编码问题不是某一个点造成的而是请求、响应、Tomcat URI 解析、IDEA 控制台四段链路里只要有一段不是 UTF-8就会乱码。Tomcat 的 Connector 默认用 ISO-8859-1 解析 URL 参数Servlet 里如果没有调用req.setCharacterEncoding(UTF-8)表单中文也会按默认编码解析IDEA 控制台默认又可能是 GBK。解决按顺序做三步。IDEA 的 Help - Edit Custom VM Options 里加一行-Dfile.encodingUTF-8Run Configuration 的 VM options 也加同样参数。conf/server.xmlConnector 加URIEncodingUTF-8。每个 Servlet 最先执行req.setCharacterEncoding(UTF-8)JSP 顶部声明% page contentTypetext/html;charsetUTF-8 %。不要觉得只要数据库建了 utf8mb4 就行Java 内存里已经乱码存进去只会更乱。这三步做完基本能保证中文从浏览器到数据库一路是 UTF-8。5.5 下载中文文件名乱码Content-Disposition 要按 RFC 5987 编码现象下载文件时文件内容本身没问题但浏览器弹窗里的文件名是%E4%BB%8A%E6%97%A5...或____.pdf这样的乱码。原因老式Content-Disposition: attachment; filename中文.pdf把非 ASCII 字符直接放进 HTTP 头多数浏览器不会正确解码。有人用URLEncoder.encode(fileName)拼在 filename 后面空格会被编码成加号浏览器又把它解析成加号于是文件名里出现奇怪字符。解决按 RFC 5987 标准用filename*同时保留filename兜底String encoded URLEncoder.encode(file.getFileName(), UTF-8).replace(, %20); resp.setHeader(Content-Disposition, attachment;filename*UTF-8 encoded);注意UTF-8后面是两个单引号中间不能有空格。filename*不支持的老旧浏览器会忽略这段所以也可以把filename${encoded}拼在后面新浏览器优先读filename*。我一般只保留filename*因为现在几乎没有用户还在用 IE8。6. 进阶优化秒传、断点续传和越权防护的最后一公里一个云盘项目能跑通之后下一步通常会被问到三个问题同样文件能不能不重复上传大文件能不能断点续传分享链接会不会泄露别人文件这三件事都不难改难点在于不要破坏现有 file_record 表结构。秒传的核心是文件哈希。前端在上传前先读文件计算 SHA-1 或 MD5后端把 hash 字段加到 file_record 表插入新文件前用SELECT id FROM file_record WHERE hash? AND user_id? AND is_dir0查一下存在就直接返回已有记录前端显示秒传。要注意 hash 只能防完全相同的文件和文件名无关不要凭客户端传来的 hash 覆盖已有文件还要比对文件大小避免碰撞。断点续传要分上传和下载两头说。下载支持续传很简单在 DownloadServlet 里解析请求头 Range通过 File 的 position 或 RandomAccessFile 跳到指定位置只输出剩余字节并把响应码设为 206。上传断点相对复杂前端把文件切片后端用一个 upload_chunk 临时目录保存分片全部到达后再合并。小项目直接分片容易踩合并顺序乱的坑建议给每个分片用文件名后缀记录顺序合并后先比对总大小再落盘。最后的越权防护是我的血泪经验。第一次做云盘时我只在页面隐藏了下载按钮没有在后端校验文件归属别人用/download?id1就能下载第一个人的文件。后来把所有 file_record 查询都强制追加AND user_id当前用户只有通过 share_code 的公开访问才放开。这个习惯我建议你内置到 DAO 层而不要依赖前端判断。把这三项加进现有 .zip 项目后小型云盘在结构和安全上就真的接近一个能给人演示的完整案例了。希望帮到你。本文还有配套的精品资源点击获取