ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JSP+Servlet+MySQL网盘系统实战:从建表到上传下载避坑指南

JSP+Servlet+MySQL网盘系统实战:从建表到上传下载避坑指南 简介这是一套面向Java Web初学者与课程设计需求的网盘系统源码采用JSPServlet技术栈后端结合MySQL数据库适合作为毕业设计、课程作业或框架入门练手项目。压缩包共123个文件约2.28MB其中22个jsp页面承担前端视图20个java源文件与20个class文件构成核心业务逻辑另有css、js、图片及字体等静态资源以及jar、properties、xml等依赖与配置结构完整、层次清晰。项目围绕用户管理、文件上传下载、关注与权限过滤等模块展开涵盖登录校验、下载拦截等典型实现便于理解Servlet请求流转与DAO分层设计。源码已本地编译通过按文档配置环境即可运行难度适中适合边调试边学习。目前已有117人学习下载可作为网盘类Web应用的参考方案帮助读者快速掌握JSPServletMySQL的整合开发思路。1. JSPServlet 网盘系统为什么这套老技术栈还在被反复拿来练手打开招聘网站搜 Java 后端满屏 Spring Boot、微服务、云原生。但如果你去翻高校课设、企业内训题库、转行自学的第一个完整项目JSPServletMySQL 这套组合出现的频率依然高得离谱。原因不复杂它把 HTTP 请求怎么进来、参数怎么解析、会话怎么维持、文件怎么落盘、数据库怎么读写这几件事全部摊开给你看没有框架帮你藏。网盘系统恰好是能把这几个点串起来的最小完整场景——用户注册登录、文件上传下载、列表展示、删除重命名每一个功能都逼你直面 Servlet 的生命周期和 JSP 的渲染逻辑。这篇文章面向的是手里已经有一个JSPservlet,网盘系统,mysql.zip这类压缩包、或者准备从零搭一个的人。我会按实际开发顺序拆环境怎么配、数据库怎么建、上传下载怎么跑通、分页和路径怎么处理、最后哪些坑一定会踩。不堆概念每一步都落到能跑的代码和能查的命令上。如果你正在找一个能写进简历、又能真正理解 JavaWeb 底层流转的项目这套东西值得花两三天认真做一遍。2. 环境与数据库从 mysql 安装到建表的最小闭环2.1 JDK、Tomcat、MySQL 的版本选择与安装顺序这套技术栈对版本不挑剔但版本错配会带来大量无意义的报错。我一般推荐 JDK 8 或 JDK 11、Tomcat 8.5 或 9.0、MySQL 5.7 或 8.0 这个区间。JDK 8 和 Tomcat 8.5 是当年 JSP/Servlet 教学最主流的组合网上能搜到的mysql安装教程、mysql安装配置教程大多也是围绕这两个版本展开遇到问题容易找到答案。MySQL 8.0 相比 5.7 在认证插件上有变化连接串需要额外注意后面会讲。安装顺序建议 JDK → Tomcat → MySQL。JDK 装完配JAVA_HOME和Path命令行执行java -version能出版本号即可。Tomcat 解压后配CATALINA_HOME双击bin/startup.batWindows或执行startup.shLinux浏览器访问http://localhost:8080看到 Tomcat 默认页就算通了。MySQL 在 Windows 上可以用安装包一路下一步Linux 上常见做法是rpm安装mysql或者用官方 tar 包解压配置装完执行net start mysql启动服务。提示Windows 上如果net start mysql报服务无法启动先去看 MySQL 安装目录下 data 文件夹里的.err错误日志九成是 my.ini 路径写错或者 data 目录初始化失败。2.2 建库建表用户表与文件表的字段设计网盘系统的数据模型很清晰两张核心表就够用户表和文件表。用户表存账号密码文件表存文件元信息真实存储路径、原始文件名、大小、上传时间、所属用户。下面是我常用的建表语句字段类型和索引都按实际查询场景定过。-- 创建数据库字符集用 utf8mb4 支持中文和 emoji CREATE DATABASE netdisk DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE netdisk; -- 用户表username 唯一索引password 存 MD5 或 BCrypt 哈希 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 文件表user_id 建普通索引因为列表查询永远按用户过滤 CREATE TABLE t_file ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, origin_name VARCHAR(255) NOT NULL, -- 用户看到的原始文件名 store_name VARCHAR(255) NOT NULL, -- 磁盘上的 UUID 文件名防重名 file_size BIGINT DEFAULT 0, -- 字节数用 BIGINT 防大文件溢出 upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明origin_name和store_name分开是关键设计。用户上传简历.pdf磁盘上存成a3f2c1...pdf这样两个用户传同名文件不会互相覆盖下载时再用origin_name还原给用户看。file_size用 BIGINT 而不是 INT因为 INT 最大约 2GB网盘场景下不够用。idx_user索引是必须的文件列表查询WHERE user_id ?会走这个索引数据量上万后差别很明显。参数说明字符集统一 utf8mb4避免中文文件名乱码。密码字段留 100 长度因为 BCrypt 哈希值有 60 位MD5 是 32 位留足余量。create_time和upload_time用DEFAULT CURRENT_TIMESTAMP插入时不用手动传时间。2.3 JDBC 连接与 mysql ssl 连接错误的处理Java 连 MySQL 用 JDBC驱动包mysql-connector-java放到WEB-INF/lib下。MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver5.7 是com.mysql.jdbc.Driver写错会报ClassNotFoundException。连接串我一般写成这样// DBUtil.java 片段获取连接 private static final String URL jdbc:mysql://localhost:3306/netdisk?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; private static final String USER root; private static final String PWD 你的密码; public static Connection getConn() throws SQLException { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(驱动未找到, e); } return DriverManager.getConnection(URL, USER, PWD); }逻辑说明useSSLfalse是解决mysql ssl连接错误最直接的办法。MySQL 8.0 默认要求 SSL本地开发环境没配证书就会连接失败报SSL connection error或者Public Key Retrieval is not allowed。加上allowPublicKeyRetrievaltrue配合useSSLfalse基本能解决。serverTimezoneAsia/Shanghai是防止时间字段差 8 小时MySQL 8.0 的时区默认是 UTC不设这个参数插入的时间会不对。参数说明characterEncodingutf8保证中文不乱码。生产环境不建议useSSLfalse但本地开发和课设场景这样配最省事。连接用完记得在 finally 里 close或者用 try-with-resources否则连接池耗尽后整个系统卡死。3. 上传下载核心链路Servlet 怎么接住文件流3.1 文件上传Servlet 3.0 的 Part API 与存储路径Servlet 3.0 之后上传不用再依赖 commons-fileupload直接用MultipartConfig注解加request.getPart()就能拿到文件流。这是目前最干净的写法代码量比老方案少一半。WebServlet(/upload) MultipartConfig(maxFileSize 1024 * 1024 * 100, maxRequestSize 1024 * 1024 * 120) public class UploadServlet extends HttpServlet { // 存储根目录实际项目建议配到 web.xml 或配置文件中 private static final String STORE_DIR D:/netdisk_data/; protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 校验登录 User user (User) req.getSession().getAttribute(user); if (user null) { resp.sendRedirect(login.jsp); return; } // 2. 拿到文件部分 Part part req.getPart(file); String originName part.getSubmittedFileName(); if (originName null || originName.isEmpty()) { resp.getWriter().write(未选择文件); return; } // 3. 生成 UUID 存储名保留扩展名 String ext originName.substring(originName.lastIndexOf(.)); String storeName UUID.randomUUID().toString().replace(-, ) ext; // 4. 确保目录存在后写入磁盘 File dir new File(STORE_DIR); if (!dir.exists()) dir.mkdirs(); part.write(STORE_DIR storeName); // 5. 元信息入库 try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement( INSERT INTO t_file(user_id, origin_name, store_name, file_size) VALUES(?,?,?,?))) { ps.setInt(1, user.getId()); ps.setString(2, originName); ps.setString(3, storeName); ps.setLong(4, part.getSize()); ps.executeUpdate(); } catch (SQLException e) { throw new ServletException(e); } resp.sendRedirect(fileList); } }逻辑说明MultipartConfig的maxFileSize限制单文件 100MBmaxRequestSize限制整个请求 120MB超出会抛IllegalStateException。part.getSubmittedFileName()拿原始文件名part.write()直接写磁盘。UUID 去横线后作为存储名避免任何特殊字符和重名问题。入库用 PreparedStatement 防 SQL 注入这是底线。参数说明STORE_DIR不要放在 Tomcat 的 webapps 目录下因为重新部署 war 包会清空目录导致文件丢失。放到项目外部固定路径比如D:/netdisk_data/或 Linux 下的/data/netdisk/。maxFileSize按需调整网盘场景可以设大一些但要注意 Tomcat 的maxSwallowSize和连接器配置。3.2 文件下载Content-Disposition 与中文文件名乱码下载的核心是设置响应头让浏览器弹出保存对话框而不是直接打开。中文文件名乱码是这里最常见的翻车点必须用 URLEncoder 编码。WebServlet(/download) public class DownloadServlet extends HttpServlet { private static final String STORE_DIR D:/netdisk_data/; protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String idStr req.getParameter(id); // 查询文件元信息同时校验归属防止越权下载 FileMeta meta fileService.findByIdAndUser( Integer.parseInt(idStr), ((User) req.getSession().getAttribute(user)).getId()); if (meta null) { resp.sendError(404); return; } File file new File(STORE_DIR meta.getStoreName()); if (!file.exists()) { resp.sendError(404); return; } // 关键中文文件名用 URLEncoder 编码空格要替换成 %20 String encodedName URLEncoder.encode(meta.getOriginName(), UTF-8) .replace(, %20); resp.setContentType(application/octet-stream); resp.setHeader(Content-Disposition, attachment; filename\ encodedName \; filename*UTF-8 encodedName); resp.setContentLengthLong(file.length()); // 流式拷贝8KB 缓冲区 try (InputStream in new FileInputStream(file); OutputStream out resp.getOutputStream()) { byte[] buf new byte[8192]; int len; while ((len in.read(buf)) ! -1) { out.write(buf, 0, len); } } } }逻辑说明Content-Disposition设成attachment触发下载。filename*UTF-8是 RFC 5987 标准写法现代浏览器优先识别这个中文不会乱码。同时保留filename兼容老浏览器。URLEncoder.encode会把空格编成但 HTTP 头里会被当成字面加号所以要替换回%20。参数说明setContentLengthLong用 long 版本大文件不会溢出。缓冲区 8KB 是经验值太小频繁 IO太大占内存。下载前必须校验文件归属否则改一下 URL 里的 id 就能下载别人的文件这是网盘系统最典型的安全漏洞。3.3 文件列表与分页SQL limit 与 JSP 渲染列表页要处理分页否则文件一多页面直接卡死。分页用 MySQL 的LIMIT offset, size配合COUNT(*)算总页数。// FileService.java 分页查询 public PageResultFileMeta listByUser(int userId, int pageNo, int pageSize) { PageResultFileMeta result new PageResult(); try (Connection conn DBUtil.getConn()) { // 先查总数 try (PreparedStatement ps conn.prepareStatement( SELECT COUNT(*) FROM t_file WHERE user_id ?)) { ps.setInt(1, userId); try (ResultSet rs ps.executeQuery()) { rs.next(); result.setTotal(rs.getInt(1)); } } // 再查当前页数据按上传时间倒序 String sql SELECT id, origin_name, file_size, upload_time FROM t_file WHERE user_id ? ORDER BY upload_time DESC LIMIT ?, ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, userId); ps.setInt(2, (pageNo - 1) * pageSize); ps.setInt(3, pageSize); try (ResultSet rs ps.executeQuery()) { ListFileMeta list new ArrayList(); while (rs.next()) { FileMeta m new FileMeta(); m.setId(rs.getInt(id)); m.setOriginName(rs.getString(origin_name)); m.setFileSize(rs.getLong(file_size)); m.setUploadTime(rs.getTimestamp(upload_time)); list.add(m); } result.setList(list); } } } catch (SQLException e) { throw new RuntimeException(e); } return result; }逻辑说明先 COUNT 再 LIMIT 是标准分页套路。ORDER BY upload_time DESC让最新上传的排前面符合用户直觉。LIMIT的 offset 从 0 开始所以是(pageNo - 1) * pageSize。JSP 页面用 JSTL 的c:forEach遍历列表分页条根据 total 和 pageSize 算总页数渲染。参数说明pageSize一般设 10 到 20太大页面加载慢太小翻页频繁。数据量超过百万行时LIMIT大 offset 会变慢那时候要改用游标分页记录上一页最后一个 id但课设和中小项目用不上。4. 避坑与排查这套组合最容易翻车的 5 个地方4.1 上传文件后重启 Tomcat 文件全没了现象上传成功列表也能看到但重启 Tomcat 或者重新部署 war 包后文件下载全部 404。原因存储目录设在了webapps/项目名/upload/下面。Tomcat 重新部署时会清空整个项目目录连带上传的文件一起删掉。解决存储根目录必须放在项目外部比如D:/netdisk_data/或/data/netdisk/。在代码里用常量或配置文件管理这个路径不要硬编码在多个 Servlet 里。如果已经踩了把文件先拷出来改完路径再传一遍。4.2 中文文件名下载变成乱码或下不下来现象下载中文名文件浏览器保存下来名字是一串%E7%AE%80%E5%8E%86或者干脆报错。原因Content-Disposition头里直接放了未编码的中文HTTP 头只认 ASCIITomcat 会按 ISO-8859-1 处理导致乱码。解决用URLEncoder.encode(name, UTF-8)编码并且把替换成%20。同时加上filename*UTF-8的 RFC 5987 写法。两个都写兼容性最好。4.3 MySQL 8.0 连接报 Public Key Retrieval is not allowed现象本地 MySQL 5.7 换到 8.0 后JDBC 连接直接抛异常提示Public Key Retrieval is not allowed。原因MySQL 8.0 默认认证插件是caching_sha2_passwordJDBC 驱动在非 SSL 连接下需要显式允许公钥检索。解决连接串加allowPublicKeyRetrievaltrueuseSSLfalse。或者把用户认证插件改回mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 密码;。前者改连接串更省事后者改数据库更彻底。4.4 文件列表查询越来越慢现象文件传到几百个后列表页打开要好几秒。原因t_file表没建user_id索引每次查询全表扫描。或者 JSP 页面里在循环中反复查数据库N1 查询。解决确认t_file表有idx_user索引用EXPLAIN看查询计划是否走索引。JSP 里绝对不要在c:forEach循环里调 Service 查数据所有数据在 Servlet 里一次性查好放进 request 再转发。分页一定要做不要一次查全部。4.5 上传大文件时 OutOfMemoryError现象传几十 MB 的文件Tomcat 日志报java.lang.OutOfMemoryError: Java heap space。原因MultipartConfig没配fileSizeThreshold默认超过阈值就写临时文件但如果代码里用part.getInputStream()读进 byte 数组再写大文件直接把堆撑爆。解决用part.write(path)让容器直接写磁盘不要自己读流到内存。如果必须处理流用缓冲区循环读写不要readAllBytes()。同时检查 Tomcat 的-Xmx参数默认可能只有 256MB大文件场景调到 512MB 或 1GB。5. 进阶技巧用 UUID 存储名 数据库事务保证一致性5.1 为什么存储名必须和原始名分离很多人图省事直接用原始文件名存磁盘。两个用户都传报告.docx后传的直接覆盖先传的用户下载发现内容不对这种问题排查起来极其痛苦。UUID 存储名 原始名入库的方案从根上杜绝了重名覆盖。删除文件时按store_name删磁盘文件按id删数据库记录两边对得上。5.2 上传入库失败时怎么清理磁盘文件上传流程是先写磁盘再写数据库。如果数据库插入失败比如连接断了磁盘上会留下一个孤儿文件永远没人引用。我一般用事务包住入库操作失败时在 catch 里删掉刚写的磁盘文件。// 上传时保证磁盘和数据库一致 String storePath STORE_DIR storeName; part.write(storePath); try (Connection conn DBUtil.getConn()) { conn.setAutoCommit(false); // 开事务 try (PreparedStatement ps conn.prepareStatement( INSERT INTO t_file(user_id, origin_name, store_name, file_size) VALUES(?,?,?,?))) { ps.setInt(1, user.getId()); ps.setString(2, originName); ps.setString(3, storeName); ps.setLong(4, part.getSize()); ps.executeUpdate(); conn.commit(); } catch (SQLException e) { conn.rollback(); new File(storePath).delete(); // 入库失败删掉磁盘文件 throw new ServletException(上传失败, e); } }逻辑说明setAutoCommit(false)开启事务插入成功commit失败rollback并删除磁盘文件。这样磁盘和数据库要么都成功要么都回滚不会出现孤儿文件。删除文件时反过来先删数据库记录事务提交再删磁盘文件如果磁盘删除失败只记日志不影响用户因为记录已经没了文件留着也不影响使用。参数说明conn.setAutoCommit(false)之后所有操作都在同一事务里注意不要在这个连接上做其他无关操作。删除磁盘文件用File.delete()返回 false 说明文件不存在或没权限记日志即可不要抛异常打断流程。5.3 一个验证清单上线前必查的 6 项做完之后别急着交按这个清单过一遍能挡掉大部分低级问题检查项验证方法预期结果未登录访问上传接口直接访问/upload跳转登录页不报 500越权下载他人文件改 URL 里的 id 为别人的返回 404不泄露文件中文文件名上传下载传测试文档.txt再下载文件名完整不乱码大文件上传传一个 50MB 文件成功无 OOM重启 Tomcat 后下载重启后下载之前传的文件文件还在能下载SQL 注入尝试登录框输入 or 11登录失败不绕过这六项是我每次交付前必跑的尤其是越权下载和 SQL 注入这两条课设答辩时老师最爱问。我自己的习惯是每加一个新功能就把这个清单里相关的项跑一遍别等最后一起测那时候问题堆在一起根本不知道从哪查起。希望帮到你。本文还有配套的精品资源点击获取
返回列表