☰
JavaWeb云盘项目实战:Servlet+Tomcat+MySQL部署与源码避坑指南
2026/9/28 21:02:55 网站建设 项目流程

简介:这是一套基于 Java Web 技术仿照百度网盘实现的小型云盘系统源码,主要面向 Java Web 初学者以及需要快速搭建个人或小团队在线存储场景的开发者。项目采用经典分层结构,将控制层、业务逻辑层、数据访问层、数据模型与视图模板清晰分离,完整实现了文件上传、下载、分享、管理以及用户权限控制等核心功能,有助于读者理解从 Servlet/JSP 到 Spring MVC 的典型开发流程。压缩包内共包含 204 个文件,其中有 65 个编译后的 class 文件、50 个 java 源代码、24 个 js 前端脚本、15 个 jar 依赖库、4 个 jsp 页面及 SQL 数据库脚本,整体大小约 4.55MB,目录划分明确,便于导入 IDE 阅读和实践。资源还附带前端静态图片、样式表、配置文件等辅助材料,适合在课程设计或毕业设计中作为基础框架进行二次扩展。当前已有 346 人浏览学习,对于希望掌握 Java Web 项目落地、文件流处理、数据库交互与前后端联调的读者而言,是一份值得参考的完整样例。

1. JavaWeb 云盘到底长什么样:先说清这套包的分层与边界

想用 Java 做一个能在线存储、下载、分享文件的 Web 系统,最适合拿来当课设或毕设的落地方案,就是仿照百度网盘做一个小型云盘。这套 zip 包里的 CloudDrive-master 就是一个 javaweb 项目完整案例,数据库用的是 MySQL,骨架是 Servlet + JSP + JDBC 的分层 MVC,没上 Spring Boot,也没有前后端分离。打开源码目录能直接看到 UpLoadServlet、DownLoadServlet、FileListBizImpl、FileDaoImpl、ShareDaoImpl 这一串类名,Controller、Biz 业务层、Dao 数据访问层的角色一眼就能认出来。

它能解决的是一个个人或小团队最朴素的云存储需求:登录后把文件上传到服务器磁盘,按目录列出自己的文件,支持下载、删除、重命名,还能生成一条分享链接丢给别人。相比直接用百度网盘成品,这套系统最大的价值不在功能多,而在每一层都能拆开讲清楚,你既能回答老师问的“Servlet 怎么接收上传文件”,也能自己动手加一个“回收站”之类的功能。正在做 JavaWeb 课程设计、找工作想放一个完整项目的人,都可以拿它跑通再改。

2. 部署到 IDEA + Tomcat:从 zip 解压到能访问的三个关键动作

把 CloudDrive-master 解压后,千万别急着在 IDEA 里直接点运行。JavaWeb 项目和 Spring Boot 不一样,它没有内置 Tomcat,需要你先匹配运行环境、导入数据库,再把工程挂到外部 Tomcat 上。这个顺序一旦乱了,后面每一步报错都会误导你。

解压时如果提示要密码,先别急着去找“zip 密码移除”工具,很多课设包在传播过程中被压缩软件加上了伪加密标志位,用 7-Zip 能直接解开。这种小问题不值得卡住你五分钟。

2.1 环境匹配:先对着项目形态选 JDK、Tomcat 和 MySQL

从类名能判断,这是典型的原生 Servlet 工程,不是 Maven 管理的 Spring Boot 工程。这种项目的运行环境其实很固定,先用一张表把环境锁死,后面排错会省很多事。

组件推荐版本为什么这么选
JDK1.8(即 Java 8)原生 Servlet + JSP 在 JDK 8 下最稳,JDK 17 在旧 Tomcat 上容易报unrecognized option
Tomcat8.5.x 或 9.0.x代码里用的是javax.servlet.*命名空间,Tomcat 10 全改成了jakarta.servlet.*,直接跑会崩
MySQL5.7 或 8.0两者都能跑;用 8.0 时注意驱动要换成com.mysql.cj.jdbc.Driver
IDEA2020.x 及以上支持 Artifacts 和 Tomcat Server 集成,配置最省事

我见过不少人在环境上翻车,原因不是不会写代码,而是装了个 Tomcat 10 去跑课设包,然后满屏NoClassDefFoundError。先记住这句话:原生 Servlet 工程就用 Tomcat 8.5 或 9.0,别去追新版本。JDK 也建议用 8,不是越新越好。

2.2 数据库初始化:一张用户表加两张业务表

这个云盘的核心数据模型可以简化成三张表:用户表 users、文件表 file_info、分享表 share_info。文件表里存的是文件元数据,不是文件本身;真正的文件字节放在服务器磁盘目录里。下面这段 SQL 是常见做法,你拿到的包里如果有clouddrive.sql之类的脚本,直接执行即可。

CREATE DATABASE IF NOT EXISTS clouddrive DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE clouddrive; CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE file_info ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, file_name VARCHAR(255) NOT NULL, file_size BIGINT DEFAULT 0, file_path VARCHAR(500) NOT NULL, file_type VARCHAR(50), is_dir TINYINT DEFAULT 0, parent_id INT DEFAULT 0, is_share TINYINT DEFAULT 0, share_code VARCHAR(32), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_parent (user_id, parent_id) ); CREATE TABLE share_info ( id INT PRIMARY KEY AUTO_INCREMENT, file_id INT NOT NULL, user_id INT NOT NULL, share_code VARCHAR(32) NOT NULL, visit_count INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_share_code (share_code) );

三个字段特别说明一下。file_path存的是文件在服务器上的物理路径,不是浏览器访问的虚拟路径,下载时靠它定位文件;parent_id用来撑起目录结构,顶层文件的 parent_id 默认 0;share_code是分享时生成的短码,别人访问/share?code=xxx就靠它命中记录。

数据库连接配置一般放在src/db.properties或src/jdbc.properties里,BaseDao 负责读配置、拿 Connection、关资源。配置内容大概是下面这样:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/clouddrive?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=你的数据库密码

如果你用 MySQL 5.7,com.mysql.cj.jdbc.Driver也能用;用 MySQL 8.0 时,serverTimezone=Asia/Shanghai和allowPublicKeyRetrieval=true这两个参数基本是必须的,不加会直接报时区或公钥检索错误。这是我在部署 JavaWeb 项目时最常见的数据库坑,后面避坑章节还会展开。

2.3 IDEA 导入与 Tomcat 部署:war exploded 别选错

环境匹配好了、数据库导入好了,接下来把工程挂到 Tomcat 上。我在 IDEA 里跑这类原生 Servlet 项目的固定步骤是这样的。

第一步,File -> Open选择解压出来的 CloudDrive-master 目录,等 IDEA 识别成普通 Java 工程。第二步,File -> Project Structure -> Project,把 Project SDK 选成 1.8;再切到 Modules 的 Dependencies 页签,确认 lib 目录下的 jar 已经加进来,没有的话手动点 “+” 添加。第三步,切到 Artifacts 页签,点 “+” 选择Web Application: Exploded,IDEA 会生成一个 exploded 部署目录。第四步,配置 Tomcat Server,Deployment 页签里把这个 exploded 包加进去,Application context 建议写成/cloud。第五步,以 debug 模式启动,看 Tomcat Localhost Log 有没有异常。

project 打包这种操作叫 war exploded,不是 war 包。war 是把工程打成压缩包再扔给 Tomcat,每次改代码都要重新打;war exploded 是直接把解压后的目录挂上去,改完 JSP 刷新浏览器就能看到效果。用 IDEA 调试课设项目,一定要选 exploded,这个选择能让你少折腾很多。

web.xml里的 Servlet 映射长这样,启动前先检查一遍路径有没有对不上:

<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>你的包名.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>

url-pattern决定浏览器地址栏输入什么能命中这个 Servlet。比如映射是/login,访问地址就是http://localhost:8080/cloud/login。注意前面的/cloud是 context path,后面跟着的是url-pattern,这两个拼在一起才是完整访问路径。很多人卡在 404,就是只记得 Servlet 路径,忘了带 context path。

3. 源码骨架走读:上传、下载、分享、列表分别落在哪些类上

项目跑起来之后,下一步是搞清楚请求到底走过了哪些类。不要逐行读代码,按“登录 -> 列表 -> 上传下载 -> 分享”这条主线走一遍,每个类各自干什么就清楚了。

3.1 登录与请求流转:从 LoginServlet 到 BaseDao

登录流程是整个系统的入口。浏览器提交用户名密码,LoginServlet接收参数,调用 Dao 层查 users 表,成功后把用户对象放进 session。常见写法是下面这样的:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { request.getSession().setAttribute("loginUser", user); response.sendRedirect(request.getContextPath() + "/listFiles"); } else { request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } }

这段代码有两个细节值得注意。第一,setAttribute("loginUser", user)把登录用户放进了 session,后续列表、上传、下载都从 session 里拿当前用户 id,这是控制“只能操作自己文件”的基础。第二,登录成功后用sendRedirect而不是forward,因为转发后浏览器地址栏还是/login,刷新页面会重复提交表单。

BaseDao在这一层的作用是屏蔽 JDBC 样板代码。它提供一个getConnection()方法和一个closeAll(conn, stmt, rs)方法,所有 Dao 实现类都继承它。你去看FileDaoImpl的时候,如果看到一堆conn.prepareStatement(sql),那就是 BaseDao 在工作。这个类的质量决定后续修改成本,建议先看一眼连接有没有正确关闭。

3.2 上传与下载:UpLoadServlet 和 DownLoadServlet 的分工

上传是整个云盘的核心。浏览器端表单必须写enctype="multipart/form-data",服务端才能用 Part 接文件。如果项目运行在 Servlet 3.0 以上的 Tomcat,上传代码可以这样写:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 从 session 拿当前登录用户 User user = (User) request.getSession().getAttribute("loginUser"); String uploadRoot = "/data/clouddrive/" + user.getId(); Part part = request.getPart("file"); String originalFileName = part.getSubmittedFileName(); // 时间戳前缀避免重名覆盖 String serverFileName = System.currentTimeMillis() + "_" + originalFileName; File dir = new File(uploadRoot); if (!dir.exists()) { dir.mkdirs(); } part.write(uploadRoot + File.separator + serverFileName); // 把元数据写入 file_info,供列表和下载查询 saveFileMeta(user.getId(), serverFileName, part.getSize(), uploadRoot + File.separator + serverFileName); response.sendRedirect(request.getContextPath() + "/listFiles"); }

part.getSubmittedFileName()是 Servlet 3.1 才提供的方法,如果项目用的 Tomcat 比较老,它会返回 null,需要从part.getHeader("content-disposition")里手动截取文件名。serverFileName加上时间戳前缀,是为了防止两个用户上传同名文件互相覆盖。文件存到磁盘之后,马上把文件名、大小、物理路径写进数据库,这样列表页才能展示出来。

下载正好反过来。DownLoadServlet根据前端传的 file_id 查 file_info,拿到物理路径后通过输出流把文件写回浏览器。核心代码是设置响应头:

File downloadFile = new File(fileInfo.getFilePath()); response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileInfo.getFileName(), "UTF-8")); try (InputStream in = new FileInputStream(downloadFile); OutputStream out = response.getOutputStream()) { byte[] buffer = new byte[4096]; int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } }

Content-Disposition里的attachment告诉浏览器这是下载不是打开;文件名那块必须用URLEncoder.encode包一层,否则中文文件名会变成乱码甚至直接下载失败。try-with-resources能保证流自动关闭,这是 Java 7 以后的语法,JDK 8 完全支持。

3.3 列表与分享:FileListBizImpl、FileManageBizImpl 与 ShareDaoImpl

列表页走的是ListFilesServlet -> FileListBizImpl -> FileDaoImpl这条链。Servlet 只负责取参数、调 Biz、转发 JSP,具体的 SQL 逻辑全部收在 FileListBizImpl 里。常见的列表查询是按用户和父目录过滤:

public List<FileInfo> listByParent(int userId, int parentId) { String sql = "SELECT * FROM file_info WHERE user_id = ? AND parent_id = ? ORDER BY create_time DESC"; // 这里通过 BaseDao 获取连接,用 PreparedStatement 执行 }

ListFilesServlet 拿到集合后放进 request,再forward到list.jsp循环展示。这里必须注意一个权限问题:SQL 里一定要带user_id = ?,只查当前登录用户的文件。如果漏了这个条件,用户 A 登录后直接在地址栏改 user_id 参数,就能看到用户 B 的文件,这是越权漏洞。

文件管理交给FileManageBizImpl,它负责删除和重命名。删除操作有个顺序讲究:先删数据库记录,再删磁盘文件。原因是先删磁盘的话,如果数据库删除失败,就留下一个“数据库里不存在但磁盘上有文件”的孤儿;反过来数据库删掉以后磁盘删不删得掉,最多是多占点硬盘,不会导致数据错乱。

分享功能由ShareDaoImpl承担。用户点击分享时,服务端生成一个短码,更新 file_info 的 share_code,并把记录写入 share_info。短码生成我一般用 UUID 截取:

String shareCode = UUID.randomUUID().toString().replace("-", "").substring(0, 8); // 更新文件表分享状态 fileDao.updateShareCode(fileId, shareCode); // 写入分享记录表 shareDao.saveShare(fileId, userId, shareCode); String shareUrl = request.getContextPath() + "/share?code=" + shareCode;

分享链接是“短码 + 查询参数”的结构,短码撞车的概率很低,但因为只有 8 位,严谨一点可以在插入前查一下重。生成链接时一定记得带request.getContextPath(),不然部署在/cloud下,别人点开会直接 404。

4. 避坑:跑 JavaWeb 云盘最容易翻车的五个点

这一章是血泪经验。下面五个坑,每一个我都见过不止一个人在群里问过,而且排错时间基本都在半小时以上。按“现象、原因、解决”记下来,能少走很多弯路。

4.1 数据库与容器的坑

第一个坑:MySQL 8 连接直接抱错Public Key Retrieval is not allowed或caching_sha2_password相关异常。现象是项目启动正常,但一登录就 500,控制台提示数据库连接失败。原因有两个:一是 MySQL 8.0 默认认证插件是caching_sha2_password,老版本的 mysql-connector-java 驱动不认识;二是连接串里没开公钥检索允许。解决方法是换com.mysql.cj.jdbc.Driver驱动,并在 jdbc.url 末尾加allowPublicKeyRetrieval=true&useSSL=false。如果你是用 MySQL 8.0 的 zip 包安装的,数据库装完还要注意服务名和密码是否和配置文件里一致,这是另一个高频低级错误。

第二个坑:Tomcat 10 跑旧工程,启动后访问任何页面都报NoSuchMethodError或ClassNotFoundException: javax.servlet.*。现象很奇怪,因为编译是过的,但运行就找不到类。原因是 Tomcat 10 把javax.servlet换成了jakarta.servlet,原生 Servlet 工程的代码里全是javax,自然找不到。解决方法是铁了心用 Tomcat 8.5 或 9.0,这是跑课设包最省心的容器版本。如果你已经在用 Tomcat 10,卸载换掉,别想着改代码适配,那工作量等于重写。

第三个坑:500 错误里出现JasperException: Unable to compile class for JSP。现象是 JSP 页面编译失败,服务器日志一堆 Java 编译错误。原因通常是 JDK 版本和 Tomcat 版本组合不对,常见于 JDK 17 + Tomcat 9 的组合,或者 lib 目录下缺少 JSTL 依赖。解决方法是把环境锁回 JDK 8 + Tomcat 8.5,并且确认WEB-INF/lib下有jstl.jar和standard.jar。我的习惯是拿到的项目先看一眼 lib 目录,没有这两个 jar 就补上。

4.2 上传下载文件名的坑

第四个坑:上传后文件名变成一串乱码,或者保存后的名字不对。现象是上传 “工作报告.doc”,列表里显示的是 “��告.doc” 或者干脆空名字。原因是表单没设置enctype="multipart/form-data",或者服务端在解析文件名时按 ISO-8859-1 读了一遍。解决方法是三层一起查:前端 form 标签加enctype="multipart/form-data";doPost 最开头写request.setCharacterEncoding("UTF-8");如果是手动从content-disposition截文件名,用new String(value.getBytes("ISO-8859-1"), "UTF-8")做一次转换。这三层缺哪一层都可能出乱码。

第五个坑:下载文件名乱码,浏览器里显示一串%E4%B8%AD%E6%96%87之类的百分号。现象是能下载,但文件名不是中文。原因是没有对文件名做 URL 编码,或者编码方式不对。解决方式就是上面下载代码里写的那样:filename=后面拼URLEncoder.encode(fileInfo.getFileName(), "UTF-8"),不要直接拼原始中文。有一点要注意,URLEncoder会把空格编码成+,多数浏览器能识别,如果遇到下载名变成加号的,就把+替换成%20。

4.3 部署与分享的坑

第六个坑隐藏得比较深:分享链接生成后别人访问 404。现象是自己本机打开分享链接没问题,换台电脑或者用手机打开就是 404。原因基本是链接拼接时漏了 context path。比如部署 context path 是/cloud,但代码里拼成/share?code=abc,Tomcat 会把请求发到根路径/share,自然找不到 Servlet。解决方法是生成分享链接时统一用request.getContextPath() + "/share?code=" + shareCode,不要裸写/share。这个坑在局域网联调时尤其常见,因为本机还能靠 Tomcat 的默认访问绕过去。

第七个坑也算一个:session 过期后,在地址栏直接访问/listFiles会 500 或者空指针。现象是登录后放着不动半小时,再去点列表页就报错。原因是 Servlet 从 session 取loginUser时取到 null,后端没做空判断就调了user.getId()。解决方法是所有需要登录的 Servlet 开头统一做登录校验:User user = (User) session.getAttribute("loginUser"); if (user == null) { response.sendRedirect("login.jsp"); return; }。这个坑顺手写一个过滤器能一劳永逸,但课设阶段在每个 Servlet 加判断最简单。

5. 进阶:用 curl、ab 和 general_log 把验证动作固定下来

功能跑通只是第一步。我建议你再做两件事:用 curl 带登录态去压一下列表接口,再打开 MySQL 的 general_log 看慢 SQL。这两个动作都不复杂,但能验证这个系统到底能不能扛住几个人同时用、数据库 SQL 写得有没有问题。

5.1 用 curl 与 ab 做带登录态的并发请求

浏览器里点来点去只能验证功能,要模拟并发得用工具。先登录拿到 session,再带着 cookie 去压接口。

curl -c cookies.txt -d "username=admin&password=123456" http://localhost:8080/cloud/login SESSION_ID=$(grep JSESSIONID cookies.txt | awk '{print $7}') ab -n 50 -c 5 -C "JSESSIONID=$SESSION_ID" "http://localhost:8080/cloud/listFiles?userId=1"

ab 的-C参数负责带 cookie,-n 50 -c 5表示总共发 50 个请求、同时 5 个并发。结果里重点看Requests per second和Failed requests。如果失败数不为 0,去 Tomcat 日志看是不是内存溢出或数据库连接不够。ab 对 multipart 文件上传支持不好,所以压列表接口最省事;想压上传,就用脚本循环调 curl。

for i in $(seq 1 20); do curl -b cookies.txt -F "file=@test_$i.txt" http://localhost:8080/cloud/upload done

-b cookies.txt复用登录状态,-F模拟 multipart 表单上传。测完看一眼服务器日志有没有超时或堆内存报警。如果上传大文件报 500,去conf/server.xml把maxPostSize调大或者改成-1,Tomcat 默认对 POST 请求体积有限制。

5.2 打开 general_log 定位慢 SQL,给 file_info 建索引

并发上来了,下一步查数据库。先临时打开 general_log,它会把所有 SQL 都记下来:

SET GLOBAL general_log = 'ON'; SET GLOBAL log_output = 'TABLE';

跑几轮列表和分享操作后,查mysql.general_log表就能看到最近执行过的 SQL 和耗时:

SELECT event_time, command_type, argument FROM mysql.general_log WHERE command_type = 'Query' ORDER BY event_time DESC LIMIT 20;

云盘这种项目最典型的慢 SQL,是按user_id和parent_id查文件列表时全表扫描。数据量从几十条涨到几千条时,没索引的查询会明显变慢。可以顺手给 file_info 加上组合索引:

ALTER TABLE file_info ADD INDEX idx_user_parent (user_id, parent_id);

idx_user_parent这个索引能同时支持“查某用户的全部文件”和“查某用户某目录下的文件”这两种高频查询。测完记得把 general_log 关掉,否则 MySQL 的日志表会快速膨胀,影响性能。

从那以后我每次拿到这类 JavaWeb 源码包,第一件事都是先跑通再改功能,改之前先备份原 zip;验证流程固定在 curl + ab + general_log 这一套上,再也不凭感觉说“应该没问题”。这套流程同样适用于你自己的课设和面试前准备。如果你手头还没有这个 zip,把它下下来照上面的顺序跑一遍,遇到的问题基本都能对症找到答案,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询